fix(frontend): harden CTE step semantics and padding
This commit is contained in:
@@ -29,7 +29,9 @@ test("uses concrete Tailwind 3 spacing for default and small cards", () => {
|
|||||||
for (const card of [defaultCard, smallCard]) {
|
for (const card of [defaultCard, smallCard]) {
|
||||||
expect(card.querySelector('[data-slot="card-header"]')).toHaveClass(
|
expect(card.querySelector('[data-slot="card-header"]')).toHaveClass(
|
||||||
"px-4",
|
"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(
|
expect(card.querySelector('[data-slot="card-content"]')).toHaveClass(
|
||||||
"px-4",
|
"px-4",
|
||||||
|
|||||||
@@ -37,11 +37,25 @@ const plan: CtePlanV2 = {
|
|||||||
purpose: "Filtra gli impianti recenti",
|
purpose: "Filtra gli impianti recenti",
|
||||||
depends_on: ["pazienti_idonei"],
|
depends_on: ["pazienti_idonei"],
|
||||||
tables: [{ name: "impianti" }],
|
tables: [{ name: "impianti" }],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
column: "impianti.data_impianto",
|
||||||
|
op: ">=",
|
||||||
|
value: "CURRENT_DATE - INTERVAL '5 years'",
|
||||||
|
},
|
||||||
|
],
|
||||||
output_columns: ["impianto_id"],
|
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", () => {
|
test("renders strategy, question, and a chain strip with CTE names in order", () => {
|
||||||
render(<CtePlanViewer plan={plan} />);
|
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();
|
expect(screen.getByRole("heading", { level: 3, name: "pazienti_idonei" })).toBeInTheDocument();
|
||||||
|
|
||||||
for (const header of container.querySelectorAll('[data-slot="card-header"]')) {
|
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"]')) {
|
for (const content of container.querySelectorAll('[data-slot="card-content"]')) {
|
||||||
expect(content).toHaveClass("p-4", "sm:p-5");
|
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", () => {
|
test("renders one card per CTE with index badge, purpose, and filter value", () => {
|
||||||
render(<CtePlanViewer plan={plan} />);
|
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", () => {
|
test("renders each filter as aligned column, operator, and value fields", () => {
|
||||||
render(<CtePlanViewer plan={plan} />);
|
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("Column")).toBeInTheDocument();
|
||||||
expect(within(filter).getByText("Operator")).toBeInTheDocument();
|
expect(within(filter).getByText("Operator")).toBeInTheDocument();
|
||||||
expect(within(filter).getByText("Value")).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", () => {
|
test("groups filters in one bordered divided list with accessible padded rows", () => {
|
||||||
render(<CtePlanViewer plan={plan} />);
|
render(<CtePlanViewer plan={plan} />);
|
||||||
|
|
||||||
const filters = screen.getAllByRole("group", { name: /Filter \d/ });
|
const cases = [
|
||||||
expect(filters).toHaveLength(2);
|
{ card: getCteCard("pazienti_idonei"), count: 2 },
|
||||||
expect(filters[0].parentElement).toBe(filters[1].parentElement);
|
{ card: getCteCard("impianti_recenti"), count: 1 },
|
||||||
expect(filters[0].parentElement).toHaveClass("border", "divide-y");
|
];
|
||||||
for (const filter of filters) {
|
const lists: HTMLElement[] = [];
|
||||||
expect(filter).toHaveClass("p-3", "sm:p-4");
|
|
||||||
|
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", () => {
|
test("renders rationale as a plain top-divided section", () => {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="gap-0 rounded-lg border py-0 shadow-none">
|
<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">
|
<div className="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-2.5">
|
||||||
<Badge variant="outline" className="font-mono">
|
<Badge variant="outline" className="font-mono">
|
||||||
CTE {cte.index}/{total}
|
CTE {cte.index}/{total}
|
||||||
@@ -203,11 +203,13 @@ export function CtePlanViewer({ plan }: { plan: CtePlanV2 }) {
|
|||||||
</div>
|
</div>
|
||||||
</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) => (
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user