// @vitest-environment jsdom import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { requestActivity } from '../../src/api/request-activity'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; import type { RegulatoryControlOverviewResponse } from '../../src/compliance/regulatory-mapping'; import { ClientIsoControls } from '../../src/compliance/ui/ClientIsoControls'; import { markRegulatoryDecisionSaved } from '../../src/compliance/ui/decision-revision'; import { workRecordIdsForResult } from '../../src/features/results-browser/work-record-lookup'; import { makeSheet } from '../../src/importers/sheet-factory'; // Client Info `ISO controls`: one tile per accepted control with its number of findings, laid out like // the Context heatmap; a tile opens exactly its findings in Work. Switched off, nothing renders or loads. const overview: RegulatoryControlOverviewResponse = { version: 1, projectId: 'dummy-project', scope: { assessmentIds: ['dummy-assessment'], scanRunIds: [] }, findingCount: 3, controls: [ { controlId: '8.20', annexAControlId: 'A.8.20', title: 'Networks security', theme: 'Technological', findings: [ { resultProjectionId: 'scanner-finding:dummy-a', sourceKind: 'scanner-finding', sourceResultId: 'dummy-a' }, { resultProjectionId: 'scanner-finding:dummy-b', sourceKind: 'scanner-finding', sourceResultId: 'dummy-b' }] }, { controlId: '5.15', annexAControlId: 'A.5.15', theme: 'Organizational', findings: [ { resultProjectionId: 'scanner-finding:dummy-c', sourceKind: 'scanner-finding', sourceResultId: 'dummy-c' }] } ] }; let host: HTMLDivElement; let root: Root; const onOpenControl = vi.fn(); const render = () => act(async () => { root.render(); await new Promise(resolve => setTimeout(resolve, 0)); }); beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); onOpenControl.mockReset(); host = document.createElement('div'); document.body.append(host); root = createRoot(host); }); afterEach(async () => { await act(async () => { root.unmount(); }); host.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); resetEnginePreferencesForTests(); }); describe('Client Info ISO controls', () => { it('shows each accepted control with its finding count and opens exactly its findings', async () => { const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlOverview').mockResolvedValue(overview); await render(); expect(request).toHaveBeenCalledWith('dummy-project', { assessmentIds: ['dummy-assessment'], scanRunIds: [] }); expect(host.querySelector('h2')?.textContent).toBe('ISO controls'); expect(host.querySelector('.iso-controls__total')?.textContent).toBe('Findings: 3'); const tiles = [...host.querySelectorAll('.iso-controls__cell')]; expect(tiles.map(tile => [...tile.children].map(part => part.textContent))).toEqual([ ['Technological', 'A.8.20 ยท Networks security', '2 findings'], ['Organizational', 'A.5.15', '1 finding'] ]); await act(async () => tiles[0].click()); expect(onOpenControl).toHaveBeenCalledWith('8.20'); }); it('reloads after a control decision and says so when nothing is accepted', async () => { const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlOverview') .mockResolvedValueOnce({ ...overview, findingCount: 0, controls: [] }).mockResolvedValue(overview); await render(); expect(host.querySelector('.iso-controls__empty')?.textContent).toBe('No accepted ISO controls in this timeslot.'); await act(async () => { markRegulatoryDecisionSaved(); await new Promise(resolve => setTimeout(resolve, 0)); }); expect(request).toHaveBeenCalledTimes(2); expect(host.querySelectorAll('.iso-controls__cell')).toHaveLength(2); }); it('renders nothing and requests nothing while the Compliance Engine is off', async () => { const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlOverview').mockResolvedValue(overview); setEngineEnabled('compliance', false); await render(); expect(host.innerHTML).toBe(''); expect(request).not.toHaveBeenCalled(); }); it('names the overview request in the activity log as Compliance Engine work', () => { expect(requestActivity('/projects/p/regulatory-control-overview?assessmentIds=a')).toEqual({ area: 'Compliance Engine', label: 'Loading ISO control overview' }); expect(requestActivity('/projects/p/regulatory-control-assignments?assessmentIds=a')).toEqual({ area: 'Compliance Engine', label: 'Loading ISO control filters' }); }); it('places the section below the Context heatmap and opens Work through the ISO control filter', () => { const source = readFileSync(join(process.cwd(), 'src/features/dashboard/DashboardPanel.tsx'), 'utf8'); expect(source.indexOf('