- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
387 lines
12 KiB
TypeScript
387 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();
|
|
}
|
|
});
|
|
});
|