import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import CompletionDocsPage from "@/app/(protected)/completion-docs/index"; import { renderWithProviders } from "@/test/test-utils"; import { BACKFLOW, SCRUBBER, serveTemplates, templateDto } from "./completion-docs-fixtures"; const api = vi.hoisted(() => ({ apiGet: vi.fn(), apiPost: vi.fn(), apiPut: vi.fn(), apiDeleteNoContent: vi.fn(), })); const auth = vi.hoisted(() => ({ userRoles: "Admin" })); vi.mock("@/api/api", async (importOriginal) => ({ ...(await importOriginal()), ...api, })); vi.mock("@/providers/auth-context", async (importOriginal) => ({ ...(await 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", withAuth: false, }); } async function openTemplate(name: string) { 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", { timeout: 15_000 }, () => { it("lists every template with its Document Title, Last Updated and Created By", async () => { renderPage(); const backflow = await screen.findByRole("row", { name: /Backflow Test Report/ }); const table = screen.getByRole("table", { name: "Completion document templates" }); const headers = within(table) .getAllByRole("columnheader") .map((cell) => cell.textContent); expect(headers.slice(0, 3)).toEqual(["Document Title", "Last Updated", "Created By"]); expect(within(backflow).getByText("Sep 25, 2026")).toBeInTheDocument(); expect(within(backflow).getByText("Jane Doe")).toBeInTheDocument(); const scrubber = within(table).getByRole("row", { name: /Scrubber Pit Augering/ }); // Never updated: Last Updated falls back to the creation date; no creator shows a dash. expect(within(scrubber).getByText("Jun 1, 2026")).toBeInTheDocument(); expect(within(scrubber).getByText("—")).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Completion Docs" })).toBeInTheDocument(); expect(screen.getAllByText("2 templates")).toHaveLength(2); }); it("filters by Document Title as the user types and explains an empty result", async () => { renderPage(); await screen.findByRole("row", { name: /Scrubber Pit Augering/ }); const search = screen.getByRole("searchbox", { name: "Search document title" }); await user.type(search, "backf"); expect( await screen.findByText("1 template", { selector: "span" }, { timeout: 2000 }), ).toBeInTheDocument(); expect(screen.queryByRole("row", { name: /Scrubber Pit Augering/ })).not.toBeInTheDocument(); expect(api.apiGet).toHaveBeenCalledWith("workorders/completion-templates", { searchParams: { search: "backf" }, }); await user.clear(search); await user.type(search, "no such document"); expect( await screen.findByText("No templates found", {}, { timeout: 2000 }), ).toBeInTheDocument(); expect(screen.getByText("Try a different search, or add a new template.")).toBeInTheDocument(); expect(screen.queryByRole("row", { name: /Backflow Test Report/ })).not.toBeInTheDocument(); }); it("reports a failed load without the empty-search message, including after a failed Retry", async () => { api.apiGet.mockRejectedValue(new Error("Templates are unavailable")); renderPage(); const retry = await screen.findByRole("button", { name: "Retry" }); expect(screen.getByText("Templates are unavailable")).toBeInTheDocument(); expect(screen.queryByText("No templates found")).not.toBeInTheDocument(); const callsBeforeRetry = api.apiGet.mock.calls.length; await user.click(retry); await waitFor(() => expect(api.apiGet.mock.calls.length).toBeGreaterThan(callsBeforeRetry)); expect(await screen.findByText("Templates are unavailable")).toBeInTheDocument(); expect(screen.queryByText("No templates found")).not.toBeInTheDocument(); }); it("opens a read-only detail panel from a row with every part of the template", async () => { renderPage(); const dialog = await openTemplate("Backflow Test Report"); expect(within(dialog).getByText("Document preview")).toBeInTheDocument(); for (const field of ["WO#", "Site Code", "Address", "Service Type"]) { expect(within(dialog).getByText(field)).toBeInTheDocument(); } const safety = within(dialog).getByRole("list", { name: "Safety notes" }); expect(within(safety).getAllByRole("listitem")).toHaveLength(4); expect(within(safety).getByText("Wear gloves")).toBeInTheDocument(); expect(within(dialog).queryByRole("textbox")).not.toBeInTheDocument(); expect( within(dialog).getByText("Created by Jane Doe · Updated Sep 25, 2026"), ).toBeInTheDocument(); }); it("shows that a template without procedures has none defined yet", async () => { renderPage(); const dialog = await openTemplate("Scrubber Pit Augering"); expect(within(dialog).getByText("No procedure defined yet")).toBeInTheDocument(); expect(within(dialog).getByText("No procedure added yet.")).toBeInTheDocument(); }); it("gives an Admin every template action", async () => { renderPage(); const row = await screen.findByRole("row", { name: /Backflow Test Report/ }); expect(screen.getByRole("button", { name: /Add Template/ })).toBeInTheDocument(); expect(within(row).getByRole("button", { name: "Edit" })).toBeInTheDocument(); const dialog = await openTemplate("Backflow Test Report"); expect(within(dialog).getByRole("button", { name: /Edit/ })).toBeInTheDocument(); expect(within(dialog).getByRole("button", { name: /Delete/ })).toBeInTheDocument(); }); it("hides delete from a Scheduler, who may still add and edit", async () => { auth.userRoles = "Scheduler"; renderPage(); const row = await screen.findByRole("row", { name: /Backflow Test Report/ }); expect(screen.getByRole("button", { name: /Add Template/ })).toBeInTheDocument(); expect(within(row).getByRole("button", { name: "Edit" })).toBeInTheDocument(); const dialog = await openTemplate("Backflow Test Report"); expect(within(dialog).getByRole("button", { name: /Edit/ })).toBeInTheDocument(); expect(within(dialog).queryByRole("button", { name: /Delete/ })).not.toBeInTheDocument(); }); it("hides add, edit and delete entirely from a user without those permissions", async () => { auth.userRoles = "Dispatcher"; serveTemplates(api, [templateDto()]); renderPage(); const row = await screen.findByRole("row", { name: /Backflow Test Report/ }); expect(screen.queryByRole("button", { name: /Add Template/ })).not.toBeInTheDocument(); expect(within(row).queryByRole("button", { name: "Edit" })).not.toBeInTheDocument(); expect(within(row).getByRole("button", { name: "View details" })).toBeInTheDocument(); const dialog = await openTemplate("Backflow Test Report"); expect(within(dialog).queryByRole("button", { name: /Edit/ })).not.toBeInTheDocument(); expect(within(dialog).queryByRole("button", { name: /Delete/ })).not.toBeInTheDocument(); }); });