diff --git a/src/test/app/(protected)/completion-docs/completion-docs-page.test.tsx b/src/test/app/(protected)/completion-docs/completion-docs-page.test.tsx index 54710f9e..3521c871 100644 --- a/src/test/app/(protected)/completion-docs/completion-docs-page.test.tsx +++ b/src/test/app/(protected)/completion-docs/completion-docs-page.test.tsx @@ -23,6 +23,9 @@ vi.mock("@/providers/auth-context", async (importOriginal) => ({ 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() { return renderWithProviders(, { route: "/completion-docs", @@ -31,16 +34,17 @@ function renderPage() { } 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"); } beforeEach(() => { + user = userEvent.setup({ delay: null }); auth.userRoles = "Admin"; 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 () => { renderPage(); @@ -66,7 +70,7 @@ describe("Completion Docs page", () => { await screen.findByRole("row", { name: /Scrubber Pit Augering/ }); const search = screen.getByRole("searchbox", { name: "Search document title" }); - await userEvent.type(search, "backf"); + await user.type(search, "backf"); expect( await screen.findByText("1 template", { selector: "span" }, { timeout: 2000 }), ).toBeInTheDocument(); @@ -75,8 +79,8 @@ describe("Completion Docs page", () => { searchParams: { search: "backf" }, }); - await userEvent.clear(search); - await userEvent.type(search, "no such document"); + await user.clear(search); + await user.type(search, "no such document"); expect( await screen.findByText("No templates found", {}, { timeout: 2000 }), ).toBeInTheDocument(); diff --git a/src/test/app/(protected)/completion-docs/completion-template-dialog.test.tsx b/src/test/app/(protected)/completion-docs/completion-template-dialog.test.tsx index f8991612..acb7a2ac 100644 --- a/src/test/app/(protected)/completion-docs/completion-template-dialog.test.tsx +++ b/src/test/app/(protected)/completion-docs/completion-template-dialog.test.tsx @@ -29,6 +29,9 @@ vi.mock("@/providers/auth-context", async (importOriginal) => ({ 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() { return renderWithProviders(, { route: "/completion-docs", @@ -39,13 +42,13 @@ function renderPage() { async function openNewTemplate() { renderPage(); 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"); } async function openTemplate(name: string) { 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"); } @@ -69,20 +72,21 @@ function unprocessable(body: { code: string; message: string }, message = body.m } async function addProcedure(dialog: HTMLElement, index: number, name: string) { - await userEvent.click(within(dialog).getByRole("button", { name: "+ Add procedure" })); - await userEvent.type(within(dialog).getByLabelText(`Procedure ${index} name`), name); + await user.click(within(dialog).getByRole("button", { name: "+ Add procedure" })); + await user.type(within(dialog).getByLabelText(`Procedure ${index} name`), name); } beforeEach(() => { + user = userEvent.setup({ delay: null }); vi.clearAllMocks(); 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 () => { 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(within(dialog).getByRole("textbox", { name: /Document Title/ })).toHaveFocus(); @@ -97,14 +101,14 @@ describe("Add Template", () => { expect(within(dialog).getByText("WO#")).toBeInTheDocument(); 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(); - 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( "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).getByTestId("completion-template-preview")).not.toHaveTextContent( "Mind the pit", @@ -120,28 +124,28 @@ describe("Add Template", () => { await addProcedure(dialog, 3, "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(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"]); }); it("saves the title, note and procedures in order, then confirms with a toast", async () => { const dialog = await openNewTemplate(); - await userEvent.type( + await user.type( within(dialog).getByRole("textbox", { name: /Document Title/ }), " PRV Check ", ); - await userEvent.click(within(dialog).getByRole("button", { name: "+ Add safety note" })); - await userEvent.type(within(dialog).getByLabelText("Extra safety note"), "Gloves"); + await user.click(within(dialog).getByRole("button", { name: "+ Add safety note" })); + await user.type(within(dialog).getByLabelText("Extra safety note"), "Gloves"); await addProcedure(dialog, 1, "Open"); - await userEvent.type(within(dialog).getByLabelText("Procedure 1 description"), "a{enter}b"); - await userEvent.click(within(dialog).getByRole("button", { name: "+ Add procedure" })); + await user.type(within(dialog).getByLabelText("Procedure 1 description"), "a{enter}b"); + await user.click(within(dialog).getByRole("button", { name: "+ Add procedure" })); 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")); expect(api.apiPost).toHaveBeenCalledWith("workorders/completion-templates", { @@ -160,9 +164,9 @@ describe("Add Template", () => { unprocessable({ code: "TitleRequired", message: "Document Title is required." }), ); 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(toast.error).not.toHaveBeenCalled(); @@ -172,9 +176,9 @@ describe("Add Template", () => { const message = "Procedure name must be 200 characters or fewer."; api.apiPost.mockRejectedValue(unprocessable({ code: "InvalidValue", message }, message)); 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)); 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 () => { 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(); const title = within(dialog).getByRole("textbox", { name: /Document Title/ }); - await userEvent.clear(title); - await userEvent.type(title, "Renamed"); - await userEvent.click(within(dialog).getByRole("button", { name: "Remove procedure 1" })); + await user.clear(title); + await user.type(title, "Renamed"); + 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).getAllByText("Backflow Test Report").length).toBeGreaterThan(0); @@ -200,7 +204,7 @@ describe("Edit template", () => { expect(previewHeadings(dialog)).toEqual(["Backflow Procedure", "Shut-off"]); 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( "Backflow Test Report", ); @@ -208,10 +212,10 @@ describe("Edit template", () => { it("saves an edit, keeps the legacy fields and confirms with a toast", async () => { const dialog = await openTemplate("Backflow Test Report"); - await userEvent.click(within(dialog).getByRole("button", { name: /Edit/ })); - await userEvent.type(within(dialog).getByRole("textbox", { name: /Document Title/ }), " v2"); + await user.click(within(dialog).getByRole("button", { name: /Edit/ })); + 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")); 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 () => { serveTemplates(api, [BACKFLOW, SCRUBBER], { count: 2, workOrderIds: [101, 205] }); 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?" }); expect( @@ -246,14 +250,14 @@ describe("Delete template", () => { ), ).toBeInTheDocument(); 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"); }); it("still asks first when nothing depends on it, then deletes with a toast", async () => { 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?" }); expect( @@ -263,7 +267,7 @@ describe("Delete template", () => { ).toBeInTheDocument(); 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")); expect(api.apiDeleteNoContent).toHaveBeenCalledWith("workorders/completion-templates/13");