import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import type { PacketRead, Stage } from "../api/types"; import { DEPARTMENTS, makeMe, renderApp } from "../test/auth"; import { callsTo, mockFetch } from "../test/fetchMock"; import { makePacket } from "../test/packets"; import { WeldSheet } from "./WeldSheet"; const DEPTS = DEPARTMENTS.map((d) => (d.code === "03" ? { ...d, weld: true, stations: [{ id: 1, name: "Stall 1" }, { id: 2, name: "Stall 2" }] } : d)); const CHECK = [ { group: "Before welding", key: "dwg", text: "Drawing CI-DWG-1 Rev 0 matches the packet" }, { group: "Before welding", key: "heat", text: "Heat numbers on material match the Materials tab" }, { group: "After welding", key: "vt", text: "Visual inspection of all welds acceptable" }, ]; function stage(over: Partial = {}): Stage { return { id: 5, dept_code: "03", station: { id: 1, name: "Stall 1" }, welder: { id: "andy", display_name: "Andy Welder", stamp: "75" }, checklist: {}, filler: null, filler_lot: null, preheat: null, notes: null, clock: { elapsed_ms: 3_723_000, running: true, running_since: "2026-10-16T08:00:00Z", started_at: "2026-10-16T08:00:00Z" }, opened_at: "2026-10-16T07:00:00Z", row_version: 1, ...over, }; } function shopPacket(over: Partial = {}): PacketRead { return makePacket({ status: "in_progress", holder: "Stall 1", station: { id: 1, name: "Stall 1" }, welder: { id: "andy", display_name: "Andy Welder", stamp: "75" }, heat_map: true, bom: [ { item: 1, qty: 1, part_id: "P-1", description: "Pipe", dwg_ref: null, length: null, material: "A106", heat: "H1", row_version: 1 }, { item: 2, qty: 2, part_id: "P-2", description: "Flange", dwg_ref: null, length: null, material: "A105", heat: null, row_version: 1 }, ], work_orders: [{ id: 1, wo_base: "L999001", lot: "001", split: "0", sub: "0", qty: 3, target_hrs: 4.5, row_version: 1 }], checklist_items: CHECK, stage: stage(), can: [], blocked: {}, ...over, }); } function base(packet: PacketRead, roles: string[], name = "Wes Super") { return { "/api/auth/me": { body: makeMe({ roles, screens: ["supervisor", "stall"], user: { id: name === "Andy Welder" ? "andy" : "wes", display_name: name } }) }, "/api/departments": { body: DEPTS }, "/api/packets/1": { body: packet }, "/api/packets/1/log": { body: [] }, "/api/departments/03/people": { body: [{ id: "andy", display_name: "Andy Welder", stamp: "75", level: "worker", station_limits: [] }] }, }; } async function sign(secret = "pw-1") { const sheet = await screen.findByRole("dialog", { name: "Sign" }); fireEvent.change(within(sheet).getByLabelText("Password"), { target: { value: secret } }); fireEvent.click(within(sheet).getByRole("button", { name: "Sign" })); await waitFor(() => expect(screen.queryByRole("dialog", { name: "Sign" })).not.toBeInTheDocument()); } describe("Checklist tab", () => { it("each tick PUTs its key and the answer replaces the packet", async () => { const p = shopPacket({ can: ["edit_checklist", "edit_stage_fields"] }); const after = shopPacket({ version: 4, can: ["edit_checklist", "edit_stage_fields"], stage: stage({ checklist: { dwg: { done: true, at: "2026-10-16T09:00:00Z", stamp: "75" } } }) }); const fetch = mockFetch({ ...base(p, ["shop:03:worker"], "Andy Welder"), "PUT /api/packets/1/stage/checklist/dwg": { body: after } }); renderApp("/packets/1"); fireEvent.click(await screen.findByRole("tab", { name: /Checklist/ })); const box = screen.getByRole("checkbox", { name: /Drawing CI-DWG-1 Rev 0 matches the packet/ }); expect(box).toBeEnabled(); fireEvent.click(box); await waitFor(() => expect(screen.getByRole("checkbox", { name: /Drawing CI-DWG-1/ })).toBeChecked()); expect(callsTo(fetch, "PUT", "/api/packets/1/stage/checklist/dwg")).toEqual([{ done: true }]); expect(screen.getByText(/^75 · /)).toBeInTheDocument(); expect(screen.getByRole("group", { name: "Weld record" })).toBeInTheDocument(); }); it("is read-only without edit_checklist", async () => { mockFetch(base(shopPacket({ can: [] }), ["shop:03:lead"])); renderApp("/packets/1"); fireEvent.click(await screen.findByRole("tab", { name: /Checklist/ })); for (const box of screen.getAllByRole("checkbox")) expect(box).toBeDisabled(); expect(screen.getByText("Checklist is completed at the stall.")).toBeInTheDocument(); }); }); describe("Weld record", () => { it("saves a field on blur with its base", async () => { const p = shopPacket({ can: ["edit_checklist", "edit_stage_fields"] }); const fetch = mockFetch({ ...base(p, ["shop:03:worker"], "Andy Welder"), "PATCH /api/packets/1/stage": { body: shopPacket({ version: 4, stage: stage({ filler: "ER70S-6" }) }) } }); renderApp("/packets/1"); fireEvent.click(await screen.findByRole("tab", { name: /Checklist/ })); const filler = screen.getByLabelText("Filler metal"); fireEvent.change(filler, { target: { value: "ER70S-6" } }); fireEvent.blur(filler); await waitFor(() => expect(callsTo(fetch, "PATCH", "/api/packets/1/stage")).toEqual([{ set: { filler: "ER70S-6" }, base: { filler: null } }])); }); }); describe("Weld sheet", () => { it("needs at least one item, then sends the point and the default label position", () => { const onSave = vi.fn(); render( undefined} onClose={() => undefined} />, ); expect(screen.getByLabelText("Size")).toHaveValue('5/16"'); expect(screen.getByLabelText("Type")).toHaveValue("Fillet"); expect(screen.getByLabelText("Process")).toHaveValue("GMAW"); expect(screen.getByLabelText("Welder stamp")).toHaveValue("75"); expect(screen.getByLabelText("Piece")).toHaveTextContent("1 of 3"); fireEvent.click(screen.getByRole("button", { name: "Log weld" })); expect(screen.getByRole("alert")).toHaveTextContent("Pick at least one BOM item this weld joins."); expect(onSave).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole("button", { name: "Item 2" })); fireEvent.click(screen.getByRole("button", { name: "Item 1" })); fireEvent.click(screen.getByRole("button", { name: "Log weld" })); expect(onSave).toHaveBeenCalledTimes(1); const body = onSave.mock.calls[0]?.[0] as Record; expect(body).toMatchObject({ page: 1, fx: 0.5, fy: 0.5, wo_base: "L999001", piece: 1, items: [1, 2], type: "Fillet", size: '5/16"', process: "GMAW", stamp: "75" }); expect(body.bfx).toBeCloseTo(0.55); expect(body.bfy).toBeCloseTo(0.43); }); it("is read-only without edit rights", () => { render( undefined} onRemove={() => undefined} onClose={() => undefined} />, ); expect(screen.getByRole("heading", { name: "Weld W4" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Save changes" })).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument(); expect(screen.getByLabelText("Type")).toBeDisabled(); }); }); describe("Packet page action bars", () => { it("sup.wes on a review packet: Send to Q.C. waits for both boxes and a location, then signs", async () => { const p = shopPacket({ status: "review", holder: "Supervisor", stage: stage({ clock: { elapsed_ms: 1000, running: false, running_since: null, started_at: null } }), can: ["send_to_qc", "return_to_station", "route_from_review"] }); const fetch = mockFetch({ ...base(p, ["shop:03:supervisor"]), "POST /api/packets/1/transitions": { body: shopPacket({ status: "qc", version: 5, holder: "Q.C.", can: [], blocked: {} }) }, }); renderApp("/packets/1"); const box = await screen.findByRole("region", { name: "Supervisor review" }); const send = within(box).getByRole("button", { name: "Send to Q.C." }); expect(send).toBeDisabled(); fireEvent.click(within(box).getByLabelText("Data book package ready")); expect(send).toBeDisabled(); fireEvent.click(within(box).getByLabelText("Spool ready for inspection")); expect(send).toBeDisabled(); fireEvent.change(within(box).getByLabelText("Spool location"), { target: { value: "Bay 2" } }); expect(send).toBeEnabled(); expect(within(box).getByRole("button", { name: "Return to station" })).toBeEnabled(); expect(within(box).getByRole("button", { name: "Send to department" })).toBeEnabled(); fireEvent.click(send); expect(await within(await screen.findByRole("dialog", { name: "Sign" })).findByText("Send to Q.C.")).toBeInTheDocument(); await sign(); expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toEqual([ { action: "send_to_qc", expect: { status: "review", dept: "03" }, book: true, spool: true, location: "Bay 2", sign: { factor: "password", secret: "pw-1" } }, ]); await waitFor(() => expect(screen.getByTestId("status-pill")).toHaveTextContent("Q.C. inspection")); }); it("a lead sees the review read-only", async () => { mockFetch(base(shopPacket({ status: "review", holder: "Supervisor", can: [] }), ["shop:03:lead"], "Lou Lead")); renderApp("/packets/1"); expect(await screen.findByTestId("review-readonly")).toHaveTextContent("A supervisor for 03 Weld Parts reviews it and sends it on."); expect(screen.queryByRole("button", { name: "Send to Q.C." })).not.toBeInTheDocument(); }); it("weld.andy in progress: the clock, Pause, and Submit with the missing steps as links", async () => { const p = shopPacket({ can: ["clock", "edit_checklist", "edit_welds", "edit_stage_fields"], blocked: { submit: ["Finish the checklist first.", "Record at least one weld first.", "Enter the filler metal first."] }, labor: { hours: 3.24, tickets: 2, per_wo: [], observed_at: null }, }); mockFetch(base(p, ["shop:03:worker"], "Andy Welder")); renderApp("/packets/1"); const bar = await screen.findByRole("region", { name: "Station" }); expect(within(bar).getByRole("button", { name: "Submit to supervisor" })).toBeDisabled(); expect(within(bar).getByTestId("submit-missing")).toHaveTextContent("Before submitting: 3 checklist items · no welds marked on the drawing · filler metal"); expect(within(bar).getByRole("button", { name: "Pause" })).toBeEnabled(); expect(within(bar).getByTestId("clock")).toHaveTextContent(/01:02:0\d running · target 4.5 h/); expect(within(bar).getByTestId("labor")).toHaveTextContent("VISUAL tickets 3.2 h"); fireEvent.click(within(bar).getByRole("button", { name: "no welds marked on the drawing" })); expect(screen.getByRole("tab", { name: /Weld map/ })).toHaveAttribute("aria-selected", "true"); }); it("Submit signs and sends the expect guard; a stale step reloads and toasts the server text", async () => { let fresh = false; const p = shopPacket({ can: ["clock", "submit"], stage: stage({ filler: "ER70S-6", checklist: { dwg: { done: true }, heat: { done: true }, vt: { done: true } } }), welds: [{ weld_no: 1, page: 1, fx: 0.4, fy: 0.4, wo_base: "L999001", piece: 1, items: [1], type: "Fillet", size: '5/16"', process: "GMAW", stamp: "75", at: null, row_version: 1, welder: null, dept_code: "03" }] }); const back = shopPacket({ status: "ready", version: 9, station: null, welder: null, stage: null, holder: "Supervisor", can: [], blocked: {} }); const fetch = mockFetch({ ...base(p, ["shop:03:worker"], "Andy Welder"), "/api/packets/1": () => ({ body: fresh ? back : p }), "POST /api/packets/1/transitions": () => { fresh = true; return { status: 409, body: { detail: "Someone else already took this step.", code: "stale_step" } }; }, }); renderApp("/packets/1"); const bar = await screen.findByRole("region", { name: "Station" }); expect(within(bar).getByText(/Everything is filled in/)).toBeInTheDocument(); fireEvent.click(within(bar).getByRole("button", { name: "Submit to supervisor" })); await sign(); expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toEqual([{ action: "submit", expect: { status: "in_progress", dept: "03" }, sign: { factor: "password", secret: "pw-1" } }]); expect(await screen.findByTestId("toast")).toHaveTextContent("Someone else already took this step."); await waitFor(() => expect(screen.getByTestId("status-pill")).toHaveTextContent("Ready to assign")); }); it("the assign sheet sends the station, the welder and the stamp", async () => { const p = shopPacket({ status: "ready", station: null, welder: null, stage: null, holder: "Supervisor", can: ["assign", "send_back_to_warehouse"] }); const fetch = mockFetch({ ...base(p, ["shop:03:supervisor"]), "POST /api/packets/1/transitions": { body: shopPacket({ status: "assigned", version: 5, can: [] }) } }); renderApp("/packets/1"); fireEvent.click(await screen.findByRole("button", { name: "Send to station" })); const sheet = await screen.findByRole("dialog", { name: "Send to station" }); fireEvent.change(within(sheet).getByLabelText("Station"), { target: { value: "2" } }); await within(sheet).findByRole("option", { name: "Andy Welder [75]" }); fireEvent.change(within(sheet).getByLabelText("Find a welder"), { target: { value: "andy" } }); fireEvent.change(within(sheet).getByLabelText("Welder"), { target: { value: "andy" } }); expect(within(sheet).getByLabelText("Stamp")).toHaveValue("75"); fireEvent.click(within(sheet).getByRole("button", { name: "Send to station" })); await waitFor(() => expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toEqual([ { action: "assign", expect: { status: "ready", dept: "03" }, station_id: 2, welder_id: "andy", stamp: "75" }, ]), ); }); }); describe("Changes tab, verify", () => { it("a supervisor of a pending department ticks, verifies and signs", async () => { const co = { id: 9, co_no: 2, label: "CO-2", origin: "manual", review: "approved", minor: false, hold: false, ecn: null, reason: "New rev", issued_at: "2026-10-16T08:00:00Z", issued_by: { id: "clara", display_name: "Clara Clerk" }, items: [], required: ["03", "09"], verified: { "09": { by: { id: "x", display_name: "Hal Hydro" }, at: "2026-10-16T09:00:00Z", note: null, signed: true } }, pending_depts: ["03"], open: true, } as const; const p = shopPacket({ change_orders: [{ ...co, items: [], verified: { ...co.verified }, required: [...co.required], pending_depts: [...co.pending_depts] }] }); const fetch = mockFetch({ ...base(p, ["shop:03:supervisor"]), "POST /api/packets/1/change-orders/2/verify": { body: shopPacket({ version: 6 }) } }); renderApp("/packets/1"); fireEvent.click(await screen.findByRole("tab", { name: /Changes/ })); const box = await screen.findByRole("group", { name: "Verify CO-2 for 03 Weld Parts" }); const verify = within(box).getByRole("button", { name: "Verify" }); expect(verify).toBeDisabled(); fireEvent.click(within(box).getByLabelText("I reviewed this change with my department")); fireEvent.change(within(box).getByLabelText("Note (optional)"), { target: { value: "Checked with the crew" } }); fireEvent.click(verify); expect(await within(await screen.findByRole("dialog", { name: "Sign" })).findByText("Verify a change order for the department")).toBeInTheDocument(); await sign(); expect(callsTo(fetch, "POST", "/api/packets/1/change-orders/2/verify")).toEqual([ { dept: "03", note: "Checked with the crew", sign: { factor: "password", secret: "pw-1" } }, ]); }); it("a lead gets no verify box", async () => { const p = shopPacket({ change_orders: [ { id: 9, co_no: 2, origin: "manual", review: "approved", minor: false, hold: false, ecn: null, reason: null, issued_at: null, issued_by: null, items: [], required: ["03"], verified: {} }, ], }); mockFetch(base(p, ["shop:03:lead"], "Lou Lead")); renderApp("/packets/1"); fireEvent.click(await screen.findByRole("tab", { name: /Changes/ })); await screen.findByRole("region", { name: "Change order CO-2" }); expect(screen.queryByRole("button", { name: "Verify" })).not.toBeInTheDocument(); }); });