121 lines
8.7 KiB
TypeScript
121 lines
8.7 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { act, render, screen, within } from "@testing-library/react";
|
|
import { setLocale } from "../i18n";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { http, HttpResponse } from "msw";
|
|
import { server } from "../test/msw";
|
|
import { EvidenceManagementPage } from "./EvidenceManagementPage";
|
|
import type { EvidencePage, EvidenceUnit } from "../api/evidence";
|
|
|
|
const item: EvidenceUnit = { id: "evidence:order-key", title: "Order key", kind: "domain", language: "en", purposes: ["sql_generation"],
|
|
applies_to: { concepts: ["Orders"], tables: ["sales.orders"], columns: [] }, payload: { rule: "Join by order number, year and company." },
|
|
provenance: { kind: "manual", declared_by: "Curator", original: { source_file: "source/orders.md", supporting_excerpts: ["Old original wording."] } },
|
|
review_items: [], file: "curated/domain/order-key.md", status: "modified", revision: "rev" };
|
|
const page: EvidencePage = { items: [item], item, total: 1, page: 1, page_size: 25, errors: [], initialized: true, active_revision: "active", pending_revision: null,
|
|
location: { repository: "/srv/workspaces/repo", workspace: "/srv/workspaces/repo/sales", runtime_workspace: "/data/registry/repo/sales", host: "Installation host",
|
|
command: "tht workspace evidence consolidate --workspace sales", git_commands: ["git status", "git diff", "git add -A", "git commit", "git push"] } };
|
|
function renderPage() {
|
|
render(<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}><EvidenceManagementPage canManage /></QueryClientProvider>);
|
|
}
|
|
beforeEach(() => server.use(
|
|
http.get("/api/workspaces", () => HttpResponse.json([{ id: "sales", displayName: "Sales" }])),
|
|
http.get("/api/workspaces/sales/evidence", () => HttpResponse.json(page)),
|
|
http.get("/api/workspaces/sales/evidence/evidence:order-key", () => HttpResponse.json(page)),
|
|
));
|
|
|
|
test("copy uses an accessible icon and copies the unmodified full path", async () => {
|
|
const user = userEvent.setup();
|
|
const copy = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue();
|
|
renderPage(); await choose();
|
|
await user.click(await screen.findByRole("button", { name: "Order key" }));
|
|
const detail = screen.getByRole("region", { name: "Evidence detail" });
|
|
const button = within(detail).getByRole("button", { name: "Copy Evidence file path" });
|
|
expect(button).toHaveTextContent("");
|
|
expect(button.querySelector("svg")).not.toBeNull();
|
|
await user.click(button);
|
|
expect(copy).toHaveBeenCalledWith("/srv/workspaces/repo/sales/evidence/curated/domain/order-key.md");
|
|
expect(within(detail).getByRole("status")).toHaveTextContent("Copied");
|
|
copy.mockRejectedValueOnce(new Error("Clipboard unavailable"));
|
|
await user.click(button);
|
|
expect(within(detail).getByRole("status")).toHaveTextContent("Select the text and copy it manually.");
|
|
copy.mockRestore();
|
|
});
|
|
async function choose() { await screen.findByRole("option", { name: "Sales" }); await userEvent.selectOptions(screen.getByLabelText("Workspace"), "sales"); }
|
|
test("switches Evidence controls and detail labels without translating archive content", async () => {
|
|
renderPage(); await choose();
|
|
await userEvent.click(await screen.findByRole("button", { name: "Order key" }));
|
|
await screen.findByText("Join by order number, year and company.");
|
|
try {
|
|
act(() => setLocale("it"));
|
|
expect(screen.getByLabelText("Cerca Evidence")).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Aggiorna file" })).toBeVisible();
|
|
expect(screen.getByRole("heading", { name: "Provenienza" })).toBeVisible();
|
|
expect(screen.getByText("Join by order number, year and company.")).toBeVisible();
|
|
act(() => setLocale("en"));
|
|
expect(screen.getByLabelText("Search Evidence")).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Refresh files" })).toBeVisible();
|
|
} finally { act(() => setLocale("en")); }
|
|
});
|
|
test("reads complete content, manual lineage and actual host file path", async () => {
|
|
renderPage(); await choose(); await userEvent.click(await screen.findByRole("button", { name: "Order key" }));
|
|
expect(await screen.findByText("Join by order number, year and company.")).toBeVisible();
|
|
expect(screen.getByText("Manual declaration by Curator.")).toBeVisible();
|
|
expect(screen.getByText(/Original document \(lineage\)/)).toBeVisible();
|
|
expect(screen.getByText("/srv/workspaces/repo/sales/evidence/curated/domain/order-key.md")).toBeVisible();
|
|
expect(screen.queryByRole("button", { name: "Save" })).not.toBeInTheDocument();
|
|
});
|
|
test("shows manual consolidation instructions and all kind templates", async () => {
|
|
renderPage(); await choose(); await userEvent.click(await screen.findByText("Edit files, consolidate and commit"));
|
|
expect(screen.getByText(page.location.command)).toBeVisible();
|
|
await userEvent.click(screen.getByText("Create a manual Evidence file"));
|
|
await userEvent.selectOptions(screen.getByLabelText("Example kind"), "formula");
|
|
expect(screen.getAllByText(/sum\(amount\)/).length).toBeGreaterThan(0);
|
|
});
|
|
test("combines complete-archive filters and refreshes pending state", async () => {
|
|
server.use(http.get("/api/workspaces/sales/evidence", ({ request }) => {
|
|
const query = new URL(request.url).searchParams;
|
|
return HttpResponse.json({ ...page, items: query.get("kind") === "domain" && query.get("q") === "order" ? [item] : [], pending_revision: "pending" });
|
|
}));
|
|
renderPage(); await choose(); await userEvent.type(screen.getByLabelText("Search Evidence"), "order");
|
|
await userEvent.selectOptions(screen.getByLabelText("Kind"), "domain");
|
|
expect(await screen.findByRole("button", { name: "Order key" })).toBeVisible();
|
|
expect(screen.getByText(/Consolidation is incomplete/)).toBeVisible();
|
|
});
|
|
test("invalid files remain actionable alongside valid units", async () => {
|
|
server.use(http.get("/api/workspaces/sales/evidence", () => HttpResponse.json({ ...page, errors: [{ file: "curated/domain/broken.md", message: "Missing Rule section" }] })));
|
|
renderPage(); await choose();
|
|
expect(await screen.findByRole("alert")).toHaveTextContent("curated/domain/broken.md");
|
|
expect(within(screen.getByRole("region", { name: "Evidence list" })).getByRole("button", { name: "Order key" })).toBeVisible();
|
|
});
|
|
|
|
test("source refresh is explicit and presents current versus proposed content before a decision", async () => {
|
|
const requests: unknown[] = [];
|
|
const source = {id: "a".repeat(64), revision: "b".repeat(64), uri: "https://docs.example.test/orders.md", status: "review", availability: "available",
|
|
current: [item], proposed: [{...item, payload: {rule: "Use the new approved key."}}], removed_ids: []};
|
|
server.use(http.get("/api/workspaces/sales/evidence", () => HttpResponse.json({...page, source_reviews: [source]})),
|
|
http.post("/api/workspaces/sales/evidence/sources", async ({request}) => { requests.push(await request.json()); return HttpResponse.json({status: "succeeded", warnings: ["Source decision activated locally."]}); }));
|
|
renderPage(); await choose();
|
|
await screen.findByText(source.uri);
|
|
expect(requests).toHaveLength(0);
|
|
await userEvent.click(screen.getByRole("button", {name: "Import or refresh sources"}));
|
|
await screen.findByText("Source decision activated locally.");
|
|
expect(requests).toEqual([{action: "refresh"}]);
|
|
await userEvent.click(screen.getByText(source.uri));
|
|
expect(screen.getByText("Current local Evidence")).toBeVisible();
|
|
expect(screen.getByText("Use the new approved key.")).toBeVisible();
|
|
await userEvent.click(screen.getByRole("button", {name: "Keep local Evidence"}));
|
|
expect(requests[1]).toEqual({action: "decide", sourceId: source.id, revision: source.revision, decision: "keep"});
|
|
});
|
|
|
|
test("saved source decisions expose retry and preserve the original choice", async () => {
|
|
const source = {id: "a".repeat(64), revision: "b".repeat(64), uri: "s3://docs/orders.md", status: "applying", decision: "replace", availability: "available",
|
|
current: [item], proposed: [item], removed_ids: []};
|
|
server.use(http.get("/api/workspaces/sales/evidence", () => HttpResponse.json({...page, source_reviews: [source]})),
|
|
http.post("/api/workspaces/sales/evidence/sources", () => HttpResponse.json({status: "failed", warnings: ["Source decision saved, but indexing failed. Retry the same decision."]})));
|
|
renderPage(); await choose(); await userEvent.click(await screen.findByText(source.uri));
|
|
expect(screen.getByRole("button", {name: "Import or refresh sources"})).toBeDisabled();
|
|
expect(screen.queryByRole("button", {name: "Keep local Evidence"})).not.toBeInTheDocument();
|
|
await userEvent.click(screen.getByRole("button", {name: "Retry saved decision"}));
|
|
expect(await screen.findByRole("alert")).toHaveTextContent("Retry the same decision");
|
|
});
|