test(completion-docs): give typing-heavy dialog tests the vendor-suite budget

Use user-event without per-keystroke delays and a 15 s budget per test, as
the vendor create modal suite does, so the dialog tests don't time out on a
loaded runner.
This commit is contained in:
Alexandre Brandizzi 2026-09-25 11:39:42 -03:00
parent c1361dbbf3
commit b6226ef1e6
2 changed files with 49 additions and 41 deletions

View file

@ -23,6 +23,9 @@ vi.mock("@/providers/auth-context", async (importOriginal) => ({
useAuthContext: () => ({ user: { id: "u1", userRoles: auth.userRoles } }), useAuthContext: () => ({ user: { id: "u1", userRoles: auth.userRoles } }),
})); }));
// delay: null skips per-keystroke timer yields that only slow typing-heavy tests down.
let user = userEvent.setup({ delay: null });
function renderPage() { function renderPage() {
return renderWithProviders(<CompletionDocsPage />, { return renderWithProviders(<CompletionDocsPage />, {
route: "/completion-docs", route: "/completion-docs",
@ -31,16 +34,17 @@ function renderPage() {
} }
async function openTemplate(name: string) { async function openTemplate(name: string) {
await userEvent.click(await screen.findByRole("row", { name: new RegExp(name) })); await user.click(await screen.findByRole("row", { name: new RegExp(name) }));
return screen.findByRole("dialog"); return screen.findByRole("dialog");
} }
beforeEach(() => { beforeEach(() => {
user = userEvent.setup({ delay: null });
auth.userRoles = "Admin"; auth.userRoles = "Admin";
serveTemplates(api, [BACKFLOW, SCRUBBER]); serveTemplates(api, [BACKFLOW, SCRUBBER]);
}); });
describe("Completion Docs page", () => { describe("Completion Docs page", { timeout: 15_000 }, () => {
it("lists every template with its Document Title, Last Updated and Created By", async () => { it("lists every template with its Document Title, Last Updated and Created By", async () => {
renderPage(); renderPage();
@ -66,7 +70,7 @@ describe("Completion Docs page", () => {
await screen.findByRole("row", { name: /Scrubber Pit Augering/ }); await screen.findByRole("row", { name: /Scrubber Pit Augering/ });
const search = screen.getByRole("searchbox", { name: "Search document title" }); const search = screen.getByRole("searchbox", { name: "Search document title" });
await userEvent.type(search, "backf"); await user.type(search, "backf");
expect( expect(
await screen.findByText("1 template", { selector: "span" }, { timeout: 2000 }), await screen.findByText("1 template", { selector: "span" }, { timeout: 2000 }),
).toBeInTheDocument(); ).toBeInTheDocument();
@ -75,8 +79,8 @@ describe("Completion Docs page", () => {
searchParams: { search: "backf" }, searchParams: { search: "backf" },
}); });
await userEvent.clear(search); await user.clear(search);
await userEvent.type(search, "no such document"); await user.type(search, "no such document");
expect( expect(
await screen.findByText("No templates found", {}, { timeout: 2000 }), await screen.findByText("No templates found", {}, { timeout: 2000 }),
).toBeInTheDocument(); ).toBeInTheDocument();

View file

@ -29,6 +29,9 @@ vi.mock("@/providers/auth-context", async (importOriginal) => ({
useAuthContext: () => ({ user: { id: "u1", userRoles: "Admin" } }), useAuthContext: () => ({ user: { id: "u1", userRoles: "Admin" } }),
})); }));
// delay: null skips per-keystroke timer yields that only slow typing-heavy tests down.
let user = userEvent.setup({ delay: null });
function renderPage() { function renderPage() {
return renderWithProviders(<CompletionDocsPage />, { return renderWithProviders(<CompletionDocsPage />, {
route: "/completion-docs", route: "/completion-docs",
@ -39,13 +42,13 @@ function renderPage() {
async function openNewTemplate() { async function openNewTemplate() {
renderPage(); renderPage();
await screen.findByRole("row", { name: /Backflow Test Report/ }); await screen.findByRole("row", { name: /Backflow Test Report/ });
await userEvent.click(screen.getByRole("button", { name: /Add Template/ })); await user.click(screen.getByRole("button", { name: /Add Template/ }));
return screen.findByRole("dialog"); return screen.findByRole("dialog");
} }
async function openTemplate(name: string) { async function openTemplate(name: string) {
renderPage(); renderPage();
await userEvent.click(await screen.findByRole("row", { name: new RegExp(name) })); await user.click(await screen.findByRole("row", { name: new RegExp(name) }));
return screen.findByRole("dialog"); return screen.findByRole("dialog");
} }
@ -69,20 +72,21 @@ function unprocessable(body: { code: string; message: string }, message = body.m
} }
async function addProcedure(dialog: HTMLElement, index: number, name: string) { async function addProcedure(dialog: HTMLElement, index: number, name: string) {
await userEvent.click(within(dialog).getByRole("button", { name: "+ Add procedure" })); await user.click(within(dialog).getByRole("button", { name: "+ Add procedure" }));
await userEvent.type(within(dialog).getByLabelText(`Procedure ${index} name`), name); await user.type(within(dialog).getByLabelText(`Procedure ${index} name`), name);
} }
beforeEach(() => { beforeEach(() => {
user = userEvent.setup({ delay: null });
vi.clearAllMocks(); vi.clearAllMocks();
serveTemplates(api, [BACKFLOW, SCRUBBER], { count: 0, workOrderIds: [] }); serveTemplates(api, [BACKFLOW, SCRUBBER], { count: 0, workOrderIds: [] });
}); });
describe("Add Template", () => { describe("Add Template", { timeout: 15_000 }, () => {
it("blocks saving without a Document Title and says why", async () => { it("blocks saving without a Document Title and says why", async () => {
const dialog = await openNewTemplate(); const dialog = await openNewTemplate();
await userEvent.click(within(dialog).getByRole("button", { name: /Add Template/ })); await user.click(within(dialog).getByRole("button", { name: /Add Template/ }));
expect(await within(dialog).findByText("Document Title is required")).toBeInTheDocument(); expect(await within(dialog).findByText("Document Title is required")).toBeInTheDocument();
expect(within(dialog).getByRole("textbox", { name: /Document Title/ })).toHaveFocus(); expect(within(dialog).getByRole("textbox", { name: /Document Title/ })).toHaveFocus();
@ -97,14 +101,14 @@ describe("Add Template", () => {
expect(within(dialog).getByText("WO#")).toBeInTheDocument(); expect(within(dialog).getByText("WO#")).toBeInTheDocument();
expect(within(dialog).getAllByRole("textbox")).toHaveLength(1); expect(within(dialog).getAllByRole("textbox")).toHaveLength(1);
await userEvent.click(within(dialog).getByRole("button", { name: "+ Add safety note" })); await user.click(within(dialog).getByRole("button", { name: "+ Add safety note" }));
expect(within(dialog).queryByRole("button", { name: "+ Add safety note" })).toBeNull(); expect(within(dialog).queryByRole("button", { name: "+ Add safety note" })).toBeNull();
await userEvent.type(within(dialog).getByLabelText("Extra safety note"), "Mind the pit"); await user.type(within(dialog).getByLabelText("Extra safety note"), "Mind the pit");
expect(within(dialog).getByTestId("completion-template-preview")).toHaveTextContent( expect(within(dialog).getByTestId("completion-template-preview")).toHaveTextContent(
"Mind the pit", "Mind the pit",
); );
await userEvent.click(within(dialog).getByRole("button", { name: "Remove safety note" })); await user.click(within(dialog).getByRole("button", { name: "Remove safety note" }));
expect(within(dialog).queryByLabelText("Extra safety note")).toBeNull(); expect(within(dialog).queryByLabelText("Extra safety note")).toBeNull();
expect(within(dialog).getByTestId("completion-template-preview")).not.toHaveTextContent( expect(within(dialog).getByTestId("completion-template-preview")).not.toHaveTextContent(
"Mind the pit", "Mind the pit",
@ -120,28 +124,28 @@ describe("Add Template", () => {
await addProcedure(dialog, 3, "Restore"); await addProcedure(dialog, 3, "Restore");
expect(previewHeadings(dialog)).toEqual(["Isolate", "Inspect", "Restore"]); expect(previewHeadings(dialog)).toEqual(["Isolate", "Inspect", "Restore"]);
await userEvent.click(within(dialog).getByRole("button", { name: "Remove procedure 2" })); await user.click(within(dialog).getByRole("button", { name: "Remove procedure 2" }));
expect(previewHeadings(dialog)).toEqual(["Isolate", "Restore"]); expect(previewHeadings(dialog)).toEqual(["Isolate", "Restore"]);
expect(within(dialog).getByLabelText("Procedure 2 name")).toHaveValue("Restore"); expect(within(dialog).getByLabelText("Procedure 2 name")).toHaveValue("Restore");
await userEvent.click(within(dialog).getByRole("button", { name: "+ Add procedure" })); await user.click(within(dialog).getByRole("button", { name: "+ Add procedure" }));
expect(previewHeadings(dialog)).toEqual(["Isolate", "Restore", "Procedure 3"]); expect(previewHeadings(dialog)).toEqual(["Isolate", "Restore", "Procedure 3"]);
}); });
it("saves the title, note and procedures in order, then confirms with a toast", async () => { it("saves the title, note and procedures in order, then confirms with a toast", async () => {
const dialog = await openNewTemplate(); const dialog = await openNewTemplate();
await userEvent.type( await user.type(
within(dialog).getByRole("textbox", { name: /Document Title/ }), within(dialog).getByRole("textbox", { name: /Document Title/ }),
" PRV Check ", " PRV Check ",
); );
await userEvent.click(within(dialog).getByRole("button", { name: "+ Add safety note" })); await user.click(within(dialog).getByRole("button", { name: "+ Add safety note" }));
await userEvent.type(within(dialog).getByLabelText("Extra safety note"), "Gloves"); await user.type(within(dialog).getByLabelText("Extra safety note"), "Gloves");
await addProcedure(dialog, 1, "Open"); await addProcedure(dialog, 1, "Open");
await userEvent.type(within(dialog).getByLabelText("Procedure 1 description"), "a{enter}b"); await user.type(within(dialog).getByLabelText("Procedure 1 description"), "a{enter}b");
await userEvent.click(within(dialog).getByRole("button", { name: "+ Add procedure" })); await user.click(within(dialog).getByRole("button", { name: "+ Add procedure" }));
await addProcedure(dialog, 3, "Close"); await addProcedure(dialog, 3, "Close");
await userEvent.click(within(dialog).getByRole("button", { name: /Add Template/ })); await user.click(within(dialog).getByRole("button", { name: /Add Template/ }));
await waitFor(() => expect(toast.success).toHaveBeenCalledWith("Template added")); await waitFor(() => expect(toast.success).toHaveBeenCalledWith("Template added"));
expect(api.apiPost).toHaveBeenCalledWith("workorders/completion-templates", { expect(api.apiPost).toHaveBeenCalledWith("workorders/completion-templates", {
@ -160,9 +164,9 @@ describe("Add Template", () => {
unprocessable({ code: "TitleRequired", message: "Document Title is required." }), unprocessable({ code: "TitleRequired", message: "Document Title is required." }),
); );
const dialog = await openNewTemplate(); const dialog = await openNewTemplate();
await userEvent.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), "X"); await user.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), "X");
await userEvent.click(within(dialog).getByRole("button", { name: /Add Template/ })); await user.click(within(dialog).getByRole("button", { name: /Add Template/ }));
expect(await within(dialog).findByText("Document Title is required")).toBeInTheDocument(); expect(await within(dialog).findByText("Document Title is required")).toBeInTheDocument();
expect(toast.error).not.toHaveBeenCalled(); expect(toast.error).not.toHaveBeenCalled();
@ -172,9 +176,9 @@ describe("Add Template", () => {
const message = "Procedure name must be 200 characters or fewer."; const message = "Procedure name must be 200 characters or fewer.";
api.apiPost.mockRejectedValue(unprocessable({ code: "InvalidValue", message }, message)); api.apiPost.mockRejectedValue(unprocessable({ code: "InvalidValue", message }, message));
const dialog = await openNewTemplate(); const dialog = await openNewTemplate();
await userEvent.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), "X"); await user.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), "X");
await userEvent.click(within(dialog).getByRole("button", { name: /Add Template/ })); await user.click(within(dialog).getByRole("button", { name: /Add Template/ }));
await waitFor(() => expect(toast.error).toHaveBeenCalledWith(message)); await waitFor(() => expect(toast.error).toHaveBeenCalledWith(message));
expect(within(dialog).queryByText("Document Title is required")).toBeNull(); expect(within(dialog).queryByText("Document Title is required")).toBeNull();
@ -182,17 +186,17 @@ describe("Add Template", () => {
}); });
}); });
describe("Edit template", () => { describe("Edit template", { timeout: 15_000 }, () => {
it("Cancel discards unsaved changes and returns to the original read-only values", async () => { it("Cancel discards unsaved changes and returns to the original read-only values", async () => {
const dialog = await openTemplate("Backflow Test Report"); const dialog = await openTemplate("Backflow Test Report");
await userEvent.click(within(dialog).getByRole("button", { name: /Edit/ })); await user.click(within(dialog).getByRole("button", { name: /Edit/ }));
expect(within(dialog).getByText("Live preview")).toBeInTheDocument(); expect(within(dialog).getByText("Live preview")).toBeInTheDocument();
const title = within(dialog).getByRole("textbox", { name: /Document Title/ }); const title = within(dialog).getByRole("textbox", { name: /Document Title/ });
await userEvent.clear(title); await user.clear(title);
await userEvent.type(title, "Renamed"); await user.type(title, "Renamed");
await userEvent.click(within(dialog).getByRole("button", { name: "Remove procedure 1" })); await user.click(within(dialog).getByRole("button", { name: "Remove procedure 1" }));
await userEvent.click(within(dialog).getByRole("button", { name: "Cancel" })); await user.click(within(dialog).getByRole("button", { name: "Cancel" }));
expect(within(dialog).queryByRole("textbox")).not.toBeInTheDocument(); expect(within(dialog).queryByRole("textbox")).not.toBeInTheDocument();
expect(within(dialog).getAllByText("Backflow Test Report").length).toBeGreaterThan(0); expect(within(dialog).getAllByText("Backflow Test Report").length).toBeGreaterThan(0);
@ -200,7 +204,7 @@ describe("Edit template", () => {
expect(previewHeadings(dialog)).toEqual(["Backflow Procedure", "Shut-off"]); expect(previewHeadings(dialog)).toEqual(["Backflow Procedure", "Shut-off"]);
expect(api.apiPut).not.toHaveBeenCalled(); expect(api.apiPut).not.toHaveBeenCalled();
await userEvent.click(within(dialog).getByRole("button", { name: /Edit/ })); await user.click(within(dialog).getByRole("button", { name: /Edit/ }));
expect(within(dialog).getByRole("textbox", { name: /Document Title/ })).toHaveValue( expect(within(dialog).getByRole("textbox", { name: /Document Title/ })).toHaveValue(
"Backflow Test Report", "Backflow Test Report",
); );
@ -208,10 +212,10 @@ describe("Edit template", () => {
it("saves an edit, keeps the legacy fields and confirms with a toast", async () => { it("saves an edit, keeps the legacy fields and confirms with a toast", async () => {
const dialog = await openTemplate("Backflow Test Report"); const dialog = await openTemplate("Backflow Test Report");
await userEvent.click(within(dialog).getByRole("button", { name: /Edit/ })); await user.click(within(dialog).getByRole("button", { name: /Edit/ }));
await userEvent.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), " v2"); await user.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), " v2");
await userEvent.click(within(dialog).getByRole("button", { name: /Save changes/ })); await user.click(within(dialog).getByRole("button", { name: /Save changes/ }));
await waitFor(() => expect(toast.success).toHaveBeenCalledWith("Template updated")); await waitFor(() => expect(toast.success).toHaveBeenCalledWith("Template updated"));
expect(api.apiPut).toHaveBeenCalledWith("workorders/completion-templates/12", { expect(api.apiPut).toHaveBeenCalledWith("workorders/completion-templates/12", {
@ -232,12 +236,12 @@ describe("Edit template", () => {
}); });
}); });
describe("Delete template", () => { describe("Delete template", { timeout: 15_000 }, () => {
it("states how many open work orders depend on it and links to exactly those", async () => { it("states how many open work orders depend on it and links to exactly those", async () => {
serveTemplates(api, [BACKFLOW, SCRUBBER], { count: 2, workOrderIds: [101, 205] }); serveTemplates(api, [BACKFLOW, SCRUBBER], { count: 2, workOrderIds: [101, 205] });
const dialog = await openTemplate("Backflow Test Report"); const dialog = await openTemplate("Backflow Test Report");
await userEvent.click(within(dialog).getByRole("button", { name: /Delete/ })); await user.click(within(dialog).getByRole("button", { name: /Delete/ }));
const confirm = await screen.findByRole("dialog", { name: "Delete this template?" }); const confirm = await screen.findByRole("dialog", { name: "Delete this template?" });
expect( expect(
@ -246,14 +250,14 @@ describe("Delete template", () => {
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(api.apiDeleteNoContent).not.toHaveBeenCalled(); expect(api.apiDeleteNoContent).not.toHaveBeenCalled();
await userEvent.click(within(confirm).getByRole("button", { name: "View linked work orders" })); await user.click(within(confirm).getByRole("button", { name: "View linked work orders" }));
expect(navigate).toHaveBeenCalledWith("/workorders?ids=101,205"); expect(navigate).toHaveBeenCalledWith("/workorders?ids=101,205");
}); });
it("still asks first when nothing depends on it, then deletes with a toast", async () => { it("still asks first when nothing depends on it, then deletes with a toast", async () => {
const dialog = await openTemplate("Scrubber Pit Augering"); const dialog = await openTemplate("Scrubber Pit Augering");
await userEvent.click(within(dialog).getByRole("button", { name: /Delete/ })); await user.click(within(dialog).getByRole("button", { name: /Delete/ }));
const confirm = await screen.findByRole("dialog", { name: "Delete this template?" }); const confirm = await screen.findByRole("dialog", { name: "Delete this template?" });
expect( expect(
@ -263,7 +267,7 @@ describe("Delete template", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
expect(within(confirm).queryByRole("button", { name: "View linked work orders" })).toBeNull(); expect(within(confirm).queryByRole("button", { name: "View linked work orders" })).toBeNull();
await userEvent.click(within(confirm).getByRole("button", { name: "Delete" })); await user.click(within(confirm).getByRole("button", { name: "Delete" }));
await waitFor(() => expect(toast.success).toHaveBeenCalledWith("Template deleted")); await waitFor(() => expect(toast.success).toHaveBeenCalledWith("Template deleted"));
expect(api.apiDeleteNoContent).toHaveBeenCalledWith("workorders/completion-templates/13"); expect(api.apiDeleteNoContent).toHaveBeenCalledWith("workorders/completion-templates/13");