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('[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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); expect(screen.getByText(/no dependencies/i)).toBeInTheDocument(); }); test("renders depends_on chips for a CTE that has dependencies", () => { render(); // "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(); 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(); 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]" ); } });