- Replaced repetitive department module URL logic with `buildDepartmentModulePath` utility in `DepartmentModulesDisplay.vue`. - Enhanced layouts and responsive behavior across components, including `PosDepartmentStep1MobileTransactionHistory.vue` and `DepartmentDailyReport.vue`. - Removed unused `SuperuserInvoicingLocalStore.vue` and refactored to `SuperuserInvoicingLocalStore.ts`. - Updated media query handling and card layout adjustments in `DepartmentDailyReport.vue`. - Added new Playwright configurations (`playwright.prod.config.ts` and `playwright.live.config.ts`) for e2e release workflows. - Extended e2e and unit test coverage for mobile POS and department modules.
2613 lines
82 KiB
JavaScript
2613 lines
82 KiB
JavaScript
const API_HOST = /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
|
|
const TINY_PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAukB9pY9ZxQAAAAASUVORK5CYII=",
|
|
"base64"
|
|
);
|
|
|
|
function json(body, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body)
|
|
};
|
|
}
|
|
|
|
function binary(body, contentType = "image/png", status = 200) {
|
|
return {
|
|
status,
|
|
contentType,
|
|
body,
|
|
};
|
|
}
|
|
|
|
function mergeFixture(base, overrides = {}) {
|
|
return {
|
|
...base,
|
|
...overrides,
|
|
previewByKey: {
|
|
...(base.previewByKey || {}),
|
|
...(overrides.previewByKey || {}),
|
|
},
|
|
summaryBySessionId: {
|
|
...(base.summaryBySessionId || {}),
|
|
...(overrides.summaryBySessionId || {}),
|
|
},
|
|
summaryByKey: {
|
|
...(base.summaryByKey || {}),
|
|
...(overrides.summaryByKey || {}),
|
|
},
|
|
answerResponseByKey: {
|
|
...(base.answerResponseByKey || {}),
|
|
...(overrides.answerResponseByKey || {}),
|
|
},
|
|
attachmentsByTaskId: {
|
|
...(base.attachmentsByTaskId || {}),
|
|
...(overrides.attachmentsByTaskId || {}),
|
|
},
|
|
attachmentDownloadByKey: {
|
|
...(base.attachmentDownloadByKey || {}),
|
|
...(overrides.attachmentDownloadByKey || {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function toSqlDateTime(value = new Date()) {
|
|
return new Date(value).toISOString().slice(0, 19).replace("T", " ");
|
|
}
|
|
|
|
function normalizeIncludeInInvoiceValue(value) {
|
|
if (value === null || value === undefined || value === "" || value === "use_department" || value === "null") {
|
|
return null;
|
|
}
|
|
|
|
if (value === true || value === 1 || value === "1" || value === "true" || value === "include") {
|
|
return true;
|
|
}
|
|
|
|
if (value === false || value === 0 || value === "0" || value === "false" || value === "exclude") {
|
|
return false;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveDepartmentIncludeInInvoice(posFixture, departmentId) {
|
|
const department = (posFixture.departments || []).find((entry) => Number(entry.id) === Number(departmentId));
|
|
if (!department) {
|
|
return true;
|
|
}
|
|
|
|
if (typeof department.exclude_from_invoicing === "boolean") {
|
|
return !department.exclude_from_invoicing;
|
|
}
|
|
|
|
if (typeof department.include_in_invoice === "boolean") {
|
|
return department.include_in_invoice;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function withEffectiveOrderState(posFixture, order) {
|
|
if (!order) {
|
|
return order;
|
|
}
|
|
|
|
const includeInInvoice = normalizeIncludeInInvoiceValue(order.include_in_invoice);
|
|
return {
|
|
...order,
|
|
include_in_invoice: includeInInvoice,
|
|
include_in_invoice_effective:
|
|
includeInInvoice === null
|
|
? resolveDepartmentIncludeInInvoice(posFixture, order.department_id)
|
|
: includeInInvoice,
|
|
};
|
|
}
|
|
|
|
function parseFilterExpressions(filters) {
|
|
return String(filters || "")
|
|
.split(",")
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
.reduce((result, entry) => {
|
|
const separatorIndex = entry.indexOf(":");
|
|
if (separatorIndex === -1) {
|
|
return result;
|
|
}
|
|
|
|
const key = entry.slice(0, separatorIndex);
|
|
const value = entry.slice(separatorIndex + 1);
|
|
if (key) {
|
|
result[key] = value;
|
|
}
|
|
return result;
|
|
}, {});
|
|
}
|
|
|
|
function filterPosOrders(posFixture, filters) {
|
|
const filterMap = parseFilterExpressions(filters);
|
|
return Object.values(posFixture.ordersById || {}).filter((order) => {
|
|
if (filterMap.id && Number(order.id) !== Number(filterMap.id)) {
|
|
return false;
|
|
}
|
|
|
|
if (filterMap.customer_id && Number(order.customer_id) !== Number(filterMap.customer_id)) {
|
|
return false;
|
|
}
|
|
|
|
if (filterMap.department_id && Number(order.department_id) !== Number(filterMap.department_id)) {
|
|
return false;
|
|
}
|
|
|
|
if (filterMap.reg_1 && String(order.reg_1 || "").toUpperCase() !== String(filterMap.reg_1).toUpperCase()) {
|
|
return false;
|
|
}
|
|
|
|
const createdAtDate = String(order.created_at || "").slice(0, 10);
|
|
if (filterMap["created_at-date_from"] && (!createdAtDate || createdAtDate < filterMap["created_at-date_from"])) {
|
|
return false;
|
|
}
|
|
|
|
if (filterMap["created_at-date_to"] && (!createdAtDate || createdAtDate > filterMap["created_at-date_to"])) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function filterNumberPlateScans(posFixture, { filters = "", search = "", order = "created_at:desc" } = {}) {
|
|
const filterMap = parseFilterExpressions(filters);
|
|
const searchValue = String(search || "").trim().toUpperCase();
|
|
const [orderBy = "created_at", orderDirection = "desc"] = String(order || "created_at:desc").split(":");
|
|
|
|
const filteredRows = (posFixture.numberPlateScans || []).filter((scan) => {
|
|
if (filterMap.department_id && Number(scan.department_id) !== Number(filterMap.department_id)) {
|
|
return false;
|
|
}
|
|
|
|
if (searchValue && !String(scan.plate || "").toUpperCase().includes(searchValue)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
return filteredRows.sort((left, right) => {
|
|
const leftValue = left?.[orderBy];
|
|
const rightValue = right?.[orderBy];
|
|
const leftComparable = typeof leftValue === "string" ? leftValue.toUpperCase() : leftValue;
|
|
const rightComparable = typeof rightValue === "string" ? rightValue.toUpperCase() : rightValue;
|
|
|
|
if (leftComparable === rightComparable) {
|
|
return 0;
|
|
}
|
|
|
|
if (leftComparable > rightComparable) {
|
|
return orderDirection === "desc" ? -1 : 1;
|
|
}
|
|
|
|
return orderDirection === "desc" ? 1 : -1;
|
|
});
|
|
}
|
|
|
|
function paginateRows(rows, page = 1, limit = 10) {
|
|
const currentPage = Math.max(1, Number.parseInt(page, 10) || 1);
|
|
const perPage = Math.max(1, Number.parseInt(limit, 10) || 10);
|
|
const startIndex = (currentPage - 1) * perPage;
|
|
const pagedRows = rows.slice(startIndex, startIndex + perPage);
|
|
|
|
return {
|
|
rows: pagedRows,
|
|
meta: {
|
|
pagination: {
|
|
current_page: currentPage,
|
|
per_page: perPage,
|
|
total: rows.length,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function createSelfServeFixture(overrides = {}) {
|
|
const departments = [
|
|
{
|
|
id: 1,
|
|
name: "Copenhagen",
|
|
address: "Alpha 1",
|
|
latitude: 55.6761,
|
|
longitude: 12.5683,
|
|
self_serve_enabled: true,
|
|
lanes: [
|
|
{
|
|
id: 7,
|
|
department: 1,
|
|
name: "7",
|
|
status: "AVAILABLE",
|
|
machine_available: true,
|
|
products: [2, 3],
|
|
relay_in_id: "IN-7",
|
|
relay_out_id: "OUT-7",
|
|
relay_machine_id: "M-7",
|
|
relay_machine_program_picker_id: "MPP-7",
|
|
relay_machine_cleaner_id: "MC-7",
|
|
dynamic_image_id: 77,
|
|
machine_type_id: null,
|
|
},
|
|
{
|
|
id: 8,
|
|
department: 1,
|
|
name: "8",
|
|
status: "FAULT",
|
|
machine_available: false,
|
|
products: [2, 3],
|
|
relay_in_id: "IN-8",
|
|
relay_out_id: "OUT-8",
|
|
relay_machine_id: "M-8",
|
|
relay_machine_program_picker_id: "MPP-8",
|
|
relay_machine_cleaner_id: "MC-8",
|
|
dynamic_image_id: 78,
|
|
machine_type_id: null,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "Odense",
|
|
address: "Beta 2",
|
|
latitude: 55.4038,
|
|
longitude: 10.4024,
|
|
self_serve_enabled: true,
|
|
lanes: [
|
|
{
|
|
id: 9,
|
|
department: 2,
|
|
name: "9",
|
|
status: "AVAILABLE",
|
|
machine_available: true,
|
|
products: [2, 4],
|
|
relay_in_id: "IN-9",
|
|
relay_out_id: "OUT-9",
|
|
relay_machine_id: "M-9",
|
|
relay_machine_program_picker_id: "MPP-9",
|
|
relay_machine_cleaner_id: "MC-9",
|
|
dynamic_image_id: 79,
|
|
machine_type_id: null,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 3,
|
|
name: "Aarhus",
|
|
address: "Gamma 3",
|
|
latitude: 56.1629,
|
|
longitude: 10.2039,
|
|
self_serve_enabled: true,
|
|
lanes: [
|
|
{
|
|
id: 10,
|
|
department: 3,
|
|
name: "10",
|
|
status: "AVAILABLE",
|
|
machine_available: false,
|
|
products: [3, 4],
|
|
relay_in_id: "IN-10",
|
|
relay_out_id: "OUT-10",
|
|
relay_machine_id: "M-10",
|
|
relay_machine_program_picker_id: "MPP-10",
|
|
relay_machine_cleaner_id: "MC-10",
|
|
dynamic_image_id: 80,
|
|
machine_type_id: null,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const products = [
|
|
{ id: 2, name: "Truck", price: 100, description: "Large truck", piktogram: "truck", is_wash: true, subscription_allowed: true, category: 4 },
|
|
{ id: 3, name: "Van", price: 80, description: "Medium van", piktogram: "van", is_wash: true, subscription_allowed: true, category: 4 },
|
|
{ id: 4, name: "Car", price: 60, description: "Small car", piktogram: "car", is_wash: true, subscription_allowed: true, category: 4 },
|
|
];
|
|
|
|
const baseFixture = {
|
|
departments,
|
|
departmentLanes: departments.flatMap((department) => department.lanes),
|
|
products,
|
|
customerVehicles: [
|
|
{ id: 1, reg: "AB12345", type: 2 },
|
|
{ id: 2, reg: "CD67890", type: 3 },
|
|
],
|
|
previewByKey: {
|
|
"7:AB12345": {
|
|
allowed: true,
|
|
machine_available: true,
|
|
lane: { id: 7, name: "7" },
|
|
session: { id: 501, status: "IN_PROGRESS", allowed: true },
|
|
questions: [
|
|
{ id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: null, order_priority: 1 },
|
|
],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [
|
|
{ id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1] },
|
|
],
|
|
allowed_services: ["MACHINE"],
|
|
},
|
|
"7:ZZ00000": {
|
|
allowed: true,
|
|
machine_available: true,
|
|
lane: { id: 7, name: "7" },
|
|
session: { id: 601, status: "IN_PROGRESS", allowed: true },
|
|
questions: [
|
|
{ id: 21, question: "Ready for direct wash?", answer: null, order_priority: 1 },
|
|
],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
allowed_services: ["MACHINE"],
|
|
},
|
|
},
|
|
summaryBySessionId: {
|
|
501: {
|
|
session: { id: 501, status: "IN_PROGRESS", allowed: true },
|
|
lane: { id: 7, name: "7" },
|
|
questions: [
|
|
{ id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: null, order_priority: 1 },
|
|
],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [
|
|
{ id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1] },
|
|
],
|
|
events: [
|
|
{ id: 1, type: "STARTED", created_at: "2026-01-01T10:00:00.000Z" },
|
|
],
|
|
},
|
|
601: {
|
|
session: { id: 601, status: "IN_PROGRESS", allowed: true },
|
|
lane: { id: 7, name: "7" },
|
|
questions: [
|
|
{ id: 21, question: "Ready for direct wash?", answer: null, order_priority: 1 },
|
|
],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
events: [
|
|
{ id: 2, type: "STARTED", created_at: "2026-01-01T11:00:00.000Z" },
|
|
],
|
|
},
|
|
},
|
|
summaryByKey: {},
|
|
answerResponseByKey: {
|
|
"7:AB12345:11:true": {
|
|
session: { id: 501, status: "IN_PROGRESS", allowed: true },
|
|
lane: { id: 7, name: "7" },
|
|
questions: [
|
|
{ id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: true, order_priority: 1 },
|
|
],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [
|
|
{ id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1] },
|
|
],
|
|
events: [
|
|
{ id: 3, type: "QUESTION_ANSWERED", created_at: "2026-01-01T10:01:00.000Z" },
|
|
],
|
|
},
|
|
"7:ZZ00000:21:true": {
|
|
session: { id: 601, status: "IN_PROGRESS", allowed: true },
|
|
lane: { id: 7, name: "7" },
|
|
questions: [
|
|
{ id: 21, question: "Ready for direct wash?", answer: true, order_priority: 1 },
|
|
],
|
|
conditions: [],
|
|
rules: [],
|
|
tasks: [],
|
|
events: [
|
|
{ id: 4, type: "QUESTION_ANSWERED", created_at: "2026-01-01T11:01:00.000Z" },
|
|
],
|
|
},
|
|
},
|
|
attachmentsByTaskId: {
|
|
9001: [
|
|
{ id: 301, content: { other: "prep.pdf" } },
|
|
{ id: 302, content: { other: "prep.jpg" } },
|
|
],
|
|
},
|
|
attachmentDownloadByKey: {
|
|
"9001:301": { download_link: "https://cdn.example.test/prep.pdf" },
|
|
"9001:302": { download_link: "https://cdn.example.test/prep.jpg" },
|
|
},
|
|
laneAllowedServices: ["MACHINE"],
|
|
commandResponse: { success: true },
|
|
relayResponse: { success: true },
|
|
dynamicImage: TINY_PNG,
|
|
};
|
|
|
|
const fixture = mergeFixture(baseFixture, overrides);
|
|
fixture.departmentLanes = fixture.departments.flatMap((department) => department.lanes || []);
|
|
fixture.laneById = fixture.departmentLanes.reduce((accumulator, lane) => {
|
|
accumulator[String(lane.id)] = lane;
|
|
return accumulator;
|
|
}, {});
|
|
return fixture;
|
|
}
|
|
|
|
function createEdgeGatewayFixture(options = {}) {
|
|
const primaryBrokerConnected = options.brokerConnected ?? true;
|
|
|
|
return {
|
|
pendingDiscoveryByGatewayId: {},
|
|
pendingUpdateByGatewayId: {},
|
|
gateways: [
|
|
{
|
|
id: 701,
|
|
department_id: 1,
|
|
label: "CPH Edge 01",
|
|
hostname: "cph-edge-01",
|
|
status: "ONLINE",
|
|
transport_mode: "gateway",
|
|
department_transport_mode: "gateway",
|
|
release_channel: "stable",
|
|
installed_version: "1.2.0",
|
|
target_version: "1.2.1",
|
|
last_heartbeat_at: "2026-04-08 08:15:00",
|
|
last_seen_ip: "10.1.0.14",
|
|
discovery_status: "READY",
|
|
metadata: {
|
|
broker_connected: primaryBrokerConnected,
|
|
system_metrics: {
|
|
latency_ms: 184,
|
|
cpu_usage_pct: 27,
|
|
memory_usage_pct: 61,
|
|
memory_used_bytes: 2621440000,
|
|
memory_total_bytes: 4294967296,
|
|
disk_usage_pct: 58,
|
|
disk_used_bytes: 249108103168,
|
|
disk_total_bytes: 429496729600,
|
|
disk_mount: "/",
|
|
},
|
|
},
|
|
inventory: [
|
|
{
|
|
id: 1,
|
|
device_id: "shelly-plus-01",
|
|
local_ip: "10.1.0.31",
|
|
model: "Shelly Plus 2PM",
|
|
channel_count: 2,
|
|
online: true,
|
|
capabilities: { generation: 2 }
|
|
},
|
|
{
|
|
id: 2,
|
|
device_id: "shelly-mini-offline",
|
|
local_ip: "10.1.0.34",
|
|
model: "Shelly Mini 1",
|
|
channel_count: 1,
|
|
online: false,
|
|
capabilities: { generation: 3 }
|
|
}
|
|
],
|
|
bindings: [
|
|
{ id: 1, relay_id: "M-7", device_id: "shelly-plus-01", local_ip: "10.1.0.31", channel: 0, binding_source: "MANUAL" },
|
|
{
|
|
id: 2,
|
|
relay_id: "M-7-LEGACY",
|
|
device_id: "shelly-missing-legacy",
|
|
local_ip: "10.1.0.99",
|
|
channel: 1,
|
|
binding_source: "MANUAL"
|
|
}
|
|
],
|
|
recent_updates: [
|
|
{ id: 11, target_version: "1.2.1", status: "COMPLETED" }
|
|
],
|
|
recent_shell_sessions: [],
|
|
recent_commands: [],
|
|
audit_logs: [
|
|
{ id: 501, created_at: "2026-04-08 08:16:00", action: "GATEWAY_CLAIMED", actor_type: "USER" }
|
|
]
|
|
},
|
|
{
|
|
id: 702,
|
|
department_id: 2,
|
|
label: "ODE Edge 01",
|
|
hostname: "ode-edge-01",
|
|
status: "OFFLINE",
|
|
transport_mode: "gateway",
|
|
department_transport_mode: "cloud",
|
|
release_channel: "stable",
|
|
installed_version: "1.1.0",
|
|
target_version: "1.1.0",
|
|
last_heartbeat_at: "2026-04-07 21:04:00",
|
|
last_seen_ip: "10.2.0.14",
|
|
discovery_status: "STALE",
|
|
metadata: {
|
|
broker_connected: false,
|
|
system_metrics: {
|
|
latency_ms: 412,
|
|
cpu_usage_pct: 9,
|
|
memory_usage_pct: 42,
|
|
memory_used_bytes: 1073741824,
|
|
memory_total_bytes: 2147483648,
|
|
disk_usage_pct: 76,
|
|
disk_used_bytes: 163208757248,
|
|
disk_total_bytes: 214748364800,
|
|
disk_mount: "/",
|
|
},
|
|
},
|
|
inventory: [],
|
|
bindings: [],
|
|
recent_updates: [],
|
|
recent_shell_sessions: [],
|
|
recent_commands: [],
|
|
audit_logs: [
|
|
{ id: 502, created_at: "2026-04-07 21:05:00", action: "HEARTBEAT_TIMEOUT", actor_type: "SYSTEM" }
|
|
]
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function buildEdgeGatewayShellSocketConfig(overrides = {}) {
|
|
return {
|
|
mode: "success",
|
|
websocketUrl: "ws://127.0.0.1:65500/ws/browser-shell?token=shell-session-token",
|
|
prompt: "root@pi:~# ",
|
|
directoryListing: "agent.mjs",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) {
|
|
const gateway = edgeGatewayFixture.gateways.find((entry) => entry.id === gatewayId) || null;
|
|
if (!gateway) {
|
|
return null;
|
|
}
|
|
|
|
const pendingDiscovery = edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId];
|
|
if (pendingDiscovery) {
|
|
pendingDiscovery.fetchCount = (pendingDiscovery.fetchCount || 0) + 1;
|
|
if (pendingDiscovery.fetchCount >= 2) {
|
|
gateway.discovery_status = "READY";
|
|
if (!gateway.inventory.some((device) => device.device_id === pendingDiscovery.device.device_id)) {
|
|
gateway.inventory = [...gateway.inventory, pendingDiscovery.device];
|
|
}
|
|
gateway.recent_commands = (gateway.recent_commands || []).map((job) =>
|
|
Number(job.id) === Number(pendingDiscovery.jobId)
|
|
? { ...job, status: "COMPLETED" }
|
|
: job
|
|
);
|
|
delete edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId];
|
|
}
|
|
}
|
|
|
|
const pendingUpdate = edgeGatewayFixture.pendingUpdateByGatewayId[gatewayId];
|
|
if (pendingUpdate) {
|
|
pendingUpdate.fetchCount = (pendingUpdate.fetchCount || 0) + 1;
|
|
if (pendingUpdate.fetchCount >= 2) {
|
|
gateway.recent_updates = (gateway.recent_updates || []).map((job) =>
|
|
Number(job.id) === Number(pendingUpdate.jobId)
|
|
? { ...job, status: "COMPLETED" }
|
|
: job
|
|
);
|
|
delete edgeGatewayFixture.pendingUpdateByGatewayId[gatewayId];
|
|
}
|
|
}
|
|
|
|
return gateway;
|
|
}
|
|
|
|
async function installEdgeGatewayShellSocketMock(page, shellConfig) {
|
|
if (!shellConfig) {
|
|
return;
|
|
}
|
|
|
|
await page.addInitScript((config) => {
|
|
const originalWebSocket = window.WebSocket;
|
|
const CONNECTING = 0;
|
|
const OPEN = 1;
|
|
const CLOSING = 2;
|
|
const CLOSED = 3;
|
|
|
|
window.__EDGE_GATEWAY_SHELL_MESSAGES__ = [];
|
|
|
|
class FakeShellSocket {
|
|
constructor(url) {
|
|
this.url = url;
|
|
this.readyState = CONNECTING;
|
|
this.inputBuffer = "";
|
|
this.listeners = new Map();
|
|
this.onopen = null;
|
|
this.onmessage = null;
|
|
this.onerror = null;
|
|
this.onclose = null;
|
|
|
|
window.setTimeout(() => {
|
|
if (config.mode === "fail-before-open") {
|
|
this.dispatch("error");
|
|
this.readyState = CLOSED;
|
|
this.dispatch("close");
|
|
return;
|
|
}
|
|
|
|
this.readyState = OPEN;
|
|
this.dispatch("open");
|
|
this.dispatch("message", {
|
|
data: JSON.stringify({ type: "opened" }),
|
|
});
|
|
this.dispatch("message", {
|
|
data: JSON.stringify({ type: "output", data: config.prompt }),
|
|
});
|
|
}, 0);
|
|
}
|
|
|
|
addEventListener(eventName, callback) {
|
|
const listeners = this.listeners.get(eventName) || [];
|
|
listeners.push(callback);
|
|
this.listeners.set(eventName, listeners);
|
|
}
|
|
|
|
removeEventListener(eventName, callback) {
|
|
const listeners = this.listeners.get(eventName) || [];
|
|
this.listeners.set(
|
|
eventName,
|
|
listeners.filter((listener) => listener !== callback)
|
|
);
|
|
}
|
|
|
|
dispatch(eventName, payload = {}) {
|
|
const event = { type: eventName, target: this, ...payload };
|
|
const handler = this[`on${eventName}`];
|
|
|
|
if (typeof handler === "function") {
|
|
handler.call(this, event);
|
|
}
|
|
|
|
for (const listener of this.listeners.get(eventName) || []) {
|
|
listener.call(this, event);
|
|
}
|
|
}
|
|
|
|
send(message) {
|
|
const parsed = JSON.parse(message);
|
|
window.__EDGE_GATEWAY_SHELL_MESSAGES__.push(parsed);
|
|
|
|
if (parsed.type === "input") {
|
|
this.inputBuffer += String(parsed.data || "");
|
|
if (/[\r\n]/.test(String(parsed.data || ""))) {
|
|
const command = this.inputBuffer.replace(/[\r\n]+/g, "").trim();
|
|
this.inputBuffer = "";
|
|
|
|
if (command !== "") {
|
|
this.dispatch("message", {
|
|
data: JSON.stringify({
|
|
type: "output",
|
|
data: `${command}\r\n${config.directoryListing}\r\n${config.prompt}`,
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parsed.type === "close") {
|
|
this.dispatch("message", {
|
|
data: JSON.stringify({ type: "closed", code: 0 }),
|
|
});
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
close() {
|
|
if (this.readyState === CLOSED) {
|
|
return;
|
|
}
|
|
|
|
this.readyState = CLOSED;
|
|
this.dispatch("close");
|
|
}
|
|
}
|
|
|
|
function MockableWebSocket(url, protocols) {
|
|
if (String(url) === config.websocketUrl) {
|
|
return new FakeShellSocket(url, protocols);
|
|
}
|
|
|
|
return new originalWebSocket(url, protocols);
|
|
}
|
|
|
|
MockableWebSocket.CONNECTING = CONNECTING;
|
|
MockableWebSocket.OPEN = OPEN;
|
|
MockableWebSocket.CLOSING = CLOSING;
|
|
MockableWebSocket.CLOSED = CLOSED;
|
|
MockableWebSocket.prototype = originalWebSocket.prototype;
|
|
|
|
window.WebSocket = MockableWebSocket;
|
|
}, shellConfig);
|
|
}
|
|
|
|
export function createPosFixture(overrides = {}) {
|
|
const defaultCustomer = {
|
|
id: 1,
|
|
customerNumber: 12345679,
|
|
name: "(TEST) Pleno Vognmandsforretning",
|
|
address: "Demo Street 1",
|
|
zip: "2630",
|
|
city: "Taastrup",
|
|
mobilePhone: "12345678",
|
|
email: "2jepp9350@gmail.com",
|
|
corporateIdentificationNumber: "12345678",
|
|
economic_customer: 12345679,
|
|
barred: false,
|
|
};
|
|
|
|
const cardCustomer = {
|
|
id: 2,
|
|
customerNumber: 999,
|
|
name: "Card Terminal Customer",
|
|
address: "Terminal Street 9",
|
|
zip: "2630",
|
|
city: "Taastrup",
|
|
mobilePhone: "87654321",
|
|
email: "card@example.com",
|
|
corporateIdentificationNumber: "99999999",
|
|
economic_customer: 999,
|
|
barred: false,
|
|
};
|
|
|
|
const products = [
|
|
{
|
|
id: 53,
|
|
name: "Forvogn",
|
|
description: "Primary wash product",
|
|
price: 649,
|
|
subscription_allowed: true,
|
|
category: 4,
|
|
piktogram: "truck",
|
|
apply_category_discount: true,
|
|
requires_note: false,
|
|
is_wash: true,
|
|
display_in_booking_form: true,
|
|
order_priority: 1,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 63,
|
|
name: "Indvendig vask Forvogn",
|
|
description: "Add-on wash service",
|
|
price: 399,
|
|
subscription_allowed: true,
|
|
category: 4,
|
|
piktogram: "truck",
|
|
apply_category_discount: true,
|
|
requires_note: false,
|
|
is_wash: false,
|
|
display_in_booking_form: true,
|
|
order_priority: 2,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 64,
|
|
name: "Vaskecertifikat - Safety Seal",
|
|
description: "Safety seal",
|
|
price: 25,
|
|
subscription_allowed: true,
|
|
category: 8,
|
|
piktogram: "certificate",
|
|
apply_category_discount: false,
|
|
requires_note: false,
|
|
is_wash: false,
|
|
display_in_booking_form: true,
|
|
order_priority: 3,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 65,
|
|
name: "Ekstraordinær pr. 10 min inkl. kemi",
|
|
description: "Additional time",
|
|
price: 299,
|
|
subscription_allowed: true,
|
|
category: 8,
|
|
piktogram: "timer",
|
|
apply_category_discount: false,
|
|
requires_note: false,
|
|
is_wash: false,
|
|
display_in_booking_form: true,
|
|
order_priority: 4,
|
|
addons: [],
|
|
},
|
|
];
|
|
|
|
const baseFixture = {
|
|
departments: [
|
|
{ id: 1, name: "Taastrup", exclude_from_invoicing: true },
|
|
{ id: 12, name: "Demo", exclude_from_invoicing: false },
|
|
],
|
|
customersByNumber: {
|
|
[defaultCustomer.customerNumber]: defaultCustomer,
|
|
[cardCustomer.customerNumber]: cardCustomer,
|
|
},
|
|
customerAttributesByNumber: {
|
|
[defaultCustomer.customerNumber]: [
|
|
{ id: 1, customer_number: defaultCustomer.customerNumber, attribute: "invoiceAllOrdersIndividually" },
|
|
],
|
|
[cardCustomer.customerNumber]: [
|
|
{ id: 2, customer_number: cardCustomer.customerNumber, attribute: "invoiceWithStripe" },
|
|
],
|
|
},
|
|
customerNotesByNumber: {},
|
|
products,
|
|
departmentCategories: [
|
|
{ id: 11, department_id: 12, category: { id: 4, name: "Udvendig", meta: { products: [53, 63] } } },
|
|
{ id: 12, department_id: 12, category: { id: 8, name: "Tillæg", meta: { products: [64, 65] } } },
|
|
],
|
|
vehicles: [
|
|
{
|
|
id: 7001,
|
|
reg: "EC21235",
|
|
customer_id: defaultCustomer.customerNumber,
|
|
customer_name: defaultCustomer.name,
|
|
type: 53,
|
|
status: "verified",
|
|
barred: false,
|
|
wash_subscription: false,
|
|
addons: { enabled: 0, available: 0, list: [] },
|
|
reference: "EC21233 - Test Ref. / Intern nummer",
|
|
last_order_id: 54518,
|
|
},
|
|
],
|
|
unknownVehicles: [],
|
|
orderBookings: [],
|
|
numberPlateScanners: [
|
|
{ id: 1, name: "North scanner" },
|
|
{ id: 2, name: "South scanner" },
|
|
],
|
|
numberPlateScans: [
|
|
{
|
|
id: 801,
|
|
department_id: 12,
|
|
plate: "AB12345",
|
|
plate_scanner_id: 1,
|
|
created_at: "2026-04-08 08:44:07",
|
|
customer_number: defaultCustomer.customerNumber,
|
|
customer_name: defaultCustomer.name,
|
|
seen_before: true,
|
|
barred: false,
|
|
},
|
|
{
|
|
id: 802,
|
|
department_id: 12,
|
|
plate: "CD67890",
|
|
plate_scanner_id: 2,
|
|
created_at: "2026-04-08 08:31:05",
|
|
customer_number: null,
|
|
customer_name: "",
|
|
seen_before: false,
|
|
barred: false,
|
|
},
|
|
],
|
|
motorApiLookupByPlate: {
|
|
AB12345: {
|
|
make: "RENAULT",
|
|
model: "Captur",
|
|
variant: "dCi 90",
|
|
type: "Personbil",
|
|
use: "Privat personkørsel",
|
|
},
|
|
CD67890: {
|
|
make: "SCANIA",
|
|
model: "R500",
|
|
variant: "Highline",
|
|
type: "Lastbil",
|
|
use: "Godstransport",
|
|
},
|
|
},
|
|
ordersById: {
|
|
54518: {
|
|
id: 54518,
|
|
customer_id: defaultCustomer.customerNumber,
|
|
department_id: 12,
|
|
reference: "EC21233 - Test Ref. / Intern nummer",
|
|
notes: "",
|
|
reg_1: "EC21235",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
closed_at: null,
|
|
created_at: "2026-04-08 08:44:07",
|
|
include_in_invoice: null,
|
|
},
|
|
},
|
|
orderItemsByOrderId: {
|
|
54518: [
|
|
{ id: 9101, order_id: 54518, product_id: 53, product: products[0], quantity: 1, notes: "", reference: "", related_item_id: null, price: 649 },
|
|
{ id: 9102, order_id: 54518, product_id: 63, product: products[1], quantity: 1, notes: "", reference: "", related_item_id: 9101, price: 399 },
|
|
{ id: 9103, order_id: 54518, product_id: 64, product: products[2], quantity: 1, notes: "", reference: "", related_item_id: 9101, price: 25 },
|
|
{ id: 9104, order_id: 54518, product_id: 65, product: products[3], quantity: 1, notes: "", reference: "", related_item_id: 9101, price: 299 },
|
|
],
|
|
},
|
|
attachmentsByOrderId: {
|
|
54518: [
|
|
{
|
|
id: 301,
|
|
object_type: "orders",
|
|
object_id: 54518,
|
|
content: {
|
|
image: null,
|
|
document: null,
|
|
relation: null,
|
|
other: "safety-seal.pdf",
|
|
src: null,
|
|
},
|
|
created_at: "2026-04-08 08:44:07",
|
|
updated_at: "2026-04-08 08:44:07",
|
|
deleted_at: null,
|
|
},
|
|
],
|
|
},
|
|
economicModuleOrdersByOrderId: {
|
|
54518: {
|
|
invoice_id: null,
|
|
invoice_draft_id: null,
|
|
},
|
|
},
|
|
stripeModuleOrdersByOrderId: {
|
|
54518: {},
|
|
},
|
|
paymentIntentsByOrderId: {},
|
|
readers: [
|
|
{ id: "reader_online_1", label: "Mobile Reader", status: "online", action: null },
|
|
],
|
|
nextOrderId: 54519,
|
|
nextOrderItemId: 9200,
|
|
nextAttachmentId: 400,
|
|
};
|
|
|
|
return {
|
|
...baseFixture,
|
|
...overrides,
|
|
customersByNumber: {
|
|
...baseFixture.customersByNumber,
|
|
...(overrides.customersByNumber || {}),
|
|
},
|
|
customerAttributesByNumber: {
|
|
...baseFixture.customerAttributesByNumber,
|
|
...(overrides.customerAttributesByNumber || {}),
|
|
},
|
|
customerNotesByNumber: {
|
|
...baseFixture.customerNotesByNumber,
|
|
...(overrides.customerNotesByNumber || {}),
|
|
},
|
|
numberPlateScanners: overrides.numberPlateScanners || baseFixture.numberPlateScanners,
|
|
numberPlateScans: overrides.numberPlateScans || baseFixture.numberPlateScans,
|
|
motorApiLookupByPlate: {
|
|
...baseFixture.motorApiLookupByPlate,
|
|
...(overrides.motorApiLookupByPlate || {}),
|
|
},
|
|
ordersById: {
|
|
...baseFixture.ordersById,
|
|
...(overrides.ordersById || {}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
...baseFixture.orderItemsByOrderId,
|
|
...(overrides.orderItemsByOrderId || {}),
|
|
},
|
|
attachmentsByOrderId: {
|
|
...baseFixture.attachmentsByOrderId,
|
|
...(overrides.attachmentsByOrderId || {}),
|
|
},
|
|
economicModuleOrdersByOrderId: {
|
|
...baseFixture.economicModuleOrdersByOrderId,
|
|
...(overrides.economicModuleOrdersByOrderId || {}),
|
|
},
|
|
stripeModuleOrdersByOrderId: {
|
|
...baseFixture.stripeModuleOrdersByOrderId,
|
|
...(overrides.stripeModuleOrdersByOrderId || {}),
|
|
},
|
|
paymentIntentsByOrderId: {
|
|
...baseFixture.paymentIntentsByOrderId,
|
|
...(overrides.paymentIntentsByOrderId || {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildPosOrderItem(product, body, id) {
|
|
const quantity = Number(body.quantity || 1);
|
|
return {
|
|
id,
|
|
order_id: Number(body.order_id),
|
|
product_id: product.id,
|
|
product,
|
|
quantity,
|
|
notes: body.notes || "",
|
|
reference: body.reference || "",
|
|
related_item_id: body.related_item_id ?? null,
|
|
price: Number(body.price ?? product.price ?? 0),
|
|
};
|
|
}
|
|
|
|
function buildPosOrderResponse(posFixture, orderId) {
|
|
const order = withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null);
|
|
if (!order) {
|
|
return { success: true, data: null, includes: {} };
|
|
}
|
|
|
|
const customer = posFixture.customersByNumber[order.customer_id] || null;
|
|
return {
|
|
success: true,
|
|
data: order,
|
|
includes: {
|
|
orderItems: posFixture.orderItemsByOrderId[orderId] || [],
|
|
customer: customer ? { ...customer, economic_customer: customer.customerNumber } : null,
|
|
cashier: { id: 7, display_name: "Backoffice User" },
|
|
economicModuleOrders: posFixture.economicModuleOrdersByOrderId[orderId] || {},
|
|
stripeModuleOrders: posFixture.stripeModuleOrdersByOrderId[orderId] || {},
|
|
},
|
|
};
|
|
}
|
|
|
|
async function handlePosRoute({ route, request, parsedUrl, pathname, method, posFixture }) {
|
|
if (!posFixture) {
|
|
return false;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments") && method === "GET") {
|
|
await route.fulfill(json({ success: true, data: posFixture.departments }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments/categories") && method === "GET") {
|
|
await route.fulfill(json({ success: true, data: posFixture.departmentCategories || [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/bookings") && method === "GET") {
|
|
await route.fulfill(json({ success: true, data: [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/order-bookings") && method === "GET") {
|
|
const id = Number(parsedUrl.searchParams.get("id") || 0);
|
|
const filters = String(parsedUrl.searchParams.get("filters") || "");
|
|
let bookings = Array.isArray(posFixture.orderBookings) ? posFixture.orderBookings : [];
|
|
|
|
if (id > 0) {
|
|
await route.fulfill(json({ success: true, data: bookings.find((booking) => booking.id === id) || null }));
|
|
return true;
|
|
}
|
|
|
|
if (filters.includes("reg_1:")) {
|
|
const reg = filters.split("reg_1:")[1]?.split(",")[0] || "";
|
|
bookings = bookings.filter((booking) => String(booking.reg_1 || "").toUpperCase() === String(reg).toUpperCase());
|
|
}
|
|
|
|
await route.fulfill(json({ success: true, data: bookings }));
|
|
return true;
|
|
}
|
|
|
|
if (/\/order-bookings\/\d+$/.test(pathname) && method === "GET") {
|
|
const bookingId = Number(pathname.split("/").pop());
|
|
const booking = (posFixture.orderBookings || []).find((entry) => entry.id === bookingId) || null;
|
|
await route.fulfill(json({ success: true, data: booking }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/numberplatescanners") && method === "GET") {
|
|
await route.fulfill(json({ success: true, data: posFixture.numberPlateScanners || [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/numberplatescans") && method === "GET") {
|
|
const filteredScans = filterNumberPlateScans(posFixture, {
|
|
filters: parsedUrl.searchParams.get("filters") || "",
|
|
search: parsedUrl.searchParams.get("search") || "",
|
|
order: parsedUrl.searchParams.get("order") || "created_at:desc",
|
|
});
|
|
const paginated = paginateRows(
|
|
filteredScans,
|
|
parsedUrl.searchParams.get("page") || 1,
|
|
parsedUrl.searchParams.get("limit") || 10
|
|
);
|
|
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: paginated.rows,
|
|
meta: paginated.meta,
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/vehicles") && method === "GET") {
|
|
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
|
|
const vehicles = (posFixture.vehicles || []).filter((vehicle) => !search || String(vehicle.reg || "").toUpperCase().includes(search));
|
|
await route.fulfill(json({ success: true, data: vehicles }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
|
|
await route.fulfill(json({ success: true, data: posFixture.unknownVehicles || [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/users/customer") && method === "GET") {
|
|
const customerNumber = Number(parsedUrl.searchParams.get("customer_number") || 0);
|
|
const customer = posFixture.customersByNumber[customerNumber] || Object.values(posFixture.customersByNumber)[0];
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
customer_name: customer?.name || "",
|
|
economic_customer: customer || null,
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/motorapi/lookup") && method === "GET") {
|
|
const licensePlate = String(parsedUrl.searchParams.get("license_plate") || "").toUpperCase();
|
|
const vehicleData = posFixture.motorApiLookupByPlate?.[licensePlate];
|
|
|
|
if (!vehicleData) {
|
|
await route.fulfill(json({
|
|
success: false,
|
|
data: null,
|
|
}, 404));
|
|
return true;
|
|
}
|
|
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: vehicleData,
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/customers") && method === "GET") {
|
|
const search = String(parsedUrl.searchParams.get("search") || "").toLowerCase();
|
|
const customers = Object.values(posFixture.customersByNumber).filter((customer) => {
|
|
if (!search) {
|
|
return true;
|
|
}
|
|
return String(customer.customerNumber).includes(search) || String(customer.name || "").toLowerCase().includes(search);
|
|
});
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: customers,
|
|
meta: {
|
|
pagination: {
|
|
page: 1,
|
|
limit: 10,
|
|
total: customers.length,
|
|
},
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/customer/attributes") && method === "GET") {
|
|
const customerNumber = Number(parsedUrl.searchParams.get("customer_number") || 0);
|
|
await route.fulfill(json({ success: true, data: posFixture.customerAttributesByNumber[customerNumber] || [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/customer/notes") && method === "GET") {
|
|
const customerNumber = Number(parsedUrl.searchParams.get("customer_number") || 0);
|
|
await route.fulfill(json({ success: true, data: posFixture.customerNotesByNumber[customerNumber] || [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/user/discounts") && method === "GET") {
|
|
await route.fulfill(json({ success: true, data: [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/products") && method === "GET") {
|
|
const productId = Number(parsedUrl.searchParams.get("id") || 0);
|
|
const category = Number(parsedUrl.searchParams.get("category") || 0);
|
|
if (productId > 0) {
|
|
await route.fulfill(json({ success: true, data: (posFixture.products || []).find((product) => product.id === productId) || null }));
|
|
return true;
|
|
}
|
|
const products = category > 0
|
|
? (posFixture.products || []).filter((product) => Number(product.category) === category)
|
|
: (posFixture.products || []);
|
|
await route.fulfill(json({ success: true, data: products }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "GET") {
|
|
const filters = String(parsedUrl.searchParams.get("filters") || "");
|
|
const orders = filterPosOrders(posFixture, filters)
|
|
.map((order) => withEffectiveOrderState(posFixture, order))
|
|
.sort((a, b) => Number(b.id) - Number(a.id));
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: orders,
|
|
meta: {
|
|
pagination: {
|
|
page: 1,
|
|
limit: orders.length || 20,
|
|
total: orders.length,
|
|
},
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = posFixture.nextOrderId++;
|
|
posFixture.ordersById[orderId] = {
|
|
id: orderId,
|
|
customer_id: Number(body.customer_id),
|
|
department_id: Number(body.department_id || body.department || 12),
|
|
reference: body.reference || "",
|
|
notes: body.notes || "",
|
|
reg_1: body.reg_1 || "",
|
|
reg_2: body.reg_2 || "",
|
|
reg_3: body.reg_3 || "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
closed_at: null,
|
|
created_at: body.created_at || toSqlDateTime(),
|
|
include_in_invoice: normalizeIncludeInInvoiceValue(body.include_in_invoice),
|
|
};
|
|
posFixture.orderItemsByOrderId[orderId] = [];
|
|
posFixture.economicModuleOrdersByOrderId[orderId] = { invoice_id: null, invoice_draft_id: null };
|
|
posFixture.stripeModuleOrdersByOrderId[orderId] = {};
|
|
await route.fulfill(json({ success: true, data: { id: orderId } }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/order") && method === "GET") {
|
|
const orderId = Number(parsedUrl.searchParams.get("id") || 0);
|
|
await route.fulfill(json(buildPosOrderResponse(posFixture, orderId)));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id || 0);
|
|
if (posFixture.ordersById[orderId]) {
|
|
posFixture.ordersById[orderId] = {
|
|
...posFixture.ordersById[orderId],
|
|
...body,
|
|
...(Object.prototype.hasOwnProperty.call(body, "include_in_invoice")
|
|
? { include_in_invoice: normalizeIncludeInInvoiceValue(body.include_in_invoice) }
|
|
: {}),
|
|
};
|
|
}
|
|
await route.fulfill(json({ success: true, data: withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null) }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/order") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id || 0);
|
|
if (posFixture.ordersById[orderId]) {
|
|
if (body.field) {
|
|
posFixture.ordersById[orderId][body.field] =
|
|
body.field === "include_in_invoice"
|
|
? normalizeIncludeInInvoiceValue(body.value)
|
|
: body.value;
|
|
} else {
|
|
posFixture.ordersById[orderId] = {
|
|
...posFixture.ordersById[orderId],
|
|
...body,
|
|
...(Object.prototype.hasOwnProperty.call(body, "include_in_invoice")
|
|
? { include_in_invoice: normalizeIncludeInInvoiceValue(body.include_in_invoice) }
|
|
: {}),
|
|
};
|
|
}
|
|
}
|
|
await route.fulfill(json({ success: true, data: withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null) }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "GET") {
|
|
const orderId = Number(parsedUrl.searchParams.get("order_id") || 0);
|
|
await route.fulfill(json({ success: true, data: posFixture.orderItemsByOrderId[orderId] || [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.order_id || 0);
|
|
const productId = Number(body.product_id || 0);
|
|
const product = (posFixture.products || []).find((entry) => entry.id === productId);
|
|
if (!product || !posFixture.ordersById[orderId]) {
|
|
await route.fulfill(json({ success: false, data: { message: "Order or product not found" } }, 422));
|
|
return true;
|
|
}
|
|
const orderItem = buildPosOrderItem(product, body, posFixture.nextOrderItemId++);
|
|
if (!Array.isArray(posFixture.orderItemsByOrderId[orderId])) {
|
|
posFixture.orderItemsByOrderId[orderId] = [];
|
|
}
|
|
posFixture.orderItemsByOrderId[orderId].push(orderItem);
|
|
await route.fulfill(json({ success: true, data: orderItem }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const targetId = Number(body.id || 0);
|
|
Object.keys(posFixture.orderItemsByOrderId).forEach((orderIdKey) => {
|
|
posFixture.orderItemsByOrderId[orderIdKey] = (posFixture.orderItemsByOrderId[orderIdKey] || []).map((item) => {
|
|
if (item.id !== targetId) {
|
|
return item;
|
|
}
|
|
return {
|
|
...item,
|
|
price: Number(body.price ?? item.price),
|
|
notes: body.notes ?? item.notes,
|
|
reference: body.reference ?? item.reference,
|
|
quantity: Number(body.quantity ?? item.quantity),
|
|
};
|
|
});
|
|
});
|
|
await route.fulfill(json({ success: true, data: { id: targetId } }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "DELETE") {
|
|
const orderItemId = Number(parsedUrl.searchParams.get("id") || 0);
|
|
Object.keys(posFixture.orderItemsByOrderId).forEach((orderIdKey) => {
|
|
posFixture.orderItemsByOrderId[orderIdKey] = (posFixture.orderItemsByOrderId[orderIdKey] || []).filter((item) => item.id !== orderItemId);
|
|
});
|
|
await route.fulfill(json({ success: true, data: true }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/attachments") && method === "GET") {
|
|
const orderId = Number(parsedUrl.searchParams.get("id") || 0);
|
|
await route.fulfill(json({ success: true, data: posFixture.attachmentsByOrderId[orderId] || [] }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/attachments/upload") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.order_id || body.id || 0);
|
|
if (!Array.isArray(posFixture.attachmentsByOrderId[orderId])) {
|
|
posFixture.attachmentsByOrderId[orderId] = [];
|
|
}
|
|
const attachmentId = posFixture.nextAttachmentId++;
|
|
const attachment = {
|
|
id: attachmentId,
|
|
object_type: "orders",
|
|
object_id: orderId,
|
|
content: {
|
|
image: null,
|
|
document: null,
|
|
relation: null,
|
|
other: `attachment-${attachmentId}.jpg`,
|
|
src: null,
|
|
},
|
|
created_at: toSqlDateTime(),
|
|
updated_at: toSqlDateTime(),
|
|
deleted_at: null,
|
|
};
|
|
posFixture.attachmentsByOrderId[orderId].push(attachment);
|
|
await route.fulfill(json({ success: true, data: attachment }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/attachments") && method === "DELETE") {
|
|
const orderId = Number(parsedUrl.searchParams.get("order_id") || 0);
|
|
const attachmentId = Number(parsedUrl.searchParams.get("attachment_id") || 0);
|
|
posFixture.attachmentsByOrderId[orderId] = (posFixture.attachmentsByOrderId[orderId] || []).filter((attachment) => attachment.id !== attachmentId);
|
|
await route.fulfill(json({ success: true, data: true }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/attachments/download") && method === "GET") {
|
|
const orderId = Number(parsedUrl.searchParams.get("order_id") || 0);
|
|
const attachmentId = Number(parsedUrl.searchParams.get("attachment_id") || 0);
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
download_link: `https://cdn.example.test/orders/${orderId}/attachments/${attachmentId}`,
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/wash-certificate") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id || 0);
|
|
|
|
if (!Array.isArray(posFixture.attachmentsByOrderId[orderId])) {
|
|
posFixture.attachmentsByOrderId[orderId] = [];
|
|
}
|
|
|
|
const existingAttachment = (posFixture.attachmentsByOrderId[orderId] || []).find((attachment) => {
|
|
return String(attachment?.content?.other || "").toUpperCase() === "WASH_CERTIFICATE";
|
|
}) || null;
|
|
|
|
if (existingAttachment) {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
order_id: orderId,
|
|
created: false,
|
|
already_existed: true,
|
|
attachment_id: existingAttachment.id,
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
const attachmentId = posFixture.nextAttachmentId++;
|
|
const attachment = {
|
|
id: attachmentId,
|
|
object_type: "orders",
|
|
object_id: orderId,
|
|
content: {
|
|
image: null,
|
|
document: `wash_certificate_${orderId}.pdf`,
|
|
relation: null,
|
|
other: "WASH_CERTIFICATE",
|
|
src: null,
|
|
},
|
|
created_at: toSqlDateTime(),
|
|
updated_at: toSqlDateTime(),
|
|
deleted_at: null,
|
|
};
|
|
posFixture.attachmentsByOrderId[orderId].push(attachment);
|
|
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
order_id: orderId,
|
|
created: true,
|
|
already_existed: false,
|
|
attachment_id: attachmentId,
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments/order/recommended") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
reg_1: {
|
|
motorapi: [53],
|
|
order_history: { 2: [], 3: [], 4: [], 5: [] },
|
|
},
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/mark_as_completed") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id || 0);
|
|
if (posFixture.ordersById[orderId]) {
|
|
posFixture.ordersById[orderId].completed_at = new Date().toISOString();
|
|
}
|
|
await route.fulfill(json({ success: true, data: { id: orderId } }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/stripe/department/terminal/readers") && method === "GET") {
|
|
await route.fulfill(json({ success: true, data: { data: posFixture.readers || [] } }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "GET") {
|
|
const orderId = Number(parsedUrl.searchParams.get("id") || 0);
|
|
const paymentIntent = posFixture.paymentIntentsByOrderId[orderId] || null;
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
payment_intent: paymentIntent,
|
|
has_payment_intent: !!paymentIntent,
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id || 0);
|
|
const paymentIntent = posFixture.paymentIntentsByOrderId[orderId] || {
|
|
id: `pi_${orderId}`,
|
|
amount: 123400,
|
|
amount_capturable: 123400,
|
|
amount_received: 0,
|
|
currency: "dkk",
|
|
status: "requires_capture",
|
|
metadata: {
|
|
order_id: String(orderId),
|
|
reader_id: String(body.reader || "reader_online_1"),
|
|
tax_percentage: String(body.tax_percentage ?? 25),
|
|
},
|
|
};
|
|
posFixture.paymentIntentsByOrderId[orderId] = paymentIntent;
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
payment_intent: paymentIntent,
|
|
has_payment_intent: true,
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "DELETE") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id || 0);
|
|
delete posFixture.paymentIntentsByOrderId[orderId];
|
|
await route.fulfill(json({ success: true, data: { payment_intent: null, has_payment_intent: false } }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent/capture") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id || 0);
|
|
const paymentIntent = {
|
|
...(posFixture.paymentIntentsByOrderId[orderId] || {}),
|
|
id: `pi_${orderId}`,
|
|
amount: 123400,
|
|
amount_capturable: 0,
|
|
amount_received: 123400,
|
|
currency: "dkk",
|
|
status: "succeeded",
|
|
metadata: {
|
|
order_id: String(orderId),
|
|
reader_id: "reader_online_1",
|
|
tax_percentage: "25",
|
|
},
|
|
};
|
|
posFixture.paymentIntentsByOrderId[orderId] = paymentIntent;
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
payment_intent: paymentIntent,
|
|
has_payment_intent: true,
|
|
},
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, method, edgeGatewayFixture, shellSocketConfig }) {
|
|
if (!edgeGatewayFixture) {
|
|
return false;
|
|
}
|
|
|
|
if (pathname.endsWith("/edge-gateways") && method === "GET") {
|
|
const departmentId = Number(parsedUrl.searchParams.get("department_id") || 0);
|
|
const gateways = departmentId > 0
|
|
? edgeGatewayFixture.gateways.filter((gateway) => gateway.department_id === departmentId)
|
|
: edgeGatewayFixture.gateways;
|
|
await route.fulfill(json({ data: gateways }));
|
|
return true;
|
|
}
|
|
|
|
if (/\/edge-gateways\/\d+$/.test(pathname) && method === "GET") {
|
|
const gatewayId = Number(pathname.split("/").pop());
|
|
await route.fulfill(json({ data: settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) }));
|
|
return true;
|
|
}
|
|
|
|
if (pathname.endsWith("/edge-gateways/install-token") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
await route.fulfill(json({
|
|
data: {
|
|
claim_token_id: 9001,
|
|
token: "edge-install-token",
|
|
expires_at: "2026-04-08 08:45:00",
|
|
install_command: "curl -fsSL https://api.truckwash.test/edge-agent/install.sh?token=edge-install-token | sudo bash",
|
|
install_url: "https://api.truckwash.test/edge-agent/install.sh?token=edge-install-token",
|
|
department_id: body.department_id ?? 1
|
|
}
|
|
}, 201));
|
|
return true;
|
|
}
|
|
|
|
if (/\/edge-gateways\/\d+\/discovery$/.test(pathname) && method === "POST") {
|
|
const gatewayId = Number(pathname.split("/")[2]);
|
|
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
|
|
if (gateway) {
|
|
const commandId = Date.now();
|
|
gateway.discovery_status = "PENDING";
|
|
gateway.recent_commands = [
|
|
{
|
|
id: commandId,
|
|
command_type: "DISCOVER_SHELLY",
|
|
status: "PENDING",
|
|
},
|
|
...(gateway.recent_commands || []),
|
|
];
|
|
edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId] = {
|
|
jobId: commandId,
|
|
fetchCount: 0,
|
|
device: {
|
|
id: 3,
|
|
device_id: "shelly-plus-new",
|
|
local_ip: "10.1.0.33",
|
|
model: "Shelly Plus 1PM",
|
|
channel_count: 1,
|
|
online: true,
|
|
capabilities: { generation: 2 }
|
|
},
|
|
};
|
|
}
|
|
await route.fulfill(json({ data: gateway }));
|
|
return true;
|
|
}
|
|
|
|
if (/\/edge-gateways\/\d+\/bindings$/.test(pathname) && method === "PUT") {
|
|
const gatewayId = Number(pathname.split("/")[2]);
|
|
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
|
|
const body = request.postDataJSON?.() || {};
|
|
if (gateway) {
|
|
gateway.bindings = (body.bindings || []).map((binding, index) => ({
|
|
id: index + 1,
|
|
...binding
|
|
}));
|
|
}
|
|
await route.fulfill(json({ data: gateway?.bindings || [] }));
|
|
return true;
|
|
}
|
|
|
|
if (/\/edge-gateways\/\d+\/update-jobs$/.test(pathname) && method === "POST") {
|
|
const gatewayId = Number(pathname.split("/")[2]);
|
|
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
|
|
const body = request.postDataJSON?.() || {};
|
|
const job = {
|
|
id: Date.now(),
|
|
target_version: body.target_version || "1.2.1",
|
|
status: "PENDING"
|
|
};
|
|
if (gateway) {
|
|
gateway.recent_updates.unshift(job);
|
|
gateway.target_version = job.target_version;
|
|
edgeGatewayFixture.pendingUpdateByGatewayId[gatewayId] = {
|
|
jobId: job.id,
|
|
fetchCount: 0,
|
|
};
|
|
}
|
|
await route.fulfill(json({ data: job }, 201));
|
|
return true;
|
|
}
|
|
|
|
if (/\/edge-gateways\/\d+\/shell-sessions$/.test(pathname) && method === "POST") {
|
|
const gatewayId = Number(pathname.split("/")[2]);
|
|
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
|
|
const body = request.postDataJSON?.() || {};
|
|
const session = {
|
|
id: 12345,
|
|
gateway_id: gatewayId,
|
|
reason: body.reason || "Maintenance",
|
|
expires_at: "2026-04-09 12:45:00"
|
|
};
|
|
if (gateway) {
|
|
gateway.recent_shell_sessions.unshift(session);
|
|
}
|
|
await route.fulfill(json({
|
|
data: {
|
|
session,
|
|
session_token: "shell-session-token",
|
|
websocket_url: shellSocketConfig?.websocketUrl || "ws://127.0.0.1:65500/ws/browser-shell?token=shell-session-token"
|
|
}
|
|
}, 201));
|
|
return true;
|
|
}
|
|
|
|
if (/\/edge-gateways\/\d+\/rotate-credentials$/.test(pathname) && method === "POST") {
|
|
const gatewayId = Number(pathname.split("/")[2]);
|
|
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
|
|
await route.fulfill(json({
|
|
data: {
|
|
gateway,
|
|
agent_token: `rotated-token-${gatewayId}`
|
|
}
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (/\/departments\/\d+\/gateway-cutover$/.test(pathname) && method === "POST") {
|
|
const departmentId = Number(pathname.split("/")[2]);
|
|
const body = request.postDataJSON?.() || {};
|
|
edgeGatewayFixture.gateways
|
|
.filter((gateway) => gateway.department_id === departmentId)
|
|
.forEach((gateway) => {
|
|
gateway.department_transport_mode = body.transport_mode || "gateway";
|
|
});
|
|
await route.fulfill(json({
|
|
data: {
|
|
department_id: departmentId,
|
|
transport_mode: body.transport_mode || "gateway"
|
|
}
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export async function mockApi(page, options = {}) {
|
|
const posFixture = options.pos ? createPosFixture(options.pos === true ? {} : options.pos) : null;
|
|
const selfServe = options.selfServe ? createSelfServeFixture(options.selfServe === true ? {} : options.selfServe) : null;
|
|
const edgeGatewayOptions =
|
|
options.edgeGateways && typeof options.edgeGateways === "object" ? options.edgeGateways : {};
|
|
const edgeGatewayFixture = options.edgeGateways === false ? null : createEdgeGatewayFixture(edgeGatewayOptions);
|
|
const shellSocketConfig = edgeGatewayFixture
|
|
? buildEdgeGatewayShellSocketConfig(edgeGatewayOptions.shellSocket || {})
|
|
: null;
|
|
|
|
await installEdgeGatewayShellSocketMock(page, shellSocketConfig);
|
|
|
|
await page.route(API_HOST, async (route) => {
|
|
const request = route.request();
|
|
const url = request.url();
|
|
const parsedUrl = new URL(url);
|
|
const pathname = parsedUrl.pathname;
|
|
const method = request.method();
|
|
|
|
if (url.includes("/auth/recaptcha/pre-check") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
recaptcha: {
|
|
enabled: false,
|
|
site_key: ""
|
|
},
|
|
rate_limit: {
|
|
enabled: false,
|
|
limit: 0,
|
|
remaining: 0,
|
|
reset: 0,
|
|
warning: null
|
|
}
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (url.includes("/auth/reCAPTCHA/public") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
recaptcha: {
|
|
enabled: false,
|
|
site_key: "",
|
|
},
|
|
rate_limit: {
|
|
enabled: false,
|
|
limit: 0,
|
|
remaining: 0,
|
|
reset: 0,
|
|
warning: null,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/ping") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
ok: true,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/worker/version") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
version: options.workerVersion || "unknown",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (url.includes("/auth/login") && method === "POST") {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
token: options.loginToken || "e2e-token"
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (url.includes("/auth/session") && method === "GET") {
|
|
if (!options.authenticated) {
|
|
await route.fulfill(json({ message: "Unauthenticated" }, 401));
|
|
return;
|
|
}
|
|
|
|
const defaultSession = {
|
|
id: 1,
|
|
customer_number: 12345,
|
|
group_id: 1,
|
|
email: "e2e@example.com",
|
|
phone: {
|
|
number: "12345678",
|
|
country_code: 45
|
|
},
|
|
notifications: {
|
|
wash_certificate_email: null,
|
|
email_notifications_enabled: true,
|
|
sms_notifications_enabled: false
|
|
},
|
|
created_at: "2026-01-01T00:00:00.000Z",
|
|
updated_at: "2026-01-01T00:00:00.000Z",
|
|
display_name: "E2E User",
|
|
permissions: options.permissions || ["user"],
|
|
economic_customer: []
|
|
};
|
|
|
|
const sessionData = {
|
|
...defaultSession,
|
|
...(options.sessionData || {})
|
|
};
|
|
|
|
if (options.permissions) {
|
|
sessionData.permissions = options.permissions;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: sessionData
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/workfeed/config")) {
|
|
const workfeedConfig = [
|
|
{ module: "workfeed", variable: "enabled", type: "bool", value: true },
|
|
{ module: "workfeed", variable: "api_url", type: "string", value: "https://api.workfeed.test" },
|
|
{ module: "workfeed", variable: "api_key", type: "string", value: "test-api-key" },
|
|
{ module: "workfeed", variable: "CompanyID", type: "string", value: "123456" },
|
|
];
|
|
|
|
if (method === "GET") {
|
|
const variable = parsedUrl.searchParams.get("variable");
|
|
if (variable) {
|
|
const match = workfeedConfig.find((entry) => entry.variable === variable);
|
|
await route.fulfill(json({ data: match ? match.value : null }));
|
|
return;
|
|
}
|
|
await route.fulfill(json({ data: workfeedConfig }));
|
|
return;
|
|
}
|
|
|
|
if (method === "POST") {
|
|
await route.fulfill(json({ data: { updated: true } }));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/workfeed/departments") && method === "GET") {
|
|
const departments = [
|
|
{
|
|
id: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3",
|
|
name: "North Facility",
|
|
timezone: "Europe/Copenhagen",
|
|
active: true,
|
|
},
|
|
{
|
|
id: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N4",
|
|
name: "South Facility",
|
|
timezone: "Europe/Copenhagen",
|
|
active: true,
|
|
},
|
|
];
|
|
|
|
await route.fulfill(json({
|
|
data: {
|
|
items: departments,
|
|
pagination: { cursor: null, nextCursor: null, limit: 20, total: departments.length },
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/workfeed/employees") && method === "GET") {
|
|
const employees = [
|
|
{
|
|
id: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
|
|
firstName: "Anne",
|
|
lastName: "Nielsen",
|
|
fullName: "Anne Nielsen",
|
|
email: "anne.nielsen@example.com",
|
|
departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3",
|
|
active: true,
|
|
},
|
|
{
|
|
id: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5Q",
|
|
firstName: "Mads",
|
|
lastName: "Jensen",
|
|
fullName: "Mads Jensen",
|
|
email: "mads.jensen@example.com",
|
|
departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N4",
|
|
active: true,
|
|
},
|
|
];
|
|
|
|
await route.fulfill(json({
|
|
data: {
|
|
items: employees,
|
|
pagination: { cursor: null, nextCursor: null, limit: 20, total: employees.length },
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (/\/modules\/workfeed\/employees\/[^/]+$/i.test(pathname) && method === "GET") {
|
|
const id = pathname.split("/").pop();
|
|
await route.fulfill(json({
|
|
data: {
|
|
id,
|
|
firstName: "Anne",
|
|
lastName: "Nielsen",
|
|
fullName: "Anne Nielsen",
|
|
email: "anne.nielsen@example.com",
|
|
departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3",
|
|
active: true,
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/workfeed/shifts") && method === "GET") {
|
|
const startFrom = parsedUrl.searchParams.get("startFrom");
|
|
const startTo = parsedUrl.searchParams.get("startTo");
|
|
const employeeID = parsedUrl.searchParams.get("employeeID");
|
|
const releasedRaw = parsedUrl.searchParams.get("released");
|
|
|
|
if (!startFrom) {
|
|
await route.fulfill(json({ message: "Missing required query parameter: startFrom" }, 422));
|
|
return;
|
|
}
|
|
if (!startTo) {
|
|
await route.fulfill(json({ message: "Missing required query parameter: startTo" }, 422));
|
|
return;
|
|
}
|
|
|
|
const shifts = [
|
|
{
|
|
id: "shf_01J5P3X9D35R9C6BDZ1S0R4V6Q",
|
|
title: "Morning Shift",
|
|
status: "published",
|
|
employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
|
|
released: true,
|
|
startAt: "2026-03-24T06:00:00Z",
|
|
endAt: "2026-03-24T14:00:00Z",
|
|
},
|
|
{
|
|
id: "shf_01J5P3X9D35R9C6BDZ1S0R4V6R",
|
|
title: "Evening Shift",
|
|
status: "draft",
|
|
employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5Q",
|
|
released: false,
|
|
startAt: "2026-03-24T14:00:00Z",
|
|
endAt: "2026-03-24T22:00:00Z",
|
|
},
|
|
];
|
|
const released = releasedRaw === null ? undefined : releasedRaw.toLowerCase() === "true";
|
|
|
|
const filtered = shifts.filter((entry) => {
|
|
if (employeeID && entry.employeeID !== employeeID) {
|
|
return false;
|
|
}
|
|
if (released !== undefined && entry.released !== released) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
await route.fulfill(json({
|
|
data: {
|
|
items: filtered,
|
|
pagination: { cursor: null, nextCursor: null, limit: 20, total: filtered.length },
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (/\/modules\/workfeed\/shifts\/[^/]+$/i.test(pathname) && method === "GET") {
|
|
const id = pathname.split("/").pop();
|
|
await route.fulfill(json({
|
|
data: {
|
|
id,
|
|
title: "Morning Shift",
|
|
status: "published",
|
|
employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
|
|
released: true,
|
|
startAt: "2026-03-24T06:00:00Z",
|
|
endAt: "2026-03-24T14:00:00Z",
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (
|
|
await handleEdgeGatewayRoute({
|
|
route,
|
|
request,
|
|
parsedUrl,
|
|
pathname,
|
|
method,
|
|
edgeGatewayFixture,
|
|
shellSocketConfig,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (await handlePosRoute({ route, request, parsedUrl, pathname, method, posFixture })) {
|
|
return;
|
|
}
|
|
|
|
if (selfServe) {
|
|
if (pathname.endsWith("/guest/departments") && method === "GET") {
|
|
await route.fulfill(json({ data: selfServe.departments }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments") && method === "GET") {
|
|
await route.fulfill(json({ data: selfServe.departments.map(({ lanes, ...department }) => department) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/products") && method === "GET") {
|
|
await route.fulfill(json({ data: selfServe.products }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/vehicles") && method === "GET") {
|
|
await route.fulfill(json({ data: selfServe.customerVehicles }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/lanes") && method === "GET") {
|
|
const laneId = parsedUrl.searchParams.get("id");
|
|
|
|
if (laneId) {
|
|
await route.fulfill(json({ data: selfServe.laneById[laneId] || null }));
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(json({ data: selfServe.departmentLanes }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/selfserve/vehicle/allowed") && method === "GET") {
|
|
const laneId = parsedUrl.searchParams.get("lane_id");
|
|
const reg = (parsedUrl.searchParams.get("reg") || "").toUpperCase();
|
|
const previewData = selfServe.previewByKey[`${laneId}:${reg}`] || null;
|
|
await route.fulfill(json({ data: previewData || { allowed: false, questions: [], tasks: [], conditions: [], rules: [] } }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/selfserve/washes/summary") && method === "GET") {
|
|
const sessionId = parsedUrl.searchParams.get("session_id");
|
|
const laneId = parsedUrl.searchParams.get("lane_id");
|
|
const reg = (parsedUrl.searchParams.get("reg") || "").toUpperCase();
|
|
|
|
const summaryData = (
|
|
(sessionId ? selfServe.summaryBySessionId[sessionId] : null)
|
|
|| selfServe.summaryByKey[`${laneId}:${reg}`]
|
|
|| null
|
|
);
|
|
|
|
await route.fulfill(json({ data: summaryData || { events: [], questions: [], tasks: [], conditions: [], rules: [] } }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/selfserve/vehicle/conditions") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const key = `${body.lane}:${String(body.reg || "").toUpperCase()}:${body.question}:${body.value}`;
|
|
const responseSummary = selfServe.answerResponseByKey[key] || null;
|
|
|
|
await route.fulfill(json({
|
|
data: {
|
|
selfserve: responseSummary,
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/selfserve/tasks/attachments") && method === "GET") {
|
|
const taskId = parsedUrl.searchParams.get("id");
|
|
await route.fulfill(json({
|
|
data: selfServe.attachmentsByTaskId[taskId] || [],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/selfserve/tasks/attachments/download") && method === "GET") {
|
|
const taskId = parsedUrl.searchParams.get("task_id");
|
|
const attachmentId = parsedUrl.searchParams.get("attachment_id");
|
|
await route.fulfill(json(selfServe.attachmentDownloadByKey[`${taskId}:${attachmentId}`] || { download_link: null }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/self-serve/lane/services/allowed") && method === "POST") {
|
|
await route.fulfill(json({
|
|
data: {
|
|
allowed_services: selfServe.laneAllowedServices,
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/self-serve/lane/relay/machine/enable") && method === "POST") {
|
|
await route.fulfill(json(selfServe.relayResponse));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/self-serve/lane/command") && method === "POST") {
|
|
await route.fulfill(json(selfServe.commandResponse));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/lanes/dynamic-image") && method === "GET") {
|
|
await route.fulfill(binary(selfServe.dynamicImage));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (pathname.endsWith("/departments") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{ id: 1, name: "Copenhagen" },
|
|
{ id: 2, name: "Odense" }
|
|
]
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (options.invoiceDistribution) {
|
|
const monthFromDate = parsedUrl.searchParams.get("dateFrom");
|
|
const monthNumber = monthFromDate ? Number(monthFromDate.split("-")[1]) : 1;
|
|
const monthBase = Number.isFinite(monthNumber) ? monthNumber * 10 : 10;
|
|
const forceDistributionLegacy = Boolean(options.invoiceDistributionForceLegacyFallback);
|
|
const forceCompareLegacy = Boolean(options.invoiceDistributionForceCompareFallback);
|
|
|
|
if (pathname.endsWith("/orders") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{
|
|
id: 1,
|
|
created_at: "2026-01-01T00:00:00.000Z"
|
|
}
|
|
]
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/invoicing/period") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
types: {
|
|
all: [
|
|
{
|
|
transactions: [
|
|
{ amount: 100 + monthBase, booked: true, excluded: false },
|
|
{ amount: 50 + monthBase, booked: true, excluded: false },
|
|
{ amount: 25, booked: false, excluded: false }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/invoicing/period/distribution/v2/all") && method === "GET") {
|
|
if (forceDistributionLegacy) {
|
|
await route.fulfill(json({ message: "v2 distribution temporarily unavailable" }, 500));
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
fixed_pricing: {
|
|
customers: [
|
|
{
|
|
id: 10,
|
|
customer_number: 1001,
|
|
customer_name: "Acme Transport",
|
|
requires_action: false,
|
|
transactions: [{ id: 1, date: "2026-01-10T00:00:00.000Z", amount: 80 + monthBase, booked: true, department_id: 1, excluded: false }],
|
|
meta: {
|
|
fixed_pricing: {
|
|
created_at: "2026-01-10T00:00:00.000Z",
|
|
price: 80 + monthBase,
|
|
original_price: 120 + monthBase,
|
|
department_totals_relative: { 1: 40 + monthBase, 2: 40 }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
collective_results: {
|
|
total_fixed_price: 80 + monthBase,
|
|
total_department_totals_relative_parsed: {
|
|
Copenhagen: 40 + monthBase,
|
|
Odense: 40
|
|
}
|
|
},
|
|
warnings: []
|
|
},
|
|
wash_subscriptions: {
|
|
customers: [
|
|
{
|
|
id: 11,
|
|
customer_number: 1002,
|
|
customer_name: "Nordic Haul",
|
|
requires_action: false,
|
|
transactions: [{ id: 2, date: "2026-01-05T00:00:00.000Z", amount: 40 + monthBase, booked: true, department_id: 1, excluded: false }],
|
|
meta: {
|
|
wash_subscription: {
|
|
created_at: "2026-01-05T00:00:00.000Z",
|
|
price: 40 + monthBase,
|
|
original_price: 55 + monthBase,
|
|
department_totals_relative: { 1: 20 + monthBase, 2: 20 }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
collective_results: {
|
|
total_subscription_price: 40 + monthBase,
|
|
subscription_price_department_distribution_parsed: {
|
|
Copenhagen: 20 + monthBase,
|
|
Odense: 20
|
|
}
|
|
},
|
|
warnings: []
|
|
},
|
|
customer_prices: {
|
|
customers: [
|
|
{
|
|
id: 12,
|
|
customer_number: 1003,
|
|
customer_name: "Discount Fleet",
|
|
requires_action: false,
|
|
transactions: [{ id: 3, date: "2026-01-08T00:00:00.000Z", amount: 15 + monthBase, booked: true, department_id: 2, excluded: false }],
|
|
meta: {
|
|
customer_price: {
|
|
created_at: "2026-01-08T00:00:00.000Z",
|
|
price: 15 + monthBase,
|
|
department_totals_relative: { 2: 15 + monthBase }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
collective_results: {
|
|
total_customer_price: 15 + monthBase,
|
|
customer_price_department_distribution_parsed: {
|
|
Odense: 15 + monthBase
|
|
}
|
|
},
|
|
warnings: []
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/invoicing/period/distribution/v2/fixed-pricing") && method === "GET") {
|
|
if (forceDistributionLegacy) {
|
|
await route.fulfill(json({ message: "v2 fixed pricing distribution temporarily unavailable" }, 500));
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
customers: [
|
|
{
|
|
id: 10,
|
|
customer_number: 1001,
|
|
customer_name: "Acme Transport",
|
|
requires_action: false,
|
|
transactions: [{ id: 1, date: "2026-01-10T00:00:00.000Z", amount: 80 + monthBase, booked: true, department_id: 1, excluded: false }],
|
|
meta: {
|
|
fixed_pricing: {
|
|
created_at: "2026-01-10T00:00:00.000Z",
|
|
price: 80 + monthBase,
|
|
original_price: 120 + monthBase,
|
|
department_totals_relative: { 1: 40 + monthBase, 2: 40 }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
collective_results: {
|
|
total_fixed_price: 80 + monthBase,
|
|
total_department_totals_relative_parsed: {
|
|
Copenhagen: 40 + monthBase,
|
|
Odense: 40
|
|
}
|
|
},
|
|
warnings: []
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/invoicing/period/distribution/v2/wash-subscriptions") && method === "GET") {
|
|
if (forceDistributionLegacy) {
|
|
await route.fulfill(json({ message: "v2 wash subscriptions distribution temporarily unavailable" }, 500));
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
customers: [
|
|
{
|
|
id: 11,
|
|
customer_number: 1002,
|
|
customer_name: "Nordic Haul",
|
|
requires_action: false,
|
|
transactions: [{ id: 2, date: "2026-01-05T00:00:00.000Z", amount: 40 + monthBase, booked: true, department_id: 1, excluded: false }],
|
|
meta: {
|
|
wash_subscription: {
|
|
created_at: "2026-01-05T00:00:00.000Z",
|
|
price: 40 + monthBase,
|
|
original_price: 55 + monthBase,
|
|
department_totals_relative: { 1: 20 + monthBase, 2: 20 }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
collective_results: {
|
|
total_subscription_price: 40 + monthBase,
|
|
subscription_price_department_distribution_parsed: {
|
|
Copenhagen: 20 + monthBase,
|
|
Odense: 20
|
|
}
|
|
},
|
|
warnings: []
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/invoicing/period/distribution/v2/customer-prices") && method === "GET") {
|
|
if (forceDistributionLegacy) {
|
|
await route.fulfill(json({ message: "v2 customer prices distribution temporarily unavailable" }, 500));
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
customers: [
|
|
{
|
|
id: 12,
|
|
customer_number: 1003,
|
|
customer_name: "Discount Fleet",
|
|
requires_action: false,
|
|
transactions: [{ id: 3, date: "2026-01-08T00:00:00.000Z", amount: 15 + monthBase, booked: true, department_id: 2, excluded: false }],
|
|
meta: {
|
|
customer_price: {
|
|
created_at: "2026-01-08T00:00:00.000Z",
|
|
price: 15 + monthBase,
|
|
department_totals_relative: { 2: 15 + monthBase }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
collective_results: {
|
|
total_customer_price: 15 + monthBase,
|
|
customer_price_department_distribution_parsed: {
|
|
Odense: 15 + monthBase
|
|
}
|
|
},
|
|
warnings: []
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/invoicing/period/distribution/fixed-pricing") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{
|
|
id: 10,
|
|
customer_number: 1001,
|
|
customer_name: "Acme Transport",
|
|
meta: {
|
|
fixed_pricing: {
|
|
created_at: "2026-01-10T00:00:00.000Z",
|
|
price: 80 + monthBase,
|
|
original_price: 120 + monthBase,
|
|
department_totals_relative: { 1: 40 + monthBase, 2: 40 }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
includes: {
|
|
collective_fixed_pricing_results: {
|
|
total_fixed_price: 80 + monthBase,
|
|
total_department_totals_relative_parsed: {
|
|
Copenhagen: 40 + monthBase,
|
|
Odense: 40
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/invoicing/period/distribution/wash-subscriptions") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{
|
|
id: 11,
|
|
customer_number: 1002,
|
|
customer_name: "Nordic Haul",
|
|
meta: {
|
|
wash_subscription: {
|
|
created_at: "2026-01-05T00:00:00.000Z",
|
|
price: 40 + monthBase,
|
|
original_price: 55 + monthBase,
|
|
department_totals_relative: { 1: 20 + monthBase, 2: 20 }
|
|
}
|
|
}
|
|
}
|
|
],
|
|
includes: {
|
|
collective_subscription_results: {
|
|
total_subscription_price: 40 + monthBase,
|
|
subscription_price_department_distribution_parsed: {
|
|
Copenhagen: 20 + monthBase,
|
|
Odense: 20
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/collected-invoices") && method === "GET") {
|
|
const invoices = [101, 102, 103, 104, 105, 106].map((invoiceId, index) => ({
|
|
id: invoiceId,
|
|
customer_number: 1001 + index,
|
|
customer_name: `Customer ${invoiceId}`,
|
|
total_net_amount: 100 + (index * 10)
|
|
}));
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: invoices
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/collected-invoices/economic/compare") && method === "GET") {
|
|
const invoiceId = Number(parsedUrl.searchParams.get("collected_invoice_id") || 0);
|
|
const mismatch = invoiceId === 101 || invoiceId === 103;
|
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
collected_invoice_id: invoiceId,
|
|
warnings: mismatch ? ["Line mismatch found"] : [],
|
|
internal_total: mismatch ? 150 : 120,
|
|
booked_total: mismatch ? 145 : 120,
|
|
draft_total: null,
|
|
difference: mismatch ? 5 : 0,
|
|
order_ids: mismatch ? [1, 2] : [3]
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/collected-invoices/economic/v2/compare/bulk") && method === "POST") {
|
|
if (forceCompareLegacy) {
|
|
await route.fulfill(json({ message: "v2 compare bulk temporarily unavailable" }, 500));
|
|
return;
|
|
}
|
|
|
|
const body = request.postDataJSON?.() || {};
|
|
const ids = Array.isArray(body.collected_invoice_ids) ? body.collected_invoice_ids : [];
|
|
const results = ids.map((invoiceId) => {
|
|
const mismatch = Number(invoiceId) === 101 || Number(invoiceId) === 103;
|
|
const internalTotal = mismatch ? 150 : 120;
|
|
const targetTotal = mismatch ? 145 : 120;
|
|
|
|
return {
|
|
collected_invoice_id: Number(invoiceId),
|
|
warnings: mismatch ? ["Line mismatch found"] : [],
|
|
details: {
|
|
order_ids: mismatch ? [1, 2] : [3],
|
|
customer: {
|
|
internal_customer_number: 4000 + Number(invoiceId),
|
|
name: `Customer ${invoiceId}`
|
|
},
|
|
internal: {
|
|
normalized: {
|
|
totals: {
|
|
net_total: internalTotal,
|
|
billable_line_count: mismatch ? 2 : 1
|
|
}
|
|
}
|
|
}
|
|
},
|
|
comparison: {
|
|
totals: {
|
|
internal_net_total: internalTotal
|
|
},
|
|
targets: {
|
|
booked: {
|
|
target: "booked",
|
|
status: mismatch ? "partial_mismatch" : "exact_match",
|
|
overall_match: !mismatch,
|
|
totals: {
|
|
target_net_total: targetTotal
|
|
},
|
|
mismatch_reasons: mismatch ? ["department_total_mismatch"] : [],
|
|
warnings: mismatch ? ["Line mismatch found"] : [],
|
|
lines: {
|
|
summary: {
|
|
internal_billable_count: mismatch ? 2 : 1,
|
|
target_billable_count: mismatch ? 2 : 1,
|
|
mismatch_count: mismatch ? 1 : 0
|
|
}
|
|
}
|
|
}
|
|
},
|
|
warnings: mismatch ? ["comparison warning"] : []
|
|
}
|
|
};
|
|
});
|
|
|
|
await route.fulfill(
|
|
json({
|
|
requested: ids.length,
|
|
compared: ids.length,
|
|
failed: 0,
|
|
results,
|
|
errors: []
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (options.fallbackPassthrough) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(json({ data: [] }));
|
|
});
|
|
}
|
|
|
|
export async function seedAuthenticatedState(page, token = "e2e-token") {
|
|
await page.addInitScript((value) => {
|
|
window.localStorage.setItem("token", value);
|
|
}, token);
|
|
}
|