2026-10-17 23:49:14 -06:00
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 > = {}) : VcloseCheck {
return { observed_at : "2026-10-17T07:00:00Z" , closed_at : null , open_wos : [], agrees : true , ... over };
}
function seal ( over : Partial < PacketSeal > = {}) : 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 > = {}) : 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 > = {}) : PacketRead {
return toClose ({ vclosed : { at : "2026-10-17T09:00:00Z" , by : CLARA }, can : [ "file" , "set_vclosed" ], blocked : {}, ... over });
}
function filed ( over : Partial < PacketRead > = {}) : 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 < string , Route > = {}, 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" , () => {
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
it ( "a hand packet: shows the state, the exports, and Clerical confirms the close" , async () => {
2026-10-17 23:49:14 -06:00
const open = vi . spyOn ( window , "open" ). mockImplementation (() => null );
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
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 }) } }));
2026-10-17 23:49:14 -06:00
renderApp ( "/packets/1" );
const box = await panel ();
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
expect ( box ). toHaveTextContent ( "Back from the shop, ready to close. Close the job in VISUAL, then click below." );
2026-10-17 23:49:14 -06:00
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\./ );
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
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 ();
2026-10-17 23:49:14 -06:00
});
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 ();
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
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 ();
2026-10-17 23:49:14 -06:00
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" } },
]);
});
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
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 }) })));
2026-10-17 23:49:14 -06:00
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." );
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
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 );
2026-10-17 23:49:14 -06:00
});
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
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." ;
2026-10-17 23:49:14 -06:00
const fetch = mockFetch (
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
base ( toFile (), { "POST /api/packets/1/transitions" : { status : 409 , body : { code : "precondition_failed" , detail } } }),
2026-10-17 23:49:14 -06:00
);
renderApp ( "/packets/1" );
fireEvent . click ( within ( await panel ()). getByRole ( "button" , { name : "File in the data book" }));
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
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" } }]);
2026-10-17 23:49:14 -06:00
});
});
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." );
});
});