15ddac178c
- 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.
117 lines
4.6 KiB
TypeScript
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()
|
|
}
|
|
}
|