Files
pleno-vue/tests/e2e/adminModuleGoals.spec.ts
T

636 lines
23 KiB
TypeScript

import { test, expect, Page } from "@playwright/test";
import { bookingTestData } from "./fixtures";
import { mockApi, seedAuthenticatedState } from "./support/network.js";
type GoalProgressBucket = {
count: number;
target: number;
date_from?: string;
date_end?: string;
};
type GoalProgressByKey = Record<string, GoalProgressBucket>;
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<string, number>;
target_duration?: "ENTIRE_DURATION" | "WEEKS" | "MONTHS" | "YEARS" | null;
target_duration_every?: number | null;
};
progress: GoalProgressByKey & {
departmental_distribution: Record<string, GoalProgressByKey>;
};
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<string, unknown> };
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 getGoalsPermissions = (departmentId: number) => [
"admin",
`department_access_${departmentId}`,
"list_department_goals",
"goals_department_create",
"goals_department_progress_alert_test",
];
const buildOperatorSessionData = (permissions: string[]) => ({
id: 11,
customer_number: 0,
group_id: 1,
email: "operator@example.com",
phone: {
number: "12345678",
country_code: 45,
},
notifications: {
wash_certificate_email: null,
email_notifications_enabled: true,
sms_notifications_enabled: false,
},
created_at: "2026-01-01T00:00:00.000Z",
updated_at: "2026-01-01T00:00:00.000Z",
display_name: "E2E Operator",
permissions,
economic_customer: [],
two_factor_enabled: false,
});
const installGoalsSessionRoute = async (page: Page, permissions: string[]) => {
await page.route("**/auth/session", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: buildOperatorSessionData(permissions) }),
});
});
};
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));
const departmentId = bookingTestData.departmentId;
const currentPermissions = getGoalsPermissions(departmentId);
const nextPermissions = Array.from(
new Set(currentPermissions.filter((permission) => !removeSet.has(permission)).concat(addList))
);
await installGoalsSessionRoute(page, nextPermissions);
};
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."
);
const departmentId = bookingTestData.departmentId;
const goalsPermissions = getGoalsPermissions(departmentId);
const goalsSessionData = buildOperatorSessionData(goalsPermissions);
await seedAuthenticatedState(page, "admin-goals-token");
await mockApi(page, {
authenticated: true,
permissions: goalsPermissions,
sessionData: goalsSessionData,
});
await installGoalsSessionRoute(page, goalsPermissions);
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();
await page.addInitScript(`
(() => {
const fixedNow = new Date("2026-04-04T10:00:00.000Z").getTime();
const RealDate = Date;
class FixedDate extends RealDate {
constructor(...args) {
super(...(args.length ? args : [fixedNow]));
}
static now() {
return fixedNow;
}
}
FixedDate.UTC = RealDate.UTC;
FixedDate.parse = RealDate.parse;
window.Date = FixedDate;
})();
`);
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: 14,
date_from: "2026-03-12T00:00:00+01:00",
date_end: "2026-03-25T23: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: 14 },
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, 14);
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, 21);
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.getByTestId("goal-start-date").locator("input").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.getByTestId("goal-start-date").locator("input").first().fill("2026-02-01");
await modal.getByTestId("goal-end-date").locator("input").first().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 saveButton = modal.getByRole("button", { name: "Gem ændringer" });
await expect(saveButton).toBeVisible();
await expect(saveButton).toBeEnabled();
const [putRequest] = await Promise.all([
page.waitForRequest((request) => request.method() === "PUT" && request.url().includes("/goals/department")),
saveButton.click(),
]);
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");
});
});