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.
38 lines
1.7 KiB
TypeScript
38 lines
1.7 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { SelectWidget } from "./SelectWidget";
|
|
|
|
test("picking an option responds with its id", async () => {
|
|
const onRespond = vi.fn();
|
|
render(<SelectWidget descriptor={{ id: "u1", widget: "select", options: [{ id: "a", label: "A" }, { id: "b", label: "B", recommended: true }] }} onRespond={onRespond} />);
|
|
expect(screen.getByText(/consigliato/i)).toBeInTheDocument();
|
|
await userEvent.click(screen.getByRole("button", { name: /A/ }));
|
|
expect(onRespond).toHaveBeenCalledWith({ id: "u1", kind: "select", choices: ["a"] });
|
|
});
|
|
test("intro bullets render as a list with the lead term bolded", () => {
|
|
render(
|
|
<SelectWidget
|
|
descriptor={{
|
|
id: "u1",
|
|
widget: "select",
|
|
intro: "Le famiglie:\n• fact_impianto → IMPIANTO INIZIALE\n• fact_controllo → CONTROLLO",
|
|
options: [{ id: "a", label: "A" }],
|
|
}}
|
|
onRespond={vi.fn()}
|
|
/>
|
|
);
|
|
const items = screen.getAllByRole("listitem");
|
|
expect(items).toHaveLength(2);
|
|
// Lead term is emphasised via <strong>, remainder stays normal weight.
|
|
const lead = screen.getByText("fact_impianto");
|
|
expect(lead.tagName).toBe("STRONG");
|
|
// The "•" is now a CSS marker, not literal text in the content.
|
|
expect(screen.queryByText(/•/)).toBeNull();
|
|
});
|
|
test("a reserved control responds with control, not a choice", async () => {
|
|
const onRespond = vi.fn();
|
|
render(<SelectWidget descriptor={{ id: "u1", widget: "select", options: [{ id: "a", label: "A" }], reserved: ["back"] }} onRespond={onRespond} />);
|
|
await userEvent.click(screen.getByRole("button", { name: /back/i }));
|
|
expect(onRespond).toHaveBeenCalledWith({ id: "u1", control: "back" });
|
|
});
|