Files
ThothII/frontend/src/widgets/ArtifactGateWidget.test.tsx
T
marcopan e11ffecfc4 feat(frontend): richer gate bodies + replay-friendly session exit
Improves how reviewer gates render their content, and adds two small
hooks that make the frontend cooperate with the standalone replay server
(also useful as generic primitives).

Gate body rendering:
- IntroBody: gate intros now render paragraphs plus real bullet lists
  with a hanging indent and emphasised bullet lead term, filling the
  card width (no narrow measure cap). Backed by introFormat (parser) +
  IntroBody (renderer), with tests.
- GateArtifactBody: artifact-review gates whose artifact is a bare file
  reference (e.g. {kind:"phase", data:{file:"question.md"}}) now fetch
  the referenced session document and render its full content, instead
  of showing the file reference as text. Falls back to ArtifactView for
  inline-content artifacts and unmapped files. Backed by gateArtifact
  (resolver) with tests. ArtifactGateWidget passes sessionId through.
- SelectWidget renders intros via IntroBody.

Replay-friendly session exit:
- AppShell: a system_event {event:"session_exit"} now triggers
  stopSession(), returning to the landing view. Generic primitive (the
  real backend can emit it too); used by the replay server's "Esci".
- SessionMenu: the ⋮ trigger is now always visible (was opacity-0 /
  group-hover only), so Resume is reachable without hovering.

Verified offline against the replay server (no VPN needed); tsc clean.
2026-07-05 18:25:02 +02:00

103 lines
3.3 KiB
TypeScript

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactElement } from "react";
import { ArtifactGateWidget } from "./ArtifactGateWidget";
vi.mock("../viewers/mermaid", () => ({
renderMermaid: vi.fn().mockResolvedValue('<svg data-testid="mm"></svg>'),
}));
vi.mock("../api/sessions", () => ({
getSessionDocuments: vi.fn().mockResolvedValue([
{
phase: "F3",
key: "revised_question",
title: "Revised question",
format: "markdown",
content: "## Domanda\n\nQuanti pazienti hanno avuto un impianto?",
},
]),
}));
function withClient(ui: ReactElement): ReactElement {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return <QueryClientProvider client={qc}>{ui}</QueryClientProvider>;
}
test("renders inside a dialog and shows the artifact via ArtifactView (data path)", () => {
const onRespond = vi.fn();
render(
<ArtifactGateWidget
descriptor={{
id: "u1",
widget: "artifact-gate",
title: "SQL finale",
artifact: { kind: "sql", data: "SELECT 42" },
options: [{ id: "approve", label: "Approva" }],
}}
onRespond={onRespond}
/>
);
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("SELECT 42")).toBeInTheDocument();
});
test("a file-reference artifact resolves to the session document, rendered with its path", async () => {
render(
withClient(
<ArtifactGateWidget
descriptor={{
id: "u1",
session_id: "s-123",
widget: "artifact-gate",
title: "Fase 3 completata",
artifact: { kind: "phase", data: { file: "question.md" } },
options: [{ id: "approve", label: "Salva e procedi" }],
}}
onRespond={vi.fn()}
sessionId="s-123"
/>
)
);
expect(await screen.findByText(/Quanti pazienti/)).toBeInTheDocument();
expect(screen.getByText("sessions/s-123/question.md")).toBeInTheDocument();
// The raw filename reference is no longer shown as the whole body.
expect(screen.queryByText("question.md", { exact: true })).toBeNull();
});
test("clicking an option without opens responds immediately", async () => {
const onRespond = vi.fn();
render(
<ArtifactGateWidget
descriptor={{
id: "u1",
widget: "artifact-gate",
artifact: { kind: "cte", content: "SELECT 1" },
options: [{ id: "approve", label: "Approva" }],
}}
onRespond={onRespond}
/>
);
await userEvent.click(screen.getByRole("button", { name: /Approva/ }));
expect(onRespond).toHaveBeenCalledWith({ id: "u1", kind: "artifact-gate", choices: ["approve"] });
});
test("reserved control responds with control field", async () => {
const onRespond = vi.fn();
render(
<ArtifactGateWidget
descriptor={{
id: "u1",
widget: "artifact-gate",
artifact: { kind: "cte", content: "SELECT 1" },
options: [{ id: "approve", label: "Approva" }],
reserved: ["back"],
}}
onRespond={onRespond}
/>
);
await userEvent.click(screen.getByRole("button", { name: /back/i }));
expect(onRespond).toHaveBeenCalledWith({ id: "u1", control: "back" });
});