diff --git a/tests/e2e/admin-department-visibility.spec.ts b/tests/e2e/admin-department-visibility.spec.ts index f45a7316..d63c8834 100644 --- a/tests/e2e/admin-department-visibility.spec.ts +++ b/tests/e2e/admin-department-visibility.spec.ts @@ -57,12 +57,15 @@ const buildOverviewPayload = () => ({ }, }); -async function mockAdminDepartmentDependencies(page, options: { - departmentDelayMs?: number; - departments?: Array>; - permissions?: string[]; - sessionData?: Record; -} = {}) { +async function mockAdminDepartmentDependencies( + page, + options: { + departmentDelayMs?: number; + departments?: Array>; + permissions?: string[]; + sessionData?: Record; + } = {} +) { await seedAuthenticatedState(page); await mockApi(page, { authenticated: true, @@ -130,10 +133,7 @@ test.describe("Admin department visibility", () => { test.skip(!isDesktopProject(testInfo), "Desktop only"); await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "department_access_5", - ], + permissions: [...adminPermissions, "department_access_5"], departments: [ { id: 1, name: "Visible North", visible: true, priority_order: 20 }, { id: 2, name: "Ingen data", visible: true, priority_order: 1 }, @@ -208,11 +208,7 @@ test.describe("Admin department visibility", () => { test.skip(!isDesktopProject(testInfo), "Desktop only"); await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "add_order", - "list_orders", - ], + permissions: [...adminPermissions, "add_order", "list_orders"], departments: [ { id: 1, name: "Visible North", visible: true }, { id: 2, name: "Visible South", visible: true }, @@ -236,17 +232,11 @@ test.describe("Admin department visibility", () => { await expect(page.getByTestId("department-pos-drafts-page")).toBeVisible(); }); - test("shows a department-scoped draft count badge in the desktop buefy menu", async ({ - page, - }, testInfo) => { + test("shows a department-scoped draft count badge in the desktop buefy menu", async ({ page }, testInfo) => { test.skip(!isDesktopProject(testInfo), "Desktop only"); await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "add_order", - "list_orders", - ], + permissions: [...adminPermissions, "add_order", "list_orders"], sessionData: { runtime_config: { economic: { @@ -315,11 +305,7 @@ test.describe("Admin department visibility", () => { const requestedFilters: string[] = []; await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "add_order", - "list_orders", - ], + permissions: [...adminPermissions, "add_order", "list_orders"], sessionData: { runtime_config: { economic: { @@ -385,10 +371,7 @@ test.describe("Admin department visibility", () => { const requestedDepartments: string[] = []; await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "list_bookings", - ], + permissions: [...adminPermissions, "list_bookings"], }); await page.route(/\/order-bookings\/counts(\?.*)?$/i, async (route) => { @@ -427,10 +410,7 @@ test.describe("Admin department visibility", () => { test.skip(!isDesktopProject(testInfo), "Desktop only"); await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "list_bookings", - ], + permissions: [...adminPermissions, "list_bookings"], }); await page.route(/\/order-bookings\/counts(\?.*)?$/i, async (route) => { @@ -471,10 +451,7 @@ test.describe("Admin department visibility", () => { const requestedDepartments: string[] = []; await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "list_bookings", - ], + permissions: [...adminPermissions, "list_bookings"], }); await page.route(/\/order-bookings\/counts(\?.*)?$/i, async (route) => { @@ -484,17 +461,18 @@ test.describe("Admin department visibility", () => { await route.fulfill( json({ - data: department === "1" - ? { - past: 2, - current: 3, - future: 1, - } - : { - past: 0, - current: 0, - future: 0, - }, + data: + department === "1" + ? { + past: 2, + current: 3, + future: 1, + } + : { + past: 0, + current: 0, + future: 0, + }, }) ); }); @@ -535,10 +513,7 @@ test.describe("Admin department visibility", () => { test.skip(!isDesktopProject(testInfo), "Desktop only"); await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "list_bookings", - ], + permissions: [...adminPermissions, "list_bookings"], }); await page.route(/\/order-bookings\/counts(\?.*)?$/i, async (route) => { @@ -547,17 +522,18 @@ test.describe("Admin department visibility", () => { await route.fulfill( json({ - data: department === "1" - ? { - past: 2, - current: 0, - future: 1, - } - : { - past: 0, - current: 0, - future: 0, - }, + data: + department === "1" + ? { + past: 2, + current: 0, + future: 1, + } + : { + past: 0, + current: 0, + future: 0, + }, }) ); }); @@ -575,19 +551,14 @@ test.describe("Admin department visibility", () => { await expect(page.getByTestId("desktop-buefy-nav-bookings-tooltip-future")).toContainText("1"); }); - test("uses the cached bookings counts endpoint for desktop buefy menu badges", async ({ - page, - }, testInfo) => { + test("uses the cached bookings counts endpoint for desktop buefy menu badges", async ({ page }, testInfo) => { test.skip(!isDesktopProject(testInfo), "Desktop only"); const countRequests: string[] = []; const listRequests: string[] = []; await mockAdminDepartmentDependencies(page, { - permissions: [ - ...adminPermissions, - "list_bookings", - ], + permissions: [...adminPermissions, "list_bookings"], }); await page.route(/\/order-bookings\/counts(\?.*)?$/i, async (route) => { diff --git a/tests/e2e/admin-pos-orders.spec.ts b/tests/e2e/admin-pos-orders.spec.ts index 0a90e229..5b8f027c 100644 --- a/tests/e2e/admin-pos-orders.spec.ts +++ b/tests/e2e/admin-pos-orders.spec.ts @@ -529,24 +529,27 @@ function createTallActionMenuPosFixture() { }; return orders; }, {}), - attachmentsByOrderId: tallOrderIds.reduce>>>((attachments, orderId) => { - attachments[orderId] = Array.from({ length: 3 }, (_, index) => ({ - id: 7000 + orderId * 10 + index, - object_type: "orders", - object_id: orderId, - content: { - image: null, - document: `action-menu-${orderId}-${index + 1}.pdf`, - relation: null, - other: `action-menu-${orderId}-${index + 1}.pdf`, - src: null, - }, - created_at: "2026-04-21 08:44:07", - updated_at: "2026-04-21 08:44:07", - deleted_at: null, - })); - return attachments; - }, {}), + attachmentsByOrderId: tallOrderIds.reduce>>>( + (attachments, orderId) => { + attachments[orderId] = Array.from({ length: 3 }, (_, index) => ({ + id: 7000 + orderId * 10 + index, + object_type: "orders", + object_id: orderId, + content: { + image: null, + document: `action-menu-${orderId}-${index + 1}.pdf`, + relation: null, + other: `action-menu-${orderId}-${index + 1}.pdf`, + src: null, + }, + created_at: "2026-04-21 08:44:07", + updated_at: "2026-04-21 08:44:07", + deleted_at: null, + })); + return attachments; + }, + {} + ), }); } @@ -1537,12 +1540,10 @@ test.describe("Admin POS Orders - desktop settings", () => { await expect(dropdownContent).toHaveCSS("border-top-style", "solid"); await expect(attachmentItems.nth(0)).toContainText(/vaskecertifikat|wash certificate/i); await expect(attachmentItems.nth(1)).toContainText(/upload/i); - }); }); +}); -test("hides the draft export action in the order rail for non-superusers", async ({ - page, -}, testInfo) => { +test("hides the draft export action in the order rail for non-superusers", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order rail coverage"); await mockApi(page, { @@ -1576,9 +1577,7 @@ test("hides the draft export action in the order rail for non-superusers", async await expect(receiptPopup.locator("body")).not.toContainText("Download Excel"); }); -test("shows the draft export and receipt actions in the order rail for superusers", async ({ - page, -}, testInfo) => { +test("shows the draft export and receipt actions in the order rail for superusers", async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order rail coverage"); await mockApi(page, { diff --git a/tests/e2e/assign-draft-order-modal-create.spec.ts b/tests/e2e/assign-draft-order-modal-create.spec.ts index 79c277e2..fd548992 100644 --- a/tests/e2e/assign-draft-order-modal-create.spec.ts +++ b/tests/e2e/assign-draft-order-modal-create.spec.ts @@ -136,8 +136,6 @@ test.describe("Assign draft order modal create invoice collection", () => { } expect(createdOptionBox.y).toBeGreaterThanOrEqual(modalBodyBox.y - 1); - expect(createdOptionBox.y + createdOptionBox.height).toBeLessThanOrEqual( - modalBodyBox.y + modalBodyBox.height + 1 - ); + expect(createdOptionBox.y + createdOptionBox.height).toBeLessThanOrEqual(modalBodyBox.y + modalBodyBox.height + 1); }); }); diff --git a/tests/e2e/change-customer.spec.ts b/tests/e2e/change-customer.spec.ts index cf5e42c6..4d2f3737 100644 --- a/tests/e2e/change-customer.spec.ts +++ b/tests/e2e/change-customer.spec.ts @@ -151,7 +151,9 @@ test.describe("Orders change customer", () => { await expect(modal).toHaveCount(0); await expect(page.getByTestId("change-customer-harness-status")).toHaveText("assigned"); - const assignment = await page.evaluate(() => (window as typeof window & { __changeCustomerAssignment?: unknown }).__changeCustomerAssignment); + const assignment = await page.evaluate( + () => (window as typeof window & { __changeCustomerAssignment?: unknown }).__changeCustomerAssignment + ); expect(assignment).toMatchObject({ customer_id: 12345679, }); diff --git a/tests/e2e/edge-gateways.smoke.spec.js b/tests/e2e/edge-gateways.smoke.spec.js index 256f73fc..b5ff0b46 100644 --- a/tests/e2e/edge-gateways.smoke.spec.js +++ b/tests/e2e/edge-gateways.smoke.spec.js @@ -75,9 +75,9 @@ test.describe("Edge gateway management smoke", () => { await expect(page.locator("body")).toContainText("Installer command copied."); await expect(page.getByTestId("gateway-installer-copy")).toContainText("Copied"); - await expect.poll(() => page.evaluate(() => window.__copiedInstallerCommand)).toContain( - "install.sh?token=edge-install-token" - ); + await expect + .poll(() => page.evaluate(() => window.__copiedInstallerCommand)) + .toContain("install.sh?token=edge-install-token"); }); test("@smoke shows fleet usage statistics on the landing page", async ({ page }) => { diff --git a/tests/e2e/pos-flow.spec.js b/tests/e2e/pos-flow.spec.js index 3a84d09e..bb905dae 100644 --- a/tests/e2e/pos-flow.spec.js +++ b/tests/e2e/pos-flow.spec.js @@ -1158,9 +1158,7 @@ test.describe("POS flow", () => { const cardButton = page.getByTestId("pos-card-payment-inline-action"); await expect(cardButton).toBeVisible(); - await expect - .poll(() => fixture.readersGet, { timeout: 10_000 }) - .toBeGreaterThan(0); + await expect.poll(() => fixture.readersGet, { timeout: 10_000 }).toBeGreaterThan(0); await expect(cardButton).toBeDisabled(); fixture.readers = [ @@ -1173,9 +1171,12 @@ test.describe("POS flow", () => { ]; await expect - .poll(async () => { - return await cardButton.isEnabled(); - }, { timeout: 12_000 }) + .poll( + async () => { + return await cardButton.isEnabled(); + }, + { timeout: 12_000 } + ) .toBe(true); }); @@ -1200,9 +1201,9 @@ test.describe("POS flow", () => { await expect.poll(() => fixture.ordersById[9300]?.id ?? null, { timeout: 10_000 }).toBe(9300); await expect.poll(() => fixture.ordersById[9300]?.reg_2 ?? null, { timeout: 10_000 }).toBe("TRAILER9"); - await expect.poll(() => fixture.ordersById[9300]?.reference ?? null, { timeout: 10_000 }).toBe( - "MANUAL-DESKTOP-SAVE" - ); + await expect + .poll(() => fixture.ordersById[9300]?.reference ?? null, { timeout: 10_000 }) + .toBe("MANUAL-DESKTOP-SAVE"); }); test("desktop auto-applies the only previous customer suggestion while selection source is none", async ({ diff --git a/tests/e2e/pos-mobile-card-payments.spec.js b/tests/e2e/pos-mobile-card-payments.spec.js index 8fad7767..a805dc5f 100644 --- a/tests/e2e/pos-mobile-card-payments.spec.js +++ b/tests/e2e/pos-mobile-card-payments.spec.js @@ -54,7 +54,9 @@ test.describe("POS mobile card payments", () => { expect(snapshot?.metadata?.reference).toBe("CARD-CTX-REF"); }); - test("card mode stays disabled when no stripe readers are assigned and re-enables after polling", async ({ page }) => { + test("card mode stays disabled when no stripe readers are assigned and re-enables after polling", async ({ + page, + }) => { const fixture = createMobilePosFixture({ readers: [], }); @@ -77,9 +79,7 @@ test.describe("POS mobile card payments", () => { const cardButton = page.getByTestId("pos-mobile-direct-card-payment"); await expect(cardButton).toBeVisible({ timeout: 10_000 }); - await expect - .poll(() => fixture.requestCounters.readersGet, { timeout: 10_000 }) - .toBeGreaterThan(0); + await expect.poll(() => fixture.requestCounters.readersGet, { timeout: 10_000 }).toBeGreaterThan(0); await expect(cardButton).toBeDisabled(); fixture.readers = [ @@ -92,9 +92,12 @@ test.describe("POS mobile card payments", () => { ]; await expect - .poll(async () => { - return await cardButton.isEnabled(); - }, { timeout: 12_000 }) + .poll( + async () => { + return await cardButton.isEnabled(); + }, + { timeout: 12_000 } + ) .toBe(true); await cardButton.click(); @@ -228,7 +231,10 @@ test.describe("POS mobile card payments", () => { await expect(page.getByTestId("pos-stripe-terminal-group-in_use")).toBeVisible(); await expect(page.getByTestId("pos-stripe-terminal-group-offline")).toBeVisible(); await expect(page.getByTestId("pos-stripe-terminal-option-reader_in_use")).toHaveAttribute("aria-disabled", "true"); - await expect(page.getByTestId("pos-stripe-terminal-option-reader_offline")).toHaveAttribute("aria-disabled", "true"); + await expect(page.getByTestId("pos-stripe-terminal-option-reader_offline")).toHaveAttribute( + "aria-disabled", + "true" + ); await expect(page.getByTestId("pos-stripe-terminal-option-dot-reader_offline")).toHaveCSS( "background-color", "rgb(220, 38, 38)" @@ -260,10 +266,7 @@ test.describe("POS mobile card payments", () => { await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled({ timeout: 10_000 }); await expect(page.getByTestId("pos-stripe-selected-reader")).toContainText("Recovered Reader"); await expect(page.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveAttribute("data-status-key", "ready"); - await expect(page.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveCSS( - "background-color", - "rgb(22, 163, 74)" - ); + await expect(page.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveCSS("background-color", "rgb(22, 163, 74)"); expect(fixture.requestCounters.paymentIntentCreate).toBe(0); }); diff --git a/tests/e2e/superuser-bookings.spec.ts b/tests/e2e/superuser-bookings.spec.ts index ef189bba..a661d664 100644 --- a/tests/e2e/superuser-bookings.spec.ts +++ b/tests/e2e/superuser-bookings.spec.ts @@ -9,9 +9,7 @@ const json = (body: unknown, status = 200) => ({ }); test.describe("Superuser bookings", () => { - test("shows a light grey loading indicator while the bookings count is fetching", async ({ - page, - }, testInfo) => { + test("shows a light grey loading indicator while the bookings count is fetching", async ({ page }, testInfo) => { test.skip(!isDesktopProject(testInfo), "Desktop only"); await seedAuthenticatedState(page); @@ -159,8 +157,7 @@ test.describe("Superuser bookings", () => { const filters = url.searchParams.get("filters") || ""; const limit = Number(url.searchParams.get("limit") || "100"); - const isPendingTodayRequest = - filters.includes("datetime-date_from:") && filters.includes("datetime-date_to:"); + const isPendingTodayRequest = filters.includes("datetime-date_from:") && filters.includes("datetime-date_to:"); const isDepartmentScoped = filters.includes("department:"); await route.fulfill( @@ -326,9 +323,7 @@ test.describe("Superuser bookings", () => { await expect(page.getByTestId("desktop-buefy-nav-bookings-tooltip-future")).toContainText("1"); }); - test("uses the cached bookings counts endpoint for superuser menu badges", async ({ - page, - }, testInfo) => { + test("uses the cached bookings counts endpoint for superuser menu badges", async ({ page }, testInfo) => { test.skip(!isDesktopProject(testInfo), "Desktop only"); const countRequests: string[] = []; diff --git a/tests/e2e/superuser-drafts.spec.ts b/tests/e2e/superuser-drafts.spec.ts index 9cd31cb9..4c158f08 100644 --- a/tests/e2e/superuser-drafts.spec.ts +++ b/tests/e2e/superuser-drafts.spec.ts @@ -9,9 +9,7 @@ const json = (body: unknown, status = 200) => ({ }); test.describe("Superuser drafts", () => { - test("shows a light grey loading indicator while the draft count is fetching", async ({ - page, - }, testInfo) => { + test("shows a light grey loading indicator while the draft count is fetching", async ({ page }, testInfo) => { test.skip(!isDesktopProject(testInfo), "Desktop only"); const orderFilters: string[] = []; @@ -94,14 +92,12 @@ test.describe("Superuser drafts", () => { await expect(loadingBadge).toHaveCount(0); await expect(draftsBadge).toHaveText("1"); - expect(orderFilters.some((filters) => filters.includes("customer_id:6001") && !filters.includes("department_id:"))).toBe( - true - ); + expect( + orderFilters.some((filters) => filters.includes("customer_id:6001") && !filters.includes("department_id:")) + ).toBe(true); }); - test("shows a drafts menu item with a count and lists drafts across departments", async ({ - page, - }, testInfo) => { + test("shows a drafts menu item with a count and lists drafts across departments", async ({ page }, testInfo) => { test.skip(!isDesktopProject(testInfo), "Desktop only"); const orderFilters: string[] = []; @@ -235,9 +231,7 @@ test.describe("Superuser drafts", () => { await expect(page.locator("body")).toContainText("DN59443"); expect( - orderFilters.filter( - (filters) => filters.includes("customer_id:6001") && filters.includes("department_id:") - ) + orderFilters.filter((filters) => filters.includes("customer_id:6001") && filters.includes("department_id:")) ).toEqual([]); }); }); diff --git a/tests/e2e/support/mobilePos.js b/tests/e2e/support/mobilePos.js index d9f8c4b9..17107bb8 100644 --- a/tests/e2e/support/mobilePos.js +++ b/tests/e2e/support/mobilePos.js @@ -753,7 +753,10 @@ export function createMobilePosFixture(overrides = {}) { ordersById: mergeObjectMaps(base.ordersById, overrides.ordersById), orderItemsByOrderId: mergeObjectMaps(base.orderItemsByOrderId, overrides.orderItemsByOrderId), attachmentsByOrderId: mergeObjectMaps(base.attachmentsByOrderId, overrides.attachmentsByOrderId), - stripeModuleOrdersByOrderId: mergeObjectMaps(base.stripeModuleOrdersByOrderId, overrides.stripeModuleOrdersByOrderId), + stripeModuleOrdersByOrderId: mergeObjectMaps( + base.stripeModuleOrdersByOrderId, + overrides.stripeModuleOrdersByOrderId + ), paymentIntentsByOrderId: mergeObjectMaps(base.paymentIntentsByOrderId, overrides.paymentIntentsByOrderId), bookingsById: mergeObjectMaps(base.bookingsById, overrides.bookingsById), cvrSearchResponses: mergeObjectMaps(base.cvrSearchResponses, overrides.cvrSearchResponses), diff --git a/tests/e2e/support/network.js b/tests/e2e/support/network.js index 8428b2a0..d36794d0 100644 --- a/tests/e2e/support/network.js +++ b/tests/e2e/support/network.js @@ -730,12 +730,7 @@ function createFixtureOperation(type, request = {}, overrides = {}) { status: overrides.status || "PENDING", request, summary: overrides.summary || { - label: - overrides.status === "COMPLETED" - ? "Completed" - : overrides.status === "FAILED" - ? "Failed" - : "Queued", + label: overrides.status === "COMPLETED" ? "Completed" : overrides.status === "FAILED" ? "Failed" : "Queued", progress: overrides.status === "COMPLETED" ? 100 : overrides.status === "FAILED" ? 100 : 0, retryable: overrides.status !== "FAILED", }, @@ -810,7 +805,8 @@ function buildEdgeGatewayRuntimeFixture(gateway) { const cloudRelays = relayHealth.filter((relay) => relay.execution_path === "cloud"); const operations = Array.isArray(gateway.operations) ? gateway.operations : []; - const activeOperation = operations.find((operation) => ["PENDING", "IN_PROGRESS"].includes(String(operation.status))) || null; + const activeOperation = + operations.find((operation) => ["PENDING", "IN_PROGRESS"].includes(String(operation.status))) || null; const versionDrift = gateway.installed_version && gateway.target_version && gateway.installed_version !== gateway.target_version; const credentialFreshnessState = gateway.metadata?.credentials_rotated_at ? "FRESH" : "UNKNOWN"; @@ -867,23 +863,26 @@ function buildEdgeGatewayRuntimeFixture(gateway) { last_successful_discovery_at: gateway.last_successful_discovery_at || gateway.last_heartbeat_at, active_operation: gateway.active_operation || activeOperation, recent_operations_summary: gateway.recent_operations_summary || buildEdgeGatewayOperationSummary(operations), - version_drift: - gateway.version_drift || { - installed_version: gateway.installed_version || null, - target_version: gateway.target_version || null, - release_channel: gateway.release_channel || "stable", - is_drifted: Boolean(versionDrift), - status: versionDrift ? "UPDATE_AVAILABLE" : "IN_SYNC", - }, - credential_freshness: - gateway.credential_freshness || { - rotated_at: gateway.metadata?.credentials_rotated_at || null, - age_days: gateway.metadata?.credentials_rotated_at ? 1 : null, - state: credentialFreshnessState, - }, + version_drift: gateway.version_drift || { + installed_version: gateway.installed_version || null, + target_version: gateway.target_version || null, + release_channel: gateway.release_channel || "stable", + is_drifted: Boolean(versionDrift), + status: versionDrift ? "UPDATE_AVAILABLE" : "IN_SYNC", + }, + credential_freshness: gateway.credential_freshness || { + rotated_at: gateway.metadata?.credentials_rotated_at || null, + age_days: gateway.metadata?.credentials_rotated_at ? 1 : null, + state: credentialFreshnessState, + }, diagnostics: gateway.diagnostics || diagnostics, error_state: - gateway.error_state || (diagnostics[0] ? { ...diagnostics[0] } : activeOperation?.error_code ? { code: activeOperation.error_code, message: activeOperation.error_message } : null), + gateway.error_state || + (diagnostics[0] + ? { ...diagnostics[0] } + : activeOperation?.error_code + ? { code: activeOperation.error_code, message: activeOperation.error_message } + : null), }; } @@ -967,16 +966,32 @@ function createEdgeGatewayFixture(options = {}) { ], recent_commands: [], operations: [ - createFixtureOperation("DISCOVERY", {}, { - id: 8801, - status: "COMPLETED", - started_at: "2026-04-08 08:14:20", - completed_at: "2026-04-08 08:14:38", - events: [ - { id: 1, level: "INFO", code: "OPERATION_STARTED", message: "Gateway started processing the operation", created_at: "2026-04-08 08:14:20" }, - { id: 2, level: "INFO", code: "OPERATION_COMPLETED", message: "Operation completed successfully", created_at: "2026-04-08 08:14:38" }, - ], - }), + createFixtureOperation( + "DISCOVERY", + {}, + { + id: 8801, + status: "COMPLETED", + started_at: "2026-04-08 08:14:20", + completed_at: "2026-04-08 08:14:38", + events: [ + { + id: 1, + level: "INFO", + code: "OPERATION_STARTED", + message: "Gateway started processing the operation", + created_at: "2026-04-08 08:14:20", + }, + { + id: 2, + level: "INFO", + code: "OPERATION_COMPLETED", + message: "Operation completed successfully", + created_at: "2026-04-08 08:14:38", + }, + ], + } + ), ], audit_logs: [{ id: 501, created_at: "2026-04-08 08:16:00", action: "GATEWAY_CLAIMED", actor_type: "USER" }], }, @@ -1100,7 +1115,9 @@ function buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, includeDetail }, binding_summary: { total: bindings.length, - fallback_overrides: bindings.filter((binding) => String(binding.fallback_mode || "PREFER_LOCAL") !== "PREFER_LOCAL").length, + fallback_overrides: bindings.filter( + (binding) => String(binding.fallback_mode || "PREFER_LOCAL") !== "PREFER_LOCAL" + ).length, }, agent_runtime: { hostname: gateway.hostname, @@ -1112,7 +1129,9 @@ function buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, includeDetail }, operations, active_operation: cloneJson(gateway.active_operation || null), - recent_operations_summary: cloneJson(gateway.recent_operations_summary || buildEdgeGatewayOperationSummary(operations)), + recent_operations_summary: cloneJson( + gateway.recent_operations_summary || buildEdgeGatewayOperationSummary(operations) + ), version_drift: cloneJson(gateway.version_drift || null), credential_freshness: cloneJson(gateway.credential_freshness || null), diagnostics: cloneJson(gateway.diagnostics || []), @@ -1266,7 +1285,10 @@ function processPendingEdgeGatewayClaims(edgeGatewayFixture) { function createHttpEdgeGatewayFixture(options = {}) { const gatewayOverridesById = new Map( - (Array.isArray(options.gatewayOverrides) ? options.gatewayOverrides : []).map((gateway) => [Number(gateway.id), gateway]) + (Array.isArray(options.gatewayOverrides) ? options.gatewayOverrides : []).map((gateway) => [ + Number(gateway.id), + gateway, + ]) ); const extraGateways = Array.isArray(options.gateways) ? options.gateways : []; @@ -1297,25 +1319,69 @@ function createHttpEdgeGatewayFixture(options = {}) { }, }, inventory: [ - { id: 1, device_id: "shelly-plus-01", local_ip: "10.1.0.31", model: "Shelly Plus 2PM", channel_count: 2, online: true }, - { id: 2, device_id: "shelly-mini-offline", local_ip: "10.1.0.34", model: "Shelly Mini 1", channel_count: 1, online: false }, + { + id: 1, + device_id: "shelly-plus-01", + local_ip: "10.1.0.31", + model: "Shelly Plus 2PM", + channel_count: 2, + online: true, + }, + { + id: 2, + device_id: "shelly-mini-offline", + local_ip: "10.1.0.34", + model: "Shelly Mini 1", + channel_count: 1, + online: false, + }, ], bindings: [ - { id: 1, relay_id: "M-7", device_id: "shelly-plus-01", local_ip: "10.1.0.31", channel: 0, fallback_mode: "PREFER_LOCAL" }, - { id: 2, relay_id: "M-7-LEGACY", device_id: "shelly-missing-legacy", local_ip: "10.1.0.99", channel: 1, fallback_mode: "CLOUD_ONLY" }, + { + id: 1, + relay_id: "M-7", + device_id: "shelly-plus-01", + local_ip: "10.1.0.31", + channel: 0, + fallback_mode: "PREFER_LOCAL", + }, + { + id: 2, + relay_id: "M-7-LEGACY", + device_id: "shelly-missing-legacy", + local_ip: "10.1.0.99", + channel: 1, + fallback_mode: "CLOUD_ONLY", + }, ], recent_commands: [], operations: [ - createFixtureOperation("DISCOVERY", {}, { - id: 8801, - status: "COMPLETED", - started_at: "2026-04-08 08:14:20", - completed_at: "2026-04-08 08:14:38", - events: [ - { id: 1, level: "INFO", code: "OPERATION_STARTED", message: "Gateway started processing the operation", created_at: "2026-04-08 08:14:20" }, - { id: 2, level: "INFO", code: "OPERATION_COMPLETED", message: "Operation completed successfully", created_at: "2026-04-08 08:14:38" }, - ], - }), + createFixtureOperation( + "DISCOVERY", + {}, + { + id: 8801, + status: "COMPLETED", + started_at: "2026-04-08 08:14:20", + completed_at: "2026-04-08 08:14:38", + events: [ + { + id: 1, + level: "INFO", + code: "OPERATION_STARTED", + message: "Gateway started processing the operation", + created_at: "2026-04-08 08:14:20", + }, + { + id: 2, + level: "INFO", + code: "OPERATION_COMPLETED", + message: "Operation completed successfully", + created_at: "2026-04-08 08:14:38", + }, + ], + } + ), ], audit_logs: [{ id: 501, created_at: "2026-04-08 08:16:00", action: "GATEWAY_CLAIMED", actor_type: "USER" }], }, @@ -1346,7 +1412,9 @@ function createHttpEdgeGatewayFixture(options = {}) { bindings: [], recent_commands: [], operations: [], - audit_logs: [{ id: 502, created_at: "2026-04-07 21:05:00", action: "HEARTBEAT_TIMEOUT", actor_type: "SYSTEM" }], + audit_logs: [ + { id: 502, created_at: "2026-04-07 21:05:00", action: "HEARTBEAT_TIMEOUT", actor_type: "SYSTEM" }, + ], }, ] .map((gateway) => mergeEdgeGatewayFixtureRecord(gateway, gatewayOverridesById.get(Number(gateway.id)) || {})) @@ -2615,14 +2683,19 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos const orderId = Number(body.order_id || 0); const existingInvoice = posFixture.stripeModuleOrdersByOrderId[orderId] || {}; if (existingInvoice?.invoice_id && !isTerminalStripeInvoiceStatus(existingInvoice.status)) { - await route.fulfill(json({ - success: false, - data: { - message: "A Stripe payment link is already active for this order.", - code: "stripe_invoice_exists", - stripeModuleOrders: existingInvoice, - }, - }, 409)); + await route.fulfill( + json( + { + success: false, + data: { + message: "A Stripe payment link is already active for this order.", + code: "stripe_invoice_exists", + stripeModuleOrders: existingInvoice, + }, + }, + 409 + ) + ); return true; } @@ -2631,18 +2704,20 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos }); posFixture.stripeModuleOrdersByOrderId[orderId] = nextInvoice; - await route.fulfill(json({ - success: true, - data: { - id: nextInvoice.invoice_id, - customer: nextInvoice.customer_id, - hosted_invoice_url: nextInvoice.url, - paid: nextInvoice.paid, - status: nextInvoice.status, - amount_due: nextInvoice.amount_due, - amount_paid: nextInvoice.amount_paid, - }, - })); + await route.fulfill( + json({ + success: true, + data: { + id: nextInvoice.invoice_id, + customer: nextInvoice.customer_id, + hosted_invoice_url: nextInvoice.url, + paid: nextInvoice.paid, + status: nextInvoice.status, + amount_due: nextInvoice.amount_due, + amount_paid: nextInvoice.amount_paid, + }, + }) + ); return true; } @@ -2653,48 +2728,50 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos if (!existingInvoice?.invoice_id) { posFixture.stripeModuleOrdersByOrderId[orderId] = {}; - await route.fulfill(json({ - success: true, - data: { - stripeModuleOrders: [], - }, - })); + await route.fulfill( + json({ + success: true, + data: { + stripeModuleOrders: [], + }, + }) + ); return true; } if (Boolean(existingInvoice.paid) || String(existingInvoice.status || "") === "paid") { - await route.fulfill(json({ - success: false, - data: { - message: "A paid Stripe payment link cannot be cancelled.", - code: "stripe_invoice_paid", - stripeModuleOrders: existingInvoice, - }, - }, 409)); + await route.fulfill( + json( + { + success: false, + data: { + message: "A paid Stripe payment link cannot be cancelled.", + code: "stripe_invoice_paid", + stripeModuleOrders: existingInvoice, + }, + }, + 409 + ) + ); return true; } posFixture.stripeModuleOrdersByOrderId[orderId] = {}; - await route.fulfill(json({ - success: true, - data: { - stripeModuleOrders: [], - }, - })); + await route.fulfill( + json({ + success: true, + data: { + stripeModuleOrders: [], + }, + }) + ); return true; } return false; } -async function handleEdgeGatewayRoute({ - route, - request, - parsedUrl, - pathname, - method, - edgeGatewayFixture, -}) { +async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, method, edgeGatewayFixture }) { if (!edgeGatewayFixture) { return false; } @@ -2707,7 +2784,9 @@ async function handleEdgeGatewayRoute({ const findGateway = (gatewayId) => edgeGatewayFixture.gateways.find((entry) => Number(entry.id) === Number(gatewayId)) || null; const gatewayResponse = (gateway, includeDetail = true) => - gateway ? json({ data: buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, includeDetail) }) : json({ message: "Gateway not found" }, 404); + gateway + ? json({ data: buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, includeDetail) }) + : json({ message: "Gateway not found" }, 404); const createOperation = (gateway, type, request = {}, overrides = {}) => { const operation = createFixtureOperation(type, request, { id: edgeGatewayFixture.nextOperationId++, @@ -2788,8 +2867,20 @@ async function handleEdgeGatewayRoute({ updated_at: now, summary: { label: "Gateway is processing the operation", progress: 20, retryable: true }, events: [ - { id: edgeGatewayFixture.nextOperationEventId++, level: "INFO", code: "OPERATION_QUEUED", message: "Operation queued for gateway execution", created_at: now }, - { id: edgeGatewayFixture.nextOperationEventId++, level: "INFO", code: "OPERATION_STARTED", message: "Gateway started processing the operation", created_at: now }, + { + id: edgeGatewayFixture.nextOperationEventId++, + level: "INFO", + code: "OPERATION_QUEUED", + message: "Operation queued for gateway execution", + created_at: now, + }, + { + id: edgeGatewayFixture.nextOperationEventId++, + level: "INFO", + code: "OPERATION_STARTED", + message: "Gateway started processing the operation", + created_at: now, + }, ], }); edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId] = { @@ -2814,10 +2905,26 @@ async function handleEdgeGatewayRoute({ completed_at: now, updated_at: now, summary: { label: "Completed", progress: 100, retryable: true }, - result: { installed_version: gateway.installed_version, target_version: gateway.target_version, restart_required: true }, + result: { + installed_version: gateway.installed_version, + target_version: gateway.target_version, + restart_required: true, + }, events: [ - { id: edgeGatewayFixture.nextOperationEventId++, level: "INFO", code: "OPERATION_QUEUED", message: "Operation queued for gateway execution", created_at: now }, - { id: edgeGatewayFixture.nextOperationEventId++, level: "INFO", code: "OPERATION_COMPLETED", message: "Operation completed successfully", created_at: now }, + { + id: edgeGatewayFixture.nextOperationEventId++, + level: "INFO", + code: "OPERATION_QUEUED", + message: "Operation queued for gateway execution", + created_at: now, + }, + { + id: edgeGatewayFixture.nextOperationEventId++, + level: "INFO", + code: "OPERATION_COMPLETED", + message: "Operation completed successfully", + created_at: now, + }, ], }); } else if (operationType === "UNINSTALL") { @@ -2834,12 +2941,29 @@ async function handleEdgeGatewayRoute({ summary: { label: "Completed", progress: 100, retryable: false }, result: { uninstalled: true, manual_cleanup_required: true }, events: [ - { id: edgeGatewayFixture.nextOperationEventId++, level: "INFO", code: "OPERATION_QUEUED", message: "Operation queued for gateway execution", created_at: now }, - { id: edgeGatewayFixture.nextOperationEventId++, level: "INFO", code: "OPERATION_COMPLETED", message: "Operation completed successfully", created_at: now }, + { + id: edgeGatewayFixture.nextOperationEventId++, + level: "INFO", + code: "OPERATION_QUEUED", + message: "Operation queued for gateway execution", + created_at: now, + }, + { + id: edgeGatewayFixture.nextOperationEventId++, + level: "INFO", + code: "OPERATION_COMPLETED", + message: "Operation completed successfully", + created_at: now, + }, ], }); } else { - await route.fulfill(json({ data: { message: "Unsupported gateway operation type", error_code: "EDGE_GATEWAY_VALIDATION_FAILED" } }, 422)); + await route.fulfill( + json( + { data: { message: "Unsupported gateway operation type", error_code: "EDGE_GATEWAY_VALIDATION_FAILED" } }, + 422 + ) + ); return true; } @@ -2848,7 +2972,17 @@ async function handleEdgeGatewayRoute({ ...(gateway.audit_logs || []), ]; Object.assign(gateway, buildEdgeGatewayRuntimeFixture(gateway)); - await route.fulfill(json({ data: { operation: cloneJson(operation), gateway: buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, true) } }, 201)); + await route.fulfill( + json( + { + data: { + operation: cloneJson(operation), + gateway: buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, true), + }, + }, + 201 + ) + ); return true; } @@ -2905,16 +3039,33 @@ async function handleEdgeGatewayRoute({ if (gateway) { const now = toSqlDateTime(); - const operation = createOperation(gateway, "DISCOVERY", {}, { - status: "IN_PROGRESS", - started_at: now, - updated_at: now, - summary: { label: "Gateway is processing the operation", progress: 20, retryable: true }, - events: [ - { id: edgeGatewayFixture.nextOperationEventId++, level: "INFO", code: "OPERATION_QUEUED", message: "Operation queued for gateway execution", created_at: now }, - { id: edgeGatewayFixture.nextOperationEventId++, level: "INFO", code: "OPERATION_STARTED", message: "Gateway started processing the operation", created_at: now }, - ], - }); + const operation = createOperation( + gateway, + "DISCOVERY", + {}, + { + status: "IN_PROGRESS", + started_at: now, + updated_at: now, + summary: { label: "Gateway is processing the operation", progress: 20, retryable: true }, + events: [ + { + id: edgeGatewayFixture.nextOperationEventId++, + level: "INFO", + code: "OPERATION_QUEUED", + message: "Operation queued for gateway execution", + created_at: now, + }, + { + id: edgeGatewayFixture.nextOperationEventId++, + level: "INFO", + code: "OPERATION_STARTED", + message: "Gateway started processing the operation", + created_at: now, + }, + ], + } + ); gateway.discovery_status = "PENDING"; edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId] = { operationId: operation.id, @@ -3013,11 +3164,16 @@ async function handleEdgeGatewayRoute({ })); Object.assign(gateway, buildEdgeGatewayRuntimeFixture(gateway)); } - await route.fulfill(json({ data: gateway ? buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, true) : null })); + await route.fulfill( + json({ data: gateway ? buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, true) : null }) + ); return true; } - if (/\/edge-gateways\/\d+\/(?:update-jobs|uninstall)(?:\/.*)?$/.test(pathname) || /\/edge-gateways\/\d+\/shell-sessions(?:\/.*)?$/.test(pathname)) { + if ( + /\/edge-gateways\/\d+\/(?:update-jobs|uninstall)(?:\/.*)?$/.test(pathname) || + /\/edge-gateways\/\d+\/shell-sessions(?:\/.*)?$/.test(pathname) + ) { await route.fulfill(json({ message: "Route not found" }, 404)); return true; } diff --git a/tests/unit/edge-gateway-service.spec.js b/tests/unit/edge-gateway-service.spec.js index a6742415..03014412 100644 --- a/tests/unit/edge-gateway-service.spec.js +++ b/tests/unit/edge-gateway-service.spec.js @@ -8,10 +8,12 @@ describe("edge gateway service contract", () => { it("wraps the v2 backend edge gateway REST endpoints", () => { expect(source).toContain('"/departments"'); expect(source).toContain('"/edge-gateways"'); - expect(source).toContain("view = \"summary\""); + expect(source).toContain('view = "summary"'); expect(source).toContain('"/edge-gateways/install-token"'); expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/operations"); - expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/operations/${encodeURIComponent(operationId)}/events"); + expect(source).toContain( + "/edge-gateways/${encodeURIComponent(gatewayId)}/operations/${encodeURIComponent(operationId)}/events" + ); expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/rotate-credentials"); expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/discovery"); expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/bindings"); diff --git a/tests/unit/edge-gateway-workflow-helpers.spec.js b/tests/unit/edge-gateway-workflow-helpers.spec.js index a590e312..4b2b9753 100644 --- a/tests/unit/edge-gateway-workflow-helpers.spec.js +++ b/tests/unit/edge-gateway-workflow-helpers.spec.js @@ -5,15 +5,18 @@ import { describe, expect, it } from "vitest"; import { inferEdgeGatewayErrorCode, normalizeEdgeGatewayError } from "@/features/edgeGateways/edgeGatewayErrors.js"; const managerSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayManager.vue"), "utf8"); -const operationsSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayOperationsPage.vue"), "utf8"); +const operationsSource = readFileSync( + join(process.cwd(), "src/features/edgeGateways/EdgeGatewayOperationsPage.vue"), + "utf8" +); const manageSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayManagePage.vue"), "utf8"); describe("edge gateway workflow helpers", () => { it("exposes v2 tabs and management actions without shell controls", () => { - expect(managerSource).toContain("id: \"overview\""); - expect(managerSource).toContain("id: \"inventory\""); - expect(managerSource).toContain("id: \"operations\""); - expect(managerSource).toContain("id: \"manage\""); + expect(managerSource).toContain('id: "overview"'); + expect(managerSource).toContain('id: "inventory"'); + expect(managerSource).toContain('id: "operations"'); + expect(managerSource).toContain('id: "manage"'); expect(managerSource).toContain("`gateway-tab-${tab.id}`"); expect(operationsSource).toContain('data-testid="gateway-operation-update"'); expect(operationsSource).toContain('data-testid="gateway-operation-uninstall"'); diff --git a/tests/unit/edge-gateway-workspace.spec.js b/tests/unit/edge-gateway-workspace.spec.js index 6deb4972..92f6c7d3 100644 --- a/tests/unit/edge-gateway-workspace.spec.js +++ b/tests/unit/edge-gateway-workspace.spec.js @@ -3,9 +3,18 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; const managerSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayManager.vue"), "utf8"); -const overviewSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayOverviewPage.vue"), "utf8"); -const inventorySource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayInventoryPage.vue"), "utf8"); -const operationsSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayOperationsPage.vue"), "utf8"); +const overviewSource = readFileSync( + join(process.cwd(), "src/features/edgeGateways/EdgeGatewayOverviewPage.vue"), + "utf8" +); +const inventorySource = readFileSync( + join(process.cwd(), "src/features/edgeGateways/EdgeGatewayInventoryPage.vue"), + "utf8" +); +const operationsSource = readFileSync( + join(process.cwd(), "src/features/edgeGateways/EdgeGatewayOperationsPage.vue"), + "utf8" +); const manageSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayManagePage.vue"), "utf8"); const routerSource = readFileSync(join(process.cwd(), "src/router.js"), "utf8"); const edgeGatewaysPageSource = readFileSync( @@ -23,10 +32,10 @@ describe("edge gateway workspace contract", () => { expect(managerSource).toContain('data-testid="gateway-error-details"'); expect(managerSource).toContain('data-testid="gateway-fleet-usage"'); expect(managerSource).toContain("response?.data?.meta?.fleet_usage"); - expect(managerSource).toContain('import EdgeGatewayOverviewPage'); - expect(managerSource).toContain('import EdgeGatewayInventoryPage'); - expect(managerSource).toContain('import EdgeGatewayOperationsPage'); - expect(managerSource).toContain('import EdgeGatewayManagePage'); + expect(managerSource).toContain("import EdgeGatewayOverviewPage"); + expect(managerSource).toContain("import EdgeGatewayInventoryPage"); + expect(managerSource).toContain("import EdgeGatewayOperationsPage"); + expect(managerSource).toContain("import EdgeGatewayManagePage"); expect(overviewSource).toContain('data-testid="gateway-overview-page"'); expect(inventorySource).toContain('data-testid="gateway-inventory-page"'); expect(operationsSource).toContain('data-testid="gateway-operations-page"'); diff --git a/tests/unit/helpers/readJsonFile.js b/tests/unit/helpers/readJsonFile.js new file mode 100644 index 00000000..f248c315 --- /dev/null +++ b/tests/unit/helpers/readJsonFile.js @@ -0,0 +1,16 @@ +import { readFileSync } from "node:fs"; + +const UTF8_BOM = "\uFEFF"; + +export const stripUtf8Bom = (value) => { + if (typeof value !== "string") { + return value; + } + + return value.startsWith(UTF8_BOM) ? value.slice(1) : value; +}; + +export const readJsonFile = (path) => { + const raw = readFileSync(path, "utf8"); + return JSON.parse(stripUtf8Bom(raw)); +}; diff --git a/tests/unit/i18n-action-settings-wheel.spec.js b/tests/unit/i18n-action-settings-wheel.spec.js index 4fa88710..b3112387 100644 --- a/tests/unit/i18n-action-settings-wheel.spec.js +++ b/tests/unit/i18n-action-settings-wheel.spec.js @@ -1,6 +1,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { readJsonFile } from "./helpers/readJsonFile"; const root = process.cwd(); const componentPath = join(root, "src/components/displays/buttons/ActionSettingsWheelButton.vue"); @@ -8,7 +9,7 @@ const activeLocales = ["da", "en", "sv", "de", "no"]; const translationCallPattern = /(?:\$t|\bt)\(\s*(["'])([^"'\\]*(?:\\.[^"'\\]*)*)\1/g; const readLocale = (locale) => { - return JSON.parse(readFileSync(join(root, `src/i18n/locales/${locale}.json`), "utf8")); + return readJsonFile(join(root, `src/i18n/locales/${locale}.json`)); }; const extractLiteralTranslationKeys = (source) => { diff --git a/tests/unit/invoice-distribution-i18n.spec.js b/tests/unit/invoice-distribution-i18n.spec.js index f8d1501c..b44fd66c 100644 --- a/tests/unit/invoice-distribution-i18n.spec.js +++ b/tests/unit/invoice-distribution-i18n.spec.js @@ -1,15 +1,15 @@ -import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { readJsonFile } from "./helpers/readJsonFile"; const root = process.cwd(); const locales = { - da: JSON.parse(readFileSync(join(root, "src/i18n/locales/da.json"), "utf8")), - en: JSON.parse(readFileSync(join(root, "src/i18n/locales/en.json"), "utf8")), - sv: JSON.parse(readFileSync(join(root, "src/i18n/locales/sv.json"), "utf8")), - de: JSON.parse(readFileSync(join(root, "src/i18n/locales/de.json"), "utf8")), - no: JSON.parse(readFileSync(join(root, "src/i18n/locales/no.json"), "utf8")), + da: readJsonFile(join(root, "src/i18n/locales/da.json")), + en: readJsonFile(join(root, "src/i18n/locales/en.json")), + sv: readJsonFile(join(root, "src/i18n/locales/sv.json")), + de: readJsonFile(join(root, "src/i18n/locales/de.json")), + no: readJsonFile(join(root, "src/i18n/locales/no.json")), }; const flattenKeys = (value, prefix = "") => { diff --git a/tests/unit/orders-change-customer.spec.js b/tests/unit/orders-change-customer.spec.js index 1d87ec0d..1f9ebb06 100644 --- a/tests/unit/orders-change-customer.spec.js +++ b/tests/unit/orders-change-customer.spec.js @@ -9,7 +9,7 @@ const createAppMock = vi.hoisted(() => use: createAppUseMock, mount: createAppMountMock, unmount: createAppUnmountMock, - })), + })) ); vi.mock("sweetalert2", () => ({ @@ -89,7 +89,7 @@ describe("Orders.showChangeCustomerForm", () => { }, onAssigned: expect.any(Function), onClose: expect.any(Function), - }), + }) ); expect(Swal.fire).not.toHaveBeenCalled(); diff --git a/tests/unit/read-json-file.helper.spec.js b/tests/unit/read-json-file.helper.spec.js new file mode 100644 index 00000000..8fa31576 --- /dev/null +++ b/tests/unit/read-json-file.helper.spec.js @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { readJsonFile, stripUtf8Bom } from "./helpers/readJsonFile"; + +describe("readJsonFile helper", () => { + it("strips UTF-8 BOM prefixes before parsing JSON", () => { + const baseDir = mkdtempSync(join(tmpdir(), "read-json-file-")); + const fixturePath = join(baseDir, "locale.json"); + + try { + writeFileSync(fixturePath, '\uFEFF{"label":"Hej"}', "utf8"); + expect(readJsonFile(fixturePath)).toEqual({ label: "Hej" }); + expect(stripUtf8Bom("\uFEFFsample")).toBe("sample"); + } finally { + rmSync(baseDir, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/unit/stripe-terminal-readers.spec.js b/tests/unit/stripe-terminal-readers.spec.js index c27f2ebf..b5209211 100644 --- a/tests/unit/stripe-terminal-readers.spec.js +++ b/tests/unit/stripe-terminal-readers.spec.js @@ -1,65 +1,67 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from "vitest"; import { getStripeTerminalStatusKey, getStripeTerminalReaderById, normalizeStripeTerminalReaders, selectPreferredStripeTerminalReaderId, STRIPE_TERMINAL_STATUS, -} from '@/components/displays/department/pos/displays/stripeTerminalReaders.js'; +} from "@/components/displays/department/pos/displays/stripeTerminalReaders.js"; -describe('stripeTerminalReaders', () => { - it('maps raw Stripe reader status into ready, in-use, and offline states', () => { - expect(getStripeTerminalStatusKey({ status: 'online', action: null })).toBe(STRIPE_TERMINAL_STATUS.ready); - expect(getStripeTerminalStatusKey({ status: 'busy', action: null })).toBe(STRIPE_TERMINAL_STATUS.inUse); - expect( - getStripeTerminalStatusKey({ status: 'online', action: { type: 'process_payment_intent' } }), - ).toBe(STRIPE_TERMINAL_STATUS.inUse); - expect(getStripeTerminalStatusKey({ status: 'offline', action: null })).toBe(STRIPE_TERMINAL_STATUS.offline); - expect(getStripeTerminalStatusKey({ status: 'unknown_state', action: null })).toBe(STRIPE_TERMINAL_STATUS.offline); +describe("stripeTerminalReaders", () => { + it("maps raw Stripe reader status into ready, in-use, and offline states", () => { + expect(getStripeTerminalStatusKey({ status: "online", action: null })).toBe(STRIPE_TERMINAL_STATUS.ready); + expect(getStripeTerminalStatusKey({ status: "busy", action: null })).toBe(STRIPE_TERMINAL_STATUS.inUse); + expect(getStripeTerminalStatusKey({ status: "online", action: { type: "process_payment_intent" } })).toBe( + STRIPE_TERMINAL_STATUS.inUse + ); + expect(getStripeTerminalStatusKey({ status: "offline", action: null })).toBe(STRIPE_TERMINAL_STATUS.offline); + expect(getStripeTerminalStatusKey({ status: "unknown_state", action: null })).toBe(STRIPE_TERMINAL_STATUS.offline); }); - it('normalizes readers, keeps blocked readers visible, and sorts them by status priority', () => { + it("normalizes readers, keeps blocked readers visible, and sorts them by status priority", () => { const normalizedReaders = normalizeStripeTerminalReaders([ - { id: 'offline_terminal', label: 'Offline terminal', status: 'offline', action: null }, - { id: 'busy_terminal', label: 'Busy terminal', status: 'online', action: { type: 'payment_intent' } }, - { id: 'ready_terminal', label: 'Ready terminal', status: 'online', action: null }, + { id: "offline_terminal", label: "Offline terminal", status: "offline", action: null }, + { id: "busy_terminal", label: "Busy terminal", status: "online", action: { type: "payment_intent" } }, + { id: "ready_terminal", label: "Ready terminal", status: "online", action: null }, ]); expect(normalizedReaders.map((reader) => reader.id)).toEqual([ - 'ready_terminal', - 'busy_terminal', - 'offline_terminal', + "ready_terminal", + "busy_terminal", + "offline_terminal", ]); expect(normalizedReaders.map((reader) => reader.selectable)).toEqual([true, false, false]); - expect(normalizedReaders.map((reader) => reader.colorToken)).toEqual(['green', 'yellow', 'red']); + expect(normalizedReaders.map((reader) => reader.colorToken)).toEqual(["green", "yellow", "red"]); }); - it('keeps the current ready reader selected when it remains ready', () => { + it("keeps the current ready reader selected when it remains ready", () => { const normalizedReaders = normalizeStripeTerminalReaders([ - { id: 'ready_terminal_a', label: 'Ready terminal A', status: 'online', action: null }, - { id: 'ready_terminal_b', label: 'Ready terminal B', status: 'online', action: null }, + { id: "ready_terminal_a", label: "Ready terminal A", status: "online", action: null }, + { id: "ready_terminal_b", label: "Ready terminal B", status: "online", action: null }, ]); - expect(selectPreferredStripeTerminalReaderId(normalizedReaders, 'ready_terminal_b')).toBe('ready_terminal_b'); + expect(selectPreferredStripeTerminalReaderId(normalizedReaders, "ready_terminal_b")).toBe("ready_terminal_b"); }); - it('falls back to the first ready reader when the current reader is no longer selectable', () => { + it("falls back to the first ready reader when the current reader is no longer selectable", () => { const normalizedReaders = normalizeStripeTerminalReaders([ - { id: 'busy_terminal', label: 'Busy terminal', status: 'busy', action: null }, - { id: 'ready_terminal', label: 'Ready terminal', status: 'online', action: null }, - { id: 'offline_terminal', label: 'Offline terminal', status: 'offline', action: null }, + { id: "busy_terminal", label: "Busy terminal", status: "busy", action: null }, + { id: "ready_terminal", label: "Ready terminal", status: "online", action: null }, + { id: "offline_terminal", label: "Offline terminal", status: "offline", action: null }, ]); - expect(selectPreferredStripeTerminalReaderId(normalizedReaders, 'busy_terminal')).toBe('ready_terminal'); - expect(getStripeTerminalReaderById(normalizedReaders, 'offline_terminal')?.statusKey).toBe(STRIPE_TERMINAL_STATUS.offline); + expect(selectPreferredStripeTerminalReaderId(normalizedReaders, "busy_terminal")).toBe("ready_terminal"); + expect(getStripeTerminalReaderById(normalizedReaders, "offline_terminal")?.statusKey).toBe( + STRIPE_TERMINAL_STATUS.offline + ); }); - it('clears the selection when no ready readers remain', () => { + it("clears the selection when no ready readers remain", () => { const normalizedReaders = normalizeStripeTerminalReaders([ - { id: 'busy_terminal', label: 'Busy terminal', status: 'online', action: { type: 'payment_intent' } }, - { id: 'offline_terminal', label: 'Offline terminal', status: 'offline', action: null }, + { id: "busy_terminal", label: "Busy terminal", status: "online", action: { type: "payment_intent" } }, + { id: "offline_terminal", label: "Offline terminal", status: "offline", action: null }, ]); - expect(selectPreferredStripeTerminalReaderId(normalizedReaders, 'busy_terminal')).toBe(''); + expect(selectPreferredStripeTerminalReaderId(normalizedReaders, "busy_terminal")).toBe(""); }); }); diff --git a/tests/unit/system-search-modal-contract.spec.js b/tests/unit/system-search-modal-contract.spec.js index e0df7de4..7c1cf1de 100644 --- a/tests/unit/system-search-modal-contract.spec.js +++ b/tests/unit/system-search-modal-contract.spec.js @@ -1,6 +1,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { readJsonFile } from "./helpers/readJsonFile"; const root = process.cwd(); const modalSource = readFileSync( @@ -15,9 +16,9 @@ const departmentGoalSupportSource = readFileSync( join(root, "src/components/viewport/page/headers/menu/systemSearchDepartmentGoalSupport.ts"), "utf8" ); -const enLocale = JSON.parse(readFileSync(join(root, "src/i18n/locales/en.json"), "utf8")); -const daLocale = JSON.parse(readFileSync(join(root, "src/i18n/locales/da.json"), "utf8")); -const deLocale = JSON.parse(readFileSync(join(root, "src/i18n/locales/de.json"), "utf8")); +const enLocale = readJsonFile(join(root, "src/i18n/locales/en.json")); +const daLocale = readJsonFile(join(root, "src/i18n/locales/da.json")); +const deLocale = readJsonFile(join(root, "src/i18n/locales/de.json")); const entityTypesMatch = supportSource.match(/export const SYSTEM_SEARCH_ENTITY_TYPES = \[(.*?)\] as const;/s); const systemSearchEntityTypes = Array.from(entityTypesMatch?.[1]?.matchAll(/'([^']+)'/g) ?? [], (match) => match[1]); diff --git a/tests/unit/workfeed-i18n.spec.js b/tests/unit/workfeed-i18n.spec.js index 4855d825..8cca1ea8 100644 --- a/tests/unit/workfeed-i18n.spec.js +++ b/tests/unit/workfeed-i18n.spec.js @@ -1,15 +1,15 @@ -import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { readJsonFile } from "./helpers/readJsonFile"; const root = process.cwd(); const locales = { - da: JSON.parse(readFileSync(join(root, "src/i18n/locales/da.json"), "utf8")), - en: JSON.parse(readFileSync(join(root, "src/i18n/locales/en.json"), "utf8")), - sv: JSON.parse(readFileSync(join(root, "src/i18n/locales/sv.json"), "utf8")), - de: JSON.parse(readFileSync(join(root, "src/i18n/locales/de.json"), "utf8")), - no: JSON.parse(readFileSync(join(root, "src/i18n/locales/no.json"), "utf8")), + da: readJsonFile(join(root, "src/i18n/locales/da.json")), + en: readJsonFile(join(root, "src/i18n/locales/en.json")), + sv: readJsonFile(join(root, "src/i18n/locales/sv.json")), + de: readJsonFile(join(root, "src/i18n/locales/de.json")), + no: readJsonFile(join(root, "src/i18n/locales/no.json")), }; const flattenKeys = (value, prefix = "") => {