Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
ci / server (pull_request) Successful in 12m48s
ci / frontend (pull_request) Successful in 54s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 4m38s

- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 16:36:49 -06:00
co-authored by Claude Opus 5.5
parent 150c507a31
commit 70a97398f1
101 changed files with 3871 additions and 1098 deletions
+32
View File
@@ -99,4 +99,36 @@ test.describe.serial("admin stamp check", () => {
await expect(results).toContainText("Job traveler");
await closeAll(ann);
});
test("C9: Q.C. checks a stamp on the Stamp check screen; Warehouse is refused", async ({ browser }) => {
const quinn = await actor(browser, "qc.quinn");
const page = quinn.page;
// the API outside /admin answers Q.C.
const ok = await api(quinn, "post", "/api/stamp-verify", { text: LINE });
expect(ok.valid).toBe(true);
expect(ok.print.packet_id).toBe(P.id);
// the top-bar link opens the screen
await page.goto("/qc");
await page.getByRole("banner").getByRole("link", { name: "Stamp check", exact: true }).click();
await expect(page).toHaveURL(/\/stamp-check$/);
await expect(page.getByRole("heading", { level: 1, name: "Stamp check" })).toBeVisible();
const form = page.locator("form").filter({ has: page.getByRole("heading", { name: "Check a stamp line" }) });
await form.getByLabel("Stamp line or code", { exact: true }).fill(LINE);
await form.getByRole("button", { name: "Check stamp", exact: true }).click();
const result = form.getByRole("status", { name: "Stamp result" });
await expect(result).toContainText("Genuine.");
await expect(result).toContainText("Job traveler");
// Clerical may check too; Warehouse may not, and its screen says so
const clara = await actor(browser, "clerk.clara");
expect((await api(clara, "post", "/api/stamp-verify", { text: LINE })).valid).toBe(true);
const wade = await actor(browser, "wh.wade");
const refused = await call(wade, "post", "/api/stamp-verify", { text: LINE });
expect(refused.status).toBe(403);
await expect(wade.page.getByRole("banner").getByRole("link", { name: "Stamp check", exact: true })).toHaveCount(0);
await wade.page.goto("/stamp-check");
await expect(wade.page.getByTestId("not-for-role")).toContainText("The Stamp check is not for your role.");
await closeAll(quinn, clara, wade);
});
});
+87
View File
@@ -0,0 +1,87 @@
import { expect, test } from "@playwright/test";
import { actor, api, closeAll } from "./shophelpers";
import { call } from "./adminhelpers";
import type { Json } from "./whhelpers";
// Change C8 on the CI stack: checklists per department. multi.mia (Clerical and 06 Code Bldg 2 supervisor) opens
// Checklists from the top bar, sees only her own department, adds an item and saves. sup.wes (03 supervisor) can
// read 06 but not change it, and Q.C. and a lead cannot change any. At the end mia sets 06 back to the default list
// so the other scenarios keep the lists they expect.
const RUN = Date.now().toString(36).toLowerCase().slice(-5);
const KEY = `e2e_${RUN}`;
const TEXT = `Nameplate checked ${RUN}`;
async function resetTo06Default(a: Parameters<typeof api>[0]): Promise<void> {
const cur = await api(a, "get", "/api/checklists/06");
if (cur.custom) await api(a, "put", "/api/checklists/06", { items: null, base: cur.version });
}
test.describe.serial("department checklists", () => {
test.setTimeout(120_000);
test("C8: a supervisor edits their own department's checklist; others cannot", async ({ browser }) => {
const mia = await actor(browser, "multi.mia");
await resetTo06Default(mia);
const list = (await api(mia, "get", "/api/checklists")) as unknown as Json[];
const editable = list.filter((d) => d.editable).map((d) => String(d.dept_code));
expect(editable).toEqual(["06"]);
// the top-bar link; one department, so no picker
const page = mia.page;
await page.goto("/clerical");
await page.getByRole("banner").getByRole("link", { name: "Checklists", exact: true }).click();
await expect(page).toHaveURL(/\/checklists$/);
await expect(page.getByRole("heading", { level: 1, name: "Checklists" })).toBeVisible();
await expect(page.getByLabel("Department", { exact: true })).toHaveCount(0);
const editor = page.locator("section").filter({ has: page.getByRole("heading", { name: /^06 / }) });
await expect(editor).toBeVisible();
await expect(editor).toContainText(/Uses the default/);
const keys = editor.getByLabel(/^Item \d+ key$/);
const n = await keys.count();
expect(n).toBeGreaterThan(0);
// add an item at the end and save
await editor.getByRole("button", { name: "Add item", exact: true }).click();
await expect(keys).toHaveCount(n + 1);
await editor.getByLabel(`Item ${n + 1} group`, { exact: true }).fill("Final");
await editor.getByLabel(`Item ${n + 1} key`, { exact: true }).fill(KEY);
await editor.getByLabel(`Item ${n + 1} text`, { exact: true }).fill(TEXT);
await editor.getByRole("button", { name: "Save checklist", exact: true }).click();
await expect(editor.getByText(/^Checklist saved\./)).toBeVisible();
await expect(editor).toContainText("Own list");
const saved = await api(mia, "get", "/api/checklists/06");
expect(saved.custom).toBe(true);
expect(saved.editable).toBe(true);
expect((saved.items as Json[]).map((i) => i.key)).toContain(KEY);
expect((saved.items as Json[]).length).toBe(n + 1);
// sup.wes reads 06 but may not change it; his own list is 03
const wes = await actor(browser, "sup.wes");
const as03 = await api(wes, "get", "/api/checklists/06");
expect(as03.editable).toBe(false);
expect((as03.items as Json[]).map((i) => i.key)).toContain(KEY);
const refused = await call(wes, "put", "/api/checklists/06", { items: [], base: saved.version });
expect(refused.status).toBe(403);
await wes.page.goto("/checklists");
await expect(wes.page.locator("section").filter({ has: wes.page.getByRole("heading", { name: /^03 / }) })).toBeVisible();
await expect(wes.page.getByRole("heading", { name: /^06 / })).toHaveCount(0);
// Q.C. and a lead: no edit, no screen
const quinn = await actor(browser, "qc.quinn");
expect((await call(quinn, "put", "/api/checklists/03", { items: [], base: 0 })).status).toBe(403);
const lou = await actor(browser, "lead.lou");
expect((await api(lou, "get", "/api/checklists/03")).editable).toBe(false);
await lou.page.goto("/checklists");
await expect(lou.page.getByTestId("not-for-role")).toContainText("The Checklists screen is not for your role.");
// a stale save is refused with the current list
const stale = await call(mia, "put", "/api/checklists/06", { items: saved.items, base: Number(saved.version) - 1 });
expect(stale.status).toBe(409);
// back to the default so the other scenarios keep their lists
await resetTo06Default(mia);
expect((await api(mia, "get", "/api/checklists/06")).custom).toBe(false);
await closeAll(mia, wes, quinn, lou);
});
});
+45 -5
View File
@@ -21,9 +21,11 @@ import {
atCompleteApi,
cardOf,
clickAndCatch,
clickAndPrint,
column,
expectPdf,
fetchOk,
itpOf,
makeCloseProject,
openClericalHome,
openPacket,
@@ -206,18 +208,20 @@ test.describe.serial("close", () => {
await sheet.getByRole("button", { name: L.cancel, exact: true }).click();
await expect(sheet).toBeHidden();
// a row opens the print in a new tab and closes the sheet; the server logs it
// a row closes the sheet and prints on the same page (C4): the stamped PDF is fetched and the print dialog
// opens, no new tab; the server logs it
const before = await printCount(clara, id, "traveler");
await box.getByRole("button", { name: L.printOpen, exact: true }).click();
sheet = page.getByRole("dialog", { name: L.printSheet, exact: true });
await expect(sheet).toBeVisible();
await clickAndCatch(
clara.ctx,
await clickAndPrint(
page,
sheet.getByRole("button", { name: L.printRows[0], exact: true }),
`/api/packets/${id}/print/traveler`,
);
await expect(sheet).toBeHidden();
await expect(page).toHaveURL(new RegExp(`/packets/${id}$`));
expect(clara.ctx.pages().length).toBe(1);
await expect.poll(() => printCount(clara, id, "traveler")).toBe(before + 1);
// the same print through the API: a PDF, and one more log row
@@ -244,6 +248,33 @@ test.describe.serial("close", () => {
await closeAll(clara);
});
test("C3: an open ITP on the unit blocks filing until Q.C. signs it off", async ({ browser }) => {
const id = pk("r1").id;
const quinn = await actor(browser, "qc.quinn");
const unitId = Number(proj.unit.id);
const started = await api(quinn, "post", `/api/units/${unitId}/itp`, {});
expect(started.status).not.toBe("complete");
const clara = await actor(browser, "clerk.clara");
const page = clara.page;
await openPacket(page, id);
const box = panel(page);
await expect(box.getByText(L.readyForBook)).toBeVisible();
const file = box.getByRole("button", { name: L.fileIt, exact: true });
await expect(file).toBeDisabled();
await expect(box.getByText(L.itpOpen)).toBeVisible();
expect((await packet(clara, id)).blocked.file).toContain(L.itpOpen);
await stepApi(clara, id, "file", { sign: sign("clerk.clara") }, 403);
expect(await statusOf(clara, id)).toBe("complete");
// signed off: File is enabled again
await signedOffItpApi(quinn, unitId);
await page.reload();
await expect(box.getByRole("button", { name: L.fileIt, exact: true })).toBeEnabled();
await expect(box.getByText(L.itpOpen)).toHaveCount(0);
await closeAll(quinn, clara);
});
test("R1: filed in the data book with the PIN; the seal holds and the filed data book downloads", async ({
browser,
}) => {
@@ -326,12 +357,14 @@ test.describe.serial("close", () => {
await closeAll(ann, clara);
});
test("ITP: signed off through the API, its PDF is filed in the unit folder and linked on the ITP page", async ({
test("ITP: signed off, its PDF is filed in the unit folder and linked on the ITP page", async ({
browser,
}) => {
const quinn = await actor(browser, "qc.quinn");
const unitId = Number(proj.unit.id);
const itp = await signedOffItpApi(quinn, unitId);
// signed off in the C3 scenario above
const itp = (await itpOf(quinn, unitId)) as Json;
expect(itp.status).toBe("complete");
const no = String(itp.no);
expect(itp.signoff.asset_id).toBeTruthy();
@@ -356,6 +389,13 @@ test.describe.serial("close", () => {
.first();
const req = await clickAndCatch(quinn.ctx, quinn.page, pdfBtn, `/api/itps/${itp.id}/pdf`);
await expectPdf(quinn, req.url());
// Print goes to the print dialog on the same page (C4)
await clickAndPrint(
quinn.page,
quinn.page.getByRole("button", { name: "Print", exact: true }),
`/api/itps/${itp.id}/print`,
);
await expect(quinn.page).toHaveURL(new RegExp(`/qc/units/${unitId}/itp$`));
const signed = quinn.page.getByRole("link", { name: L.signedPdf, exact: true });
await expect(signed).toBeVisible();
await expect(signed).toHaveAttribute("href", new RegExp(`/_blob/${itp.signoff.asset_id}`));
+36 -2
View File
@@ -27,6 +27,8 @@ export const L = {
sealUnknown: "Seal unknown",
reopenedLine: /^Reopened by .+: .+\. The earlier seal is kept as broken\./,
blocked: "Close the job in VISUAL first.",
itpOpen: "Complete the unit's ITP first. Q.C. signs it off on the ITP page.", // C3
shortsOpen: "Every short must be received and approved by the supervisor first.", // C7
visualLine: "visual-line", // testid of the VISUAL observed line
sealState: "seal-state", // testid of the seal line
reopened: "reopened", // testid of the reopen line
@@ -34,7 +36,6 @@ export const L = {
jobClosed: "Job closed in VISUAL",
moveBack: "Not closed in VISUAL, move back",
fileIt: "File in the data book",
fileAnyway: "File anyway",
dataBookPdf: "Data book (PDF)",
download: "Download",
csv: "Traceability (CSV)",
@@ -42,7 +43,6 @@ export const L = {
reopenIt: "Reopen (IT)",
// sheets
printSheet: "Print",
openWosSheet: "File with open work orders?",
reopenSheet: "Reopen a filed packet",
// print rows (each a button whose name starts with the row title)
printRows: ["Job traveler", "Drawing with heat map", "Drawing with weld map", "Drawing (no markup)", "Data book"],
@@ -202,6 +202,40 @@ export async function clickAndCatch(ctx: BrowserContext, page: Page, target: Loc
return { url: () => fixed, response };
}
/**
* Click a print control and check it prints on the same page (change C4): the page fetches the stamped PDF (the URL
* contains `part`), draws it and calls window.print, which is stubbed here. No tab is opened. Returns the PDF URL.
*/
export async function clickAndPrint(page: Page, target: Locator, part: string): Promise<string> {
await expect(target).toBeVisible();
await page.evaluate(() => {
const w = window as unknown as {
__printed: number;
__opened: string[];
print: () => void;
open: (u?: string | URL) => Window | null;
};
w.__printed = 0;
w.__opened = [];
w.print = () => {
w.__printed += 1;
};
w.open = (u?: string | URL) => {
w.__opened.push(String(u ?? ""));
return null;
};
});
const fetched = page.waitForResponse((r) => r.url().includes(part), { timeout: 30_000 });
await target.click();
const res = await fetched;
expect(res.status(), `print fetch ${res.url()}`).toBe(200);
await expect
.poll(async () => page.evaluate(() => (window as unknown as { __printed: number }).__printed), { timeout: 30_000 })
.toBe(1);
expect(await page.evaluate(() => (window as unknown as { __opened: string[] }).__opened)).toEqual([]);
return res.url();
}
/** GET a URL through the actor's API context and check the status. */
export async function fetchOk(a: Actor, url: string): Promise<APIResponse> {
const r = await a.page.request.get(url);
+20
View File
@@ -180,4 +180,24 @@ test.describe.serial("project management", () => {
await expect(pmTab(page, L.pm.tabs.hours)).toHaveAttribute("aria-selected", "true"); // ps.pmtab
await closeAll(paula);
});
test("C10: the PM screen and /api/pm are for Project Management and Admin only", async ({ browser }) => {
const clara = await actor(browser, "clerk.clara");
const wes = await actor(browser, "sup.wes");
for (const a of [clara, wes]) {
for (const url of ["/api/pm/overview", "/api/pm/hours", "/api/pm/export.csv", "/api/pm/report.pdf"]) {
const r = await a.page.request.get(url);
expect(r.status(), `${a.sam} ${url}`).toBe(403);
}
await a.page.goto(L.homes.pm);
await expect(a.page.getByTestId("not-for-role")).toContainText(
"The Project Management screen is not for your role.",
);
await expect(a.page.getByRole("heading", { level: 1, name: L.pm.head })).toHaveCount(0);
}
// Admin still reads it
const ann = await actor(browser, "admin.ann");
const r = await ann.page.request.get("/api/pm/overview");
expect(r.status()).toBe(200);
await closeAll(clara, wes, ann);
});
});
+122 -3
View File
@@ -7,6 +7,8 @@ import {
closeAll,
packet,
packetTab,
assignApi,
finishStageApi,
pullAndRelease,
setPin,
sign,
@@ -43,6 +45,8 @@ import {
type QcProject,
} from "./qchelpers";
import type { Json } from "./whhelpers";
import { L as CL, finishInspectionApi } from "./closehelpers";
import { pullWithHeat } from "./pmhelpers";
// 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,
@@ -60,6 +64,8 @@ const T = {
reject: `QC reject spool ${RUN}`,
wp: `ITP weld spool ${RUN}`,
fa: `ITP final spool ${RUN}`,
ncr: `QC ncr spool ${RUN}`,
short: `QC short spool ${RUN}`,
};
let proj: QcProject;
@@ -104,6 +110,20 @@ test.describe.serial("qc", () => {
const rel = await stepApi(clara, fa.id, "release_to_shop", { dept: "11" });
expect(rel.status).toBe("ready");
P.fa = fa;
// C2: a packet to reject without an NCR number
P.ncr = await atQcApi(crew, proj.units[0], T.ncr, "Stall 4", "Bay 2");
// C7: a packet with item 2 short; wh.wade receives it before the shop works it
const sh = await makeQcDraft(clara, proj.units[0], T.short);
await pullWithHeat(wade, sh.id, `HS${RUN}-`, [2]);
const rec = await api(wade, "post", `/api/packets/${sh.id}/bom/2/short/receipts`, { qty: 2, heat: `HS${RUN}-R2` });
expect(rec.shorts_unapproved).toBe(1);
await assignApi(wes, sh.id, "Stall 4", andyId);
await stepApi(andy, sh.id, "start");
await finishStageApi(andy, sh.id, 2);
await stepApi(andy, sh.id, "submit", { sign: sign("weld.andy") });
const shq = await stepApi(wes, sh.id, "send_to_qc", { book: true, spool: true, location: "Bay 2", sign: sign("sup.wes") });
expect(shq.status).toBe("qc");
P.short = sh;
await setPin(quinn, PIN);
await closeAll(clara, wade, wes, andy, quinn);
});
@@ -184,7 +204,7 @@ test.describe.serial("qc", () => {
await closeAll(quinn);
});
test("reject: one weld rejected, an NCR number only, the password; the supervisor sends it again", async ({
test("reject: one weld rejected, a reason and an NCR number, the password; the supervisor sends it again", async ({
browser,
}) => {
const id = pk("reject").id;
@@ -206,7 +226,7 @@ test.describe.serial("qc", () => {
})
.toContain("reject:undercut");
// the reject sheet: enabled on an NCR number alone
// the reject sheet: an NCR number alone is not enough, the reason needs 8 characters (C2)
const bar = qcBar(page);
await bar.getByRole("button", { name: L.reject, exact: true }).click();
const sheet = page.getByRole("dialog", { name: L.rejectSheet, exact: true });
@@ -214,6 +234,12 @@ test.describe.serial("qc", () => {
const submitReject = sheet.getByRole("button", { name: L.rejectSubmit });
await expect(submitReject).toBeDisabled();
await sheet.getByLabel(L.ncrNumber, { exact: true }).fill(NCR);
await expect(submitReject).toBeDisabled();
const reason = sheet.getByLabel(L.rejectReason, { exact: true });
await reason.fill("Short");
await expect(sheet.getByText(L.reasonShort)).toBeVisible();
await expect(submitReject).toBeDisabled();
await reason.fill("Undercut on W1, grind and reweld");
await expect(submitReject).toBeEnabled();
await submitReject.click();
await signWithPassword(page, "qc.quinn");
@@ -530,12 +556,105 @@ test.describe.serial("qc", () => {
await closeAll(quinn, wes, ann);
});
test("C2: a reject without an NCR number needs the number at the accept", async ({ browser }) => {
const id = pk("ncr").id;
const NCR2 = "NCR-2026-0042";
const quinn = await actor(browser, "qc.quinn");
const wes = await actor(browser, "sup.wes");
// rejected with a detailed reason and no NCR number; sup.wes sends it again; a fresh inspection is complete
const rej = await stepApi(quinn, id, "qc_reject", { reason: "Porosity on W2, grind out and reweld", sign: sign("qc.quinn") });
expect(rej.status).toBe("review");
expect(rej.ncr_required).toBe(true);
await stepApi(wes, id, "send_to_qc", { book: true, spool: true, location: "Bay 3", sign: sign("sup.wes") });
await finishInspectionApi(quinn, id);
expect((await packet(quinn, id)).ncr_required).toBe(true);
// the API refuses the accept without the number (422)
const r = await quinn.page.request.post(`/api/packets/${id}/transitions`, {
headers: { Origin: new URL(quinn.page.url()).origin, "X-Requested-With": "PacketStation" },
data: { action: "qc_accept", expect: { status: "qc", dept: "03" }, routed_to: "databook", sign: sign("qc.quinn") },
});
expect(r.status()).toBe(422);
expect(((await r.json()) as Json).detail).toBe(L.ncrFirst);
// the Q.C. bar asks for it; Accept waits for a well-formed number
const page = quinn.page;
await open(page, id);
const bar = qcBar(page);
await expect(bar.getByTestId("ncr-required")).toBeVisible();
const acceptBtn = bar.getByRole("button", { name: L.accept, exact: true });
const field = bar.getByLabel(L.ncrOfReject, { exact: true });
await expect(acceptBtn).toBeDisabled();
await field.fill("NCR-12");
await expect(acceptBtn).toBeDisabled();
await field.fill(NCR2);
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");
const after = await packet(quinn, id);
expect(after.qc_last.ncr).toBe(NCR2);
expect(after.ncr_required).toBe(false);
await closeAll(quinn, wes);
});
test("C7: a received short needs the supervisor's approval before Q.C. accepts", async ({ browser }) => {
const id = pk("short").id;
const quinn = await actor(browser, "qc.quinn");
await finishInspectionApi(quinn, id);
let p = await packet(quinn, id);
expect(p.shorts_unapproved).toBe(1);
expect(p.blocked.qc_accept).toContain(CL.shortsOpen);
await stepApi(quinn, id, "qc_accept", { routed_to: "databook", sign: sign("qc.quinn") }, 403);
const receipt = Number(((p.shorts as Json[])[0].receipts as Json[])[0].id);
const approveUrl = `/api/packets/${id}/shorts/receipts/${receipt}/approve`;
// the Q.C. bar says why Accept is off
await open(quinn.page, id);
const bar = qcBar(quinn.page);
await expect(bar.getByText(CL.shortsOpen)).toBeVisible();
await expect(bar.getByRole("button", { name: L.accept, exact: true })).toBeDisabled();
// Q.C. and a lead may not approve; the department supervisor does
await api(quinn, "post", approveUrl, { sign: sign("qc.quinn") }, { status: 403 });
const lou = await actor(browser, "lead.lou");
await api(lou, "post", approveUrl, { sign: sign("lead.lou") }, { status: 403 });
const wes = await actor(browser, "sup.wes");
await open(wes.page, id, L.tabMaterials);
const receipts = wes.page.getByRole("group", { name: "Receipts for item 2" });
await expect(receipts).toContainText(`heat HS${RUN}-R2`);
await expect(receipts).toContainText(L.waitingApproval);
await receipts.getByRole("button", { name: "Approve", exact: true }).click();
await signWithPassword(wes.page, "sup.wes");
await expect(signSheet(wes.page)).toBeHidden();
await expect(receipts).toContainText("Approved");
await expect(receipts).not.toContainText(L.waitingApproval);
p = await packet(wes, id);
expect(p.shorts_unapproved).toBe(0);
// a second approval of the same receipt is refused
await api(wes, "post", approveUrl, { sign: sign("sup.wes") }, { status: 409 });
// now Q.C. accepts
await quinn.page.reload();
const acceptBtn = qcBar(quinn.page).getByRole("button", { name: L.accept, exact: true });
await expect(acceptBtn).toBeEnabled();
await acceptBtn.click();
await signWithPin(quinn.page, PIN);
await expect(signSheet(quinn.page)).toBeHidden();
await expect.poll(() => statusOf(quinn, id)).toBe("complete");
await closeAll(quinn, lou, wes);
});
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.
// A guard for the helpers: a reject without a reason is refused before any signature is asked, and an NCR
// number does not stand in for the reason (C2).
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 stepApi(quinn, p.id, "qc_reject", { ncr: NCR, sign: sign("qc.quinn") }, 400);
await stepApi(quinn, p.id, "qc_reject", { reason: "Bad", ncr: NCR, sign: sign("qc.quinn") }, 400);
await closeAll(quinn);
});
});
+5
View File
@@ -55,6 +55,11 @@ export const L = {
rejectSheet: "Reject",
rejectReason: "Reason / what failed",
ncrNumber: "NCR number",
reasonShort: "At least 8 characters.", // C2: under the reason while it is short
ncrOfReject: "NCR number of the reject", // C2: the accept's field when ncr_required
ncrFirst: "Enter the NCR number of the reject first.", // C2: the 422 text
tabMaterials: /^Materials/,
waitingApproval: "Waiting for the supervisor's approval.", // C7
rejectSubmit: "Reject to supervisor",
// the Q.C. tab (packet/QcTab.tsx)
inspector: "Inspector",
+21 -4
View File
@@ -121,12 +121,16 @@ test.describe.serial("shop", () => {
expect(assigned.welder.id).toBe(andyId);
expect(assigned.welder.stamp).toBe("A7");
// weld.andy on the Stall screen
// weld.andy on the Stall screen: no station picker (C11); his packets come from GET /api/stall/mine, each
// with the station sup.wes picked
const andy = await actor(browser, "weld.andy");
const mineApi = await api(andy, "get", "/api/stall/mine");
const mineCard = ((mineApi.mine ?? []) as Json[]).find((c) => Number(c.id) === id);
expect(mineCard?.station_name).toBe("Stall 1");
const stallMine = andy.page.waitForResponse((r) => r.url().includes("/api/stall/mine"));
await andy.page.goto("/stall");
const picker = andy.page.getByRole("combobox", { name: L.stallPicker });
if (await picker.count())
await picker.first().selectOption({ label: L.stallOption("Stall 1") });
expect((await stallMine).status()).toBe(200);
await expect(andy.page.getByRole("combobox", { name: L.stallPicker })).toHaveCount(0);
const myHead = andy.page.getByRole("heading", {
name: new RegExp(`^${L.myPackets}`),
});
@@ -642,3 +646,16 @@ test.describe.serial("shop", () => {
await closeAll(wes);
});
});
test("C11: a person with nothing assigned sees that the supervisor assigns packets", async ({ browser }) => {
// both.bo (03 worker and supervisor) has no packet assigned to him in these scenarios
const bo = await actor(browser, "both.bo");
const mine = await api(bo, "get", "/api/stall/mine");
expect(mine.mine).toEqual([]);
expect(mine.submitted).toEqual([]);
expect(String(mine.empty)).toContain("Your supervisor assigns packets to you.");
await bo.page.goto("/stall");
await expect(bo.page.getByText("Your supervisor assigns packets to you.").first()).toBeVisible();
await expect(bo.page.getByRole("combobox", { name: L.stallPicker })).toHaveCount(0);
await closeAll(bo);
});