fix(frontend): harden CTE step semantics and padding
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user