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 } }),
}));
// delay: null skips per-keystroke timer yields that only slow typing-heavy tests down.
let user = userEvent.setup({ delay: null });
function renderPage() {
return renderWithProviders(<CompletionDocsPage />, {
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();

View file

@ -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(<CompletionDocsPage />, {
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");