// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, expect, it, vi } from 'vitest'; import { useApp } from '../../src/app/AppProvider'; import type { ImportedDataset } from '../../src/domain/dataset'; import { POLICY_EVALUATION_ENGINE, type PolicyEvaluationResult, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation'; import type { UnifiedResultProjection } from '../../src/domain/unified-result'; import { DatasetViewer } from '../../src/features/dataset-viewer/DatasetViewer'; import { appendPolicyObservationsToDataset } from '../../src/features/project-import/policy-observations-to-dataset'; import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records'; import { makeSheet } from '../../src/importers/sheet-factory'; vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; }); vi.mock('../../src/features/search-filter/use-filtered-records', () => ({ useFilteredRecords: vi.fn() })); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); Reflect.deleteProperty(HTMLElement.prototype, 'scrollIntoView'); }); /** Mixed project sheet as the canonical projection returns it: the CIS record knows its run's section titles. */ function scannerDataset(): ImportedDataset { const sheet = makeSheet('Synthetic · All Records', [ ['Source Rule ID', 'Title', 'Status', 'Source: Scan'], ['2.2.2', '2.2.2 Ensure a synthetic user right is restricted', 'Fail', 'CIS-CAT Pro · 2026-01-01T00:00:00.000Z'] ], '', 'Synthetic', { isProjectSheet: true }); const record = sheet.analysis.records[0]; record.scanRunId = 'run-synthetic'; record.benchmarkSectionTitles = new Map([['2', 'Synthetic Local Policies'], ['2.2', 'Synthetic User Rights Assignment']]); return { sheets: [sheet], rawText: '', profileId: 'generic', scanTool: 'Multiple Tools' } as unknown as ImportedDataset; } /** A timeslot that also holds GPO results: Work rebuilds the project sheet to append them. */ function withPolicyObservations(dataset: ImportedDataset): ImportedDataset { const observation = { sourceKind: 'policy-observation', sourceResultId: 'synthetic-observation', projectId: 'synthetic', assessmentId: 'slot', policyBackupInstanceId: 'backup-1', title: 'Synthetic setting', importedAt: '2026-01-01T00:00:00Z', policyScope: 'computer', policyDeliveryMechanism: 'registry-policy', policyIdentityStatus: 'resolved', policyIdentityKey: 'synthetic-setting', policyValues: [{ kind: 'registry-policy', operation: 'set-value', key: 'Software\\Policies\\Synthetic', valueName: 'Mode', typeCode: 4, data: { kind: 'uint32', value: 0 } }] } as unknown as UnifiedResultProjection; const comparison = { id: 'synthetic-comparison', observationIds: [observation.sourceResultId], baselineId: 'baseline', baselineLabel: 'Synthetic baseline', identity: { id: 'setting-synthetic', scope: 'computer', target: { kind: 'registry-value', key: 'Synthetic', valueName: 'Mode' } }, baselineState: 'explicit-recommendation', customerState: 'configured', classification: 'confirmed_configured_deviation', diagnosticCodes: [], comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'different', baseline: { kind: 'integer', value: '1' }, actual: { kind: 'integer', value: '0' }, diagnosticCodes: [] } } as unknown as PolicyEvaluationResult; const evaluation = { run: { id: 'evaluation', projectId: 'synthetic', aggregationRunId: 'aggregation', completedAt: '2026-01-01T00:00:00Z', evaluator: POLICY_EVALUATION_ENGINE }, results: [comparison] } as StoredPolicyEvaluation; return appendPolicyObservationsToDataset(dataset, 'Synthetic', [observation], [evaluation])!; } // User correction 2026-09-26: Work headings name the CIS benchmark section instead of the rule number; // the exact number stays visible as Source Rule ID in the report. Regression 2026-09-26: with GPO results // in the timeslot, Work rebuilt the project sheet and dropped the titles, so the headings kept the number. it.each([ ['without GPO results', (dataset: ImportedDataset) => dataset], ['with GPO results appended to the project sheet', withPolicyObservations] ])('opens a CIS finding in mixed Work %s with the section title in the list and report headings', async (_case, project) => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { value: vi.fn(), configurable: true }); const dataset = project(scannerDataset()); const sheet = dataset.sheets[0]; const record = sheet.analysis.records.find(item => item.fields.some(field => field.label === 'Source Rule ID' && field.value === '2.2.2'))!; expect(record.scanRunId).toBe('run-synthetic'); const state = { dataset, activeSheet: sheet, view: 'records', search: '', titleColumn: 'Title', selectedRecordId: record.id, reviews: {}, facetFilters: {}, statusFilter: 'all', actionOnly: false, setView: vi.fn(), setSelectedRecord: vi.fn(), updateReview: vi.fn() } as unknown as ReturnType; vi.mocked(useApp).mockReturnValue(state); vi.mocked(useFilteredRecords).mockReturnValue([record]); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); const heading = 'Synthetic User Rights Assignment: Ensure a synthetic user right is restricted'; expect(container.querySelector('.finding-reader h2')?.textContent).toBe(heading); expect(container.textContent!.split(heading).length).toBeGreaterThan(2); expect(container.textContent).not.toContain('2.2.2 Ensure'); expect(container.querySelector('[data-source-rule-id] code')?.textContent).toBe('2.2.2'); expect([...container.querySelectorAll('.finding-reader [data-derived-category="cis"]')].map(badge => badge.getAttribute('title'))) .toEqual(['Synthetic Local Policies', 'Synthetic User Rights Assignment']); expect(record.fields.find(field => field.label === 'Title')?.value).toBe('2.2.2 Ensure a synthetic user right is restricted'); } finally { await act(async () => root.unmount()); container.remove(); } });