- Replaced repetitive department module URL logic with `buildDepartmentModulePath` utility in `DepartmentModulesDisplay.vue`. - Enhanced layouts and responsive behavior across components, including `PosDepartmentStep1MobileTransactionHistory.vue` and `DepartmentDailyReport.vue`. - Removed unused `SuperuserInvoicingLocalStore.vue` and refactored to `SuperuserInvoicingLocalStore.ts`. - Updated media query handling and card layout adjustments in `DepartmentDailyReport.vue`. - Added new Playwright configurations (`playwright.prod.config.ts` and `playwright.live.config.ts`) for e2e release workflows. - Extended e2e and unit test coverage for mobile POS and department modules.
361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
|
|
const adminPermissions = [
|
|
"admin",
|
|
"list_department_daily_reports",
|
|
"list_bookings",
|
|
"department_access_1",
|
|
"department_access_2",
|
|
"department_access_3",
|
|
];
|
|
|
|
const json = (body: unknown, status = 200) => ({
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
const departments = [
|
|
{ id: 1, name: "North" },
|
|
{ id: 2, name: "South" },
|
|
{ id: 3, name: "Missing Hours" },
|
|
];
|
|
|
|
const overviewByRange: Record<string, Record<string, unknown>> = {
|
|
"2026-03-23|2026-03-24": {
|
|
department_ids: [1, 2, 3],
|
|
date: "2026-03-23",
|
|
date_to: "2026-03-24",
|
|
metrics: {
|
|
bookings: { state: "ready", value: 9, out_of: 12, message: null },
|
|
complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." },
|
|
night_washes: {
|
|
state: "ready",
|
|
value: 5,
|
|
out_of: null,
|
|
message: null,
|
|
by_source: {
|
|
orders: 1,
|
|
xlvask: 2,
|
|
selfserve: 2,
|
|
},
|
|
has_missing_opening_hours: true,
|
|
missing_department_ids: [3],
|
|
},
|
|
revenue: { state: "ready", value: 5400, out_of: null, message: null },
|
|
washes: { state: "ready", value: 18, out_of: null, message: null },
|
|
products_sold: { state: "ready", value: 26, out_of: null, message: null },
|
|
transactions: { state: "ready", value: 15, out_of: null, message: null },
|
|
water_usage: { state: "ready", value: 44, out_of: null, message: null },
|
|
overtime: { state: "ready", value: 2.5, out_of: null, message: null },
|
|
},
|
|
products: [],
|
|
},
|
|
"2026-03-23|2026-03-25": {
|
|
department_ids: [1, 2, 3],
|
|
date: "2026-03-23",
|
|
date_to: "2026-03-25",
|
|
metrics: {
|
|
bookings: { state: "ready", value: 12, out_of: 16, message: null },
|
|
complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." },
|
|
night_washes: {
|
|
state: "ready",
|
|
value: 7,
|
|
out_of: null,
|
|
message: null,
|
|
by_source: {
|
|
orders: 2,
|
|
xlvask: 2,
|
|
selfserve: 3,
|
|
},
|
|
has_missing_opening_hours: true,
|
|
missing_department_ids: [3],
|
|
},
|
|
revenue: { state: "ready", value: 6900, out_of: null, message: null },
|
|
washes: { state: "ready", value: 23, out_of: null, message: null },
|
|
products_sold: { state: "ready", value: 31, out_of: null, message: null },
|
|
transactions: { state: "ready", value: 19, out_of: null, message: null },
|
|
water_usage: { state: "ready", value: 58, out_of: null, message: null },
|
|
overtime: { state: "ready", value: 3.25, out_of: null, message: null },
|
|
},
|
|
products: [],
|
|
},
|
|
};
|
|
|
|
const transactionCountsByRange: Record<string, Record<number, Record<string, unknown>>> = {
|
|
"2026-03-23|2026-03-24": {
|
|
1: {
|
|
earnings: 2200,
|
|
products: 9,
|
|
quantity: 6,
|
|
washes: 8,
|
|
outside_hours: {
|
|
total: 2,
|
|
by_source: { orders: 1, xlvask: 1, selfserve: 0 },
|
|
has_missing_opening_hours: false,
|
|
missing_department_ids: [],
|
|
},
|
|
},
|
|
2: {
|
|
earnings: 3200,
|
|
products: 12,
|
|
quantity: 9,
|
|
washes: 10,
|
|
outside_hours: {
|
|
total: 3,
|
|
by_source: { orders: 0, xlvask: 1, selfserve: 2 },
|
|
has_missing_opening_hours: false,
|
|
missing_department_ids: [],
|
|
},
|
|
},
|
|
3: {
|
|
earnings: 500,
|
|
products: 2,
|
|
quantity: 1,
|
|
washes: 2,
|
|
outside_hours: {
|
|
total: 0,
|
|
by_source: { orders: 0, xlvask: 0, selfserve: 0 },
|
|
has_missing_opening_hours: true,
|
|
missing_department_ids: [3],
|
|
},
|
|
},
|
|
},
|
|
"2026-03-23|2026-03-25": {
|
|
1: {
|
|
earnings: 2700,
|
|
products: 11,
|
|
quantity: 8,
|
|
washes: 10,
|
|
outside_hours: {
|
|
total: 3,
|
|
by_source: { orders: 1, xlvask: 1, selfserve: 1 },
|
|
has_missing_opening_hours: false,
|
|
missing_department_ids: [],
|
|
},
|
|
},
|
|
2: {
|
|
earnings: 3700,
|
|
products: 14,
|
|
quantity: 10,
|
|
washes: 11,
|
|
outside_hours: {
|
|
total: 4,
|
|
by_source: { orders: 1, xlvask: 1, selfserve: 2 },
|
|
has_missing_opening_hours: false,
|
|
missing_department_ids: [],
|
|
},
|
|
},
|
|
3: {
|
|
earnings: 500,
|
|
products: 2,
|
|
quantity: 1,
|
|
washes: 2,
|
|
outside_hours: {
|
|
total: 0,
|
|
by_source: { orders: 0, xlvask: 0, selfserve: 0 },
|
|
has_missing_opening_hours: true,
|
|
missing_department_ids: [3],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const trendByRange: Record<string, Record<string, unknown>> = {
|
|
"2026-03-23|2026-03-24": {
|
|
points: [
|
|
{
|
|
date: "2026-03-23",
|
|
total: 2,
|
|
by_source: {
|
|
orders: 1,
|
|
xlvask: 0,
|
|
selfserve: 1,
|
|
},
|
|
},
|
|
{
|
|
date: "2026-03-24",
|
|
total: 3,
|
|
by_source: {
|
|
orders: 0,
|
|
xlvask: 2,
|
|
selfserve: 1,
|
|
},
|
|
},
|
|
],
|
|
has_missing_opening_hours: true,
|
|
missing_department_ids: [3],
|
|
},
|
|
"2026-03-23|2026-03-25": {
|
|
points: [
|
|
{
|
|
date: "2026-03-23",
|
|
total: 2,
|
|
by_source: {
|
|
orders: 1,
|
|
xlvask: 0,
|
|
selfserve: 1,
|
|
},
|
|
},
|
|
{
|
|
date: "2026-03-24",
|
|
total: 3,
|
|
by_source: {
|
|
orders: 0,
|
|
xlvask: 2,
|
|
selfserve: 1,
|
|
},
|
|
},
|
|
{
|
|
date: "2026-03-25",
|
|
total: 2,
|
|
by_source: {
|
|
orders: 1,
|
|
xlvask: 0,
|
|
selfserve: 1,
|
|
},
|
|
},
|
|
],
|
|
has_missing_opening_hours: true,
|
|
missing_department_ids: [3],
|
|
},
|
|
};
|
|
|
|
async function mockOverviewDependencies(page) {
|
|
await seedAuthenticatedState(page);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: adminPermissions,
|
|
});
|
|
|
|
await page.route(/\/departments(\?.*)?$/i, async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const departmentId = Number(url.searchParams.get("id"));
|
|
if (departmentId > 0) {
|
|
await route.fulfill(json({
|
|
data: departments.find((department) => department.id === departmentId) || null,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(json({ data: departments }));
|
|
});
|
|
|
|
await page.route(/\/departments\/weather(\?.*)?$/i, async (route) => {
|
|
await route.fulfill(json({ data: [] }));
|
|
});
|
|
|
|
await page.route(/\/departments\/daily-reports\/get(\?.*)?$/i, async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const date = url.searchParams.get("date") || "2026-03-23";
|
|
await route.fulfill(json({
|
|
data: {
|
|
id: 10,
|
|
department_id: 1,
|
|
water_usage: 32,
|
|
notes: "",
|
|
filled_by: 1,
|
|
created_at: `${date} 00:00:00`,
|
|
},
|
|
}));
|
|
});
|
|
|
|
await page.route(/\/departments\/daily-reports\/overview(\?.*)?$/i, async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to") || url.searchParams.get("date")}`;
|
|
await route.fulfill(json({
|
|
data: overviewByRange[key] || overviewByRange["2026-03-23|2026-03-24"],
|
|
}));
|
|
});
|
|
|
|
await page.route(/\/departments\/daily-reports\/transaction-count(\?.*)?$/i, async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to") || url.searchParams.get("date")}`;
|
|
const departmentId = Number(url.searchParams.get("department_id"));
|
|
const payload = transactionCountsByRange[key]?.[departmentId] || transactionCountsByRange["2026-03-23|2026-03-24"][departmentId];
|
|
await route.fulfill(json({
|
|
data: payload,
|
|
}));
|
|
});
|
|
|
|
await page.route(/\/departments\/daily-reports\/outside-hours-trend(\?.*)?$/i, async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to")}`;
|
|
await route.fulfill(json({
|
|
data: trendByRange[key] || trendByRange["2026-03-23|2026-03-24"],
|
|
}));
|
|
});
|
|
|
|
await page.route(/\/departments\/daily-reports(?!\/overview|\/transaction-count|\/outside-hours-trend|\/get)(\?.*)?$/i, async (route) => {
|
|
await route.fulfill(json({ data: [] }));
|
|
});
|
|
}
|
|
|
|
async function expandOverviewReportsOnMobile(page, testInfo) {
|
|
if (!testInfo.project.use.isMobile) {
|
|
return;
|
|
}
|
|
|
|
if (await page.getByTestId("daily-report-page").count()) {
|
|
return;
|
|
}
|
|
|
|
await page.getByTestId("admin-mobile-reports-toggle").click();
|
|
await expect(page.getByTestId("daily-report-page")).toBeVisible();
|
|
}
|
|
|
|
test.describe("Admin overview night washes", () => {
|
|
test("renders deduped Døgnvask totals across cards, list, and chart with stable range updates", async ({ page }, testInfo) => {
|
|
await mockOverviewDependencies(page);
|
|
|
|
await page.goto("/admin?departmentIds=1,2,3&dateFrom=2026-03-23&dateTo=2026-03-24");
|
|
await expandOverviewReportsOnMobile(page, testInfo);
|
|
|
|
await expect(page.getByTestId("daily-report-page")).toBeVisible();
|
|
await expect(page.getByTestId("daily-report-tile-night-washes")).toContainText("5");
|
|
await expect(page.getByTestId("daily-report-night-washes-breakdown")).toContainText("Ordre: 1");
|
|
await expect(page.getByTestId("daily-report-night-washes-breakdown")).toContainText("XL Vask: 2");
|
|
await expect(page.getByTestId("daily-report-night-washes-breakdown")).toContainText("Self-serve: 2");
|
|
await expect(page.getByTestId("daily-report-night-washes-warning")).toContainText("Missing Hours");
|
|
|
|
const tabs = page.locator(".tabs li");
|
|
await tabs.nth(1).click();
|
|
|
|
await expect(page.getByTestId("overview-summary-night-washes-total")).toContainText("5");
|
|
await expect(page.getByTestId("overview-summary-night-washes-breakdown")).toContainText("Self-serve: 2");
|
|
await expect(page.getByTestId("overview-summary-night-washes-warning")).toContainText("Missing Hours");
|
|
await expect(page.getByTestId("overview-department-1-night-washes-total")).toContainText("2");
|
|
await expect(page.getByTestId("overview-department-2-night-washes-total")).toContainText("3");
|
|
await expect(page.getByTestId("overview-department-3-night-washes-total")).toContainText("0");
|
|
|
|
await tabs.nth(2).click();
|
|
await expect(page.getByTestId("outside-hours-chart")).toBeVisible();
|
|
await expect(page.getByTestId("outside-hours-chart-warning")).toContainText("Missing Hours");
|
|
|
|
const endDateInput = page.locator("[data-testid='date-period-end']:visible").first();
|
|
await endDateInput.fill("2026-03-25");
|
|
await endDateInput.blur();
|
|
|
|
await tabs.nth(0).click();
|
|
await expect(page.getByTestId("daily-report-tile-night-washes")).toContainText("7");
|
|
await expect(page.getByTestId("daily-report-night-washes-breakdown")).toContainText("Self-serve: 3");
|
|
|
|
await tabs.nth(1).click();
|
|
await expect(page.getByTestId("overview-summary-night-washes-total")).toContainText("7");
|
|
await expect(page.getByTestId("overview-summary-night-washes-breakdown")).toContainText("Ordre: 2");
|
|
await expect(page.getByTestId("overview-summary-night-washes-breakdown")).toContainText("XL Vask: 2");
|
|
await expect(page.getByTestId("overview-summary-night-washes-breakdown")).toContainText("Self-serve: 3");
|
|
|
|
await tabs.nth(2).click();
|
|
await expect(page.getByTestId("outside-hours-chart")).toBeVisible();
|
|
|
|
if (testInfo.project.use.isMobile) {
|
|
await tabs.nth(0).click();
|
|
await expect(page.getByTestId("daily-report-tile-night-washes")).toBeVisible();
|
|
await tabs.nth(1).click();
|
|
await expect(page.getByTestId("overview-summary-night-washes-total")).toBeVisible();
|
|
}
|
|
});
|
|
});
|