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.
103 lines
3.3 KiB
TypeScript
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" });
|
|
});
|