// @vitest-environment jsdom
import { act, type ReactNode } 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 { AppProvider, useApp } from '../../src/app/AppProvider';
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
import type { RegulatoryControlOverviewResponse } from '../../src/compliance/regulatory-mapping';
import {
complianceWorkFacetIndex,
ISO_CONTROL_FACET_ID,
ISO_THEME_FACET_ID,
resetComplianceWorkFacetsForTests,
useComplianceWorkFacets
} from '../../src/compliance/ui/compliance-work-facets';
import { markRegulatoryDecisionSaved } from '../../src/compliance/ui/decision-revision';
import { filterAndSortRecords } from '../../src/features/search-filter/filter-records';
import { selectWorkFacets } from '../../src/features/search-filter/work-facets';
import { makeSheet } from '../../src/importers/sheet-factory';
import { genericProfile } from '../../src/profiles/generic/generic-profile';
// Work `ISO control` and `ISO theme` (Compliance Engine): a second reading of accepted controls that
// filters Work like the Context Cluster facet, sits before the final `Source: Scan`, and is absent while
// the engine is off.
const gpoKey = `gpo-work-setting:${encodeURIComponent(JSON.stringify(['dummy', 'baseline', 'computer', 'registry:dummy:setting']))}`;
const assignments: 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' },
{ resultProjectionId: 'policy-evaluation-comparison:dummy', sourceKind: 'policy-evaluation', sourceResultId: 'dummy-comparison',
sourceRuleId: 'registry:dummy:setting', policyScope: 'computer' }] },
{ controlId: '5.15', annexAControlId: 'A.5.15', theme: 'Organizational', findings: [
{ resultProjectionId: 'scanner-finding:dummy-b', sourceKind: 'scanner-finding', sourceResultId: 'dummy-b' }] }
]
};
function workSheet() {
const sheet = makeSheet('Dummy', [
['Title', 'Status', 'Source: Scan'],
['Dummy check A', 'Failed', 'Purple Knight'],
['Dummy check B', 'Passed', 'Purple Knight'],
['Dummy GPO setting', 'Failed', 'GPOs'],
['Dummy unmapped check', 'Failed', 'Purple Knight']
], '', 'dummy.csv', { isProjectSheet: true });
const [a, b, gpo] = sheet.analysis.records;
a.findingOccurrenceId = 'dummy-a';
b.findingOccurrenceId = 'dummy-b';
(gpo as { id: string }).id = gpoKey;
return sheet;
}
const options = (statusFilter = 'all', facetFilters = {}) => ({ dataset: null, activeProfile: genericProfile, reviews: {}, search: '',
actionOnly: false, statusFilter, facetFilters, complianceFacets: complianceWorkFacetIndex(assignments) });
describe('Work ISO control and ISO theme facets', () => {
it('offers accepted controls and themes before the final Source: Scan control', () => {
const facets = selectWorkFacets(workSheet(), options());
const ids = facets.map(facet => facet.id);
expect(ids.at(-1)).toBe('provenance:source-scan');
expect(ids.slice(-3, -1)).toEqual([ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID]);
const control = facets.find(facet => facet.id === ISO_CONTROL_FACET_ID)!;
expect(control.label).toBe('ISO control');
expect(control.values.map(value => [value.label, value.count])).toEqual([['A.5.15', 1], ['A.8.20 ยท Networks security', 3]]);
const theme = facets.find(facet => facet.id === ISO_THEME_FACET_ID)!;
expect(theme.values.map(value => [value.value, value.label, value.count])).toEqual([['organizational', 'Organizational', 1], ['technological', 'Technological', 3]]);
});
it('filters Work records by control and theme, matching scanner occurrences and GPO settings', () => {
const sheet = workSheet();
const titles = (filters: object, status = 'all') => filterAndSortRecords(sheet, { ...options(status, filters), sortColumn: '', sortDirection: 'asc' })
.map(record => record.title);
expect(titles({ [ISO_CONTROL_FACET_ID]: { values: ['5.15'] } })).toEqual(['Dummy check B']);
expect(titles({ [ISO_THEME_FACET_ID]: { values: ['technological'] } })).toEqual(['Dummy check A', 'Dummy check B', 'Dummy GPO setting']);
// Other filters still apply: the status filter narrows the ISO selection.
expect(titles({ [ISO_CONTROL_FACET_ID]: { values: ['8.20'] } }, 'deviation')).toEqual(['Dummy check A', 'Dummy GPO setting']);
});
it('counts each control against the other active filters', () => {
const facets = selectWorkFacets(workSheet(), options('deviation'));
expect(facets.find(facet => facet.id === ISO_CONTROL_FACET_ID)!.values.map(value => [value.value, value.count])).toEqual([['8.20', 2]]);
});
it('adds no ISO facet without loaded assignments', () => {
const facets = selectWorkFacets(workSheet(), { ...options(), complianceFacets: undefined });
expect(facets.some(facet => facet.id.startsWith('compliance:'))).toBe(false);
});
});
vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() }));
let host: HTMLDivElement;
let root: Root;
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
localStorage.clear();
resetEnginePreferencesForTests();
resetComplianceWorkFacetsForTests();
vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never);
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();
});
function Consumer(): ReactNode {
const index = useComplianceWorkFacets();
return ;
}
function Harness({ withScope }: { withScope: boolean }): ReactNode {
const app = useApp();
return <>
{withScope && <>>}
>;
}
describe('Work ISO facet loading and the engine switch', () => {
it('shares one request for every Work consumer and reloads after a control decision', async () => {
const { loadCanonicalProjectWorkspace } = await import('../../src/features/project-import/load-canonical-project-workspace');
vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ assessmentIds: ['dummy-assessment'], models: [],
dataset: { fileName: 'Dummy', scanTool: 'Generic', sheets: [workSheet()] } } as never);
vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue({} as never);
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments').mockResolvedValue(assignments);
await act(async () => root.render());
await act(async () => { host.querySelector('button')!.click(); await new Promise(resolve => setTimeout(resolve, 0)); });
await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); });
expect(request).toHaveBeenCalledTimes(1);
expect([...host.querySelectorAll('[data-iso-facets]')].map(node => node.textContent)).toEqual(['2,2', '2,2']);
await act(async () => { markRegulatoryDecisionSaved(); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(request).toHaveBeenCalledTimes(2);
});
it('loads nothing and clears an active ISO filter when the Compliance Engine is switched off', async () => {
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments').mockResolvedValue(assignments);
await act(async () => root.render());
await act(async () => host.querySelectorAll('button')[1].click());
expect(JSON.parse(host.querySelector('[data-filters]')!.textContent!)).toEqual({ [ISO_CONTROL_FACET_ID]: { values: ['8.20'] } });
await act(async () => { setEngineEnabled('compliance', false); });
expect(JSON.parse(host.querySelector('[data-filters]')!.textContent!)).toEqual({});
await act(async () => root.render());
expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('none');
expect(request).not.toHaveBeenCalled();
});
});