Fix locale test JSON parsing with UTF-8 BOM support

This commit is contained in:
Jeppe B
2026-04-21 17:36:37 +02:00
parent 9f9d6de03f
commit 7bdfb43635
22 changed files with 508 additions and 332 deletions
+44 -73
View File
@@ -57,12 +57,15 @@ const buildOverviewPayload = () => ({
},
});
async function mockAdminDepartmentDependencies(page, options: {
departmentDelayMs?: number;
departments?: Array<Record<string, unknown>>;
permissions?: string[];
sessionData?: Record<string, unknown>;
} = {}) {
async function mockAdminDepartmentDependencies(
page,
options: {
departmentDelayMs?: number;
departments?: Array<Record<string, unknown>>;
permissions?: string[];
sessionData?: Record<string, unknown>;
} = {}
) {
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) => {
+24 -25
View File
@@ -529,24 +529,27 @@ function createTallActionMenuPosFixture() {
};
return orders;
}, {}),
attachmentsByOrderId: tallOrderIds.reduce<Record<number, Array<Record<string, unknown>>>>((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<Record<number, Array<Record<string, unknown>>>>(
(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, {
@@ -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);
});
});
+3 -1
View File
@@ -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,
});
+3 -3
View File
@@ -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 }) => {
+10 -9
View File
@@ -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 ({
+15 -12
View File
@@ -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);
});
+3 -8
View File
@@ -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[] = [];
+6 -12
View File
@@ -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([]);
});
});
+4 -1
View File
@@ -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),
+276 -120
View File
@@ -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;
}
+4 -2
View File
@@ -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");
@@ -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"');
+16 -7
View File
@@ -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"');
+16
View File
@@ -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));
};
@@ -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) => {
+6 -6
View File
@@ -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 = "") => {
+2 -2
View File
@@ -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();
+20
View File
@@ -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 });
}
});
});
+35 -33
View File
@@ -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("");
});
});
@@ -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]);
+6 -6
View File
@@ -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 = "") => {