import { type Page } from "@playwright/test"; import { test, expect } from "../fixtures/purchase-order-lifecycle"; import { PurchaseOrderAmendFormPage } from "../pages/purchase-order-amend-form"; import { PurchaseOrderCreateFormPage } from "../pages/purchase-order-create-form"; import { PurchaseOrderDetailPage } from "../pages/purchase-order-detail"; import { PurchaseOrderEditFormPage } from "../pages/purchase-order-edit-form"; import { PurchaseOrderListPage } from "../pages/purchase-order-list"; /** * E2E coverage for the `purchase-order-lifecycle` business flow, driven end to * end through the live frontend by the purchase agent (an account that holds * both the Purchaser 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"; /** * Create a DRAFT PO with a single line, tagged with `externalRef` so the list * row can be found. Returns the PO docNumber resolved from the detail URL. */ async function createDraftPurchaseOrder(page: Page, externalRef: string): Promise { const createForm = new PurchaseOrderCreateFormPage(page); await createForm.goto(); await expect(createForm.supplierCombobox).toBeVisible(); await createForm.selectFirstSupplier(); await createForm.selectFirstReceivingSite(); await createForm.fillExternalReference(externalRef); await createForm.selectFirstLineItem(0); await createForm.setQuantity(0, ORDER_QTY); await createForm.setUnitPrice(0, UNIT_PRICE); await createForm.submit(); const listPage = new PurchaseOrderListPage(page); await expect(listPage.heading).toBeVisible(); await listPage.getViewLink(externalRef).first().click(); await page.waitForURL(/\/procurement\/purchase-order\/[^/]+$/); const detailPage = new PurchaseOrderDetailPage(page); await expect(detailPage.heading).toBeVisible(); const docNumber = new URL(page.url()).pathname.split("/").pop(); if (!docNumber) { throw new Error("Could not resolve PO docNumber from the detail URL"); } return docNumber; } // Flow main path: Create -> Edit (DRAFT) -> Submit -> Approve (ORDERED) -> // Amend (revision) -> Close (CLOSED). Exercises both actors on one PO. test("purchase agent creates, edits, submits, approves, amends, and closes a purchase order", async ({ page, }) => { const externalRef = `E2E-PO-${Date.now()}`; await createDraftPurchaseOrder(page, externalRef); const detailPage = new PurchaseOrderDetailPage(page); // Edit Purchase Order — the PO is still editable while DRAFT; raise the // quantity and save. Editing is only offered before submission. await expect(detailPage.editButton).toBeVisible(); await detailPage.editButton.click(); const editForm = new PurchaseOrderEditFormPage(page); await expect(editForm.heading).toBeVisible(); await editForm.setQuantity(0, "12"); await editForm.submit(); await page.waitForURL(/\/procurement\/purchase-order\/[^/]+$/); await expect(detailPage.heading).toBeVisible(); // Submit Purchase Order — DRAFT -> SUBMITTED; the Approver actions surface. await expect(detailPage.submitButton).toBeVisible(); await detailPage.submitButton.click(); await expect(detailPage.approveButton).toBeVisible(); await expect(detailPage.rejectButton).toBeVisible(); // Approve Purchase Order — SUBMITTED -> ORDERED. Post-approval the PO is // committed: edit/submit disappear and the ORDERED-only actions surface. await detailPage.approveButton.click(); await expect(detailPage.amendButton).toBeVisible(); await expect(detailPage.closeButton).toBeVisible(); await expect(detailPage.createInboundShipmentButton).toBeVisible(); await expect(detailPage.editButton).toHaveCount(0); await expect(detailPage.submitButton).toHaveCount(0); // Amend Purchase Order — an ORDERED PO is amended through the amend form // (not the edit form). Modify the existing line's quantity with a reason. await detailPage.amendButton.click(); const amendForm = new PurchaseOrderAmendFormPage(page); await expect(amendForm.heading).toBeVisible(); await amendForm.fillReason("E2E quantity correction after supplier confirmation"); await amendForm.setExistingLineAction(0, "MODIFY"); await amendForm.setExistingLineQuantity(0, "15"); await amendForm.submit(); await page.waitForURL(/\/procurement\/purchase-order\/[^/]+$/); await expect(detailPage.heading).toBeVisible(); // The revision history is now available, reflecting the recorded amendment. await expect(detailPage.revisionHistoryButton).toBeVisible(); // Close Purchase Order — ORDERED -> CLOSED. Residual open quantity exists // (nothing received), so acknowledge it with a close reason (write-off). await expect(detailPage.closeButton).toBeVisible(); await detailPage.closeButton.click(); await expect(detailPage.closeDialog).toBeVisible(); // Nothing was received, so the PO has open quantity — acknowledge the // write-off (which makes the close reason mandatory) before confirming. await detailPage.writeOffRemainingCheckbox.check(); await detailPage.closeReasonInput.fill("E2E: supplier will not ship the remainder"); await detailPage.confirmCloseButton.click(); // Terminal CLOSED state: the amend/close/submit actions are gone. await expect(detailPage.closeButton).toHaveCount(0); await expect(detailPage.amendButton).toHaveCount(0); await expect(detailPage.submitButton).toHaveCount(0); }); // Reject branch: Create -> Submit -> Reject (back to DRAFT with reason) -> // Edit + resubmit -> Approve. Mirrors the reject-edit-resubmit cycle. test("approver rejects a submitted purchase order, then the purchaser revises, resubmits, and it is approved", async ({ page, }) => { const externalRef = `E2E-REJECT-${Date.now()}`; await createDraftPurchaseOrder(page, externalRef); const detailPage = new PurchaseOrderDetailPage(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. await detailPage.rejectButton.click(); await expect(detailPage.rejectDialog).toBeVisible(); await detailPage.rejectReasonInput.fill("E2E: unit price too high, please renegotiate"); await detailPage.confirmRejectButton.click(); // Back in DRAFT: the PO is editable and submittable again. await expect(detailPage.editButton).toBeVisible(); await expect(detailPage.submitButton).toBeVisible(); // Revise the rejected PO, then resubmit through the normal path. await detailPage.editButton.click(); const editForm = new PurchaseOrderEditFormPage(page); await expect(editForm.heading).toBeVisible(); await editForm.setUnitPrice(0, "18"); await editForm.submit(); await page.waitForURL(/\/procurement\/purchase-order\/[^/]+$/); await expect(detailPage.submitButton).toBeVisible(); await detailPage.submitButton.click(); await expect(detailPage.approveButton).toBeVisible(); // Approve the resubmitted PO — SUBMITTED -> ORDERED. await detailPage.approveButton.click(); await expect(detailPage.amendButton).toBeVisible(); await expect(detailPage.closeButton).toBeVisible(); }); // Cancel branch + list triage: cancel a DRAFT PO, then confirm the list view // surfaces it (status filtering / pipeline triage) from the list screen. test("purchaser cancels a draft purchase order and finds it in the list", async ({ page }) => { const externalRef = `E2E-CANCEL-${Date.now()}`; const docNumber = await createDraftPurchaseOrder(page, externalRef); const detailPage = new PurchaseOrderDetailPage(page); // Cancel Purchase Order — a DRAFT PO with no downstream artifacts cancels // unconditionally. A cancelled PO is terminal: no edit/submit/cancel remain. await expect(detailPage.cancelButton).toBeVisible(); await detailPage.cancelButton.click(); await expect(detailPage.cancelButton).toHaveCount(0); await expect(detailPage.editButton).toHaveCount(0); await expect(detailPage.submitButton).toHaveCount(0); // View Purchase Order List — the cancelled PO is still listed for triage, // linked by its document number to the detail page. const listPage = new PurchaseOrderListPage(page); await listPage.goto(); await expect(listPage.heading).toBeVisible(); await expect(listPage.getColumnHeader("Status")).toBeVisible(); await expect(listPage.getRow(externalRef).first()).toBeVisible(); await listPage.getPurchaseOrderLink(docNumber).click(); await page.waitForURL(/\/procurement\/purchase-order\/[^/]+$/); await expect(detailPage.heading).toBeVisible(); });