The Q.C. screen (KPIs, boards, project folders), the Q.C. tab on the packet page (check items from the qc_check_items settings row, visual exam per weld, NDE, hardness with the NACE label, notes), accept with a route and a signature gated by qc_missing_empty, reject with a reason or an NCR number closing the inspection into qc_history, Q.C. folder documents per project and unit with superseding revisions, packet documents as a closed set with take-out reasons, the digital ITP per unit from the frozen template (line initials with CAS, unsign, N/A, customer codes, rejects with NCR, block signatures, sign-off with the PDF hook, admin reopen, the ITP log), hold-point warnings by department page group, the scoped policy check for unit and project writes, migration 0007, tests, Playwright scenarios and docs. Co-Authored-By: Claude Fable 5.1 <[email protected]>
542 lines
25 KiB
TypeScript
542 lines
25 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
||
import { PIN } from "./helpers";
|
||
import {
|
||
L as SL,
|
||
actor,
|
||
api,
|
||
closeAll,
|
||
packet,
|
||
packetTab,
|
||
pullAndRelease,
|
||
setPin,
|
||
sign,
|
||
signSheet,
|
||
signWithPassword,
|
||
signWithPin,
|
||
statusOf,
|
||
stepApi,
|
||
userId,
|
||
type Actor,
|
||
} from "./shophelpers";
|
||
import {
|
||
L,
|
||
NCR,
|
||
TEMPLATE_ITEMS,
|
||
atQcApi,
|
||
blockSign,
|
||
cardOf,
|
||
column,
|
||
enabledApprove,
|
||
itpBlock,
|
||
itpRow,
|
||
itpSection,
|
||
logActions,
|
||
makeQcDraft,
|
||
makeQcProject,
|
||
part,
|
||
pdfFile,
|
||
qcBar,
|
||
qcChecks,
|
||
textFile,
|
||
type Crew,
|
||
type QcPacket,
|
||
type QcProject,
|
||
} from "./qchelpers";
|
||
import type { Json } from "./whhelpers";
|
||
|
||
// Phase 6 Q.C. scenarios on the CI stack, modelled on the old tests/ps titp, tqcf and tpdocs, plus the accept and
|
||
// reject flows of the old qcTab. clerk.clara makes a project with two units; the crew (wh.wade, sup.wes,
|
||
// weld.andy) takes two packets of unit 1 to Q.C. through the API. Accept: qc.quinn inspects on the Q.C. tab and
|
||
// accepts to the data book with her PIN. Reject: one weld rejected, an NCR number only, her password; sup.wes sees
|
||
// the callout and sends it to Q.C. again. tqcf: project and unit folders, a new revision, a refused file, wh.wade
|
||
// has no Q.C. screen. tpdocs: a packet document added and taken out. titp: the digital ITP on unit 2 with the hold
|
||
// callout on a 03 and an 11 packet, every line and block signed, sign-off, and an admin reopen.
|
||
// Every UI label used here is in L (qchelpers.ts).
|
||
|
||
const RUN = Date.now().toString(36).toUpperCase().slice(-5);
|
||
const PROJECT = `Q6${RUN}`;
|
||
const T = {
|
||
accept: `QC accept spool ${RUN}`,
|
||
reject: `QC reject spool ${RUN}`,
|
||
wp: `ITP weld spool ${RUN}`,
|
||
fa: `ITP final spool ${RUN}`,
|
||
};
|
||
|
||
let proj: QcProject;
|
||
const P: Partial<Record<keyof typeof T, QcPacket>> = {};
|
||
|
||
function pk(k: keyof typeof T): QcPacket {
|
||
const p = P[k];
|
||
if (!p) throw new Error(`packet ${k} was not made`);
|
||
return p;
|
||
}
|
||
|
||
async function open(page: Page, id: number, tabName?: RegExp): Promise<void> {
|
||
await page.goto(`/packets/${id}`);
|
||
await expect(page.getByRole("heading").first()).toBeVisible();
|
||
if (tabName) await packetTab(page, tabName).click();
|
||
}
|
||
|
||
async function openQcHome(page: Page): Promise<void> {
|
||
await page.goto("/qc");
|
||
await expect(page.getByRole("heading", { name: L.homeHead })).toBeVisible();
|
||
}
|
||
|
||
test.describe.serial("qc", () => {
|
||
test.setTimeout(300_000);
|
||
|
||
test("setup: a project with two units, two packets of unit 1 at Q.C., qc.quinn's PIN", async ({ browser }) => {
|
||
const clara = await actor(browser, "clerk.clara");
|
||
const wade = await actor(browser, "wh.wade");
|
||
const wes = await actor(browser, "sup.wes");
|
||
const andy = await actor(browser, "weld.andy");
|
||
const quinn = await actor(browser, "qc.quinn");
|
||
const andyId = await userId(andy, "weld.andy");
|
||
const crew: Crew = { clara, wade, wes, andy, andyId };
|
||
proj = await makeQcProject(clara, PROJECT);
|
||
P.accept = await atQcApi(crew, proj.units[0], T.accept, "Stall 2", "Bay 2");
|
||
P.reject = await atQcApi(crew, proj.units[0], T.reject, "Stall 3", "Bay 2");
|
||
// unit 2: a 03 packet released to the shop, and a draft released straight to 11 Final Assembly - North
|
||
const wp = await makeQcDraft(clara, proj.units[1], T.wp);
|
||
await pullAndRelease(wade, wp.id);
|
||
P.wp = wp;
|
||
const fa = await makeQcDraft(clara, proj.units[1], T.fa, { dept: "11", stay: true });
|
||
const rel = await stepApi(clara, fa.id, "release_to_shop", { dept: "11" });
|
||
expect(rel.status).toBe("ready");
|
||
P.fa = fa;
|
||
await setPin(quinn, PIN);
|
||
await closeAll(clara, wade, wes, andy, quinn);
|
||
});
|
||
|
||
test("accept: inspect on the Q.C. tab and accept to the data book with the PIN", async ({ browser }) => {
|
||
const id = pk("accept").id;
|
||
const quinn = await actor(browser, "qc.quinn");
|
||
const page = quinn.page;
|
||
|
||
// the Q.C. screen lists it as waiting, with the spool location
|
||
await openQcHome(page);
|
||
await expect(page.getByText(L.homeEyebrow).first()).toBeVisible();
|
||
for (const k of [L.kpiWaiting, L.kpiAccepted, L.kpiRejected])
|
||
await expect(page.getByText(k).first()).toBeVisible();
|
||
const ready = column(page, L.colReady);
|
||
const card = cardOf(ready, id);
|
||
await expect(card).toBeVisible();
|
||
await expect(card).toContainText(L.spoolAt("Bay 2"));
|
||
await card.click();
|
||
await expect(page).toHaveURL(new RegExp(`/packets/${id}`));
|
||
|
||
// the Q.C. bar lists what is missing and Accept is disabled
|
||
const bar = qcBar(page);
|
||
await expect(bar).toBeVisible();
|
||
const missing = bar.getByTestId(L.qcMissing);
|
||
await expect(missing).toContainText(L.stillNeeded);
|
||
await expect(missing).toContainText("10 inspection items");
|
||
await expect(missing).toContainText("2 welds without a visual result");
|
||
await expect(missing).toContainText("inspector name");
|
||
const acceptBtn = bar.getByRole("button", { name: L.accept, exact: true });
|
||
await expect(acceptBtn).toBeDisabled();
|
||
|
||
// the Q.C. tab: inspector and stamp first, so the ticks carry the stamp
|
||
await packetTab(page, L.tabQc).click();
|
||
await page.getByLabel(L.inspector, { exact: true }).fill("Quinn QC");
|
||
await page.getByLabel(L.stamp, { exact: true }).fill("Q1");
|
||
await page.getByLabel(L.stamp, { exact: true }).blur();
|
||
await expect.poll(async () => (await packet(quinn, id)).qc.stamp).toBe("Q1");
|
||
const boxes = qcChecks(page);
|
||
await expect(boxes).toHaveCount(10);
|
||
for (let i = 0; i < 10; i++) {
|
||
await boxes.nth(i).check();
|
||
await expect(boxes.nth(i)).toBeChecked();
|
||
}
|
||
await expect(page.getByText(L.stampTick).first()).toBeVisible();
|
||
// both welds accepted
|
||
const vt = part(page, L.vtSection);
|
||
for (const no of [1, 2]) {
|
||
const btn = vt.getByRole("row", { name: L.weldRow(no) }).getByRole("button", { name: L.vtAccept, exact: true });
|
||
await btn.click();
|
||
await expect(btn).toHaveAttribute("aria-pressed", "true");
|
||
}
|
||
// NDE: RT, Acceptable
|
||
await page.getByLabel(L.ndeMethod, { exact: true }).selectOption("RT");
|
||
await page.getByLabel(L.ndeResult, { exact: true }).selectOption("Acceptable");
|
||
await expect.poll(async () => (await packet(quinn, id)).qc.nde_result).toBe("Acceptable");
|
||
|
||
// Accept is enabled; route to the data book; sign with the PIN
|
||
await expect(missing).toBeHidden();
|
||
await expect(acceptBtn).toBeEnabled();
|
||
await bar.getByLabel(L.route, { exact: true }).selectOption({ label: L.routeDataBook });
|
||
await acceptBtn.click();
|
||
await signWithPin(page, PIN);
|
||
await expect(signSheet(page)).toBeHidden();
|
||
await expect.poll(() => statusOf(quinn, id)).toBe("complete");
|
||
await expect(page.getByTestId(L.statusPill)).toContainText(L.statusRouted);
|
||
const after = await packet(quinn, id);
|
||
expect(after.routed_to).toBe("databook");
|
||
|
||
// History has the accept text
|
||
await packetTab(page, L.tabHistory).click();
|
||
await expect(page.getByText(`Q.C. accepted → ${L.routeDataBook}`).first()).toBeVisible();
|
||
expect(await logActions(quinn, id)).toContain(`Q.C. accepted → ${L.routeDataBook}`);
|
||
|
||
// the Q.C. screen's Accepted column has it
|
||
await openQcHome(page);
|
||
await expect(cardOf(column(page, L.colAccepted), id)).toBeVisible();
|
||
await closeAll(quinn);
|
||
});
|
||
|
||
test("reject: one weld rejected, an NCR number only, the password; the supervisor sends it again", async ({
|
||
browser,
|
||
}) => {
|
||
const id = pk("reject").id;
|
||
const quinn = await actor(browser, "qc.quinn");
|
||
const page = quinn.page;
|
||
await open(page, id, L.tabQc);
|
||
|
||
// weld 1 rejected with a note
|
||
const w1 = part(page, L.vtSection).getByRole("row", { name: L.weldRow(1) });
|
||
await w1.getByRole("button", { name: L.vtReject, exact: true }).click();
|
||
const defect = w1.getByLabel(L.weldNote(1));
|
||
await expect(defect).toHaveAttribute("placeholder", L.defectNote);
|
||
await defect.fill("undercut");
|
||
await defect.blur();
|
||
await expect
|
||
.poll(async () => {
|
||
const exam = (await packet(quinn, id)).qc.weld_exam as Json;
|
||
return Object.values(exam).map((e) => `${(e as Json).result}:${(e as Json).note ?? ""}`);
|
||
})
|
||
.toContain("reject:undercut");
|
||
|
||
// the reject sheet: enabled on an NCR number alone
|
||
const bar = qcBar(page);
|
||
await bar.getByRole("button", { name: L.reject, exact: true }).click();
|
||
const sheet = page.getByRole("dialog", { name: L.rejectSheet, exact: true });
|
||
await expect(sheet).toBeVisible();
|
||
const submitReject = sheet.getByRole("button", { name: L.rejectSubmit });
|
||
await expect(submitReject).toBeDisabled();
|
||
await sheet.getByLabel(L.ncrNumber, { exact: true }).fill(NCR);
|
||
await expect(submitReject).toBeEnabled();
|
||
await submitReject.click();
|
||
await signWithPassword(page, "qc.quinn");
|
||
await expect.poll(() => statusOf(quinn, id)).toBe("review");
|
||
await expect(page.getByTestId(L.statusPill)).toContainText(L.statusReview);
|
||
const after = await packet(quinn, id);
|
||
expect(after.qc).toBeNull();
|
||
expect(after.qc_last.result).toBe("rejected");
|
||
expect(after.qc_last.ncr).toBe(NCR);
|
||
|
||
// the Q.C. screen's rejected column has it
|
||
await openQcHome(page);
|
||
await expect(cardOf(column(page, L.colRejected), id)).toBeVisible();
|
||
|
||
// sup.wes sees the callout with the NCR link and the rejected weld
|
||
const wes = await actor(browser, "sup.wes");
|
||
await open(wes.page, id);
|
||
const callout = wes.page.getByRole("note").filter({ hasText: L.qcRejected }).first();
|
||
await expect(callout).toBeVisible();
|
||
const link = callout.getByRole("link", { name: new RegExp(NCR) });
|
||
await expect(link).toHaveAttribute("href", new RegExp(`/ncrs/${NCR}$`));
|
||
await expect(link).toHaveAttribute("rel", /noopener/);
|
||
await expect(callout.getByText(L.rejectedWelds("W1"), { exact: false })).toBeVisible();
|
||
|
||
// the review box sends it to Q.C. again (password: sup.wes has no PIN)
|
||
await wes.page.getByLabel(SL.bookReady).check();
|
||
await wes.page.getByLabel(SL.spoolReady).check();
|
||
await wes.page.getByLabel(SL.spoolLocation).fill("Bay 4");
|
||
await wes.page.getByRole("button", { name: SL.sendToQc, exact: true }).click();
|
||
await signWithPassword(wes.page, "sup.wes");
|
||
await expect.poll(() => statusOf(wes, id)).toBe("qc");
|
||
|
||
// a fresh empty inspection; the first one is under Earlier inspections
|
||
await open(page, id, L.tabQc);
|
||
const boxes = qcChecks(page);
|
||
await expect(boxes).toHaveCount(10);
|
||
for (let i = 0; i < 10; i++) await expect(boxes.nth(i)).not.toBeChecked();
|
||
const earlier = part(page, L.earlier);
|
||
await expect(earlier).toBeVisible();
|
||
await expect(earlier).toContainText(L.rejectedPill);
|
||
await expect(earlier).toContainText(NCR);
|
||
await closeAll(quinn, wes);
|
||
});
|
||
|
||
test("tqcf: project and unit folders, a new revision, a refused file, no Q.C. screen for wh.wade", async ({
|
||
browser,
|
||
}) => {
|
||
const quinn = await actor(browser, "qc.quinn");
|
||
const page = quinn.page;
|
||
const unit1 = proj.units[0];
|
||
|
||
// the home's folder card
|
||
await openQcHome(page);
|
||
const folderCard = part(page, L.folders).locator(L.pcard).filter({ hasText: PROJECT }).first();
|
||
await expect(folderCard).toBeVisible();
|
||
await expect(folderCard).toContainText(L.folderUnits(2));
|
||
await folderCard.click();
|
||
await expect(page).toHaveURL(new RegExp(`/qc/projects/${proj.id}`));
|
||
await expect(page.getByText(L.allFolders).first()).toBeVisible();
|
||
|
||
// a project WPS
|
||
await page.getByRole("button", { name: L.addQcDoc }).first().click();
|
||
let sheet = page.getByRole("dialog", { name: L.qcDocSheetAdd });
|
||
await expect(sheet).toBeVisible();
|
||
await sheet.getByLabel(L.qdType, { exact: true }).selectOption("WPS / PQR");
|
||
await sheet.getByLabel(L.qdTitle, { exact: true }).fill("WPS-101 GTAW/SMAW");
|
||
await sheet.getByLabel(L.qdRev, { exact: true }).fill("2");
|
||
await sheet.getByLabel(L.qdFile).setInputFiles(pdfFile("wps.pdf"));
|
||
await sheet.getByRole("button", { name: L.addToFolder }).click();
|
||
await expect(sheet).toBeHidden();
|
||
const projectDocs = part(page, L.projectDocs);
|
||
await expect(projectDocs.getByText("WPS-101 GTAW/SMAW").first()).toBeVisible();
|
||
await expect(projectDocs.getByText("Rev 2").first()).toBeVisible();
|
||
|
||
// unit 1: no ITP yet; add an ITP document
|
||
await page.locator(L.qcUnit).filter({ hasText: String(unit1.number) }).first().click();
|
||
await expect(page).toHaveURL(new RegExp(`/qc/units/${unit1.id}`));
|
||
await expect(page.getByText(L.noItpCallout)).toBeVisible();
|
||
await page.getByRole("button", { name: L.addQcDoc }).first().click();
|
||
sheet = page.getByRole("dialog", { name: L.qcDocSheetAdd });
|
||
await expect(sheet.getByLabel(L.qdType, { exact: true })).toHaveValue("ITP");
|
||
await sheet.getByLabel(L.qdTitle, { exact: true }).fill(`ITP ${unit1.number}`);
|
||
await sheet.getByLabel(L.qdFile).setInputFiles(pdfFile("itp.pdf"));
|
||
await sheet.getByRole("button", { name: L.addToFolder }).click();
|
||
await expect(sheet).toBeHidden();
|
||
const unitDocs = part(page, L.unitDocs);
|
||
await expect(unitDocs.getByText(`ITP ${unit1.number}`).first()).toBeVisible();
|
||
await expect(page.getByText(L.noItpCallout)).toBeHidden();
|
||
// the project's WPS is listed as inherited
|
||
await expect(part(page, L.fromProject(PROJECT)).getByText("WPS-101 GTAW/SMAW").first()).toBeVisible();
|
||
|
||
// a text file is refused with the server's message
|
||
await page.getByRole("button", { name: L.addQcDoc }).first().click();
|
||
sheet = page.getByRole("dialog", { name: L.qcDocSheetAdd });
|
||
await sheet.getByLabel(L.qdFile).setInputFiles(textFile());
|
||
await sheet.getByRole("button", { name: L.addToFolder }).click();
|
||
await expect(sheet.getByRole("alert")).toContainText(L.badFile);
|
||
await page.keyboard.press("Escape");
|
||
await expect(sheet).toBeHidden();
|
||
|
||
// a new revision of the ITP document
|
||
await unitDocs.getByRole("button", { name: L.newRevision }).first().click();
|
||
sheet = page.getByRole("dialog", { name: L.qcDocSheetRev });
|
||
await expect(sheet).toBeVisible();
|
||
await expect(sheet).toContainText("The old one stays on file under Superseded.");
|
||
await sheet.getByLabel(L.qdRev, { exact: true }).fill("1");
|
||
await sheet.getByLabel(L.qdFile).setInputFiles(pdfFile("itp-r1.pdf"));
|
||
await sheet.getByRole("button", { name: L.fileNewRev }).click();
|
||
await expect(sheet).toBeHidden();
|
||
await expect(unitDocs.locator(L.qcDocRow)).toHaveCount(1);
|
||
await expect(unitDocs.getByText("Rev 1").first()).toBeVisible();
|
||
const old = part(page, L.superseded);
|
||
await old.getByRole("button", { name: L.show, exact: true }).click();
|
||
await expect(old.getByText(`ITP ${unit1.number}`).first()).toBeVisible();
|
||
await expect(old.getByText(/superseded .* by /i).first()).toBeVisible();
|
||
|
||
// back to the project, back to home
|
||
await page.getByRole("button", { name: L.back }).or(page.getByRole("link", { name: L.back })).first().click();
|
||
await expect(page).toHaveURL(new RegExp(`/qc/projects/${proj.id}`));
|
||
await expect(page.locator(L.qcUnit)).toHaveCount(2);
|
||
await page.getByRole("button", { name: L.back }).or(page.getByRole("link", { name: L.back })).first().click();
|
||
await expect(page).toHaveURL(/\/qc\/?$/);
|
||
|
||
// a packet of unit 1 lists both documents in its Documents tab
|
||
await open(page, pk("accept").id, L.tabDocs);
|
||
const fromFolder = part(page, L.alsoInDataBook);
|
||
await expect(fromFolder.getByText("WPS-101 GTAW/SMAW").first()).toBeVisible();
|
||
await expect(fromFolder.getByText(`ITP ${unit1.number}`).first()).toBeVisible();
|
||
|
||
// wh.wade has no Q.C. screen
|
||
const wade = await actor(browser, "wh.wade");
|
||
await wade.page.goto("/qc");
|
||
await expect(wade.page).not.toHaveURL(/\/qc(\/|$)/);
|
||
await closeAll(quinn, wade);
|
||
});
|
||
|
||
test("tpdocs: a packet document added, refused, seen read-only, taken out", async ({ browser }) => {
|
||
const id = pk("reject").id;
|
||
const quinn = await actor(browser, "qc.quinn");
|
||
const page = quinn.page;
|
||
await open(page, id, L.tabDocs);
|
||
await expect(part(page, L.docsSection)).toBeVisible();
|
||
|
||
await page.getByRole("button", { name: L.addDocument }).click();
|
||
let sheet = page.getByRole("dialog", { name: L.addDocSheet });
|
||
await expect(sheet).toBeVisible();
|
||
await sheet.getByLabel(L.docType, { exact: true }).selectOption({ label: L.ndeKind });
|
||
await sheet.getByLabel(L.docTitle, { exact: true }).fill("RT report W1-W4");
|
||
await sheet.getByLabel(L.docNote).fill("RPT-2210, accept");
|
||
await sheet.getByLabel(L.docFile).setInputFiles(pdfFile("rt.pdf"));
|
||
await sheet.getByRole("button", { name: L.addToPacket }).click();
|
||
await expect(sheet).toBeHidden();
|
||
const docs = part(page, L.docsSection);
|
||
await expect(docs.getByText("RT report W1-W4").first()).toBeVisible();
|
||
expect(await logActions(quinn, id)).toContain("Document added");
|
||
await packetTab(page, L.tabHistory).click();
|
||
await expect(page.getByText("Document added").first()).toBeVisible();
|
||
|
||
// a text file is refused
|
||
await packetTab(page, L.tabDocs).click();
|
||
await page.getByRole("button", { name: L.addDocument }).click();
|
||
sheet = page.getByRole("dialog", { name: L.addDocSheet });
|
||
await sheet.getByLabel(L.docFile).setInputFiles(textFile());
|
||
await sheet.getByRole("button", { name: L.addToPacket }).click();
|
||
await expect(sheet.getByRole("alert")).toContainText(L.badFile);
|
||
await page.keyboard.press("Escape");
|
||
await expect(sheet).toBeHidden();
|
||
|
||
// wh.wade sees the row and no Add document
|
||
const wade = await actor(browser, "wh.wade");
|
||
await open(wade.page, id, L.tabDocs);
|
||
await expect(part(wade.page, L.docsSection).getByText("RT report W1-W4").first()).toBeVisible();
|
||
await expect(wade.page.getByRole("button", { name: L.addDocument })).toHaveCount(0);
|
||
await expect(wade.page.getByRole("button", { name: L.takeOut })).toHaveCount(0);
|
||
|
||
// qc.quinn takes it out with a reason
|
||
await docs.getByRole("button", { name: L.takeOut }).first().click();
|
||
const out = page.getByRole("dialog", { name: L.takeOutSheet });
|
||
await expect(out).toBeVisible();
|
||
await out.getByLabel(L.takeOutReason, { exact: true }).fill("wrong packet");
|
||
await out.getByRole("button", { name: L.takeOutSubmit, exact: true }).click();
|
||
await expect(out).toBeHidden();
|
||
const toggle = page.getByRole("button", { name: L.showTakenOut(1) });
|
||
await expect(toggle).toBeVisible();
|
||
await toggle.click();
|
||
await expect(page.getByText(L.takenOutBy).first()).toContainText("wrong packet");
|
||
const p = await packet(quinn, id);
|
||
const row = (p.docs as Json[]).find((d) => d.title === "RT report W1-W4") as Json;
|
||
expect(row.removed.reason).toBe("wrong packet");
|
||
await packetTab(page, L.tabHistory).click();
|
||
await expect(page.getByText("Document taken out of the data book").first()).toBeVisible();
|
||
await closeAll(quinn, wade);
|
||
});
|
||
|
||
test("titp: the digital ITP, the hold callout, every block signed, sign-off, reopen", async ({ browser }) => {
|
||
const unit2 = proj.units[1];
|
||
const quinn = await actor(browser, "qc.quinn");
|
||
const page = quinn.page;
|
||
|
||
// start from the unit folder
|
||
await page.goto(`/qc/units/${unit2.id}`);
|
||
await expect(page.getByText(L.noItp)).toBeVisible();
|
||
await page.getByRole("button", { name: L.startItp }).click();
|
||
await expect(page).toHaveURL(new RegExp(`/qc/units/${unit2.id}/itp`));
|
||
const no = `${unit2.number}-ITP`;
|
||
await expect(page.getByRole("heading", { level: 1 })).toHaveText(no);
|
||
await expect(page.locator(L.itpRow)).toHaveCount(TEMPLATE_ITEMS);
|
||
const banners = page.locator(L.itpPage);
|
||
await expect(banners).toHaveCount(3);
|
||
for (let i = 0; i < 3; i++) await expect(banners.nth(i)).toContainText(L.pageBanners[i]);
|
||
await expect(page.getByLabel(L.hdr.no, { exact: true })).toHaveValue(no);
|
||
await expect(page.getByLabel(L.hdr.client, { exact: true })).toHaveValue("ACME");
|
||
await expect(page.getByLabel(L.hdr.pname, { exact: true })).toHaveValue("Separator skids");
|
||
await expect(page.getByLabel(L.hdr.pno, { exact: true })).toHaveValue(PROJECT);
|
||
await expect(page.getByLabel(L.hdr.po, { exact: true })).toHaveValue("5541");
|
||
await expect(page.getByLabel(L.hdr.unit, { exact: true })).toHaveValue(String(unit2.number));
|
||
|
||
// approve 1.1: the initials badge shows
|
||
await itpRow(page, "1.1").getByRole("button", { name: L.approve, exact: true }).click();
|
||
await expect(itpRow(page, "1.1").locator(L.initials)).toHaveText("QQ");
|
||
|
||
// section 5 N/A: its rows get the na class
|
||
await itpSection(page, "5").getByRole("button", { name: L.sectionNa }).click();
|
||
await expect(itpSection(page, "5").locator(`${L.itpRow}.na`)).toHaveCount(5);
|
||
|
||
// line 1.2 details: a customer witness and a note
|
||
await page.getByRole("button", { name: L.lineDetails("1.2") }).click();
|
||
const lineSheet = page.getByRole("dialog", { name: L.lineSheet });
|
||
await expect(lineSheet).toBeVisible();
|
||
await lineSheet.getByLabel(L.custWitness).fill("Rick Inspector");
|
||
await lineSheet.getByLabel(L.lineNote, { exact: true }).fill("Reviewed WPS-101 R2");
|
||
await lineSheet.getByRole("button", { name: L.save, exact: true }).click();
|
||
await expect(lineSheet).toBeHidden();
|
||
await expect(itpRow(page, "1.2")).toContainText("Rick Inspector");
|
||
|
||
// block 5 (all N/A) can be signed; block 3 cannot yet
|
||
await expect(itpBlock(page, "5")).toContainText(L.blockScope("Section 5"));
|
||
await expect(itpBlock(page, "3")).toContainText(L.blockScope("Sections 1–3"));
|
||
await expect(blockSign(page, "5")).toBeEnabled();
|
||
await expect(blockSign(page, "5")).toHaveText(L.signAllNa);
|
||
await expect(blockSign(page, "3")).toBeDisabled();
|
||
await blockSign(page, "5").click();
|
||
await signWithPin(page, PIN);
|
||
await expect(signSheet(page)).toBeHidden();
|
||
await expect(itpBlock(page, "5")).toContainText("Quinn QC");
|
||
|
||
// sup.wes on the 03 packet of unit 2: WP points only; the 11 packet: FA points
|
||
const wes = await actor(browser, "sup.wes");
|
||
await open(wes.page, pk("wp").id);
|
||
const hold = wes.page.locator(L.holdCallout);
|
||
await expect(hold).toBeVisible();
|
||
await expect(hold).toContainText(`ITP ${no}`);
|
||
await expect(hold).toContainText("3.4");
|
||
await expect(hold).not.toContainText("4.2");
|
||
await expect(hold).toContainText(L.holdAdvice);
|
||
await open(wes.page, pk("fa").id);
|
||
await expect(wes.page.locator(L.holdCallout)).toContainText("9.6");
|
||
await expect(wes.page.locator(L.holdCallout)).not.toContainText("3.4");
|
||
|
||
// qc.quinn approves every remaining line
|
||
const approve = enabledApprove(page);
|
||
for (let k = 0; k < TEMPLATE_ITEMS + 5; k++) {
|
||
const n = await approve.count();
|
||
if (!n) break;
|
||
await approve.first().click();
|
||
await expect(approve).toHaveCount(n - 1);
|
||
}
|
||
await expect(approve).toHaveCount(0);
|
||
// the customer on block 3, then blocks 3, 4, 6, 7, 8, 9
|
||
const b3 = itpBlock(page, "3");
|
||
await b3.getByLabel(L.custRepName).fill("Rick Inspector");
|
||
await b3.getByRole("button", { name: L.record, exact: true }).click();
|
||
await expect(b3).toContainText("recorded by");
|
||
for (const key of ["3", "4", "6", "7", "8", "9"]) {
|
||
await expect(blockSign(page, key)).toBeEnabled();
|
||
await blockSign(page, key).click();
|
||
await signWithPin(page, PIN);
|
||
await expect(signSheet(page)).toBeHidden();
|
||
await expect(blockSign(page, key)).toHaveCount(0);
|
||
}
|
||
|
||
// sign off with the PIN
|
||
const signOffBtn = page.getByRole("button", { name: L.signOff });
|
||
await expect(signOffBtn).toBeVisible();
|
||
await signOffBtn.click();
|
||
await signWithPin(page, PIN);
|
||
await expect(signSheet(page)).toBeHidden();
|
||
await expect(page.getByRole("note").filter({ hasText: L.signedOffCallout }).first()).toBeVisible();
|
||
await expect(page.locator(L.itpRow).getByRole("button", { name: L.approve, exact: true })).toHaveCount(0);
|
||
await expect(page.getByLabel(L.hdr.client, { exact: true })).toBeDisabled();
|
||
const itp = await api(quinn, "get", `/api/units/${unit2.id}/itp`);
|
||
expect(itp.status).toBe("complete");
|
||
|
||
// the hold callout on the 03 packet is gone
|
||
await open(wes.page, pk("wp").id);
|
||
await expect(wes.page.locator(L.holdCallout)).toHaveCount(0);
|
||
|
||
// the project folder's unit tile shows ITP ✓
|
||
await page.goto(`/qc/projects/${proj.id}`);
|
||
await expect(page.locator(L.qcUnit).filter({ hasText: String(unit2.number) })).toContainText(L.itpTick);
|
||
|
||
// admin.ann reopens with a reason; the page is editable again
|
||
const ann = await actor(browser, "admin.ann");
|
||
await ann.page.goto(`/qc/units/${unit2.id}/itp`);
|
||
await ann.page.getByRole("button", { name: L.reopen, exact: true }).click();
|
||
const reopen = ann.page.getByRole("dialog", { name: L.reopenSheet });
|
||
await expect(reopen).toBeVisible();
|
||
await reopen.getByLabel(L.reopenReason, { exact: true }).fill("Customer asked to re-inspect 4.2");
|
||
await reopen.getByRole("button", { name: new RegExp(`^${L.reopen}`) }).click();
|
||
await expect(reopen).toBeHidden();
|
||
await expect.poll(async () => (await api(ann, "get", `/api/units/${unit2.id}/itp`)).status).toBe("open");
|
||
await page.goto(`/qc/units/${unit2.id}/itp`); // quinn's page was on the project folder
|
||
await expect(page.getByLabel(L.hdr.client, { exact: true })).toBeEnabled();
|
||
await closeAll(quinn, wes, ann);
|
||
});
|
||
|
||
test("api: the signature and the step body used here still answer as the contract says", async ({ browser }) => {
|
||
// A guard for the helpers: a reject without a reason or an NCR is refused before any signature is asked.
|
||
const quinn: Actor = await actor(browser, "qc.quinn");
|
||
const p = await packet(quinn, pk("reject").id);
|
||
expect(p.status).toBe("qc");
|
||
await stepApi(quinn, p.id, "qc_reject", { sign: sign("qc.quinn") }, 400);
|
||
await closeAll(quinn);
|
||
});
|
||
});
|