- 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.
512 lines
14 KiB
JavaScript
512 lines
14 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi } from "./support/network.js";
|
|
|
|
function json(body, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
function apiMessage(message, status = 422) {
|
|
return json(
|
|
{
|
|
data: {
|
|
message,
|
|
},
|
|
},
|
|
status
|
|
);
|
|
}
|
|
|
|
async function suppressVueDevtoolsOverlay(page) {
|
|
await page.addInitScript(() => {
|
|
const STYLE_ID = "__e2e-hide-vue-devtools";
|
|
|
|
const apply = () => {
|
|
const target = document.head || document.documentElement;
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
if (!document.getElementById(STYLE_ID)) {
|
|
const style = document.createElement("style");
|
|
style.id = STYLE_ID;
|
|
style.textContent =
|
|
"#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
|
|
target.appendChild(style);
|
|
}
|
|
|
|
const container = document.getElementById("__vue-devtools-container__");
|
|
if (container) {
|
|
container.style.display = "none";
|
|
container.style.pointerEvents = "none";
|
|
}
|
|
};
|
|
|
|
apply();
|
|
const startObserving = () => {
|
|
if (!document.documentElement) {
|
|
requestAnimationFrame(startObserving);
|
|
return;
|
|
}
|
|
|
|
const observer = new MutationObserver(apply);
|
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
};
|
|
|
|
startObserving();
|
|
});
|
|
}
|
|
|
|
async function openHarness(page) {
|
|
await page.goto("/__e2e/economic-queue");
|
|
await expect(page.getByTestId("economic-queue-harness-page")).toBeVisible();
|
|
}
|
|
|
|
test.describe("Economic queue async export workflow", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await mockApi(page);
|
|
});
|
|
|
|
test("invoice export enqueues, polls, completes, and stops polling in terminal state", async ({ page }) => {
|
|
let statusCalls = 0;
|
|
|
|
await page.route("**/economic/invoice/export", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
data: {
|
|
job: {
|
|
id: 1101,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued export",
|
|
},
|
|
},
|
|
},
|
|
202
|
|
)
|
|
);
|
|
});
|
|
|
|
await page.route("**/economic/invoice/export/status**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
statusCalls += 1;
|
|
if (statusCalls === 1) {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1101,
|
|
status: "PROCESSING",
|
|
progress_percent: 40,
|
|
progress_message: "Preparing invoice payload",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1101,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Completed",
|
|
result: {
|
|
message: "Invoice export completed by queue worker.",
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openHarness(page);
|
|
|
|
const submitButton = page.getByTestId("economic-invoice-export-submit");
|
|
await submitButton.click();
|
|
await expect(submitButton).toBeDisabled();
|
|
await expect(page.getByTestId("economic-invoice-export-progress")).toContainText("Preparing invoice payload");
|
|
|
|
await expect(page.getByTestId("economic-invoice-export-completed")).toContainText(
|
|
"Invoice export completed by queue worker."
|
|
);
|
|
await expect(page.getByTestId("economic-invoice-export-completed")).toBeVisible();
|
|
|
|
const terminalCallCount = statusCalls;
|
|
await page.waitForTimeout(2300);
|
|
expect(statusCalls).toBe(terminalCallCount);
|
|
});
|
|
|
|
test("invoice export shows queue failure message and supports retry to completion", async ({ page }) => {
|
|
let phase = "initial";
|
|
let retryStatusCalls = 0;
|
|
|
|
await page.route("**/economic/invoice/export", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
data: {
|
|
job: {
|
|
id: 1201,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued export",
|
|
},
|
|
},
|
|
},
|
|
202
|
|
)
|
|
);
|
|
});
|
|
|
|
await page.route("**/economic/invoice/export/retry", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
phase = "retry";
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
data: {
|
|
job: {
|
|
id: 1201,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Retry queued",
|
|
},
|
|
},
|
|
},
|
|
200
|
|
)
|
|
);
|
|
});
|
|
|
|
await page.route("**/economic/invoice/export/status**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
if (phase === "initial") {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1201,
|
|
status: "FAILED",
|
|
progress_percent: 10,
|
|
progress_message: "Failed",
|
|
error_message: "no billable order items after zero-price/zero-quantity skips",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
retryStatusCalls += 1;
|
|
if (retryStatusCalls === 1) {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1201,
|
|
status: "PROCESSING",
|
|
progress_percent: 70,
|
|
progress_message: "Retry processing",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1201,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Retry completed",
|
|
result: {
|
|
message: "Invoice export completed after retry.",
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openHarness(page);
|
|
|
|
await page.getByTestId("economic-invoice-export-submit").click();
|
|
await expect(page.getByTestId("economic-invoice-export-failed")).toContainText(
|
|
"no billable order items after zero-price/zero-quantity skips"
|
|
);
|
|
|
|
await page.getByTestId("economic-invoice-export-retry").click();
|
|
await expect(page.getByTestId("economic-invoice-export-progress")).toContainText("Retry processing");
|
|
await expect(page.getByTestId("economic-invoice-export-completed")).toContainText(
|
|
"Invoice export completed after retry."
|
|
);
|
|
});
|
|
|
|
test("draft export surfaces API transport errors separately from queue failure state", async ({ page }) => {
|
|
await page.route("**/economic/invoice/draft/export", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(apiMessage("order_id is invalid", 422));
|
|
});
|
|
|
|
await openHarness(page);
|
|
|
|
await page.getByTestId("economic-draft-export-submit").click();
|
|
await expect(page.getByTestId("economic-draft-export-transport-error")).toContainText("order_id is invalid");
|
|
await expect(page.getByTestId("economic-draft-export-failed")).toHaveCount(0);
|
|
});
|
|
|
|
test("collected invoice create action enqueues and completes with returned result", async ({ page }) => {
|
|
let statusCalls = 0;
|
|
|
|
await page.route("**/collected-invoices/economic", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const payload = route.request().postDataJSON();
|
|
expect(payload).toMatchObject({
|
|
id: 6001,
|
|
send_as_is: false,
|
|
});
|
|
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
data: {
|
|
job: {
|
|
id: 1301,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Collected invoice queued",
|
|
},
|
|
},
|
|
},
|
|
202
|
|
)
|
|
);
|
|
});
|
|
|
|
await page.route("**/collected-invoices/economic/queue/status**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
statusCalls += 1;
|
|
if (statusCalls === 1) {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1301,
|
|
status: "PROCESSING",
|
|
progress_percent: 55,
|
|
progress_message: "Building e-conomic payload",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1301,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Completed",
|
|
result: {
|
|
message: "Collected invoice exported.",
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openHarness(page);
|
|
|
|
await page.getByTestId("collected-economic-create-invoice").click();
|
|
await expect(page.getByTestId("collected-economic-progress")).toContainText("Building e-conomic payload");
|
|
await expect(page.getByTestId("collected-economic-completed")).toContainText("Collected invoice exported.");
|
|
});
|
|
|
|
test("collected invoice create accepts enqueue responses with data.job_id alias", async ({ page }) => {
|
|
let statusCalls = 0;
|
|
|
|
await page.route("**/collected-invoices/economic", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
data: {
|
|
message: "Queued collected invoice export",
|
|
job_id: 1302,
|
|
job: {
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Collected invoice queued",
|
|
},
|
|
},
|
|
},
|
|
202
|
|
)
|
|
);
|
|
});
|
|
|
|
await page.route("**/collected-invoices/economic/queue/status**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
statusCalls += 1;
|
|
if (statusCalls === 1) {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1302,
|
|
status: "PROCESSING",
|
|
progress_percent: 65,
|
|
progress_message: "Creating draft invoice",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1302,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Completed",
|
|
result: {
|
|
message: "Collected invoice exported from alias response.",
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openHarness(page);
|
|
|
|
await page.getByTestId("collected-economic-create-invoice").click();
|
|
await expect(page.getByTestId("collected-economic-progress")).toContainText("Creating draft invoice");
|
|
await expect(page.getByTestId("collected-economic-completed")).toContainText(
|
|
"Collected invoice exported from alias response."
|
|
);
|
|
});
|
|
|
|
test("invalid boolean send_as_is is blocked and shown as validation error", async ({ page }) => {
|
|
let enqueueCalls = 0;
|
|
|
|
await page.route("**/collected-invoices/economic", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
enqueueCalls += 1;
|
|
await route.fulfill(apiMessage("This endpoint should not be called for invalid boolean.", 500));
|
|
});
|
|
|
|
await openHarness(page);
|
|
|
|
await page.getByTestId("economic-queue-harness-invalid-boolean-trigger").click();
|
|
await expect(page.getByTestId("economic-queue-harness-invalid-boolean-error")).toContainText(
|
|
"send_as_is must be a boolean."
|
|
);
|
|
expect(enqueueCalls).toBe(0);
|
|
});
|
|
|
|
test("invoice export polling is cancelled when component unmounts", async ({ page }) => {
|
|
let statusCalls = 0;
|
|
|
|
await page.route("**/economic/invoice/export", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
data: {
|
|
job: {
|
|
id: 1401,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued export",
|
|
},
|
|
},
|
|
},
|
|
202
|
|
)
|
|
);
|
|
});
|
|
|
|
await page.route("**/economic/invoice/export/status**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
statusCalls += 1;
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 1401,
|
|
status: "PROCESSING",
|
|
progress_percent: 25,
|
|
progress_message: "Still processing",
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openHarness(page);
|
|
|
|
await page.getByTestId("economic-invoice-export-submit").click();
|
|
await expect.poll(() => statusCalls).toBeGreaterThanOrEqual(1);
|
|
|
|
await page.getByTestId("economic-queue-harness-toggle-invoice-export").click();
|
|
const statusCallsAtUnmount = statusCalls;
|
|
|
|
await page.waitForTimeout(2600);
|
|
expect(statusCalls).toBe(statusCallsAtUnmount);
|
|
});
|
|
});
|