shoc-frontend-new/src/test/app/(protected)/completion-docs/completion-docs-page.test.tsx
Alexandre Brandizzi 50b97a96d0 feat(completion-docs): manage completion document templates
Add the Completion Docs page under Data Management: a searchable template
table, a read-only detail panel, add and edit with a live document preview,
and delete behind a confirmation that counts the open work orders still
using the template and links to them. Add, edit and delete are hidden for
users whose role lacks the matching template permission.

The preview shares the work-order completion document's page frame,
header, safety bullets and procedure heading so the two stay identical.
2026-09-25 11:08:21 -03:00

148 lines
6.6 KiB
TypeScript

import { screen, 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<typeof import("@/api/api")>()),
...api,
}));
vi.mock("@/providers/auth-context", async (importOriginal) => ({
...(await importOriginal<typeof import("@/providers/auth-context")>()),
useAuthContext: () => ({ user: { id: "u1", userRoles: auth.userRoles } }),
}));
function renderPage() {
return renderWithProviders(<CompletionDocsPage />, {
route: "/completion-docs",
withAuth: false,
});
}
async function openTemplate(name: string) {
await userEvent.click(await screen.findByRole("row", { name: new RegExp(name) }));
return screen.findByRole("dialog");
}
beforeEach(() => {
auth.userRoles = "Admin";
serveTemplates(api, [BACKFLOW, SCRUBBER]);
});
describe("Completion Docs page", () => {
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 userEvent.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 userEvent.clear(search);
await userEvent.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("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();
});
});