Files
ERP-core/Testing/e2e/pages/GrnPages.ts
ImanThiyanga 15ddac178c Refactor production and stock tests to remove UOM dependency
- Updated production.spec.ts, stock-adjustments.spec.ts, and stock-transfers.spec.ts to eliminate UOM references in API seeder and test cases.
- Adjusted ApiSeeder methods to remove UOM parameters from stock receiving and production template creation.
- Revised documentation to reflect changes in UOM handling, emphasizing that stock is counted in base UOM only.
- Introduced new enums for MeasureUnit and StageQtyUnit to clarify content size and stage input quantities.
- Implemented ItemContent service to validate and normalize content sizes.
- Updated smoke tests to validate production stage inputs expressed in content units, ensuring correct consumption calculations.
- Modified frontend UOM label handling to reflect the removal of per-line UOMs in document lines.
2026-08-11 11:32:40 +05:30

117 lines
4.6 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 is only a combobox when the row is NOT tied to a PO line (`line.poLineId` gates the
* cell in the source - a PO line renders it as plain text instead). Checked per-cell
* (td:nth(0)) 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.
*
* td:nth(1) is still the UOM column, but it is now a read-only label showing the chosen
* item's base UOM: lines carry no unit of their own, so there is nothing to pick. The
* column was kept rather than removed, which is why every index below is unchanged.
*/
async fillFirstLine(opts: { item?: 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)
}
}
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()
}
}