/** * Mobile ( = {}): SecurityOverview { return { scannedImages: 0, critical: 0, high: 0, fixable: 0, secrets: 0, misconfigs: 0, staleScans: 0, failedScans: 0, lastSuccessfulScanAt: null, scanner: { available: false, source: 'managed', version: null, autoUpdate: false }, deployEnforcement: { eligibleBlockPolicies: 0, honorSuppressionsOnDeploy: false }, ...o, }; } function summary(o: Partial & { image_ref: string; scan_id: number }): ScanSummary { return { highest_severity: null, scanned_at: 1_700_000_000_000, total: 0, critical: 0, high: 0, medium: 0, low: 0, unknown: 0, fixable: 0, secret_count: 0, misconfig_count: 0, ...o, }; } function asMap(...list: ScanSummary[]): Record { return Object.fromEntries(list.map((s) => [s.image_ref, s])); } function installMatchMedia(matches: boolean) { window.matchMedia = vi.fn().mockReturnValue({ matches, media: '', onchange: null, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), dispatchEvent: vi.fn(), }) as unknown as typeof window.matchMedia; } const TABS: SecurityMobileTab[] = [ { value: 'overview', label: 'Overview' }, { value: 'images', label: 'Images' }, { value: 'scanner', label: 'Scanner setup' }, ]; describe('SecurityMobileTabs', () => { it('renders every tab and marks the active one selected', () => { render(); expect(screen.getAllByRole('tab')).toHaveLength(3); expect(screen.getByRole('tab', { name: 'Images' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'false'); }); it('gives the active tab the cyan underline and brand ink', () => { render(); const active = screen.getByRole('tab', { name: 'Images' }); expect(active.className).toContain('text-brand'); expect(active.className).toContain('shadow-[inset_0_-2px_0_0_var(--brand)]'); }); it('calls onSelect with the tab value on click', async () => { const onSelect = vi.fn(); render(); await userEvent.click(screen.getByRole('tab', { name: 'Scanner setup' })); expect(onSelect).toHaveBeenCalledWith('scanner'); }); }); describe('ImageScanRow', () => { it('shows critical and high count tags', () => { render(); expect(screen.getByText('nginx:1')).toBeInTheDocument(); expect(screen.getByText('5C')).toBeInTheDocument(); expect(screen.getByText('2H')).toBeInTheDocument(); }); it('shows a clean tag when there are no findings', () => { render(); expect(screen.getByText('clean')).toBeInTheDocument(); }); it('does not call a medium/low-only image clean', () => { // A medium-only image has no critical/high/secret/misconfig findings but is // not clean; the dot and tags must follow getSeverityKey, not an all-zero check. render(); expect(screen.queryByText('clean')).not.toBeInTheDocument(); }); it('opens the scan on the vulns tab when tapped', async () => { const onInspect = vi.fn(); render(); await userEvent.click(screen.getByText('redis:7')); expect(onInspect).toHaveBeenCalledWith(9, 'vulns', undefined); }); }); describe('SecuritySevStrip', () => { it('tints critical and high by severity when nonzero', () => { render(); expect(screen.getByText('2').className).toContain('text-destructive'); expect(screen.getByText('4').className).toContain('text-warning'); }); it('keeps zero counts at the neutral value tone', () => { render(); // Two cells read 0 (critical, high); both stay neutral, neither tinted. for (const cell of screen.getAllByText('0')) { expect(cell.className).toContain('text-stat-value'); expect(cell.className).not.toContain('text-destructive'); } }); it('reads "never" when the node has no successful scan', () => { render(); expect(screen.getByText('never')).toBeInTheDocument(); }); }); describe('ImagesTab (mobile)', () => { const original = window.matchMedia; afterEach(() => { window.matchMedia = original; vi.clearAllMocks(); }); const base = { loading: false, error: false, onInspect: vi.fn(), canScan: false, scanningRef: null as string | null, onScan: vi.fn(), }; it('renders the severity chips and a row list below the breakpoint', () => { installMatchMedia(true); render( , ); // Chips are present and the stack scan is excluded, like the desktop table. expect(screen.getByRole('button', { name: 'Fixable' })).toBeInTheDocument(); expect(screen.getByText('nginx:1')).toBeInTheDocument(); expect(screen.getByText('5C')).toBeInTheDocument(); expect(screen.queryByText('stack:web')).not.toBeInTheDocument(); }); it('filters the list to fixable images via the chip', async () => { installMatchMedia(true); render( , ); await userEvent.click(screen.getByRole('button', { name: 'Fixable' })); expect(screen.getByText('fix:1')).toBeInTheDocument(); expect(screen.queryByText('nofix:1')).not.toBeInTheDocument(); }); it('shows the targeting banner and Clear on the phone layout', async () => { installMatchMedia(true); const onClear = vi.fn(); render( , ); expect(screen.getByText(/Network-exposed images not yet classified ยท 1 affected image/)).toBeInTheDocument(); expect(screen.getByText('exp:1')).toBeInTheDocument(); expect(screen.getByText('Network exposed')).toBeInTheDocument(); expect(screen.getByText('Intent: not classified')).toBeInTheDocument(); expect(screen.queryByText('other:1')).not.toBeInTheDocument(); await userEvent.click(screen.getByRole('button', { name: 'Clear' })); expect(onClear).toHaveBeenCalled(); }); it('shows standing intent on the phone layout without targeting', () => { installMatchMedia(true); render( , ); expect(screen.getByText('Network exposed')).toBeInTheDocument(); expect(screen.getByText('Intent: LAN')).toBeInTheDocument(); }); it('keeps mixed-version Network exposed without inventing intent on phone', () => { installMatchMedia(true); render( , ); expect(screen.getByText('Network exposed')).toBeInTheDocument(); expect(screen.queryByText(/Intent:/)).not.toBeInTheDocument(); }); });