- Introduced e2e test for "Change Invoice Collection" in `change-invoice-collection.spec.ts`. - Added unit tests for `CollectedOrderInvoiceOverview.vue` including edit flow for `closed_at`. - Implemented tests for queue reliability, state handling, and customer actions: - `CollectedOrderInvoicesQueueHistory.vue`: polling, error handling, richer diagnostics, and retry logic. - `InvoicingBillingPeriod` views: refresh and queue state handling. - Enhanced test coverage for Stripe queue functionality and related actions.
1234 lines
39 KiB
JavaScript
1234 lines
39 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
|
|
const API_HOST = /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
|
|
const DEFAULT_DEPARTMENT_ID = 1;
|
|
const DEFAULT_ORDER_ID = 9201;
|
|
const REGULAR_CUSTOMER_ID = 12345;
|
|
const CARD_CUSTOMER_ID = 999;
|
|
const MOBILE_PERMISSIONS = ["admin", "department_access_1"];
|
|
|
|
function json(body, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
function suppressVueDevtoolsOverlay(page) {
|
|
return page.addInitScript(() => {
|
|
const style = document.createElement("style");
|
|
style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
|
|
document.documentElement.appendChild(style);
|
|
});
|
|
}
|
|
|
|
async function primeSession(page, { token, permissions, sessionData = {} }) {
|
|
await seedAuthenticatedState(page, token);
|
|
await page.goto("/login");
|
|
await page.evaluate(async ({ sessionToken, sessionPermissions, data }) => {
|
|
const sessionModule = await import("/src/components/session/token/SessionUser.vue");
|
|
window.localStorage.setItem("token", sessionToken);
|
|
sessionModule.SessionUser.token.value = sessionToken;
|
|
sessionModule.SessionUser.authenticated.value = true;
|
|
sessionModule.SessionUser.permissions.value = sessionPermissions;
|
|
sessionModule.SessionUser.initiated.value = true;
|
|
sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
|
|
sessionModule.SessionUser.user.display_name.value = data.display_name ?? "POS Mobile Card";
|
|
}, {
|
|
sessionToken: token,
|
|
sessionPermissions: permissions,
|
|
data: sessionData,
|
|
});
|
|
}
|
|
|
|
function createPaymentIntent(orderId, {
|
|
status = "requires_capture",
|
|
amount = 123400,
|
|
readerId = "reader_online_1",
|
|
taxPercentage = 25,
|
|
} = {}) {
|
|
const isSucceeded = status === "succeeded";
|
|
return {
|
|
id: `pi_${orderId}`,
|
|
amount,
|
|
amount_capturable: isSucceeded ? 0 : amount,
|
|
amount_received: isSucceeded ? amount : 0,
|
|
currency: "dkk",
|
|
status,
|
|
payment_method_types: ["card_present"],
|
|
created: Math.floor(Date.now() / 1000),
|
|
metadata: {
|
|
order_id: String(orderId),
|
|
reader_id: String(readerId),
|
|
reader: String(readerId),
|
|
tax_percentage: String(taxPercentage),
|
|
},
|
|
};
|
|
}
|
|
|
|
function paymentIntentResponse(paymentIntent, extra = {}) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
payment_intent: paymentIntent || null,
|
|
has_payment_intent: !!paymentIntent,
|
|
...extra,
|
|
},
|
|
};
|
|
}
|
|
|
|
function createMobileCardFixture(overrides = {}) {
|
|
const baseFixture = {
|
|
cardCustomerId: CARD_CUSTOMER_ID,
|
|
regularCustomerId: REGULAR_CUSTOMER_ID,
|
|
readers: [
|
|
{
|
|
id: "reader_online_1",
|
|
label: "Mobile Reader",
|
|
status: "online",
|
|
action: null,
|
|
},
|
|
],
|
|
failureBudget: {
|
|
paymentIntentGet: 0,
|
|
paymentIntentCreate: 0,
|
|
paymentIntentCapture: 0,
|
|
},
|
|
requestCounters: {
|
|
readersGet: 0,
|
|
paymentIntentGet: 0,
|
|
paymentIntentCreate: 0,
|
|
paymentIntentDelete: 0,
|
|
paymentIntentCapture: 0,
|
|
orderItemsGet: 0,
|
|
orderItemsPost: 0,
|
|
markAsCompleted: 0,
|
|
},
|
|
customersByNumber: {
|
|
[REGULAR_CUSTOMER_ID]: {
|
|
id: 1,
|
|
customerNumber: REGULAR_CUSTOMER_ID,
|
|
name: "Pleno Logistics",
|
|
address: "Demo Street 1",
|
|
zip: "2630",
|
|
city: "Taastrup",
|
|
mobilePhone: "12345678",
|
|
email: "pos-mobile@example.com",
|
|
corporateIdentificationNumber: "12345678",
|
|
barred: false,
|
|
},
|
|
[CARD_CUSTOMER_ID]: {
|
|
id: 2,
|
|
customerNumber: CARD_CUSTOMER_ID,
|
|
name: "Card Terminal Customer",
|
|
address: "Card Street 9",
|
|
zip: "2630",
|
|
city: "Taastrup",
|
|
mobilePhone: "87654321",
|
|
email: "card-customer@example.com",
|
|
corporateIdentificationNumber: "99999999",
|
|
barred: false,
|
|
},
|
|
},
|
|
products: [
|
|
{
|
|
id: 53,
|
|
name: "Tankvogn med hænger",
|
|
description: "Mobile primary wash product",
|
|
price: 599,
|
|
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: "Dolly",
|
|
description: "Support product",
|
|
price: 275,
|
|
subscription_allowed: true,
|
|
category: 4,
|
|
piktogram: "truck",
|
|
apply_category_discount: true,
|
|
requires_note: false,
|
|
is_wash: true,
|
|
display_in_booking_form: true,
|
|
order_priority: 2,
|
|
addons: [],
|
|
},
|
|
],
|
|
departmentCategories: [
|
|
{
|
|
id: 11,
|
|
department_id: DEFAULT_DEPARTMENT_ID,
|
|
category: {
|
|
id: 4,
|
|
name: "Udvendig",
|
|
meta: {
|
|
products: [53, 63],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
ordersById: {
|
|
[DEFAULT_ORDER_ID]: {
|
|
id: DEFAULT_ORDER_ID,
|
|
customer_id: CARD_CUSTOMER_ID,
|
|
department_id: DEFAULT_DEPARTMENT_ID,
|
|
reference: "REF-9201",
|
|
notes: "",
|
|
reg_1: "AB12345",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
created_at: "2026-01-01T10:00:00.000Z",
|
|
},
|
|
},
|
|
orderItemsByOrderId: {
|
|
[DEFAULT_ORDER_ID]: [],
|
|
},
|
|
paymentIntentsByOrderId: {},
|
|
markCompletedOrderIds: [],
|
|
nextOrderId: 9300,
|
|
nextOrderItemId: 9800,
|
|
};
|
|
|
|
const fixture = {
|
|
...baseFixture,
|
|
...overrides,
|
|
failureBudget: {
|
|
...baseFixture.failureBudget,
|
|
...(overrides.failureBudget || {}),
|
|
},
|
|
requestCounters: {
|
|
...baseFixture.requestCounters,
|
|
...(overrides.requestCounters || {}),
|
|
},
|
|
customersByNumber: {
|
|
...baseFixture.customersByNumber,
|
|
...(overrides.customersByNumber || {}),
|
|
},
|
|
ordersById: {
|
|
...baseFixture.ordersById,
|
|
...(overrides.ordersById || {}),
|
|
},
|
|
orderItemsByOrderId: {
|
|
...baseFixture.orderItemsByOrderId,
|
|
...(overrides.orderItemsByOrderId || {}),
|
|
},
|
|
paymentIntentsByOrderId: {
|
|
...baseFixture.paymentIntentsByOrderId,
|
|
...(overrides.paymentIntentsByOrderId || {}),
|
|
},
|
|
};
|
|
|
|
return fixture;
|
|
}
|
|
|
|
function buildOrderItem(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 extractOrderId(request, parsedUrl) {
|
|
const queryId = Number(parsedUrl.searchParams.get("id"));
|
|
if (Number.isInteger(queryId) && queryId > 0) {
|
|
return queryId;
|
|
}
|
|
const body = request.postDataJSON?.() || {};
|
|
const bodyId = Number(body.id ?? body.order_id);
|
|
if (Number.isInteger(bodyId) && bodyId > 0) {
|
|
return bodyId;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function mockMobilePosAndStripeApi(page, fixture) {
|
|
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 (pathname.endsWith("/modules/stripe/department/terminal/readers") && method === "GET") {
|
|
fixture.requestCounters.readersGet += 1;
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
data: fixture.readers,
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "GET") {
|
|
fixture.requestCounters.paymentIntentGet += 1;
|
|
if (fixture.failureBudget.paymentIntentGet > 0) {
|
|
fixture.failureBudget.paymentIntentGet -= 1;
|
|
await route.fulfill(json({
|
|
success: false,
|
|
data: {
|
|
message: "Unable to load payment intent state",
|
|
},
|
|
}, 500));
|
|
return;
|
|
}
|
|
|
|
const orderId = extractOrderId(request, parsedUrl);
|
|
await route.fulfill(json(
|
|
paymentIntentResponse(orderId ? fixture.paymentIntentsByOrderId[orderId] || null : null, {
|
|
message: "No active payment intent for this order.",
|
|
}),
|
|
));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "POST") {
|
|
fixture.requestCounters.paymentIntentCreate += 1;
|
|
if (fixture.failureBudget.paymentIntentCreate > 0) {
|
|
fixture.failureBudget.paymentIntentCreate -= 1;
|
|
await route.fulfill(json({
|
|
success: false,
|
|
data: {
|
|
message: "Unable to create payment intent",
|
|
},
|
|
}, 500));
|
|
return;
|
|
}
|
|
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id);
|
|
const readerId = body.reader || "reader_online_1";
|
|
const existingIntent = fixture.paymentIntentsByOrderId[orderId] || null;
|
|
const paymentIntent = existingIntent || createPaymentIntent(orderId, {
|
|
status: "requires_capture",
|
|
readerId,
|
|
taxPercentage: Number(body.tax_percentage ?? 25),
|
|
});
|
|
paymentIntent.metadata = {
|
|
...(paymentIntent.metadata || {}),
|
|
order_id: String(orderId),
|
|
reader_id: String(readerId),
|
|
reader: String(readerId),
|
|
tax_percentage: String(body.tax_percentage ?? paymentIntent.metadata?.tax_percentage ?? 25),
|
|
};
|
|
fixture.paymentIntentsByOrderId[orderId] = paymentIntent;
|
|
await route.fulfill(json(
|
|
paymentIntentResponse(paymentIntent, {
|
|
reused: !!existingIntent,
|
|
}),
|
|
));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "DELETE") {
|
|
fixture.requestCounters.paymentIntentDelete += 1;
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id);
|
|
delete fixture.paymentIntentsByOrderId[orderId];
|
|
await route.fulfill(json(
|
|
paymentIntentResponse(null, {
|
|
cleared: true,
|
|
message: "Payment intent cleared successfully.",
|
|
}),
|
|
));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent/capture") && method === "POST") {
|
|
fixture.requestCounters.paymentIntentCapture += 1;
|
|
if (fixture.failureBudget.paymentIntentCapture > 0) {
|
|
fixture.failureBudget.paymentIntentCapture -= 1;
|
|
await route.fulfill(json({
|
|
success: false,
|
|
data: {
|
|
message: "Unable to capture payment intent",
|
|
},
|
|
}, 500));
|
|
return;
|
|
}
|
|
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id);
|
|
const currentIntent = fixture.paymentIntentsByOrderId[orderId] || createPaymentIntent(orderId, {
|
|
status: "requires_capture",
|
|
});
|
|
const succeededIntent = {
|
|
...currentIntent,
|
|
status: "succeeded",
|
|
amount_capturable: 0,
|
|
amount_received: Number(currentIntent.amount || 0),
|
|
};
|
|
fixture.paymentIntentsByOrderId[orderId] = succeededIntent;
|
|
await route.fulfill(json(paymentIntentResponse(succeededIntent)));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: [
|
|
{
|
|
id: DEFAULT_DEPARTMENT_ID,
|
|
name: "Taastrup",
|
|
},
|
|
],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments/categories") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: fixture.departmentCategories,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/bookings") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: [],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/vehicles") && method === "GET") {
|
|
const search = (parsedUrl.searchParams.get("search") || "").toUpperCase();
|
|
const vehicles = [
|
|
{
|
|
id: 7001,
|
|
reg: "AB12345",
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
customer_name: fixture.customersByNumber[REGULAR_CUSTOMER_ID]?.name || "Pleno Logistics",
|
|
type: 53,
|
|
status: "verified",
|
|
barred: false,
|
|
wash_subscription: false,
|
|
addons: {
|
|
enabled: 0,
|
|
available: 0,
|
|
list: [],
|
|
},
|
|
reference: "REF-AB12345",
|
|
last_order_id: null,
|
|
},
|
|
].filter((vehicle) => !search || vehicle.reg.includes(search));
|
|
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: vehicles,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: [],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/users/customer") && method === "GET") {
|
|
const customerNumber = Number(parsedUrl.searchParams.get("customer_number") || REGULAR_CUSTOMER_ID);
|
|
const customer = fixture.customersByNumber[customerNumber] || fixture.customersByNumber[REGULAR_CUSTOMER_ID];
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
customer_name: customer.name,
|
|
economic_customer: customer,
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customers") && method === "GET") {
|
|
const search = String(parsedUrl.searchParams.get("search") || "").toLowerCase();
|
|
const list = Object.values(fixture.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: list,
|
|
meta: {
|
|
pagination: {
|
|
page: 1,
|
|
limit: 10,
|
|
total: list.length,
|
|
},
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customer/notes") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: [],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customer/attributes") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: [],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/user/discounts") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: [],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/products") && method === "GET") {
|
|
const productId = parsedUrl.searchParams.get("id");
|
|
const category = parsedUrl.searchParams.get("category");
|
|
if (productId) {
|
|
const product = fixture.products.find((entry) => Number(entry.id) === Number(productId));
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: product || null,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
const list = category
|
|
? fixture.products.filter((entry) => Number(entry.category) === Number(category))
|
|
: fixture.products;
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: list,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "GET") {
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: [],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = fixture.nextOrderId++;
|
|
fixture.ordersById[orderId] = {
|
|
id: orderId,
|
|
customer_id: Number(body.customer_id),
|
|
department_id: Number(body.department_id),
|
|
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,
|
|
created_at: new Date().toISOString(),
|
|
};
|
|
fixture.orderItemsByOrderId[orderId] = [];
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
id: orderId,
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order") && method === "GET") {
|
|
const orderId = Number(parsedUrl.searchParams.get("id"));
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: fixture.ordersById[orderId] || null,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id);
|
|
if (fixture.ordersById[orderId]) {
|
|
if (body.field) {
|
|
fixture.ordersById[orderId][body.field] = body.value;
|
|
} else {
|
|
fixture.ordersById[orderId] = {
|
|
...fixture.ordersById[orderId],
|
|
...body,
|
|
};
|
|
}
|
|
}
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: fixture.ordersById[orderId] || null,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "GET") {
|
|
fixture.requestCounters.orderItemsGet += 1;
|
|
const orderId = Number(parsedUrl.searchParams.get("order_id"));
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: fixture.orderItemsByOrderId[orderId] || [],
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "POST") {
|
|
fixture.requestCounters.orderItemsPost += 1;
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.order_id);
|
|
const productId = Number(body.product_id);
|
|
const product = fixture.products.find((entry) => Number(entry.id) === productId);
|
|
|
|
if (!product || !fixture.ordersById[orderId]) {
|
|
await route.fulfill(json({
|
|
success: false,
|
|
data: {
|
|
message: "Order or product not found",
|
|
},
|
|
}, 422));
|
|
return;
|
|
}
|
|
|
|
const item = buildOrderItem(product, body, fixture.nextOrderItemId++);
|
|
if (!Array.isArray(fixture.orderItemsByOrderId[orderId])) {
|
|
fixture.orderItemsByOrderId[orderId] = [];
|
|
}
|
|
fixture.orderItemsByOrderId[orderId].push(item);
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: item,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || {};
|
|
const targetId = Number(body.id);
|
|
Object.keys(fixture.orderItemsByOrderId).forEach((orderIdKey) => {
|
|
fixture.orderItemsByOrderId[orderIdKey] = (fixture.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;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "DELETE") {
|
|
const targetId = Number(parsedUrl.searchParams.get("id"));
|
|
Object.keys(fixture.orderItemsByOrderId).forEach((orderIdKey) => {
|
|
fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).filter((item) => item.id !== targetId);
|
|
});
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: true,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/mark_as_completed") && method === "POST") {
|
|
fixture.requestCounters.markAsCompleted += 1;
|
|
const body = request.postDataJSON?.() || {};
|
|
const orderId = Number(body.id);
|
|
fixture.markCompletedOrderIds.push(orderId);
|
|
if (fixture.ordersById[orderId]) {
|
|
fixture.ordersById[orderId].completed_at = new Date().toISOString();
|
|
}
|
|
await route.fulfill(json({
|
|
success: true,
|
|
data: {
|
|
id: orderId,
|
|
},
|
|
}));
|
|
return;
|
|
}
|
|
|
|
await route.fallback();
|
|
});
|
|
}
|
|
|
|
function buildMobilePosState({
|
|
customerId = CARD_CUSTOMER_ID,
|
|
reg = "AB12345",
|
|
reference = "REF-9201",
|
|
includePrimaryItem = true,
|
|
} = {}) {
|
|
return {
|
|
vehicles: {
|
|
vehicle_1: {
|
|
reg,
|
|
customer_id: customerId,
|
|
type: 53,
|
|
status: "verified",
|
|
barred: false,
|
|
booking_id: null,
|
|
addons: [],
|
|
reference,
|
|
},
|
|
vehicle_2: null,
|
|
vehicle_3: null,
|
|
activeVehicleIndex: 1,
|
|
},
|
|
views: {
|
|
manualInput: false,
|
|
vehicleSelection: false,
|
|
additionalItemSelection: false,
|
|
transactionHistoryView: false,
|
|
},
|
|
transactionItems: {
|
|
primaryItem: includePrimaryItem ? {
|
|
id: 53,
|
|
name: "Tankvogn med hænger",
|
|
description: "Mobile primary wash product",
|
|
price: 599,
|
|
category: 4,
|
|
is_wash: true,
|
|
addons: [],
|
|
} : null,
|
|
additionalItems: [],
|
|
},
|
|
categories: {
|
|
list: [],
|
|
selected: null,
|
|
},
|
|
productList: {
|
|
list: [],
|
|
},
|
|
metadata: {
|
|
customerId,
|
|
notes: "",
|
|
reference,
|
|
washId: null,
|
|
bookingId: null,
|
|
laneId: null,
|
|
},
|
|
attachments: {
|
|
files: [],
|
|
base64: [],
|
|
wash_certificate: false,
|
|
},
|
|
transactionHistory: [],
|
|
lastVehicleOrders: {
|
|
vehicle_1: null,
|
|
vehicle_2: null,
|
|
vehicle_3: null,
|
|
},
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
function seedMobilePosState(page, options = {}) {
|
|
const snapshot = buildMobilePosState(options);
|
|
return page.addInitScript((payload) => {
|
|
window.localStorage.removeItem("pos_order_id");
|
|
window.localStorage.setItem("pos", JSON.stringify(payload));
|
|
}, snapshot);
|
|
}
|
|
|
|
function getStoredPosSnapshot(page) {
|
|
return page.evaluate(() => {
|
|
const value = window.localStorage.getItem("pos");
|
|
return value ? JSON.parse(value) : null;
|
|
});
|
|
}
|
|
|
|
async function gotoPos(page, {
|
|
step = 1,
|
|
orderId = null,
|
|
customerId = null,
|
|
departmentId = DEFAULT_DEPARTMENT_ID,
|
|
} = {}) {
|
|
const params = new URLSearchParams();
|
|
if (orderId !== null && orderId !== undefined) {
|
|
params.set("id", String(orderId));
|
|
}
|
|
if (customerId !== null && customerId !== undefined) {
|
|
params.set("customer_id", String(customerId));
|
|
}
|
|
if (step !== null && step !== undefined) {
|
|
params.set("step", String(step));
|
|
}
|
|
|
|
const queryString = params.toString();
|
|
const route = `/admin/${departmentId}/modules/pos${queryString ? `?${queryString}` : ""}`;
|
|
await page.goto(route);
|
|
}
|
|
|
|
async function setupMobileCardPage(page, fixture, {
|
|
token = "pos-mobile-card-token",
|
|
seedState = {},
|
|
route = {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
} = {}) {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: MOBILE_PERMISSIONS,
|
|
});
|
|
await mockMobilePosAndStripeApi(page, fixture);
|
|
await seedMobilePosState(page, seedState);
|
|
await primeSession(page, {
|
|
token,
|
|
permissions: MOBILE_PERMISSIONS,
|
|
sessionData: {
|
|
display_name: "POS Mobile Card E2E",
|
|
},
|
|
});
|
|
await gotoPos(page, route);
|
|
}
|
|
|
|
test.describe("POS mobile card payments", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(testInfo.project.name !== "chromium-mobile", "Mobile card payment suite is scoped to chromium-mobile.");
|
|
await suppressVueDevtoolsOverlay(page);
|
|
});
|
|
|
|
test("card mode entry sets customer 999 and preserves order context", async ({ page }) => {
|
|
const fixture = createMobileCardFixture();
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-entry-token",
|
|
seedState: {
|
|
customerId: null,
|
|
reg: "ZZ00000",
|
|
reference: "CARD-CTX-REF",
|
|
includePrimaryItem: false,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect(page.getByTestId("pos-mobile-direct-card-payment")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-direct-card-payment").click();
|
|
await expect(page.locator(".popup-container")).toBeHidden({ timeout: 10_000 });
|
|
|
|
await expect.poll(async () => {
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
return snapshot?.metadata?.customerId ?? null;
|
|
}, { timeout: 10_000 }).toBe(CARD_CUSTOMER_ID);
|
|
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
expect(snapshot?.vehicles?.vehicle_1?.reg).toBe("ZZ00000");
|
|
expect(snapshot?.metadata?.reference).toBe("CARD-CTX-REF");
|
|
});
|
|
|
|
test("step 2 transitions to mobile card payment step 3 for customer 999", async ({ page }) => {
|
|
const fixture = createMobileCardFixture();
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-step-transition-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page).toHaveURL(/step=3/);
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(0);
|
|
});
|
|
|
|
test("reader unavailable state is recoverable and non-fatal", async ({ page }) => {
|
|
const fixture = createMobileCardFixture({
|
|
readers: [],
|
|
});
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-reader-unavailable-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-no-readers")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-reader-unavailable-message")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toHaveCount(0);
|
|
await expect(page.getByTestId("pos-stripe-error-message")).toHaveCount(0);
|
|
|
|
fixture.readers = [
|
|
{
|
|
id: "reader_online_2",
|
|
label: "Recovered Reader",
|
|
status: "online",
|
|
action: null,
|
|
},
|
|
];
|
|
|
|
await page.getByTestId("pos-stripe-no-readers").click();
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-selected-reader")).toContainText("Recovered Reader");
|
|
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
|
|
});
|
|
|
|
test("reader available creates intent once and reaches capture state", async ({ page }) => {
|
|
const fixture = createMobileCardFixture();
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-create-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled();
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(0);
|
|
});
|
|
|
|
test("step 3 back offers resume or cancel when payment is active", async ({ page }) => {
|
|
const fixture = createMobileCardFixture();
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-cancel-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-mobile-step-3-back").click();
|
|
await expect(page.locator(".swal2-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.locator(".swal2-confirm").click();
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
expect(fixture.requestCounters.paymentIntentDelete).toBe(0);
|
|
|
|
await page.getByTestId("pos-mobile-step-3-back").click();
|
|
await expect(page.locator(".swal2-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.locator(".swal2-deny").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentDelete, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page).toHaveURL(/step=2/);
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(0);
|
|
});
|
|
|
|
test("capture success completes once and resets scanner flow", async ({ page }) => {
|
|
const fixture = createMobileCardFixture();
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-capture-success-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-stripe-capture-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
|
|
await page.waitForTimeout(1000);
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(1);
|
|
|
|
await expect.poll(() => {
|
|
const currentUrl = new URL(page.url());
|
|
return currentUrl.searchParams.get("step");
|
|
}, { timeout: 12_000 }).toBe("1");
|
|
|
|
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("guardrail: completion never runs before capture success", async ({ page }) => {
|
|
const fixture = createMobileCardFixture();
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-guardrail-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 2_500 }).toBe(0);
|
|
});
|
|
|
|
test("step 3 bootstrap recovery fetches existing requires_capture intent", async ({ page }) => {
|
|
const fixture = createMobileCardFixture({
|
|
paymentIntentsByOrderId: {
|
|
[DEFAULT_ORDER_ID]: createPaymentIntent(DEFAULT_ORDER_ID, {
|
|
status: "requires_capture",
|
|
}),
|
|
},
|
|
});
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-recovery-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentGet, { timeout: 10_000 }).toBeGreaterThan(0);
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
|
|
});
|
|
|
|
test("step 3 bootstrap recovery completes already succeeded intent once", async ({ page }) => {
|
|
const fixture = createMobileCardFixture({
|
|
paymentIntentsByOrderId: {
|
|
[DEFAULT_ORDER_ID]: createPaymentIntent(DEFAULT_ORDER_ID, {
|
|
status: "succeeded",
|
|
}),
|
|
},
|
|
});
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-succeeded-recovery-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentGet, { timeout: 10_000 }).toBeGreaterThan(0);
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
|
|
await page.waitForTimeout(1000);
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(1);
|
|
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
|
|
expect(fixture.requestCounters.paymentIntentCapture).toBe(0);
|
|
|
|
await expect.poll(() => {
|
|
const currentUrl = new URL(page.url());
|
|
return currentUrl.searchParams.get("step");
|
|
}, { timeout: 12_000 }).toBe("1");
|
|
});
|
|
|
|
test("returning to step 2 and re-entering step 3 does not duplicate order items", async ({ page }) => {
|
|
const fixture = createMobileCardFixture();
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-step-reentry-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-mobile-step-3-back").click();
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.waitForTimeout(2100);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.requestCounters.orderItemsGet, { timeout: 10_000 }).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("payment-intent get/create/capture failures surface retryable error states", async ({ page }) => {
|
|
const fixture = createMobileCardFixture({
|
|
failureBudget: {
|
|
paymentIntentGet: 0,
|
|
paymentIntentCreate: 2,
|
|
paymentIntentCapture: 2,
|
|
},
|
|
});
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-card-error-recovery-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(2);
|
|
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
|
|
|
|
fixture.failureBudget.paymentIntentGet = 2;
|
|
await page.getByTestId("pos-stripe-error").click();
|
|
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-error").click();
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(3);
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-stripe-capture-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(2);
|
|
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-error").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-stripe-capture-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(3);
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
|
|
});
|
|
|
|
test("regression: non-card mobile customer still follows step-2 complete/reset path", async ({ page }) => {
|
|
const fixture = createMobileCardFixture({
|
|
ordersById: {
|
|
[DEFAULT_ORDER_ID]: {
|
|
id: DEFAULT_ORDER_ID,
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
department_id: DEFAULT_DEPARTMENT_ID,
|
|
reference: "REF-9201",
|
|
notes: "",
|
|
reg_1: "AB12345",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
created_at: "2026-01-01T10:00:00.000Z",
|
|
},
|
|
},
|
|
});
|
|
await setupMobileCardPage(page, fixture, {
|
|
token: "mobile-non-card-regression-token",
|
|
seedState: {
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: REGULAR_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
|
|
await expect.poll(() => {
|
|
const currentUrl = new URL(page.url());
|
|
return currentUrl.searchParams.get("step");
|
|
}, { timeout: 12_000 }).toBe("1");
|
|
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(0);
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toHaveCount(0);
|
|
});
|
|
});
|