Files
ThothII/frontend/src/viewers/CtePlanViewer.test.tsx
T

258 lines
9.6 KiB
TypeScript

import { render, screen, within } from "@testing-library/react";
import { CtePlanViewer } from "./CtePlanViewer";
import type { CtePlanV2 } from "./artifactV2";
const plan: CtePlanV2 = {
schema_version: 2,
question: "Quanti pazienti hanno avuto un impianto negli ultimi 5 anni?",
strategy: "Filtriamo prima i pazienti idonei, poi joiniamo gli impianti.",
ctes: [
{
name: "pazienti_idonei",
index: 1,
purpose: "Seleziona i pazienti con follow-up attivo",
rationale: "Base della catena: riduce il volume prima dei join",
depends_on: [],
tables: [{ name: "pazienti", description: "Anagrafica pazienti" }],
keys: ["paziente_id"],
filters: [
{
column: "pazienti.stato",
op: "IS",
value: "TRUE",
description: "Flag attivo",
rationale: "Solo pazienti in follow-up",
},
{
column: "pazienti.data_ultimo_follow_up",
op: ">=",
value: "CURRENT_DATE - INTERVAL '5 years'",
},
],
output_columns: ["paziente_id", "data_nascita"],
},
{
name: "impianti_recenti",
index: 2,
purpose: "Filtra gli impianti recenti",
depends_on: ["pazienti_idonei"],
tables: [{ name: "impianti" }],
filters: [
{
column: "impianti.data_impianto",
op: ">=",
value: "CURRENT_DATE - INTERVAL '5 years'",
},
],
output_columns: ["impianto_id"],
},
],
};
function getCteCard(name: string): HTMLElement {
const heading = screen.getByRole("heading", { level: 3, name });
const card = heading.closest<HTMLElement>('[data-slot="card"]');
if (!card) throw new Error(`Missing card for CTE ${name}`);
return card;
}
test("renders strategy, question, and a chain strip with CTE names in order", () => {
render(<CtePlanViewer plan={plan} />);
expect(screen.getByText("Question")).toBeInTheDocument();
expect(screen.getByText("Strategy")).toBeInTheDocument();
expect(screen.getByText("Execution order")).toBeInTheDocument();
expect(screen.getByText(plan.question!)).toBeInTheDocument();
expect(screen.getByText(plan.strategy!)).toBeInTheDocument();
expect(screen.getAllByText("pazienti_idonei").length).toBeGreaterThan(0);
expect(screen.getAllByText("impianti_recenti").length).toBeGreaterThan(0);
});
test("uses compact CTE section rhythm with moderate lateral padding", () => {
const { container } = render(<CtePlanViewer plan={plan} />);
for (const header of container.querySelectorAll('[data-slot="card-header"]')) {
expect(header).toHaveClass("gap-2", "px-3", "py-2", "sm:px-4", "[&.border-b]:pb-2");
expect(header).not.toHaveClass("gap-3", "p-4", "sm:p-5");
}
for (const content of container.querySelectorAll('[data-slot="card-content"]')) {
expect(content).toHaveClass("gap-3", "px-3", "py-2", "sm:px-4");
expect(content).not.toHaveClass("gap-5", "p-4", "sm:p-5");
}
});
test("renders the CTE cards as ordered plan steps", () => {
render(<CtePlanViewer plan={plan} />);
const list = screen.getByRole("list", { name: "CTE plan steps" });
const steps = within(list).getAllByRole("listitem");
expect(steps).toHaveLength(2);
expect(within(steps[0]).getByRole("heading", { level: 3 })).toHaveTextContent(
"pazienti_idonei"
);
expect(within(steps[1]).getByRole("heading", { level: 3 })).toHaveTextContent(
"impianti_recenti"
);
expect(steps[0].querySelector('[data-slot="card"]')).toBeInTheDocument();
expect(steps[1].querySelector('[data-slot="card"]')).toBeInTheDocument();
});
test("renders one card per CTE with index badge, purpose, and filter value", () => {
render(<CtePlanViewer plan={plan} />);
expect(screen.getByText("CTE 1/2")).toBeInTheDocument();
expect(screen.getByText("CTE 2/2")).toBeInTheDocument();
expect(screen.getByText("Seleziona i pazienti con follow-up attivo")).toBeInTheDocument();
expect(screen.getByText(/pazienti\.stato/)).toBeInTheDocument();
expect(screen.getByText(/TRUE/)).toBeInTheDocument();
});
test("renders each filter as aligned column, operator, and value fields", () => {
render(<CtePlanViewer plan={plan} />);
const filter = within(getCteCard("pazienti_idonei")).getByRole("group", {
name: "Filter 1",
});
expect(within(filter).getByText("Column")).toBeInTheDocument();
expect(within(filter).getByText("Operator")).toBeInTheDocument();
expect(within(filter).getByText("Value")).toBeInTheDocument();
expect(within(filter).getByText("pazienti.stato")).toBeInTheDocument();
expect(within(filter).getByText("IS")).toBeInTheDocument();
expect(within(filter).getByText("TRUE")).toBeInTheDocument();
});
test("groups filters in one bordered divided list with accessible padded rows", () => {
render(<CtePlanViewer plan={plan} />);
const cases = [
{ card: getCteCard("pazienti_idonei"), count: 2 },
{ card: getCteCard("impianti_recenti"), count: 1 },
];
const lists: HTMLElement[] = [];
for (const { card, count } of cases) {
const filters = within(card).getAllByRole("group", { name: /Filter \d/ });
expect(filters).toHaveLength(count);
const list = filters[0].parentElement;
expect(list).not.toBeNull();
expect(list).toHaveClass("border", "divide-y");
for (const filter of filters) {
expect(filter.parentElement).toBe(list);
expect(filter).toHaveClass("px-3", "py-1", "sm:px-4");
expect(filter).not.toHaveClass("p-3", "sm:p-4");
}
lists.push(list!);
}
expect(lists[0]).not.toBe(lists[1]);
});
test("renders rationale as a plain top-divided section", () => {
render(<CtePlanViewer plan={plan} />);
const rationale = screen
.getByText("Base della catena: riduce il volume prima dei join")
.closest("section");
expect(rationale).toHaveClass("border-t");
expect(rationale).not.toHaveClass("border-l-2", "bg-primary/5", "rounded-lg");
});
test("compacts table, filter, detail, and rationale rows", () => {
render(<CtePlanViewer plan={plan} />);
const card = getCteCard("pazienti_idonei");
const tableRow = within(card).getByText("pazienti").parentElement;
expect(tableRow).toHaveClass("px-3", "py-1", "sm:px-4");
const filter = within(card).getByRole("group", { name: "Filter 1" });
expect(filter).toHaveClass("space-y-2", "px-3", "py-1", "sm:px-4");
expect(filter.firstElementChild).toHaveClass("gap-2");
const filterDetails = within(card).getByText("Solo pazienti in follow-up").parentElement;
expect(filterDetails).toHaveClass("gap-y-1", "pt-1");
const rationale = within(card)
.getByText("Base della catena: riduce il volume prima dei join")
.closest("section");
expect(rationale).toHaveClass("pt-1");
expect(within(rationale!).getByText("Rationale")).toHaveClass("mb-1");
});
test("shows 'No dependencies' for a CTE with an empty depends_on", () => {
render(<CtePlanViewer plan={plan} />);
expect(screen.getByText(/no dependencies/i)).toBeInTheDocument();
});
test("renders depends_on chips for a CTE that has dependencies", () => {
render(<CtePlanViewer plan={plan} />);
// "pazienti_idonei" appears both as chain-strip entry and as a depends_on chip
// for impianti_recenti — assert at least one occurrence beyond the strip.
expect(screen.getAllByText("pazienti_idonei").length).toBeGreaterThan(1);
});
test("omits sections for missing optional fields without rendering 'undefined'", () => {
const minimalPlan: CtePlanV2 = {
schema_version: 2,
ctes: [{ name: "minimal", index: 1 }],
};
render(<CtePlanViewer plan={minimalPlan} />);
expect(screen.queryByText(/undefined/i)).not.toBeInTheDocument();
expect(screen.queryByText("Question")).not.toBeInTheDocument();
expect(screen.queryByText("Strategy")).not.toBeInTheDocument();
expect(screen.queryByText("Purpose")).not.toBeInTheDocument();
expect(screen.queryByText("Tables")).not.toBeInTheDocument();
expect(screen.queryByText("Filters")).not.toBeInTheDocument();
expect(screen.queryByText("Keys")).not.toBeInTheDocument();
expect(screen.queryByText("Output columns")).not.toBeInTheDocument();
expect(screen.queryByText("Rationale")).not.toBeInTheDocument();
});
test("allows long CTE, table, filter, and chip values to wrap safely", () => {
const longName = "cte_" + "extremely_long_unbroken_name_".repeat(6);
const longTable = "schema." + "extremely_long_table_name_".repeat(6);
const longColumn = "schema.table." + "extremely_long_filter_column_".repeat(5);
const longOperator = "EXTREMELY_LONG_OPERATOR_WITHOUT_BREAKS";
const longValue = "value_" + "extremely_long_unbroken_value_".repeat(6);
const longChip = "column_" + "extremely_long_unbroken_chip_".repeat(6);
const longPlan: CtePlanV2 = {
schema_version: 2,
ctes: [
{
name: longName,
index: 1,
depends_on: [longChip],
tables: [{ name: longTable }],
keys: [longChip],
filters: [{ column: longColumn, op: longOperator, value: longValue }],
output_columns: [longChip],
},
],
};
render(<CtePlanViewer plan={longPlan} />);
expect(screen.getByRole("heading", { level: 3, name: longName })).toHaveClass(
"min-w-0",
"[overflow-wrap:anywhere]"
);
expect(screen.getByText(longTable)).toHaveClass(
"min-w-0",
"whitespace-pre-wrap",
"[overflow-wrap:anywhere]"
);
expect(screen.getByText(longColumn)).toHaveClass("min-w-0", "[overflow-wrap:anywhere]");
expect(screen.getByText(longOperator)).toHaveClass("min-w-0", "[overflow-wrap:anywhere]");
expect(screen.getByText(longValue)).toHaveClass(
"min-w-0",
"whitespace-pre-wrap",
"[overflow-wrap:anywhere]"
);
for (const chip of screen.getAllByText(longChip)) {
expect(chip).toHaveClass(
"min-w-0",
"whitespace-pre-wrap",
"[overflow-wrap:anywhere]"
);
}
});