// @vitest-environment jsdom import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import type { Assessment, ImportFileRecord, ScanRun } from '../../src/domain/canonical-model'; import type { FindingRecord } from '../../src/domain/finding'; import type { TimeslotComparisonReport } from '../../src/comparison/timeslot-comparison-report'; import { ComparisonTab } from '../../src/features/comparison/ComparisonTab'; import { FindingComparison } from '../../src/features/comparison/FindingComparison'; import { resetTimeslotComparisonRequestsForTests } from '../../src/features/comparison/use-timeslot-comparison'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; // Finding Reader `Compare` section: present once the Compare tab loaded a comparison of the open timeslot B, // with both states and the GPO values side by side; absent for an unrelated record or with the engine off. vi.mock('../../src/app/AppProvider', () => ({ useOptionalApp: () => ({ targetProjectId: 'dummy-project', activeAssessmentIds: ['assessment-b'] }) })); const at = { a: '2026-01-10T10:00:00.000Z', b: '2026-02-10T10:00:00.000Z' }; const assessment = (id: string, effectiveAt: string): Assessment => ({ id, projectId: 'dummy-project', name: id, createdAt: effectiveAt, updatedAt: effectiveAt, effectiveAt }); const run = (id: string, assessmentId: string, time: string): ScanRun => ({ id, assessmentId, sequence: 1, label: id, tool: { id: 'cis-cat', name: 'CIS-CAT Pro' }, importedAt: time }); const file = (runId: string, time: string) => ({ id: `file-${runId}`, scanRunId: runId, fileName: 'synthetic.html', format: 'HTML', sizeBytes: 1, importedAt: time, lastModifiedAt: time, detectedTool: { id: 'cis-cat', name: 'CIS-CAT Pro' } }) as unknown as ImportFileRecord; const gpoRecordId = (identity: string) => `gpo-work-setting:${encodeURIComponent(JSON.stringify(['dummy-project', 'aggregation-b', 'computer', identity]))}`; const record = (id: string, findingOccurrenceId?: string) => ({ id, fields: [], ...(findingOccurrenceId ? { findingOccurrenceId } : {}) }) as unknown as FindingRecord; const report = { version: '1.0.0', projectId: 'dummy-project', baselineRunIds: ['run-a'], targetRunIds: ['run-b'], sources: [], changes: [ { sourceId: 'cis-cat', change: 'resolved', side: 'baseline', recordIds: ['a-1'], counterpartIds: ['b-1'], reasons: [] }, { sourceId: 'group-policy', change: 'value-changed', side: 'baseline', recordIds: ['ga-1'], counterpartIds: ['gb-1'], identityKey: JSON.stringify(['computer', 'MinimumPasswordLength']), reasons: ['same-setting-current-deviation'], values: { baseline: ['{"kind":"number","value":7}'], target: ['{"kind":"number","value":10}'] } } ], summary: { resolved: 1, unchanged: 0, 'value-changed': 1, unverified: 0, 'not-comparable': 0, 'not-applicable': 0, new: 0, regressed: 0, targetNotComparable: 0 }, score: { sources: [], excludedSources: [] }, scanner: {} } as unknown as TimeslotComparisonReport; let host: HTMLDivElement; let root: Root; beforeEach(() => { localStorage.clear(); resetEnginePreferencesForTests(); resetTimeslotComparisonRequestsForTests(); vi.spyOn(AssessorApiClient.prototype, 'listProjectImportFiles').mockResolvedValue([file('run-a', at.a), file('run-b', at.b)]); vi.spyOn(AssessorApiClient.prototype, 'listProjectAssessmentHistory').mockResolvedValue({ assessments: [assessment('assessment-a', at.a), assessment('assessment-b', at.b)], scanRuns: [run('run-a', 'assessment-a', at.a), run('run-b', 'assessment-b', at.b)] } as never); vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never); vi.spyOn(AssessorApiClient.prototype, 'getTimeslotComparison').mockResolvedValue(report); host = document.createElement('div'); document.body.append(host); root = createRoot(host); }); afterEach(async () => { await act(async () => root.unmount()); host.remove(); vi.restoreAllMocks(); resetEnginePreferencesForTests(); }); const settle = () => act(async () => { for (let index = 0; index < 5; index++) await Promise.resolve(); }); it('shows the comparison of a finding and of a GPO setting after the Compare tab loaded it', async () => { await act(async () => root.render(<>
)); await settle(); const section = (id: string) => host.querySelector(`[data-testid="${id}"] .finding-comparison`); expect(section('scanner')?.hasAttribute('open')).toBe(false); expect(section('scanner')?.textContent).toContain('Change: resolved.'); expect(section('scanner')?.querySelector('tbody tr')?.textContent).toBe('Verdict✕ Deviation✓ Passed'); // Internal evidence codes are never shown; only reasons with a readable text. expect(section('gpo')?.textContent).not.toContain('same-setting-current-deviation'); const gpo = section('gpo')!; expect(gpo.textContent).toContain('Change: value changed, verdict unchanged.'); const valueRow = [...gpo.querySelectorAll('tbody tr')].find(row => row.textContent?.startsWith('Configured value')); expect(valueRow?.querySelectorAll('td')[1].textContent).toContain('7'); expect(valueRow?.querySelectorAll('td')[2].textContent).toContain('10'); expect(section('other')).toBeNull(); }); it('renders nothing while the Comparison Engine is off', async () => { setEngineEnabled('comparison', false); await act(async () => root.render(<>
)); await settle(); expect(host.querySelector('[data-testid="scanner"] .finding-comparison')).toBeNull(); });