fix(ui): address compliance review feedback

This commit is contained in:
alejandrobailo
2026-07-22 14:25:18 +02:00
parent 09d9aa27fc
commit 33116f6a6d
8 changed files with 139 additions and 6 deletions
@@ -28,7 +28,12 @@ vi.mock("@sentry/nextjs", () => ({
captureException: captureExceptionMock,
}));
import { getCrossAccountComplianceOverview } from "./cross-account";
import {
generateCrossAccountPdf,
getCrossAccountComplianceOverview,
getCrossAccountPdfBinary,
getLatestCrossAccountPdf,
} from "./cross-account";
const jsonResponse = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
@@ -42,6 +47,15 @@ const lastFetchUrl = () => {
return new URL(String(call[0]));
};
const fetchCallAt = (index: number) => {
const call = fetchMock.mock.calls[index];
if (!call) throw new Error(`fetch call ${index} was not found`);
return {
init: call[1] as RequestInit,
url: new URL(String(call[0])),
};
};
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", fetchMock);
@@ -81,6 +95,73 @@ describe("cross-account compliance actions", () => {
expect(url.searchParams.get("filter[provider_groups__in]")).toBe("group-1");
});
it("routes PDF operations through the cross-account endpoints", async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({ data: { type: "tasks", id: "task-1" } }, 202),
)
.mockResolvedValueOnce(
new Response(Buffer.from("pdf-bytes"), {
headers: {
"Content-Disposition": 'attachment; filename="report.pdf"',
"Content-Type": "application/pdf",
},
}),
)
.mockResolvedValueOnce(
jsonResponse({
data: {
id: "task-2",
attributes: { result: { filename: "latest.pdf" } },
},
}),
);
await generateCrossAccountPdf({
complianceId: "cis_2.0_aws",
providerType: "aws",
filters: { scanIds: ["scan-1"] },
reportName: "report.pdf",
});
await getCrossAccountPdfBinary("task-1");
await getLatestCrossAccountPdf({
complianceId: "cis_2.0_aws",
providerType: "aws",
filters: { providerIds: "provider-1" },
});
const generation = fetchCallAt(0);
expect(generation.init.method).toBe("POST");
expect(generation.url.pathname).toBe(
"/api/v1/cross-account-compliance-overviews/pdf",
);
expect(generation.url.searchParams.get("filter[compliance_id]")).toBe(
"cis_2.0_aws",
);
expect(generation.url.searchParams.get("filter[provider_type]")).toBe(
"aws",
);
expect(generation.url.searchParams.get("filter[scan__in]")).toBe("scan-1");
expect(generation.url.searchParams.get("report_name")).toBe("report.pdf");
const binary = fetchCallAt(1);
expect(binary.url.pathname).toBe(
"/api/v1/cross-account-compliance-overviews/pdf/task-1",
);
const latest = fetchCallAt(2);
expect(latest.url.pathname).toBe(
"/api/v1/cross-account-compliance-overviews/pdf/latest",
);
expect(latest.url.searchParams.get("filter[provider_id__in]")).toBe(
"provider-1",
);
expect([generation, binary, latest].every(({ init }) => init.signal)).toBe(
true,
);
});
it("aborts a stalled request and reports the network failure", async () => {
vi.useFakeTimers();
let requestSignal: AbortSignal | undefined;
@@ -77,7 +77,7 @@ export const AggregatedComplianceDetail = ({
<ClientAccordionWrapper
items={accordionItems}
defaultExpandedKeys={initialExpandedKeys}
scrollToKey={initialExpandedKeys[0]}
scrollToKey={initialExpandedKeys.at(-1)}
/>
</div>
);
@@ -40,6 +40,7 @@ describe("ProviderCoverageCard", () => {
expect(screen.getByTestId("coverage-row-aws")).toBeInTheDocument();
expect(screen.queryByTestId("coverage-row-gcp")).not.toBeInTheDocument();
expect(screen.queryByText("No completed scan")).not.toBeInTheDocument();
expect(screen.getByText("8/10 · 1 manual")).toBeInTheDocument();
});
it("shows an empty state when no provider has a scan", () => {
@@ -44,7 +44,7 @@ describe("RequirementAccountChips", () => {
render(<RequirementAccountChips accounts={accounts} accountMeta={meta} />);
const summary = screen.getByRole("button", {
name: "Show status breakdown for 13 providers",
name: "Show status breakdown for 13 accounts",
});
expect(summary).toHaveTextContent("Fail×2");
expect(summary).toHaveTextContent("Manual×1");
@@ -40,6 +40,7 @@ export const RequirementAccountChips = ({
if (entries.length > MAX_INLINE_ACCOUNT_CHIPS) {
return (
<RequirementStatusSummary
entityLabel="accounts"
entries={entries.map((account) => ({
key: account.id,
label: accountDisplayLabel(account),
@@ -39,8 +39,10 @@ const SCROLLABLE_BREAKDOWN_MIN_ROWS = 13;
*/
export const RequirementStatusSummary = ({
entries,
entityLabel = "providers",
}: {
entries: RequirementStatusEntry[];
entityLabel?: "accounts" | "providers";
}) => {
const counts = STATUS_ORDER.map((status) => ({
status,
@@ -74,7 +76,7 @@ export const RequirementStatusSummary = ({
type="button"
variant="bare"
size="link-xs"
aria-label={`Show status breakdown for ${entries.length} providers`}
aria-label={`Show status breakdown for ${entries.length} ${entityLabel}`}
data-testid="requirement-status-summary"
>
{counts.map(({ status, count }) => (
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import type { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
import type { Framework } from "@/types/compliance";
import { getAggregatedInitialExpandedKeys } from "../aggregated-compliance-detail";
describe("getAggregatedInitialExpandedKeys", () => {
it("returns the ancestor path for a nested section deep link", () => {
const data = [
{ name: "Operational" },
{ name: "Organizational" },
] as Framework[];
const accordionItems: AccordionItemProps[] = [
{
key: "Operational",
title: null,
content: null,
items: [
{
key: "Operational-Access control",
title: null,
content: null,
},
],
},
];
expect(
getAggregatedInitialExpandedKeys(data, accordionItems, "Access control"),
).toEqual(["Operational", "Operational-Access control"]);
});
});
@@ -23,6 +23,21 @@ export const getAggregatedInitialExpandedKeys = (
const candidates = new Set(
data.map((framework) => `${framework.name}-${targetSection}`),
);
const match = accordionItems.find((item) => candidates.has(item.key));
return match ? [match.key] : [];
const findExpandedPath = (
items: AccordionItemProps[],
ancestors: string[] = [],
): string[] | undefined => {
for (const item of items) {
const path = [...ancestors, item.key];
if (candidates.has(item.key)) return path;
const nestedMatch = findExpandedPath(item.items ?? [], path);
if (nestedMatch) return nestedMatch;
}
return undefined;
};
return findExpandedPath(accordionItems) ?? [];
};