)
@@ -84,7 +84,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
=",
+ value: "CURRENT_DATE - INTERVAL '5 years'",
+ },
],
output_columns: ["paziente_id", "data_nascita"],
},
@@ -40,12 +45,31 @@ const plan: CtePlanV2 = {
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 semantic CTE headings and explicit responsive card padding", () => {
+ const { container } = render(
);
+
+ 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");
+ }
+ for (const content of container.querySelectorAll('[data-slot="card-content"]')) {
+ expect(content).toHaveClass("p-4", "sm:p-5");
+ }
+ for (const card of container.querySelectorAll('[data-slot="card"]')) {
+ expect(card).toHaveClass("shadow-none");
+ }
+});
+
test("renders one card per CTE with index badge, purpose, and filter value", () => {
render(
);
@@ -68,6 +92,28 @@ test("renders each filter as aligned column, operator, and value fields", () =>
expect(within(filter).getByText("TRUE")).toBeInTheDocument();
});
+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");
+ }
+});
+
+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", "pt-4");
+ expect(rationale).not.toHaveClass("border-l-2", "bg-primary/5", "rounded-lg");
+});
+
test("shows 'no dependencies' for a CTE with an empty depends_on", () => {
render(
);
expect(screen.getByText(/no dependencies/i)).toBeInTheDocument();
@@ -81,8 +127,69 @@ test("renders depends_on chips for a CTE that has dependencies", () => {
});
test("omits sections for missing optional fields without rendering 'undefined'", () => {
- render(
);
+ const minimalPlan: CtePlanV2 = {
+ schema_version: 2,
+ ctes: [{ name: "minimal", index: 1 }],
+ };
+
+ render(
);
+
expect(screen.queryByText(/undefined/i)).not.toBeInTheDocument();
- // impianti_recenti has no rationale/keys/filters — no crash, no stray text
- expect(screen.getAllByText("impianti_recenti").length).toBeGreaterThan(0);
+ 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]"
+ );
+ }
});
diff --git a/frontend/src/viewers/CtePlanViewer.tsx b/frontend/src/viewers/CtePlanViewer.tsx
index 9a6731e4..92739a8d 100644
--- a/frontend/src/viewers/CtePlanViewer.tsx
+++ b/frontend/src/viewers/CtePlanViewer.tsx
@@ -1,54 +1,59 @@
import type { ReactNode } from "react";
-import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
+import { Card, CardContent, CardHeader } from "../components/ui/card";
import { Badge } from "../components/ui/badge";
import type { CtePlanCte, CtePlanFilter, CtePlanV2 } from "./artifactV2";
-const CHIP = "rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/90";
+const CHIP =
+ "min-w-0 max-w-full whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/90";
const FIELD_LABEL = "text-[0.65rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground";
function Chip({ children }: { children: ReactNode }) {
return
{children};
}
-function FilterCard({ filter, index }: { filter: CtePlanFilter; index: number }) {
+function FilterRow({ filter, index }: { filter: CtePlanFilter; index: number }) {
return (
-
+
Column
-
+
{filter.column}
-
{(filter.description || filter.rationale) && (
-
+
{filter.description && (
<>
Description
-
{filter.description}
+
+ {filter.description}
+
>
)}
{filter.rationale && (
<>
Rationale
-
{filter.rationale}
+
+ {filter.rationale}
+
>
)}
@@ -61,26 +66,28 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
const dependsOn = cte.depends_on ?? [];
return (
-
-
+
+
CTE {cte.index}/{total}
-
+
{cte.name}
-
+
{cte.purpose && (
- {cte.purpose}
+
+ {cte.purpose}
+
)}
-
-
-
+
+
+
Depends on
{dependsOn.length ? (
-
+
{dependsOn.map((dependency) => (
{dependency}
))}
@@ -91,9 +98,9 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
{cte.keys && cte.keys.length > 0 && (
-
+
Keys
-
+
{cte.keys.map((key) => (
{key}
))}
@@ -103,19 +110,19 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
{cte.tables && cte.tables.length > 0 && (
-
+
Tables
-
+
{cte.tables.map((table) => (
-
+
{table.name}
{table.description && (
-
+
{table.description}
)}
@@ -126,20 +133,20 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
)}
{cte.filters && cte.filters.length > 0 && (
-
+
Filters
-
+
{cte.filters.map((f, i) => (
-
+
))}
)}
{cte.output_columns && cte.output_columns.length > 0 && (
-
+
Output columns
-
+
{cte.output_columns.map((column) => (
{column}
))}
@@ -148,9 +155,11 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
)}
{cte.rationale && (
-
+
Rationale
- {cte.rationale}
+
+ {cte.rationale}
+
)}
@@ -160,17 +169,38 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
export function CtePlanViewer({ plan }: { plan: CtePlanV2 }) {
return (
-
- {plan.question &&
{plan.question}
}
- {plan.strategy &&
{plan.strategy}
}
+
+
+ {plan.question && (
+
+
Question
+
+ {plan.question}
+
+
+ )}
+ {plan.strategy && (
+
+
Strategy
+
+ {plan.strategy}
+
+
+ )}
-
- {plan.ctes.map((c, i) => (
-
- {i > 0 && →}
- {c.name}
-
- ))}
+
+
Execution order
+
+ {plan.ctes.map((c, i) => (
+
+ {i > 0 && →}
+
+ {c.name}
+
+
+ ))}
+
+