c31e23c2b9
- Introduced Playwright configuration for e2e testing. - Implemented authentication tests to validate login functionality. - Created tests for GRN (Goods Receipt Note) to ensure proper stock handling. - Developed production run tests to verify lifecycle and stock posting. - Added stock transfer tests to check movement between warehouses. - Implemented stock adjustment tests for positive and negative adjustments. - Established API seeder for test data setup and verification. - Enhanced utility functions for UI interactions and response handling.
121 lines
4.8 KiB
TypeScript
121 lines
4.8 KiB
TypeScript
import { Page, expect } from "@playwright/test"
|
|
import { clickToReveal, selectOption, comboboxByLabel } from "../support/ui"
|
|
|
|
// Locators verified against Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx
|
|
// and .../grn/[id]/page.tsx. Things that DOM inspection caught and a placeholder-only guess
|
|
// would not have:
|
|
// - The Qty/Unit cost/Disc%/VAT% <Input type="number"> cells carry no accessible name
|
|
// (no htmlFor/aria-label) - located by column position within the row instead.
|
|
// - On a PO-based line (line.poLineId set) Item/UOM render as plain text, not a Select -
|
|
// fillFirstLine() only touches the item combobox when one is actually present (checked by
|
|
// role alone, not name - see below).
|
|
// - Every page here intermittently throws a real React hydration error (#418) that blanks a
|
|
// random subset of Select triggers' placeholder text for that page's lifetime, WITHOUT
|
|
// affecting their sibling <Label> or role="combobox" attribute (support/ui.ts has the full
|
|
// writeup). So triggers are located via comboboxByLabel() (label + role, no name lookup)
|
|
// instead of getByRole("combobox", { name }) - the row-scoped item/uom/bin combos have no
|
|
// adjacent label and are instead found by position, which is equally immune to the bug.
|
|
export class GrnNewPage {
|
|
constructor(private readonly page: Page) {}
|
|
|
|
async goto() {
|
|
await this.page.goto("/dashboard/receiving/grn/new")
|
|
}
|
|
|
|
async useDirectReceipt() {
|
|
await clickToReveal(
|
|
this.page.getByRole("button", { name: /direct receipt/i }),
|
|
comboboxByLabel(this.page, "Vendor")
|
|
)
|
|
}
|
|
|
|
async useAgainstPo() {
|
|
await clickToReveal(
|
|
this.page.getByRole("button", { name: /against po/i }),
|
|
comboboxByLabel(this.page, "Purchase order")
|
|
)
|
|
}
|
|
|
|
async selectVendor(name: string) {
|
|
await selectOption(this.page, comboboxByLabel(this.page, "Vendor"), name)
|
|
}
|
|
|
|
async selectWarehouse(name: string) {
|
|
await selectOption(this.page, comboboxByLabel(this.page, "Warehouse"), name)
|
|
}
|
|
|
|
/** `docNo` is what the PO option renders (`{docNo} — Vendor #{vendorId} ({status})`) - not the numeric id. */
|
|
async selectPurchaseOrder(docNo: string) {
|
|
await selectOption(this.page, comboboxByLabel(this.page, "Purchase order"), new RegExp(docNo))
|
|
}
|
|
|
|
private firstRow() {
|
|
return this.page.locator("table tbody tr").first()
|
|
}
|
|
|
|
/**
|
|
* Item and UOM are each only a combobox when the row is NOT tied to a PO line
|
|
* (`line.poLineId` gates both cells identically in the source - a PO line renders them as
|
|
* plain text instead). Checked per-cell (td:nth(0) for Item, td:nth(1) for UOM) rather than
|
|
* "row has any combobox", since the Bin/Hold-status cells always have one regardless of PO
|
|
* mode - a row-wide check would false-positive on a PO line and select the wrong control.
|
|
* Selecting the app doesn't auto-fill UOM from the chosen item, so a direct-receipt/off-PO
|
|
* line needs it set explicitly or submit blocks with "Select a UOM".
|
|
*/
|
|
async fillFirstLine(opts: { item?: string; uom?: string; qty: number; unitCost?: number }) {
|
|
const row = this.firstRow()
|
|
const cells = row.locator("td")
|
|
if (opts.item) {
|
|
const itemCombo = cells.nth(0).getByRole("combobox")
|
|
if (await itemCombo.count()) {
|
|
await selectOption(this.page, itemCombo, opts.item)
|
|
}
|
|
}
|
|
if (opts.uom) {
|
|
const uomCombo = cells.nth(1).getByRole("combobox")
|
|
if (await uomCombo.count()) {
|
|
await selectOption(this.page, uomCombo, opts.uom)
|
|
}
|
|
}
|
|
const numberInputs = row.locator('input[type="number"]')
|
|
await numberInputs.nth(0).fill(String(opts.qty)) // Qty
|
|
if (opts.unitCost !== undefined) {
|
|
await numberInputs.nth(1).fill(String(opts.unitCost)) // Unit cost
|
|
}
|
|
}
|
|
|
|
async submit() {
|
|
await this.page.getByRole("button", { name: /create grn/i }).click()
|
|
}
|
|
}
|
|
|
|
export class GrnDetailPage {
|
|
constructor(private readonly page: Page) {}
|
|
|
|
async gotoById(grnId: number) {
|
|
await this.page.goto(`/dashboard/receiving/grn/${grnId}`)
|
|
}
|
|
|
|
/** GrnStatusBadge/HoldStatusBadge render the raw status string verbatim - exact match avoids
|
|
* matching prose like "Confirmed — stock layers created" in the post-confirm success panel. */
|
|
async expectStatus(status: "Draft" | "Confirmed") {
|
|
await expect(this.page.getByText(status, { exact: true })).toBeVisible()
|
|
}
|
|
|
|
async confirm() {
|
|
await this.page.getByRole("button", { name: /confirm grn/i }).click()
|
|
}
|
|
|
|
async releaseFirstOnHoldLine() {
|
|
await this.page.getByRole("button", { name: /^release$/i }).first().click()
|
|
}
|
|
|
|
async rejectFirstOnHoldLine() {
|
|
await this.page.getByRole("button", { name: /^reject$/i }).first().click()
|
|
}
|
|
|
|
async expectCreateReturnLink() {
|
|
await expect(this.page.getByRole("link", { name: /create return/i })).toBeVisible()
|
|
}
|
|
}
|