import { type Page } from "@playwright/test"; import { test, expect } from "../fixtures/stock-adjustment-processing"; import { StockAdjustmentCreateFormPage } from "../pages/stock-adjustment-create-form"; import { StockAdjustmentDetailPage } from "../pages/stock-adjustment-detail"; import { StockAdjustmentListPage } from "../pages/stock-adjustment-list"; /** * E2E coverage for the `stock-adjustment-processing` business flow, driven end to * end through the live frontend by the machine user (an account that holds both * the Inventory Manager capabilities — create / submit / cancel — and the * Approver's confirm / reject, since 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. Nothing touches * real stock until the adjustment is CONFIRMED, so the happy path uses an INCREASE * CORRECTION (which never fails for insufficient stock) to exercise confirmation. */ const LINE_QTY = "5"; /** * Create a DRAFT stock adjustment carrying a unique reason code (so the list row * can be found) with a single INCREASE CORRECTION line, then land on its detail * page. Returns the adjustment id resolved from the detail URL. */ async function createDraftCorrectionAdjustment(page: Page, reasonCode: string): Promise { const createForm = new StockAdjustmentCreateFormPage(page); await createForm.goto(); await expect(createForm.adjustmentTypeSelect).toBeVisible(); await createForm.selectAdjustmentType(/correction/i); await createForm.fillReasonCode(reasonCode); await createForm.selectFirstLineItem(0); await createForm.selectFirstLineStorageLocation(0); await createForm.setQuantity(0, LINE_QTY); // CORRECTION lines carry a per-line direction; INCREASE never fails on confirm // for lack of stock, keeping the happy path deterministic against seed data. await createForm.selectDirection(0, /increase/i); await createForm.submit(); const listPage = new StockAdjustmentListPage(page); const detailPage = new StockAdjustmentDetailPage(page); // On success the create form navigates to the new record; depending on the // AppShell route nesting this lands directly on the detail page or on the // list. Handle both: if we land on the list, follow the new row's View link. await expect(detailPage.heading.or(listPage.heading)).toBeVisible(); if (await listPage.heading.isVisible()) { await listPage.getViewLink(reasonCode).first().click(); await page.waitForURL(/\/inventory\/stock-adjustment\/[^/]+$/); await expect(detailPage.heading).toBeVisible(); } const id = new URL(page.url()).pathname.split("/").pop(); if (!id) { throw new Error("Could not resolve stock adjustment id from the detail URL"); } return id; } // Flow main path: Create (DRAFT) -> Submit (SUBMITTED) -> Confirm (CONFIRMED). // Exercises both actors on one adjustment; confirmation applies the movement. test("inventory manager creates and submits a correction adjustment, then the approver confirms it", async ({ page, }) => { const reasonCode = `E2E-CONFIRM-${Date.now()}`; await createDraftCorrectionAdjustment(page, reasonCode); const detailPage = new StockAdjustmentDetailPage(page); // DRAFT: the adjustment is submittable and cancellable; no approval actions yet. await expect(detailPage.statusBadge).toHaveText(/draft/i); await expect(detailPage.submitButton).toBeVisible(); await expect(detailPage.cancelButton).toBeVisible(); // Submit for approval — DRAFT -> SUBMITTED; the Approver actions surface and the // author-only actions retire. await detailPage.submitButton.click(); await expect(detailPage.statusBadge).toHaveText(/submitted/i); await expect(detailPage.confirmButton).toBeVisible(); await expect(detailPage.rejectButton).toBeVisible(); await expect(detailPage.submitButton).toHaveCount(0); // Confirm — SUBMITTED -> CONFIRMED. Posting the movement is terminal: no // submit / cancel / confirm / reject actions remain. await detailPage.confirmButton.click(); await expect(detailPage.statusBadge).toHaveText(/confirmed/i); await expect(detailPage.confirmButton).toHaveCount(0); await expect(detailPage.rejectButton).toHaveCount(0); await expect(detailPage.cancelButton).toHaveCount(0); }); // Reject branch: Create -> Submit -> Reject (SUBMITTED -> REJECTED). The native // resubmit transition is not surfaced by a resolver, so REJECTED is terminal here. test("approver rejects a submitted stock adjustment", async ({ page }) => { const reasonCode = `E2E-REJECT-${Date.now()}`; await createDraftCorrectionAdjustment(page, reasonCode); const detailPage = new StockAdjustmentDetailPage(page); // Submit for approval — DRAFT -> SUBMITTED. await expect(detailPage.submitButton).toBeVisible(); await detailPage.submitButton.click(); await expect(detailPage.statusBadge).toHaveText(/submitted/i); await expect(detailPage.rejectButton).toBeVisible(); // Reject — SUBMITTED -> REJECTED. No stock moved; the approval actions retire. await detailPage.rejectButton.click(); await expect(detailPage.statusBadge).toHaveText(/rejected/i); await expect(detailPage.confirmButton).toHaveCount(0); await expect(detailPage.rejectButton).toHaveCount(0); }); // Cancel branch + list triage: cancel a DRAFT adjustment, then confirm the list // view surfaces it (status triage) linked back to the detail page. test("inventory manager cancels a draft stock adjustment and finds it in the list", async ({ page, }) => { const reasonCode = `E2E-CANCEL-${Date.now()}`; await createDraftCorrectionAdjustment(page, reasonCode); const detailPage = new StockAdjustmentDetailPage(page); // Cancel — a DRAFT adjustment cancels unconditionally into the terminal // CANCELLED state: no submit / cancel actions remain. await expect(detailPage.cancelButton).toBeVisible(); await detailPage.cancelButton.click(); await expect(detailPage.statusBadge).toHaveText(/cancelled/i); await expect(detailPage.cancelButton).toHaveCount(0); await expect(detailPage.submitButton).toHaveCount(0); // View Stock Adjustment List — the cancelled adjustment is still listed for // triage and links back to its detail page. const listPage = new StockAdjustmentListPage(page); await listPage.goto(); await expect(listPage.heading).toBeVisible(); await expect(listPage.getRow(reasonCode).first()).toBeVisible(); await listPage.getViewLink(reasonCode).first().click(); await page.waitForURL(/\/inventory\/stock-adjustment\/[^/]+$/); await expect(detailPage.heading).toBeVisible(); });