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 = () => { 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; }"; (document.head || document.documentElement).appendChild(style); } const container = document.getElementById("__vue-devtools-container__"); if (container) { container.style.display = "none"; container.style.pointerEvents = "none"; } }; apply(); const observer = new MutationObserver(apply); observer.observe(document.documentElement, { childList: true, subtree: true }); }); } 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); }); });