Introduce completeBookingCreationFlow utility and refactor booking flow tests:

- Added `completeBookingCreationFlow` utility to streamline booking creation logic in `bookingFlow.ts`.
- Refactored `userBookings.spec.ts` to replace inline booking creation steps with the new utility for improved readability and consistency.
- Applied `data-testid` attributes across booking-related components for enhanced testability and test coverage.
- Enhanced Playwright navigation error handling with `isBenignNavigationError` and `settleAuthenticatedNavigation` utilities to improve resilience and test robustness.
This commit is contained in:
Jeppe Bundgaard
2026-04-13 13:39:48 +02:00
parent 828e2fd312
commit ade565cbca
28 changed files with 726 additions and 538 deletions
+132 -54
View File
@@ -70,6 +70,17 @@ function normalizeCreatedAtValue(value) {
return normalized;
}
function normalizeRegistrationValue(value) {
if (value === null || value === undefined) {
return "";
}
return String(value)
.trim()
.toUpperCase()
.replace(/[^A-Z0-9]/g, "");
}
function normalizeIncludeInInvoiceValue(value) {
if (value === null || value === undefined || value === "" || value === "use_department" || value === "null") {
return null;
@@ -807,13 +818,39 @@ export function createPosFixture(overrides = {}) {
[defaultCustomer.customerNumber]: defaultCustomer,
[cardCustomer.customerNumber]: cardCustomer,
},
collectedInvoices: [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,
})),
nextCollectedInvoiceId: 200,
collectedInvoices: [
{
id: 101,
customer_number: defaultCustomer.customerNumber,
customer_name: defaultCustomer.name,
total_net_amount: 100,
created_at: "2026-04-10",
closed_at: "2026-04-10",
},
{
id: 200,
customer_number: cardCustomer.customerNumber,
customer_name: cardCustomer.name,
total_net_amount: 250,
created_at: "2026-04-30",
closed_at: "2026-04-30",
},
{
id: 201,
customer_number: cardCustomer.customerNumber,
customer_name: cardCustomer.name,
total_net_amount: 325,
created_at: "2026-05-01",
closed_at: "2026-05-01",
},
...[102, 103, 104, 105, 106].map((invoiceId, index) => ({
id: invoiceId,
customer_number: 1002 + index,
customer_name: `Customer ${invoiceId}`,
total_net_amount: 110 + index * 10,
})),
],
nextCollectedInvoiceId: 300,
customerAttributesByNumber: {
[defaultCustomer.customerNumber]: [
{ id: 1, customer_number: defaultCustomer.customerNumber, attribute: "invoiceAllOrdersIndividually" },
@@ -1298,9 +1335,9 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
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 || "",
reg_1: normalizeRegistrationValue(body.reg_1),
reg_2: normalizeRegistrationValue(body.reg_2),
reg_3: normalizeRegistrationValue(body.reg_3),
invoice_collection_id: null,
booking_id: null,
completed_at: null,
@@ -1328,6 +1365,15 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
posFixture.ordersById[orderId] = {
...posFixture.ordersById[orderId],
...body,
...(Object.prototype.hasOwnProperty.call(body, "reg_1")
? { reg_1: normalizeRegistrationValue(body.reg_1) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "reg_2")
? { reg_2: normalizeRegistrationValue(body.reg_2) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "reg_3")
? { reg_3: normalizeRegistrationValue(body.reg_3) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "created_at")
? { created_at: normalizeCreatedAtValue(body.created_at) }
: {}),
@@ -1350,6 +1396,8 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
posFixture.ordersById[orderId][body.field] =
body.field === "include_in_invoice"
? normalizeIncludeInInvoiceValue(body.value)
: body.field === "reg_1" || body.field === "reg_2" || body.field === "reg_3"
? normalizeRegistrationValue(body.value)
: body.field === "created_at"
? normalizeCreatedAtValue(body.value)
: body.value;
@@ -1357,6 +1405,15 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
posFixture.ordersById[orderId] = {
...posFixture.ordersById[orderId],
...body,
...(Object.prototype.hasOwnProperty.call(body, "reg_1")
? { reg_1: normalizeRegistrationValue(body.reg_1) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "reg_2")
? { reg_2: normalizeRegistrationValue(body.reg_2) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "reg_3")
? { reg_3: normalizeRegistrationValue(body.reg_3) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "created_at")
? { created_at: normalizeCreatedAtValue(body.created_at) }
: {}),
@@ -1936,7 +1993,10 @@ async function handleEdgeGatewayRoute({
}
export async function mockApi(page, options = {}) {
const posFixture = options.pos ? createPosFixture(options.pos === true ? {} : options.pos) : null;
const shouldMockPosFixture = Boolean(options.pos || options.invoiceDistribution);
const posFixture = shouldMockPosFixture
? createPosFixture(options.pos && options.pos !== true ? options.pos : {})
: null;
const selfServe = options.selfServe
? createSelfServeFixture(options.selfServe === true ? {} : options.selfServe)
: null;
@@ -2402,6 +2462,61 @@ export async function mockApi(page, options = {}) {
return;
}
if (pathname.endsWith("/collected-invoices") && method === "GET") {
const invoiceFixture = posFixture || { collectedInvoices: [] };
const filters = parseFilterExpressions(parsedUrl.searchParams.get("filters") || "");
const customerNumberFilter = Number(filters.customer_number || 0);
const collectedInvoices = (invoiceFixture.collectedInvoices || []).filter((invoice) => {
if (!customerNumberFilter) {
return true;
}
return Number(invoice.customer_number) === customerNumberFilter;
});
await route.fulfill(
json({
data: collectedInvoices,
})
);
return;
}
if (pathname.endsWith("/collected-invoices") && method === "POST") {
const invoiceFixture = posFixture || {
collectedInvoices: [],
customersByNumber: {},
nextCollectedInvoiceId: 200,
};
const body = request.postDataJSON?.() || {};
const invoiceId = Number(invoiceFixture.nextCollectedInvoiceId || 200);
const customerNumber = Number(body.customer_number || 0);
const customer = invoiceFixture.customersByNumber?.[customerNumber] || null;
invoiceFixture.nextCollectedInvoiceId = invoiceId + 1;
invoiceFixture.collectedInvoices = [
{
id: invoiceId,
customer_number: customerNumber,
customer_name: customer?.name || `Customer ${invoiceId}`,
total_net_amount: 0,
created_at: body.closed_at || toSqlDateTime().slice(0, 10),
closed_at: body.closed_at || null,
},
...(invoiceFixture.collectedInvoices || []),
];
await route.fulfill(
json({
success: true,
data: {
id: invoiceId,
},
})
);
return;
}
if (options.invoiceDistribution) {
const monthFromDate = parsedUrl.searchParams.get("dateFrom");
const monthNumber = monthFromDate ? Number(monthFromDate.split("-")[1]) : 1;
@@ -2766,45 +2881,6 @@ export async function mockApi(page, options = {}) {
return;
}
if (pathname.endsWith("/collected-invoices") && method === "GET") {
await route.fulfill(
json({
data: posFixture.collectedInvoices || [],
})
);
return;
}
if (pathname.endsWith("/collected-invoices") && method === "POST") {
const body = request.postDataJSON?.() || {};
const invoiceId = Number(posFixture.nextCollectedInvoiceId || 200);
const customerNumber = Number(body.customer_number || 0);
const customer = posFixture.customersByNumber?.[customerNumber] || null;
posFixture.nextCollectedInvoiceId = invoiceId + 1;
posFixture.collectedInvoices = [
{
id: invoiceId,
customer_number: customerNumber,
customer_name: customer?.name || `Customer ${invoiceId}`,
total_net_amount: 0,
created_at: body.closed_at || toSqlDateTime().slice(0, 10),
closed_at: body.closed_at || null,
},
...(posFixture.collectedInvoices || []),
];
await route.fulfill(
json({
success: true,
data: {
id: invoiceId,
},
})
);
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;
@@ -2912,11 +2988,13 @@ export async function seedAuthenticatedState(page, token = "e2e-token") {
}, token);
}
export async function primeMockSession(page, { token = "e2e-token", bootPath = "/login" } = {}) {
export async function primeMockSession(page, { token = "e2e-token", bootPath = "/redirect" } = {}) {
await seedAuthenticatedState(page, token);
const sessionRequest = page.waitForResponse((response) => {
return response.request().method() === "GET" && response.url().includes("/auth/session");
});
const sessionRequest = page
.waitForResponse((response) => {
return response.request().method() === "GET" && response.url().includes("/auth/session");
}, { timeout: 10_000 })
.catch(() => null);
await page.goto(bootPath);
await sessionRequest;
}