Files
PacketStation/tests/e2e/tests/qc.spec.ts
T
spencermandClaude Fable 5.1 00531cb329
ci / frontend (push) Successful in 37s
ci / server (push) Successful in 9m18s
ci / image (push) Successful in 35s
ci / e2e (push) Successful in 2m17s
Phase 6: Q.C. inspection, project folders, packet documents and the digital ITP
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]>
2026-10-17 20:08:53 -06:00

542 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});