import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; import { useScansStore } from "@/store"; import { ProviderProps } from "@/types"; import { ScansPageShell } from "./scans-page-shell"; const { pushMock, replaceMock, searchParamsValue } = vi.hoisted(() => ({ pushMock: vi.fn(), replaceMock: vi.fn(), searchParamsValue: { current: "" }, })); const { scansFilterBarSpy } = vi.hoisted(() => ({ scansFilterBarSpy: vi.fn(), })); const { onboardingTriggerSpy } = vi.hoisted(() => ({ onboardingTriggerSpy: vi.fn(), })); const localStorageMock = (() => { let store: Record = {}; return { getItem: vi.fn((key: string) => store[key] ?? null), setItem: vi.fn((key: string, value: string) => { store[key] = value; }), removeItem: vi.fn((key: string) => { delete store[key]; }), clear: vi.fn(() => { store = {}; }), get length() { return Object.keys(store).length; }, key: vi.fn((index: number) => Object.keys(store)[index] ?? null), }; })(); Object.defineProperty(window, "localStorage", { value: localStorageMock, writable: true, }); vi.mock("next/navigation", () => ({ usePathname: () => "/scans", useRouter: () => ({ push: pushMock, replace: replaceMock, }), useSearchParams: () => new URLSearchParams(searchParamsValue.current), })); vi.mock("./scans-filter-bar", () => ({ ScansFilterBar: (props: { showStatusFilter: boolean; onScheduleTypeChange: (value: string) => void; onScanStatusChange: (value: string) => void; }) => { scansFilterBarSpy(props); return ( <>
Shared scan filters
{props.showStatusFilter && ( )} ); }, })); vi.mock("./launch-scan-modal", () => ({ LaunchScanModal: ({ open, onOpenChange, }: { open: boolean; onOpenChange: (open: boolean) => void; }) => open ? (
Launch scan
) : null, })); vi.mock("@/components/providers/muted-findings-config-button", () => ({ MutedFindingsConfigButton: () => Configure Mutelist, })); vi.mock("@/components/onboarding", () => ({ OnboardingTrigger: (props: unknown) => { onboardingTriggerSpy(props); return
; }, PageReady: () =>
, })); const providers: ProviderProps[] = [ { id: "provider-1", type: "providers" as const, attributes: { provider: "aws" as const, is_dynamic: false, uid: "123456789012", alias: "Production", status: "completed" as const, resources: 0, connection: { connected: true, last_checked_at: "2026-04-13T00:00:00Z", }, scanner_args: { only_logs: false, excluded_checks: [], aws_retries_max_attempts: 3, }, inserted_at: "2026-04-13T00:00:00Z", updated_at: "2026-04-13T00:00:00Z", created_by: { object: "user", id: "user-1", }, }, relationships: { secret: { data: null, }, provider_groups: { meta: { count: 0, }, data: [], }, }, }, ]; const disconnectedProviders: ProviderProps[] = [ { ...providers[0], attributes: { ...providers[0].attributes, connection: { ...providers[0].attributes.connection, connected: false, }, }, }, ]; describe("ScansPageShell", () => { afterEach(() => { vi.unstubAllEnvs(); vi.clearAllMocks(); localStorageMock.clear(); searchParamsValue.current = ""; useScansStore.getState().closeLaunchScanModal(); }); it("uses the shared scan filter bar for scan filters", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect(screen.getByText("Shared scan filters")).toBeInTheDocument(); expect(scansFilterBarSpy).toHaveBeenCalledWith( expect.objectContaining({ providers, scheduleType: "all", }), ); }); it("clears the active sort when switching tabs", async () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "tab=active&sort=trigger"; const user = userEvent.setup(); render(
Scans table
, ); await user.click(screen.getByRole("tab", { name: /completed/i })); expect(pushMock).toHaveBeenCalled(); const calledUrl = pushMock.mock.calls.at(-1)?.[0] as string; expect(calledUrl).toContain("tab=completed"); expect(calledUrl).not.toContain("sort="); }); it("uses a generic type filter label in Cloud", () => { vi.stubEnv("UI_CLOUD_ENABLED", "true"); render(
Scans table
, ); expect(screen.getByRole("combobox", { name: /all types/i })).toBeVisible(); }); it.each(["Cloud", "Private Cloud"])( "shows Import Findings in %s with Manage Ingestions", () => { // Given vi.stubEnv("UI_CLOUD_ENABLED", "true"); // When render(
Scans table
, ); // Then expect( screen.getByRole("button", { name: /import findings/i }), ).toBeVisible(); }, ); it("hides Import Findings without Manage Ingestions", () => { // Given vi.stubEnv("UI_CLOUD_ENABLED", "true"); // When render(
Scans table
, ); // Then expect( screen.queryByRole("button", { name: /import findings/i }), ).not.toBeInTheDocument(); }); it("hides Import Findings in OSS and Local Server", () => { // Given vi.stubEnv("UI_CLOUD_ENABLED", "false"); // When render(
Scans table
, ); // Then expect( screen.queryByRole("button", { name: /import findings/i }), ).not.toBeInTheDocument(); }); it("shows the CLI import banner in Cloud", () => { vi.stubEnv("UI_CLOUD_ENABLED", "true"); render(
Scans table
, ); expect(screen.getByRole("alert")).toHaveTextContent( /import findings from prowler cli/i, ); expect(screen.getByRole("link", { name: /learn more/i })).toHaveAttribute( "href", "https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings", ); }); it("hides the CLI import banner outside Cloud", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); it("keeps launch scan with filters and mutelist with tabs", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect( screen.getByRole("group", { name: /scan filters and actions/i }), ).toContainElement(screen.getByRole("button", { name: /launch scan/i })); expect( screen.getByRole("group", { name: /scan filters and actions/i }), ).not.toContainElement( screen.getByRole("link", { name: /configure mutelist/i }), ); expect(screen.getByRole("group", { name: /scan tabs/i })).toContainElement( screen.getByRole("link", { name: /configure mutelist/i }), ); }); it("shows the active scans count in the in progress tab", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect( screen.getByRole("tab", { name: /in progress \(3\)/i }), ).toBeVisible(); expect(screen.getByRole("tab", { name: /^completed$/i })).toBeVisible(); }); it("opens the launch scan modal from the URL", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "launchScan=true"; render(
Scans table
, ); expect(screen.getByRole("dialog")).toHaveTextContent(/launch scan/i); }); it("does not open the launch scan modal from the URL when no provider is connected", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "launchScan=true"; render(
Scans table
, ); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); it("does not open the launch scan modal from client state when no provider is connected", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); useScansStore.getState().openLaunchScanModal(); render(
Scans table
, ); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); it("does not open the launch scan modal from the URL without manage scans permission", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "launchScan=true"; render(
Scans table
, ); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); it("strips the launchScan URL param via the History API when closing the URL-opened modal", async () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "tab=completed&launchScan=true"; const replaceStateSpy = vi.spyOn(window.history, "replaceState"); const user = userEvent.setup(); render(
Scans table
, ); await user.click(screen.getByRole("button", { name: /close/i })); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); // History API (not router.replace) so Next.js does not refetch and reload the page; // scanOnDemand's revalidatePath already refreshes the list when a scan launches. expect(replaceStateSpy).toHaveBeenCalledWith( null, "", "/scans?tab=completed", ); expect(replaceMock).not.toHaveBeenCalled(); expect(pushMock).not.toHaveBeenCalled(); replaceStateSpy.mockRestore(); }); it("opens and closes the launch scan modal from client state without navigation", async () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); const user = userEvent.setup(); useScansStore.getState().openLaunchScanModal(); render(
Scans table
, ); expect(screen.getByRole("dialog")).toHaveTextContent(/launch scan/i); await user.click(screen.getByRole("button", { name: /close/i })); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(pushMock).not.toHaveBeenCalled(); }); it("shows the status filter only on the completed tab", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "tab=completed"; render(
Scans table
, ); expect( screen.getByRole("combobox", { name: /all statuses/i }), ).toBeVisible(); }); it("hides the status filter outside of the completed tab", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "tab=active"; render(
Scans table
, ); expect( screen.queryByRole("combobox", { name: /all statuses/i }), ).not.toBeInTheDocument(); }); it("clears status filter when switching scan tabs", async () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "tab=completed&filter%5Bstate__in%5D=failed"; const user = userEvent.setup(); render(
Scans table
, ); await user.click(screen.getByRole("tab", { name: /in progress/i })); const calledUrl = pushMock.mock.calls.at(-1)?.[0] as string; expect(calledUrl).toContain("tab=active"); expect(calledUrl).not.toContain("filter%5Bstate__in%5D"); }); it("clears type filter when switching to scheduled scans", async () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); searchParamsValue.current = "tab=completed&filter%5Btrigger%5D=manual"; const user = userEvent.setup(); render(
Scans table
, ); await user.click(screen.getByRole("tab", { name: /scheduled/i })); const calledUrl = pushMock.mock.calls.at(-1)?.[0] as string; expect(calledUrl).toContain("tab=scheduled"); expect(calledUrl).not.toContain("filter%5Btrigger%5D"); }); it("shows a non-blocking hint when no provider is connected, while still rendering the table", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect(screen.getByText("No Connected Providers")).toBeInTheDocument(); // The table (and therefore imported scans) must still render below the hint. expect(screen.getByText("Scans table")).toBeInTheDocument(); }); it("shows the no-providers hint when there are no providers, while still rendering the table", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect(screen.getByText("No Providers Configured")).toBeInTheDocument(); expect(screen.getByText("Scans table")).toBeInTheDocument(); }); it("does not show the providers hint when a provider is connected", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect( screen.queryByText("No Connected Providers"), ).not.toBeInTheDocument(); expect( screen.queryByText("No Providers Configured"), ).not.toBeInTheDocument(); }); it("starts the view-first-scan tour when a provider is connected", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect(screen.getByTestId("onboarding-trigger")).toBeInTheDocument(); }); it("suppresses the view-first-scan tour when no provider is connected, since Launch Scan is disabled", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect(screen.queryByTestId("onboarding-trigger")).not.toBeInTheDocument(); // The table (and therefore imported scans) must still render even with the tour suppressed. expect(screen.getByText("Scans table")).toBeInTheDocument(); }); it("suppresses the view-first-scan tour when there are no providers", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect(screen.queryByTestId("onboarding-trigger")).not.toBeInTheDocument(); }); it("suppresses the view-first-scan tour when a provider is connected but manage scans is missing", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); // Launch Scan is disabled without manage_scans, so the tour must not anchor to it. expect(screen.queryByTestId("onboarding-trigger")).not.toBeInTheDocument(); // The table (and therefore imported scans) must still render. expect(screen.getByText("Scans table")).toBeInTheDocument(); }); it("still signals page-ready without a connected provider so the navbar replay fallback works", () => { vi.stubEnv("UI_CLOUD_ENABLED", "false"); render(
Scans table
, ); expect(screen.getByTestId("page-ready")).toBeInTheDocument(); }); });