import { test, expect, Page } from "@playwright/test"; import { bookingTestData, loginAsOperator } from "./fixtures"; type GoalProgressBucket = { count: number; target: number; date_from?: string; date_end?: string; }; type GoalProgressByKey = Record; 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; target_duration?: "ENTIRE_DURATION" | "WEEKS" | "MONTHS" | "YEARS" | null; target_duration_every?: number | null; }; progress: GoalProgressByKey & { 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 }, year: { count: monthCount, target: params.target }, to_date: { count: allCount, 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 }, year: { count: monthCount, target: params.target }, to_date: { count: allCount, 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 }); }; const openCreateGoalModalProgrammatically = async (page: Page, departmentId: number) => { await page.evaluate( async ({ targetDepartmentId }) => { const modalModule = await import( "/src/views/dashboards/departmentDashboard/modules/goals/functions/showGoalFormModal.js" ); void modalModule.showGoalCreateModal(null, { departments: [targetDepartmentId] }); }, { targetDepartmentId: departmentId } ); }; const openEditGoalModalProgrammatically = async (page: Page, goal: MockGoal) => { await page.evaluate( async ({ editableGoal }) => { const modalModule = await import( "/src/views/dashboards/departmentDashboard/modules/goals/functions/showGoalFormModal.js" ); void modalModule.showGoalEditModal(null, editableGoal); }, { editableGoal: goal } ); }; const overrideSessionPermissionsForNextReload = async ( page: Page, options: { remove?: string[]; add?: string[]; } ) => { const removeSet = new Set((options.remove || []).map((permission) => String(permission))); const addList = (options.add || []).map((permission) => String(permission)); await page.route("**/auth/session", async (route) => { const response = await route.fetch(); const status = response.status(); const headers = response.headers(); const body = await response.json(); const responseDataRoot = body && typeof body === "object" ? body : {}; const nestedData = responseDataRoot?.data?.data; const flatData = responseDataRoot?.data; const sessionPayload = nestedData && typeof nestedData === "object" ? nestedData : flatData && typeof flatData === "object" ? flatData : responseDataRoot; const currentPermissions = Array.isArray(sessionPayload?.permissions) ? sessionPayload.permissions.map((permission) => String(permission)) : []; const nextPermissions = Array.from( new Set(currentPermissions.filter((permission) => !removeSet.has(permission)).concat(addList)) ); if (nestedData && typeof nestedData === "object") { responseDataRoot.data.data.permissions = nextPermissions; } else if (flatData && typeof flatData === "object") { responseDataRoot.data.permissions = nextPermissions; } else { responseDataRoot.permissions = nextPermissions; } await route.fulfill({ status, headers, body: JSON.stringify(responseDataRoot), }); }); }; test.beforeEach(async ({ page }, testInfo) => { test.skip(testInfo.project.name.toLowerCase().includes("mobile"), "Goals admin suite is desktop-focused"); test.skip( !testInfo.project.name.toLowerCase().includes("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("updates progress target when timeframe changes for cadence goals", async ({ page }) => { const departmentId = bookingTestData.departmentId; const nowIso = new Date().toISOString(); const cadenceGoal: MockGoal = { id: 301, created_by: 11, departments: [departmentId], criteria: { label: "Cadence Biweekly Goal", type: "PRODUCT", target: 7, start: "2026-03-24T00:00:00+01:00", end: "2026-07-24T23:59:59+01:00", products: [1], users: [], departments: [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: ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"], progress_alert_time_of_day: null, department_daily_targets: {}, target_duration: "WEEKS", target_duration_every: 2, }, progress: { all: { count: 11, target: 63, date_from: "2026-03-24T00:00:00+01:00", date_end: "2026-07-24T23:59:59+01:00" }, to_date: { count: 6, target: 7, date_from: "2026-03-24T00:00:00+01:00", date_end: "2026-03-24T23:59:59+01:00" }, today: { count: 1, target: 2, date_from: "2026-04-04T00:00:00+01:00", date_end: "2026-04-04T23:59:59+01:00" }, week: { count: 4, target: 7, date_from: "2026-03-23T00:00:00+01:00", date_end: "2026-03-29T23:59:59+01:00" }, month: { count: 9, target: 7, date_from: "2026-04-01T00:00:00+01:00", date_end: "2026-04-30T23:59:59+01:00" }, year: { count: 20, target: 7, date_from: "2026-01-01T00:00:00+01:00", date_end: "2026-12-31T23:59:59+01:00" }, every_2_weeks: { count: 5, target: 14, date_from: "2026-03-12T00:00:00+01:00", date_end: "2026-03-25T23:59:59+01:00", }, departmental_distribution: { [String(departmentId)]: { all: { count: 11, target: 63 }, to_date: { count: 6, target: 7 }, today: { count: 1, target: 2 }, week: { count: 4, target: 7 }, month: { count: 9, target: 7 }, year: { count: 20, target: 7 }, every_2_weeks: { count: 5, target: 14 }, }, }, }, created_at: nowIso, updated_at: nowIso, }; await page.unroute("**/goals/department**"); await page.route("**/goals/department**", async (route) => { const method = route.request().method(); if (method === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: [cadenceGoal] }), }); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ data: {} }), }); }); await page.reload(); const timeframeTabs = page.locator(".tabs.is-toggle").nth(1); const card = page.locator(".goal-card", { hasText: "Cadence Biweekly Goal" }).first(); const progressBar = card.locator("progress.progress").first(); const expectProgress = async (value: number, max: number) => { await expect(progressBar).toHaveAttribute("value", new RegExp(`^${value}(?:\\.0+)?$`)); await expect(progressBar).toHaveAttribute("max", new RegExp(`^${max}(?:\\.0+)?$`)); }; await expectProgress(11, 63); await timeframeTabs.locator("li").nth(1).click(); // Indtil nu await expect(timeframeTabs.locator("li").nth(1)).toHaveClass(/is-active/); await expectProgress(6, 7); await timeframeTabs.locator("li").nth(2).click(); // I dag await expect(timeframeTabs.locator("li").nth(2)).toHaveClass(/is-active/); await expectProgress(1, 2); await timeframeTabs.locator("li").nth(3).click(); // Denne uge await expect(timeframeTabs.locator("li").nth(3)).toHaveClass(/is-active/); await expectProgress(5, 14); await timeframeTabs.locator("li").nth(4).click(); // Denne måned await expect(timeframeTabs.locator("li").nth(4)).toHaveClass(/is-active/); await expectProgress(9, 7); await timeframeTabs.locator("li").nth(5).click(); // År await expect(timeframeTabs.locator("li").nth(5)).toHaveClass(/is-active/); await expectProgress(20, 63); }); test("opens create goal modal and supports cancel", async ({ page }) => { await openCreateGoalModalProgrammatically(page, bookingTestData.departmentId); 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 modal.locator(".tabs li", { hasText: "Alert Configuration" }).click(); 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 }) => { const editableGoal = makeMockGoal({ id: 1, departmentId: bookingTestData.departmentId, label: "Mock Active Goal", target: 100, allCount: 45, todayCount: 10, weekCount: 28, monthCount: 45, start: "2026-01-01", end: "2099-01-01", }); await openEditGoalModalProgrammatically(page, editableGoal); 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"); await modal.locator('input[type="date"]').first().fill("2026-02-01"); await modal.locator('input[type="date"]').nth(1).fill("2026-03-15"); await modal .locator("select") .filter({ has: page.locator('option[value="LEGACY"]') }) .first() .selectOption("WEEKS"); await modal.locator('input[type="number"]').nth(1).fill("2"); 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; target_duration?: string; target_duration_every?: number; start?: string; end?: string; }; }; expect(putPayload.criteria?.label).toBe("Mock Active Goal Updated"); expect(Number(putPayload.criteria?.target)).toBe(321); expect(putPayload.criteria?.target_duration).toBe("WEEKS"); expect(Number(putPayload.criteria?.target_duration_every)).toBe(2); expect(putPayload.criteria?.start).toMatch(/^2026-02-01T00:00:00(?:Z|[+-]\d{2}:\d{2})$/); expect(putPayload.criteria?.end).toMatch(/^2026-03-15T23:59:59(?:Z|[+-]\d{2}:\d{2})$/); 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 overrideSessionPermissionsForNextReload(page, { add: ["goals_department_progress_alert_test", "admin", `department_access_${bookingTestData.departmentId}`], }); await page.reload(); await expect(page.locator("nav.level.box")).toBeVisible({ timeout: 15000 }); const testAlertRequestPromise = page.waitForRequest( (request) => request.method() === "POST" && request.url().includes("/goals/department/progress-alert/test") ); await openGoalAction(page, "Mock Active Goal", "Test alert"); const modal = page.locator(".swal2-popup"); await expect(modal).toBeVisible({ timeout: 10000 }); await modal.locator("#goal-test-alert-destination").selectOption("EMAIL"); await modal.locator("#goal-test-alert-email-to").fill("tester@example.com"); await modal.locator("#goal-test-alert-subject").fill("Dept Goal Progress Test"); await modal.locator(".swal2-confirm").click(); const testAlertRequest = await testAlertRequestPromise; const testAlertPayload = testAlertRequest.postDataJSON() as { id?: number; overrideDestination?: string; email_to?: string; subject?: string; }; expect(Number(testAlertPayload.id)).toBe(1); expect(testAlertPayload.overrideDestination).toBe("EMAIL"); expect(testAlertPayload.email_to).toBe("tester@example.com"); expect(testAlertPayload.subject).toBe("Dept Goal Progress Test"); 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 }); }); test("hides create and test-alert controls when permission is missing and reports COMPONENT entries", async ({ page, }) => { await overrideSessionPermissionsForNextReload(page, { remove: ["goals_department_create", "goals_department_progress_alert_test", "superuser"], add: ["admin", `department_access_${bookingTestData.departmentId}`], }); await page.reload(); await expect(page.locator("nav.level.box")).toBeVisible({ timeout: 15000 }); await expect(page.locator("button.button.is-dark:has(.fa-plus)")).toHaveCount(0); await expect(page.locator("button.button.is-dark.mt-2")).toHaveCount(0); const card = page.locator(".goal-card", { hasText: "Mock Active Goal" }).first(); await expect(card).toBeVisible({ timeout: 15000 }); await card.locator(".dropdown.is-hoverable").first().hover(); await expect(card.locator(".dropdown-item", { hasText: "Test alert" })).toHaveCount(0); await page.keyboard.press("Shift"); await page.keyboard.press("Shift"); await page.keyboard.press("Shift"); const missingPermissionsBox = page.locator("[data-testid='request-queue-missing-permissions-box']"); await expect(missingPermissionsBox).toBeVisible({ timeout: 10000 }); await expect(missingPermissionsBox).toContainText("goals_department_create"); await expect(missingPermissionsBox).toContainText("goals_department_progress_alert_test"); await expect(missingPermissionsBox).toContainText("COMPONENT"); }); });