fix(frontend): harden CTE step semantics and padding

This commit is contained in:
User
2026-07-14 23:33:40 +02:00
parent 964bff9612
commit e5366d14a6
3 changed files with 68 additions and 13 deletions
+59 -8
View File
@@ -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<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} />);
@@ -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(<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} />);
@@ -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(<CtePlanViewer plan={plan} />);
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(<CtePlanViewer plan={plan} />);
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", () => {
+6 -4
View File
@@ -67,7 +67,7 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
return (
<Card className="gap-0 rounded-lg border py-0 shadow-none">
<CardHeader className="gap-3 rounded-t-lg border-b border-border/60 bg-muted/15 p-4 sm:p-5">
<CardHeader className="gap-3 rounded-t-lg border-b border-border/60 bg-muted/15 p-4 sm:p-5 sm:[&.border-b]:pb-5">
<div className="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-2.5">
<Badge variant="outline" className="font-mono">
CTE {cte.index}/{total}
@@ -203,11 +203,13 @@ export function CtePlanViewer({ plan }: { plan: CtePlanV2 }) {
</div>
</div>
<div className="flex flex-col gap-3">
<ol aria-label="CTE plan steps" className="flex flex-col gap-3">
{plan.ctes.map((cte) => (
<CteCard key={cte.name} cte={cte} total={plan.ctes.length} />
<li key={cte.name} className="min-w-0">
<CteCard cte={cte} total={plan.ctes.length} />
</li>
))}
</div>
</ol>
</div>
);
}