feat(frontend): v2 artifact viewers for cte_plan/cte_result/phase
Add CtePlanViewer, CteResultViewer, and PhaseSummaryViewer to render the structured v2 payloads (schema_version: 2) the harness now emits for artifact-gate widgets, per contracts.md. Extract PreviewGrid from ResultsPanel as a reusable AG Grid component shared by CteResultViewer. ArtifactView dispatches to the new viewers on a schema_version/shape guard, falling back to the existing legacy renderers unchanged for older sessions and replay fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { CteResultViewer } from "./CteResultViewer";
|
||||
import type { CteResultV2 } from "./artifactV2";
|
||||
|
||||
vi.mock("./highlight", () => ({
|
||||
highlightSql: vi.fn().mockResolvedValue('<pre data-testid="hl">SQL</pre>'),
|
||||
}));
|
||||
|
||||
const okResult: CteResultV2 = {
|
||||
schema_version: 2,
|
||||
name: "pazienti_idonei",
|
||||
index: 1,
|
||||
total: 2,
|
||||
purpose: "Seleziona i pazienti con follow-up attivo",
|
||||
rationale: "Base della catena",
|
||||
depends_on: [],
|
||||
sql: "WITH pazienti_idonei AS (SELECT 1)",
|
||||
status: "ok",
|
||||
execution_ms: 142,
|
||||
row_sample: 10,
|
||||
warnings: [],
|
||||
columns: [{ name: "paziente_id", description: "Identificativo paziente" }],
|
||||
preview: { columns: ["paziente_id"], rows: [[1], [2]] },
|
||||
note: "Nota del modello",
|
||||
};
|
||||
|
||||
test("renders header meta: index/total badge, mono name, status badge, execution_ms, row_sample", async () => {
|
||||
render(<CteResultViewer result={okResult} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.getByText("CTE 1/2")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("pazienti_idonei").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText(/success|ok/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/142 ms/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/10 rows/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders warnings as badges", async () => {
|
||||
const withWarnings: CteResultV2 = { ...okResult, warnings: ["colonna troncata"] };
|
||||
render(<CteResultViewer result={withWarnings} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.getByText("colonna troncata")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("passes sql to SqlViewer with mapped test status", async () => {
|
||||
render(<CteResultViewer result={okResult} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.getByText(/passed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("error status maps to failed test status and destructive badge", async () => {
|
||||
const errored: CteResultV2 = { ...okResult, status: "error" };
|
||||
render(<CteResultViewer result={errored} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.getByText(/failed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders PreviewGrid rows when preview is present", async () => {
|
||||
render(<CteResultViewer result={okResult} />);
|
||||
await screen.findByTestId("hl");
|
||||
const cells = screen.getAllByRole("gridcell").map((c) => c.textContent);
|
||||
expect(cells).toContain("1");
|
||||
expect(cells).toContain("2");
|
||||
});
|
||||
|
||||
test("shows a discreet empty-state when preview is absent", async () => {
|
||||
const noPreview: CteResultV2 = { ...okResult, preview: undefined };
|
||||
render(<CteResultViewer result={noPreview} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.getByText(/no preview rows/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows empty-state when preview has no rows", async () => {
|
||||
const emptyPreview: CteResultV2 = { ...okResult, preview: { columns: ["paziente_id"], rows: [] } };
|
||||
render(<CteResultViewer result={emptyPreview} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.getByText(/no preview rows/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders columns definition list with descriptions", async () => {
|
||||
const { container } = render(<CteResultViewer result={okResult} />);
|
||||
await screen.findByTestId("hl");
|
||||
const dt = container.querySelector("dl dt");
|
||||
expect(dt).toHaveTextContent("paziente_id");
|
||||
expect(screen.getByText("Identificativo paziente")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("omits the columns definition list when all descriptions are empty", async () => {
|
||||
const noDescriptions: CteResultV2 = {
|
||||
...okResult,
|
||||
columns: [{ name: "paziente_id", description: "" }],
|
||||
};
|
||||
const { container } = render(<CteResultViewer result={noDescriptions} />);
|
||||
await screen.findByTestId("hl");
|
||||
// No <dl> definition list should render when all descriptions are empty.
|
||||
expect(container.querySelector("dl")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders note at the end", async () => {
|
||||
render(<CteResultViewer result={okResult} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.getByText("Nota del modello")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders depends_on chips when present", async () => {
|
||||
const withDeps: CteResultV2 = { ...okResult, depends_on: ["cte_base"] };
|
||||
render(<CteResultViewer result={withDeps} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.getByText("cte_base")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not render 'undefined' strings for missing optional fields", async () => {
|
||||
const minimal: CteResultV2 = {
|
||||
schema_version: 2,
|
||||
name: "final",
|
||||
index: 2,
|
||||
total: 2,
|
||||
sql: "SELECT 1",
|
||||
status: "ok",
|
||||
};
|
||||
render(<CteResultViewer result={minimal} />);
|
||||
await screen.findByTestId("hl");
|
||||
expect(screen.queryByText(/undefined/i)).not.toBeInTheDocument();
|
||||
});
|
||||
Reference in New Issue
Block a user