import { type Page } from "@playwright/test"; import { test, expect } from "../fixtures/sales-order-lifecycle"; import { SalesOrderCreateFormPage } from "../pages/sales-order-create-form"; import { SalesOrderDetailPage } from "../pages/sales-order-detail"; import { SalesOrderListPage } from "../pages/sales-order-list"; /** * E2E coverage for the `sales-order-lifecycle` business flow, driven end to end * through the live frontend by the sales agent (an account that holds both the * Sales Rep and Approver capabilities — separation of duties is not enforced at * the application level, per the actor docs). * * Each test walks the flow diagram's main path as a single journey, asserting * the visible status outcomes at every hand-off between the two roles. */ const ORDER_QTY = "10"; const UNIT_PRICE = "25"; const SHIPPING_ADDRESS = "1-1-1 Chiyoda, Chiyoda-ku, Tokyo 100-0001, JP"; const DETAIL_URL = /\/sales\/sales-order\/(?!create$)[^/]+$/; /** * Create a DRAFT sales order with a single priced line and a shipping address, * then land on its detail page. Returns the sales order id resolved from the * detail URL. * * The create form calls `navigate("../")` on success, which resolves * relative to the AppShell route tree. Depending on how the router nests * routes, this may land directly on the detail page OR navigate to the list * page first. We handle both outcomes: if we land on the list we navigate to * the most recently created order (top row, sorted createdAt Desc). */ async function createDraftSalesOrder(page: Page): Promise { const createForm = new SalesOrderCreateFormPage(page); await createForm.goto(); await expect(createForm.customerCombobox).toBeVisible(); await createForm.selectFirstCustomer(); await createForm.fillShippingAddress(SHIPPING_ADDRESS); await createForm.selectFirstLineItem(0); await createForm.setQuantity(0, ORDER_QTY); await createForm.setUnitPrice(0, UNIT_PRICE); await createForm.submit(); const listPage = new SalesOrderListPage(page); const detailPage = new SalesOrderDetailPage(page); // Wait for navigation away from the create form: either the detail heading // or the list heading will appear first. The create form uses a relative // `navigate("../")` that may resolve to the detail page or, if the // AppShell route nesting treats the `create` segment differently, to the // list page. await expect(detailPage.heading.or(listPage.heading)).toBeVisible(); if (await listPage.heading.isVisible()) { // Landed on the list — click the first View link (most recently created // order is at the top because the list is sorted by createdAt Desc). const firstViewLink = listPage.table.getByRole("link", { name: /^view$/i }).first(); await expect(firstViewLink).toBeVisible(); await firstViewLink.click(); await page.waitForURL(DETAIL_URL); await expect(detailPage.heading).toBeVisible(); } const id = new URL(page.url()).pathname.split("/").pop(); if (!id) { throw new Error("Could not resolve sales order id from the detail URL"); } return id; } // Flow main path: Create (DRAFT) -> Submit (SUBMITTED) -> Confirm (CONFIRMED). // Exercises both actors on one order end to end. The flow's final Close step is // out of scope: `closeSalesOrder` needs every line shipped and invoiced first, // and those steps are journeys of the outbound-shipment-processing and // receivables flows, each with its own fixtures, rather than of this one. test("sales agent creates, submits, and confirms a sales order", async ({ page }) => { await createDraftSalesOrder(page); const detailPage = new SalesOrderDetailPage(page); // DRAFT: the Sales Rep can submit or cancel; the approver actions are hidden. await expect(detailPage.statusBadge).toHaveText(/draft/i); await expect(detailPage.submitButton).toBeVisible(); await expect(detailPage.cancelButton).toBeVisible(); await expect(detailPage.confirmButton).toHaveCount(0); // Submit Sales Order — DRAFT -> SUBMITTED; the Approver actions surface. await detailPage.submitButton.click(); await expect(detailPage.confirmButton).toBeVisible(); await expect(detailPage.rejectButton).toBeVisible(); await expect(detailPage.submitButton).toHaveCount(0); // Confirm Sales Order — SUBMITTED -> CONFIRMED. Post-confirmation the order // is committed: submit/confirm/reject disappear and the fulfilment shortcut // plus the close action surface. await detailPage.confirmButton.click(); await expect(detailPage.createOutboundShipmentButton).toBeVisible(); await expect(detailPage.closeButton).toBeVisible(); await expect(detailPage.confirmButton).toHaveCount(0); await expect(detailPage.rejectButton).toHaveCount(0); }); // Reject branch: Create -> Submit -> Reject (back to DRAFT) -> resubmit -> // Confirm. Mirrors the reject / revise / resubmit cycle from the flow diagram. test("approver rejects a submitted sales order, then it is resubmitted and confirmed", async ({ page, }) => { await createDraftSalesOrder(page); const detailPage = new SalesOrderDetailPage(page); // Submit for approval — DRAFT -> SUBMITTED. await expect(detailPage.submitButton).toBeVisible(); await detailPage.submitButton.click(); await expect(detailPage.rejectButton).toBeVisible(); // Reject with a mandatory reason — SUBMITTED -> DRAFT. A rejected order is // back in DRAFT and can be submitted again; the approver actions disappear. await detailPage.rejectButton.click(); await expect(detailPage.rejectDialog).toBeVisible(); await detailPage.rejectReasonInput.fill("E2E: wrong customer PO number, please fix and resubmit"); await detailPage.confirmRejectButton.click(); await expect(detailPage.submitButton).toBeVisible(); await expect(detailPage.confirmButton).toHaveCount(0); await expect(detailPage.rejectButton).toHaveCount(0); await expect(detailPage.statusBadge).toHaveText(/draft/i); // Resubmit through the normal path — DRAFT -> SUBMITTED. await detailPage.submitButton.click(); await expect(detailPage.confirmButton).toBeVisible(); // Confirm the resubmitted order — SUBMITTED -> CONFIRMED. await detailPage.confirmButton.click(); await expect(detailPage.createOutboundShipmentButton).toBeVisible(); await expect(detailPage.closeButton).toBeVisible(); }); // Cancel branch + list triage: cancel a DRAFT order, then confirm the list view // surfaces it (status filtering / pipeline triage) from the list screen. test("sales rep cancels a draft sales order and finds it in the list", async ({ page }) => { const id = await createDraftSalesOrder(page); const detailPage = new SalesOrderDetailPage(page); // Cancel Sales Order — a DRAFT order with no downstream artifacts cancels // unconditionally. A cancelled order is terminal: no submit/cancel remain. await expect(detailPage.cancelButton).toBeVisible(); await detailPage.cancelButton.click(); await expect(detailPage.cancelButton).toHaveCount(0); await expect(detailPage.submitButton).toHaveCount(0); // View Sales Order List — the cancelled order is still listed for triage, // linked by its id to the detail page. The list is the sales pipeline view. const listPage = new SalesOrderListPage(page); await listPage.goto(); await expect(listPage.heading).toBeVisible(); await expect(listPage.getColumnHeader("Order Status")).toBeVisible(); // Each row links to the detail page by the order id (both the document-number // cell and the View action). Follow the created order's row back to its // detail page to close the loop. const detailLink = page.locator(`a[href$="/${id}"]`).first(); await expect(detailLink).toBeVisible(); await detailLink.click(); await page.waitForURL(DETAIL_URL); await expect(detailPage.heading).toBeVisible(); });