Merge pull request #47 from copenhagentruckwash/run-tests-and-fix-issues
Fix BOM-safe locale parsing in i18n tests
This commit is contained in:
@@ -16,7 +16,8 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
format-tests:
|
||||
runs-on: ubuntu-latest
|
||||
# Match the labels exposed by the Coolify-managed GitHub runner.
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
@@ -38,7 +39,7 @@ jobs:
|
||||
|
||||
build-and-unit:
|
||||
needs: format-tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
@@ -63,7 +64,7 @@ jobs:
|
||||
e2e-smoke:
|
||||
if: github.event_name != 'schedule'
|
||||
needs: build-and-unit
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -102,7 +103,7 @@ jobs:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch
|
||||
needs: build-and-unit
|
||||
name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -2,7 +2,8 @@ import path from "node:path";
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const devPort = Number(process.env.PLAYWRIGHT_DEV_PORT || 5173);
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://localhost:${devPort}`;
|
||||
const devHost = process.env.PLAYWRIGHT_DEV_HOST || "127.0.0.1";
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://${devHost}:${devPort}`;
|
||||
const isCI = !!process.env.CI;
|
||||
const artifactNamespace = (process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || "").trim();
|
||||
const artifactRoot = artifactNamespace
|
||||
|
||||
+127
-22
@@ -1,17 +1,20 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const devPort = Number(process.env.PLAYWRIGHT_DEV_PORT || 5173);
|
||||
const baseURL = `http://localhost:${devPort}`;
|
||||
const devHost = process.env.PLAYWRIGHT_DEV_HOST || "127.0.0.1";
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://${devHost}:${devPort}`;
|
||||
const runtimeNamespace = String(process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || `port-${devPort}`)
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
||||
const pidFile = path.resolve(process.cwd(), "output/playwright", `dev-server-${runtimeNamespace}.json`);
|
||||
const posViewsDirectory = path.resolve(process.cwd(), "src/views/dashboards/departmentDashboard/modules/Pos");
|
||||
const shopComponentsDirectory = path.resolve(process.cwd(), "src/components/shop");
|
||||
const serverOutputLimit = 80;
|
||||
|
||||
async function getListeningProcessOnWindows(port) {
|
||||
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], {
|
||||
@@ -52,8 +55,46 @@ async function getListeningProcessOnUnix(port) {
|
||||
CommandLine: commandMatch ? commandMatch[1] : "",
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
// Fall through to other tools that are commonly available on Linux runners.
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await execFileAsync("ss", ["-ltnp", `sport = :${port}`], {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
const line = stdout
|
||||
.split(/\r?\n/)
|
||||
.find((entry) => entry.includes("LISTEN") && entry.includes(`:${port}`) && entry.includes("pid="));
|
||||
const pidMatch = line?.match(/pid=(\d+)/);
|
||||
if (pidMatch) {
|
||||
return {
|
||||
Id: Number(pidMatch[1]),
|
||||
CommandLine: line.match(/users:\(\("([^"]+)"/)?.[1] || "",
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to netstat.
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await execFileAsync("netstat", ["-ltnp"], {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
const line = stdout
|
||||
.split(/\r?\n/)
|
||||
.find((entry) => entry.includes("LISTEN") && entry.match(new RegExp(`:${port}\\s`)));
|
||||
const pidMatch = line?.match(/\s(\d+)\/([^\s]+)\s*$/);
|
||||
if (pidMatch) {
|
||||
return {
|
||||
Id: Number(pidMatch[1]),
|
||||
CommandLine: pidMatch[2],
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// No more fallbacks available.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getListeningProcess(port) {
|
||||
@@ -85,23 +126,82 @@ async function killProcessTree(pid) {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForServerReady(url, timeoutMs = 120_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(url, { redirect: "manual" });
|
||||
if (response.status < 500) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// keep polling until ready
|
||||
function captureProcessOutput(stream, lines) {
|
||||
const reader = readline.createInterface({ input: stream });
|
||||
reader.on("line", (line) => {
|
||||
lines.push(line);
|
||||
if (lines.length > serverOutputLimit) {
|
||||
lines.splice(0, lines.length - serverOutputLimit);
|
||||
}
|
||||
});
|
||||
return reader;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
function formatProcessOutputSection(label, lines) {
|
||||
if (lines.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for the local Playwright dev server at ${url}.`);
|
||||
return `\n${label}:\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
function formatServerExitError(url, exitInfo, stdoutLines, stderrLines) {
|
||||
const exitLabel = exitInfo.signal ? `signal ${exitInfo.signal}` : `code ${exitInfo.code ?? 0}`;
|
||||
return new Error(
|
||||
`The local Playwright dev server exited before ${url} became ready (${exitLabel}).${formatProcessOutputSection(
|
||||
"stdout",
|
||||
stdoutLines
|
||||
)}${formatProcessOutputSection("stderr", stderrLines)}`
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForServerReady(url, serverProcess, stdoutLines, stderrLines, timeoutMs = 120_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let exitInfo = null;
|
||||
const handleExit = (code, signal) => {
|
||||
exitInfo = {
|
||||
code,
|
||||
signal,
|
||||
};
|
||||
};
|
||||
|
||||
serverProcess.on("exit", handleExit);
|
||||
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
if (exitInfo || serverProcess.exitCode !== null || serverProcess.signalCode !== null) {
|
||||
throw formatServerExitError(
|
||||
url,
|
||||
exitInfo || {
|
||||
code: serverProcess.exitCode,
|
||||
signal: serverProcess.signalCode,
|
||||
},
|
||||
stdoutLines,
|
||||
stderrLines
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { redirect: "manual" });
|
||||
if (response.status < 500) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// keep polling until ready
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Timed out waiting for the local Playwright dev server at ${url}.${formatProcessOutputSection(
|
||||
"stdout",
|
||||
stdoutLines
|
||||
)}${formatProcessOutputSection("stderr", stderrLines)}`
|
||||
);
|
||||
} finally {
|
||||
serverProcess.off("exit", handleExit);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildWarmupTargets() {
|
||||
@@ -180,35 +280,40 @@ export default async function globalSetup() {
|
||||
|
||||
const serverProcess =
|
||||
process.platform === "win32"
|
||||
? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host localhost --port ${devPort} --strictPort`], {
|
||||
? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host ${devHost} --port ${devPort} --strictPort`], {
|
||||
cwd: process.cwd(),
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
PLAYWRIGHT: "1",
|
||||
},
|
||||
stdio: "ignore",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
})
|
||||
: spawn("npm", ["run", "dev", "--", "--host", "localhost", "--port", String(devPort), "--strictPort"], {
|
||||
: spawn("npm", ["run", "dev", "--", "--host", devHost, "--port", String(devPort), "--strictPort"], {
|
||||
cwd: process.cwd(),
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
PLAYWRIGHT: "1",
|
||||
},
|
||||
stdio: "ignore",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
serverProcess.unref();
|
||||
const stdoutLines = [];
|
||||
const stderrLines = [];
|
||||
const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines);
|
||||
const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines);
|
||||
|
||||
await fs.writeFile(pidFile, JSON.stringify({ pid: serverProcess.pid, port: devPort }), "utf8");
|
||||
|
||||
try {
|
||||
await waitForServerReady(baseURL);
|
||||
await waitForServerReady(baseURL, serverProcess, stdoutLines, stderrLines);
|
||||
await warmUpDevServer(baseURL);
|
||||
} catch (error) {
|
||||
await killProcessTree(serverProcess.pid);
|
||||
throw error;
|
||||
} finally {
|
||||
stdoutReader.close();
|
||||
stderrReader.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const baseURL = "http://localhost:4173";
|
||||
const baseURL = "http://127.0.0.1:4173";
|
||||
const isCI = !!process.env.CI;
|
||||
|
||||
process.env.PLAYWRIGHT_BASE_URL = baseURL;
|
||||
@@ -13,10 +13,7 @@ export default defineConfig({
|
||||
forbidOnly: isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
workers: isCI ? 2 : 3,
|
||||
reporter: [
|
||||
["list"],
|
||||
["html", { open: "never", outputFolder: "output/playwright/prod/report" }],
|
||||
],
|
||||
reporter: [["list"], ["html", { open: "never", outputFolder: "output/playwright/prod/report" }]],
|
||||
outputDir: "output/playwright/prod/test-results",
|
||||
use: {
|
||||
baseURL,
|
||||
|
||||
+11
-10
@@ -2,19 +2,20 @@ export const IS_DEV = import.meta.env.VITE_IS_DEV; // Set to false in production
|
||||
|
||||
// Development mode
|
||||
export const POS_STEP_1_VERSION = 2; //(IS_DEV ? 2 : 1);
|
||||
export const API_URL = (IS_DEV ? 'https://api.truckwash.io:4433' : 'https://api.truckwash.io');
|
||||
export const API_URL = IS_DEV ? "https://api.truckwash.io:4433" : "https://api.truckwash.io";
|
||||
|
||||
// Allowed origins
|
||||
export const ALLOWED_ORIGINS = [
|
||||
'https://truckwash.io',
|
||||
'https://www.truckwash.io',
|
||||
'http://localhost:5173',
|
||||
'http://localhost:4173',
|
||||
'http://localhost:4174',
|
||||
'http://127.0.0.1:4173',
|
||||
'http://127.0.0.1:4174',
|
||||
"https://truckwash.io",
|
||||
"https://www.truckwash.io",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:4173",
|
||||
"http://localhost:4174",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://127.0.0.1:4173",
|
||||
"http://127.0.0.1:4174",
|
||||
];
|
||||
export const MIGRATION_ORIGIN = 'https://truckwash.io';
|
||||
export const MIGRATION_ORIGIN = "https://truckwash.io";
|
||||
|
||||
// Global request queue / retry / progress configuration
|
||||
export const REQUEST_QUEUE_CONFIG = Object.freeze({
|
||||
@@ -43,7 +44,7 @@ export const REQUEST_QUEUE_CONFIG = Object.freeze({
|
||||
jitterMs: 50,
|
||||
}),
|
||||
ping: Object.freeze({
|
||||
endpoint: '/ping',
|
||||
endpoint: "/ping",
|
||||
intervalMs: 30000,
|
||||
timeoutMs: 5000,
|
||||
}),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"');
|
||||
|
||||
@@ -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"');
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = "") => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 = "") => {
|
||||
|
||||
Reference in New Issue
Block a user