diff --git a/frontend/src/components/ui/card.test.tsx b/frontend/src/components/ui/card.test.tsx index 1fd88723..defb1fa4 100644 --- a/frontend/src/components/ui/card.test.tsx +++ b/frontend/src/components/ui/card.test.tsx @@ -29,7 +29,9 @@ test("uses concrete Tailwind 3 spacing for default and small cards", () => { for (const card of [defaultCard, smallCard]) { expect(card.querySelector('[data-slot="card-header"]')).toHaveClass( "px-4", - "group-data-[size=sm]/card:px-3" + "group-data-[size=sm]/card:px-3", + "[&.border-b]:pb-4", + "group-data-[size=sm]/card:[&.border-b]:pb-3" ) expect(card.querySelector('[data-slot="card-content"]')).toHaveClass( "px-4", diff --git a/frontend/src/viewers/CtePlanViewer.test.tsx b/frontend/src/viewers/CtePlanViewer.test.tsx index 7a414850..7b50b3d3 100644 --- a/frontend/src/viewers/CtePlanViewer.test.tsx +++ b/frontend/src/viewers/CtePlanViewer.test.tsx @@ -37,11 +37,25 @@ const plan: CtePlanV2 = { 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(); @@ -60,7 +74,12 @@ test("uses semantic CTE headings and explicit responsive card padding", () => { expect(screen.getByRole("heading", { level: 3, name: "pazienti_idonei" })).toBeInTheDocument(); for (const header of container.querySelectorAll('[data-slot="card-header"]')) { - expect(header).toHaveClass("p-4", "sm:p-5"); + expect(header).toHaveClass( + "p-4", + "sm:p-5", + "[&.border-b]:pb-4", + "sm:[&.border-b]:pb-5" + ); } for (const content of container.querySelectorAll('[data-slot="card-content"]')) { expect(content).toHaveClass("p-4", "sm:p-5"); @@ -70,6 +89,23 @@ test("uses semantic CTE headings and explicit responsive card padding", () => { } }); +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(); @@ -83,7 +119,9 @@ test("renders one card per CTE with index badge, purpose, and filter value", () test("renders each filter as aligned column, operator, and value fields", () => { render(); - const filter = screen.getByRole("group", { name: "Filter 1" }); + 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(); @@ -95,13 +133,26 @@ test("renders each filter as aligned column, operator, and value fields", () => test("groups filters in one bordered divided list with accessible padded rows", () => { render(); - const filters = screen.getAllByRole("group", { name: /Filter \d/ }); - expect(filters).toHaveLength(2); - expect(filters[0].parentElement).toBe(filters[1].parentElement); - expect(filters[0].parentElement).toHaveClass("border", "divide-y"); - for (const filter of filters) { - expect(filter).toHaveClass("p-3", "sm:p-4"); + 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("p-3", "sm:p-4"); + } + lists.push(list!); } + + expect(lists[0]).not.toBe(lists[1]); }); test("renders rationale as a plain top-divided section", () => { diff --git a/frontend/src/viewers/CtePlanViewer.tsx b/frontend/src/viewers/CtePlanViewer.tsx index 92739a8d..5fcb9dc8 100644 --- a/frontend/src/viewers/CtePlanViewer.tsx +++ b/frontend/src/viewers/CtePlanViewer.tsx @@ -67,7 +67,7 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) { return ( - +
CTE {cte.index}/{total} @@ -203,11 +203,13 @@ export function CtePlanViewer({ plan }: { plan: CtePlanV2 }) {
-
+
    {plan.ctes.map((cte) => ( - +
  1. + +
  2. ))} -
+ ); }