import { fireEvent, screen, waitFor, within } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { PacketRead, PacketSeal, SealView, VcloseCheck } from "../api/types"; import { DEPARTMENTS, makeMe, renderApp } from "../test/auth"; import { callsTo, mockFetch, type Route } from "../test/fetchMock"; import { makePacket } from "../test/packets"; const CLARA = { id: "11111111-1111-1111-1111-111111111111", display_name: "Clara Clerk" }; const ANN = { id: "22222222-2222-2222-2222-222222222222", display_name: "Ann Admin" }; function check(over: Partial = {}): VcloseCheck { return { observed_at: "2026-10-17T07:00:00Z", closed_at: null, open_wos: [], agrees: true, ...over }; } function seal(over: Partial = {}): PacketSeal { return { id: 4, sealed_at: "2026-10-17T10:00:00Z", sealed_by: CLARA, key_id: "k1", signed: true, broken_at: null, broken_by: null, broken_reason: null, open_wos: null, databook: { asset_id: "a-db", url: "/_blob/a-db", download_url: "/_blob/a-db?download=1", pages: 14, size: 90000, file_name: "L999-LotL999001_CI-DWG-1_Manufacturing_Data_Book.pdf" }, ...over, }; } /** Complete, routed to the data book, not confirmed closed in VISUAL. */ function toClose(over: Partial = {}): PacketRead { return makePacket({ status: "complete", routed_to: "databook", holder: "Clerical", source_kind: "visual", can: ["set_vclosed"], blocked: { file: ["Close the job in VISUAL first."] }, vclose_check: check(), ...over, }); } /** Complete and confirmed closed: ready for the data book. */ function toFile(over: Partial = {}): PacketRead { return toClose({ vclosed: { at: "2026-10-17T09:00:00Z", by: CLARA }, can: ["file", "set_vclosed"], blocked: {}, ...over }); } function filed(over: Partial = {}): PacketRead { return toFile({ status: "filed", version: 9, databook_at: "2026-10-17T10:00:00Z", seal: seal(), seals: 1, can: [], blocked: {}, ...over }); } function base(packet: PacketRead, extra: Record = {}, roles = ["clerical"]) { return { "/api/auth/me": { body: makeMe({ roles }) }, "/api/departments": { body: DEPARTMENTS }, "/api/packets/1": { body: packet }, "/api/packets/1/log": { body: [] }, ...extra, }; } function sealView(valid: boolean | null, s: PacketSeal = seal()): SealView { return { seal: s, valid, seals: [s] }; } 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()); } const panel = () => screen.findByRole("region", { name: "Close and file" }); afterEach(() => { vi.restoreAllMocks(); }); describe("Close and file: ready to close", () => { it("a hand packet: shows the state, the exports, and Clerical confirms the close", async () => { const open = vi.spyOn(window, "open").mockImplementation(() => null); const hand = { source_kind: "hand" as const, vclose_check: null }; const fetch = mockFetch(base(toClose(hand), { "POST /api/packets/1/vclosed": { body: toFile({ version: 4, ...hand }) } })); renderApp("/packets/1"); const box = await panel(); expect(box).toHaveTextContent("Back from the shop, ready to close. Close the job in VISUAL, then click below."); const file = within(box).getByRole("button", { name: "File in the data book" }); expect(file).toBeDisabled(); expect(within(box).getByText("Close the job in VISUAL first.")).toHaveClass("why"); fireEvent.click(within(box).getByRole("button", { name: "Data book (PDF)" })); expect(open).toHaveBeenCalledWith("/api/packets/1/pdf/databook", "_blank", "noopener"); expect(within(box).getByRole("link", { name: "Download" })).toHaveAttribute("href", "/api/packets/1/pdf/databook?download=1"); expect(within(box).getByRole("link", { name: "Download" })).toHaveAttribute("download"); expect(within(box).getByRole("link", { name: "Traceability (CSV)" })).toHaveAttribute("href", "/api/packets/1/export/traceability.csv"); fireEvent.click(within(box).getByRole("button", { name: "Print…" })); expect(await screen.findByRole("dialog", { name: "Print" })).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Cancel" })); fireEvent.click(within(box).getByRole("button", { name: "Job closed in VISUAL" })); await waitFor(() => expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent("Ready for the data book.")); expect(callsTo(fetch, "POST", "/api/packets/1/vclosed")).toEqual([{ closed: true }]); expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent(/Job closed in VISUAL .* by Clara Clerk\./); expect(screen.getByRole("button", { name: "Not closed in VISUAL, move back" })).toBeEnabled(); }); it("a VISUAL packet waits for the feed: no manual close, File says why it is blocked", async () => { const why = "VISUAL still shows a work order open on this packet. Close it in VISUAL first."; mockFetch(base(toClose({ blocked: { file: [why] }, vclose_check: check({ open_wos: ["L999001/001"] }) }))); renderApp("/packets/1"); const box = await panel(); expect(box).toHaveTextContent("Back from the shop, ready to close. Close the job in VISUAL. The packet moves on to the data book when the feed sees it closed."); expect(within(box).queryByRole("button", { name: "Job closed in VISUAL" })).not.toBeInTheDocument(); expect(within(box).getByRole("button", { name: "File in the data book" })).toBeDisabled(); expect(within(box).getByText(why)).toHaveClass("why"); }); it("a VISUAL packet the feed saw closed is ready for the data book", async () => { mockFetch(base(toClose({ can: ["file"], blocked: {}, vclose_check: check({ closed_at: "2026-10-17T06:00:00Z", agrees: false }) }))); renderApp("/packets/1"); const box = await panel(); expect(box).toHaveTextContent(/^Ready for the data book\. VISUAL shows the job closed since .+\. Check the data book, then file it\./); expect(within(box).getByRole("button", { name: "File in the data book" })).toBeEnabled(); expect(within(box).queryByRole("button", { name: "Not closed in VISUAL, move back" })).not.toBeInTheDocument(); }); it.each([ ["work orders still open", check({ open_wos: ["L999001/001", "L999001/002"] }), /^VISUAL still shows 2 work orders open: L999001\/001, L999001\/002\.$/], ["one work order open", check({ open_wos: ["L999001/001"] }), /^VISUAL still shows 1 work order open: L999001\/001\.$/], ["the feed has not looked", check({ observed_at: null }), /^The feed has not checked VISUAL yet\.$/], ])("the VISUAL line when %s", async (_name, c, text) => { mockFetch(base(toClose({ vclose_check: c }))); renderApp("/packets/1"); const box = await panel(); expect(within(box).getByTestId("visual-line")).toHaveTextContent(text); }); it("a hand packet has no VISUAL line", async () => { mockFetch(base(toClose({ source_kind: "hand", vclose_check: null }))); renderApp("/packets/1"); const box = await panel(); expect(box).toHaveTextContent("Back from the shop, ready to close."); expect(within(box).queryByTestId("visual-line")).not.toBeInTheDocument(); }); it("is not shown before complete or to a role with nothing to do", async () => { mockFetch(base(toClose({ can: [], blocked: {} }), {}, ["warehouse"])); renderApp("/packets/1"); await screen.findByRole("tab", { name: /^Materials/ }); expect(screen.queryByRole("region", { name: "Close and file" })).not.toBeInTheDocument(); }); }); describe("Close and file: ready for the data book", () => { it("files with the signature", async () => { const fetch = mockFetch( base(toFile(), { "POST /api/packets/1/transitions": (req) => (req.body as { sign?: unknown }).sign ? { body: filed() } : { status: 400, body: { code: "signature_required", detail: "This step needs your signature.", meaning: "File in the data book" } }, "/api/packets/1/seal": { body: sealView(true) }, }), ); renderApp("/packets/1"); const box = await panel(); expect(box).toHaveTextContent(/^Ready for the data book\. Job closed in VISUAL .+ by Clara Clerk\./); // A VISUAL packet: the feed decides, so there is no manual move back. expect(within(box).queryByRole("button", { name: "Not closed in VISUAL, move back" })).not.toBeInTheDocument(); fireEvent.click(within(box).getByRole("button", { name: "File in the data book" })); expect(await within(await screen.findByRole("dialog", { name: "Sign" })).findByText("File in the data book")).toBeInTheDocument(); await sign(); await waitFor(() => expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent("Filed in the data book")); expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toEqual([ { action: "file", expect: { status: "complete", dept: "03" } }, { action: "file", expect: { status: "complete", dept: "03" }, sign: { factor: "password", secret: "pw-1" } }, ]); }); it("File is blocked while VISUAL shows work orders open, with no second confirmation", async () => { const why = "VISUAL still shows a work order open on this packet. Close it in VISUAL first."; const fetch = mockFetch(base(toFile({ can: [], blocked: { file: [why] }, vclose_check: check({ open_wos: ["L999001/001"], agrees: false }) }))); renderApp("/packets/1"); const box = await panel(); const line = within(box).getByTestId("visual-line"); expect(line).toHaveClass("callout", "warn"); expect(line).toHaveTextContent("VISUAL still shows 1 work order open: L999001/001."); const file = within(box).getByRole("button", { name: "File in the data book" }); expect(file).toBeDisabled(); expect(within(box).getByText(why)).toHaveClass("why"); fireEvent.click(file); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toHaveLength(0); }); it("shows the server's refusal when filing is refused after all", async () => { const detail = "VISUAL still shows a work order open on this packet. Close it in VISUAL first."; const fetch = mockFetch( base(toFile(), { "POST /api/packets/1/transitions": { status: 409, body: { code: "precondition_failed", detail } } }), ); renderApp("/packets/1"); fireEvent.click(within(await panel()).getByRole("button", { name: "File in the data book" })); expect(await screen.findByRole("alert")).toHaveTextContent(detail); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toEqual([{ action: "file", expect: { status: "complete", dept: "03" } }]); }); }); describe("Close and file: filed", () => { it("shows the seal intact and opens the filed data book", async () => { const open = vi.spyOn(window, "open").mockImplementation(() => null); mockFetch(base(filed(), { "/api/packets/1/seal": { body: sealView(true) } })); renderApp("/packets/1"); const box = await panel(); expect(box).toHaveTextContent(/Filed in the data book .* by Clara Clerk\. The data book has 14 pages\./); expect(await within(box).findByText("Seal intact (key k1)")).toHaveClass("pill", "st-ok"); fireEvent.click(within(box).getByRole("button", { name: "Data book (PDF)" })); expect(open).toHaveBeenCalledWith("/_blob/a-db", "_blank", "noopener"); expect(within(box).getByRole("link", { name: "Download" })).toHaveAttribute("href", "/_blob/a-db?download=1"); expect(within(box).getByRole("link", { name: "Traceability (CSV)" })).toBeInTheDocument(); expect(within(box).getByRole("button", { name: "Print…" })).toBeInTheDocument(); expect(within(box).queryByRole("button", { name: "Reopen (IT)" })).not.toBeInTheDocument(); expect(within(box).queryByRole("button", { name: "File in the data book" })).not.toBeInTheDocument(); }); it("says when the seal does not match", async () => { mockFetch(base(filed(), { "/api/packets/1/seal": { body: sealView(false) } })); renderApp("/packets/1"); const box = await panel(); const state = await within(box).findByTestId("seal-state"); expect(state).toHaveClass("callout", "warn"); expect(state).toHaveTextContent("Seal does not match the record."); }); it("says when the seal cannot be checked", async () => { mockFetch(base(filed(), { "/api/packets/1/seal": { body: sealView(null) } })); renderApp("/packets/1"); const box = await panel(); expect(await within(box).findByTestId("seal-state")).toHaveTextContent("Seal unknown (key not available)"); }); it("IT reopens with the reason and the signature; the panel keeps the broken seal line", async () => { const reopened = toFile({ version: 10, can: ["admin_reopen"], seal: seal({ broken_at: "2026-10-17T11:00:00Z", broken_by: ANN, broken_reason: "wrong MTR filed" }), seals: 1, }); const fetch = mockFetch( base(filed({ can: ["admin_reopen"] }), { "/api/packets/1/seal": { body: sealView(true) }, "POST /api/packets/1/reopen": { body: reopened } }, ["admin"]), ); renderApp("/packets/1"); const box = await panel(); fireEvent.click(within(box).getByRole("button", { name: "Reopen (IT)" })); const sheet = await screen.findByRole("dialog", { name: "Reopen a filed packet" }); fireEvent.change(within(sheet).getByLabelText("Reason"), { target: { value: "wrong MTR filed" } }); fireEvent.click(within(sheet).getByRole("button", { name: "Reopen" })); expect(await within(await screen.findByRole("dialog", { name: "Sign" })).findByText("Reopen a filed packet")).toBeInTheDocument(); await sign(); await waitFor(() => expect(screen.getByTestId("reopened")).toHaveTextContent(/^Reopened by Ann Admin .+: wrong MTR filed\. The earlier seal is kept as broken\.$/)); expect(callsTo(fetch, "POST", "/api/packets/1/reopen")).toEqual([{ reason: "wrong MTR filed", sign: { factor: "password", secret: "pw-1" } }]); expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent("Ready for the data book."); }); });