import { test, expect, Page } from '@playwright/test'; import { bookingTestData, loginAsOperator } from './fixtures'; type GoalProgressBucket = { count: number; target: number; }; type MockGoal = { id: number; created_by: number; departments: number[]; criteria: { label: string; type: 'REVENUE' | 'PRODUCT' | 'VISITS'; target: number; start: string; end: string; products: number[]; users: number[]; departments: number[]; progress_alert_frequency: 'NONE' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'CHANGED'; progress_alert_destination: 'NONE' | 'SLACK' | 'EMAIL' | 'SMS'; progress_alert_progress_type: string; progress_alert_style: string; progress_alert_format: string | null; progress_alert_weekdays: string[]; progress_alert_time_of_day: string | null; department_daily_targets: Record; }; progress: { all: GoalProgressBucket; today: GoalProgressBucket; week: GoalProgressBucket; month: GoalProgressBucket; departmental_distribution: Record>; }; created_at: string; updated_at: string; }; const makeMockGoal = (params: { id: number; departmentId: number; label: string; target: number; start: string; end: string; allCount?: number; todayCount?: number; weekCount?: number; monthCount?: number; }): MockGoal => { const allCount = params.allCount ?? 0; const todayCount = params.todayCount ?? 0; const weekCount = params.weekCount ?? allCount; const monthCount = params.monthCount ?? allCount; return { id: params.id, created_by: 11, departments: [params.departmentId], criteria: { label: params.label, type: 'REVENUE', target: params.target, start: params.start, end: params.end, products: [], users: [], departments: [params.departmentId], progress_alert_frequency: 'NONE', progress_alert_destination: 'NONE', progress_alert_progress_type: 'ALL', progress_alert_style: 'NONE', progress_alert_format: null, progress_alert_weekdays: [], progress_alert_time_of_day: null, department_daily_targets: {}, }, progress: { all: { count: allCount, target: params.target }, today: { count: todayCount, target: params.target }, week: { count: weekCount, target: params.target }, month: { count: monthCount, target: params.target }, departmental_distribution: { [String(params.departmentId)]: { all: { count: allCount, target: params.target }, today: { count: todayCount, target: params.target }, week: { count: weekCount, target: params.target }, month: { count: monthCount, target: params.target }, }, }, }, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; }; const setupGoalsApiMock = async (page: Page, departmentId: number) => { let goals: MockGoal[] = [ makeMockGoal({ id: 1, departmentId, label: 'Mock Active Goal', target: 100, allCount: 45, todayCount: 10, weekCount: 28, monthCount: 45, start: '2026-01-01', end: '2099-01-01', }), makeMockGoal({ id: 2, departmentId, label: 'Mock Expired Goal', target: 200, allCount: 180, todayCount: 0, weekCount: 0, monthCount: 0, start: '2024-01-01', end: '2024-02-01', }), ]; await page.route('**/goals/department/progress-alert/test**', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { destination: 'EMAIL' } }), }); }); await page.route('**/goals/department**', async (route) => { const request = route.request(); const method = request.method(); if (method === 'GET') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: goals }), }); return; } if (method === 'PUT') { const body = request.postDataJSON() as { id: number; departments: number[]; criteria: Record }; const id = Number(body.id); const departments = (body.departments || [departmentId]).map((x) => Number(x)); goals = goals.map((goal) => { if (goal.id !== id) return goal; return { ...goal, departments, criteria: { ...goal.criteria, ...(body.criteria || {}), departments, }, updated_at: new Date().toISOString(), }; }); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: goals.find((g) => g.id === id) || null }), }); return; } if (method === 'DELETE') { const id = Number(new URL(request.url()).searchParams.get('id')); goals = goals.filter((goal) => goal.id !== id); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: { success: true } }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: {} }), }); }); }; test.describe('Admin Module - Goals', () => { const getGoalsUrl = (departmentId: string) => `/admin/${departmentId}/modules/goals`; const openGoalAction = async (page: Page, label: string, actionLabel: string) => { const card = page.locator('.goal-card', { hasText: label }).first(); await expect(card).toBeVisible({ timeout: 15000 }); const dropdown = card.locator('.dropdown.is-hoverable').first(); await dropdown.hover(); await card.locator('.dropdown-item', { hasText: actionLabel }).click({ force: true }); }; test.beforeEach(async ({ page }, testInfo) => { test.skip(testInfo.project.name.toLowerCase().includes('mobile'), 'Goals admin suite is desktop-focused'); test.skip(testInfo.project.name !== 'chromium', 'Goals admin suite is stabilized for Chromium in this environment.'); await loginAsOperator(page); const departmentId = bookingTestData.departmentId; await setupGoalsApiMock(page, departmentId); await page.goto(getGoalsUrl(String(departmentId))); await expect(page).toHaveURL(new RegExp(`/admin/${departmentId}/modules/goals`)); await expect(page.locator('nav.level.box')).toBeVisible({ timeout: 15000 }); }); test('loads goals page and supports status/timeframe tab switching', async ({ page }) => { const statusTabs = page.locator('.tabs.is-toggle').first(); const timeframeTabs = page.locator('.tabs.is-toggle').nth(1); await expect(page.locator('.goal-card')).toHaveCount(1); await statusTabs.locator('li').nth(1).click(); await expect(statusTabs.locator('li').nth(1)).toHaveClass(/is-active/); await expect(page.locator('.goal-card')).toHaveCount(1); await statusTabs.locator('li').nth(2).click(); await expect(statusTabs.locator('li').nth(2)).toHaveClass(/is-active/); await expect(page.locator('.goal-card')).toHaveCount(2); await statusTabs.locator('li').nth(0).click(); await expect(statusTabs.locator('li').nth(0)).toHaveClass(/is-active/); await timeframeTabs.locator('li', { hasText: 'Indtil nu' }).click(); await expect(timeframeTabs.locator('li', { hasText: 'Indtil nu' })).toHaveClass(/is-active/); await timeframeTabs.locator('li').nth(2).click(); await expect(timeframeTabs.locator('li').nth(2)).toHaveClass(/is-active/); await timeframeTabs.locator('li').nth(3).click(); await expect(timeframeTabs.locator('li').nth(3)).toHaveClass(/is-active/); await timeframeTabs.locator('li').nth(4).click(); await expect(timeframeTabs.locator('li').nth(4)).toHaveClass(/is-active/); await timeframeTabs.locator('li').nth(0).click(); await expect(timeframeTabs.locator('li').nth(0)).toHaveClass(/is-active/); }); test('opens create goal modal and supports cancel', async ({ page }) => { await page.locator('button:has(.fa-plus), button:has-text("Opret")').first().click(); const modal = page.locator('.swal2-popup'); await expect(modal).toBeVisible({ timeout: 15000 }); await expect(modal.locator('input[placeholder*="f.eks."]').first()).toBeVisible(); await expect(modal.locator('input[type="number"]').first()).toBeVisible(); await expect(modal.locator('input[type="date"]').first()).toBeVisible(); await expect(modal.locator('textarea').first()).toBeVisible(); await modal.locator('button.button:has-text("Annuller")').click(); await expect(page.locator('.swal2-popup')).toBeHidden({ timeout: 10000 }); }); test('submits edit modal and sends expected update payload', async ({ page }) => { await openGoalAction(page, 'Mock Active Goal', 'Rediger'); const modal = page.locator('.swal2-popup'); await expect(modal).toBeVisible({ timeout: 15000 }); await modal.locator('input[placeholder*="f.eks."]').first().fill('Mock Active Goal Updated'); await modal.locator('input[type="number"]').first().fill('321'); const putRequestPromise = page.waitForRequest((request) => request.method() === 'PUT' && request.url().includes('/goals/department') ); await modal.locator('.buttons.is-right .button.is-dark').click({ force: true }); const putRequest = await putRequestPromise; const putPayload = putRequest.postDataJSON() as { criteria?: { label?: string; target?: number } }; expect(putPayload.criteria?.label).toBe('Mock Active Goal Updated'); expect(Number(putPayload.criteria?.target)).toBe(321); const successDialog = page.locator('.swal2-popup:has-text("Succes")').first(); await expect(successDialog).toBeVisible({ timeout: 15000 }); await expect(successDialog).toBeHidden({ timeout: 15000 }); }); test('supports test-alert action and delete request flow', async ({ page }) => { await openGoalAction(page, 'Mock Active Goal', 'Test alert'); const toast = page.locator('.v-toast__item').first(); await expect(toast).toBeVisible({ timeout: 10000 }); await expect(toast).toContainText(/Test alert sendt via|Kunne ikke sende test alert/); await openGoalAction(page, 'Mock Active Goal', 'Slet'); await expect(page.locator('.swal2-popup')).toBeVisible({ timeout: 10000 }); const deleteRequestPromise = page.waitForRequest((request) => request.method() === 'DELETE' && request.url().includes('/goals/department') ); await page.locator('.swal2-popup .swal2-confirm').click(); const deleteRequest = await deleteRequestPromise; expect(deleteRequest.url()).toContain('id='); await expect(page.locator('.swal2-popup')).toBeHidden({ timeout: 10000 }); }); });