// @vitest-environment jsdom import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel'; import { useApp } from '../../src/app/AppProvider'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { makeSheet } from '../../src/importers/sheet-factory'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; import { resetTimeslotComparisonRequestsForTests } from '../../src/features/comparison/use-timeslot-comparison'; import type { Assessment, ImportFileRecord, ScanRun } from '../../src/domain/canonical-model'; import type { ImportedDataset } from '../../src/domain/dataset'; import type { TimeslotComparisonReport } from '../../src/comparison/timeslot-comparison-report'; vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; }); vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => Synthetic timeslot })); // Compare tab → Work (mockup B): a selected change opens its exact finding in Work, in the open timeslot B or, // after opening it, in timeslot A. Synthetic data only. 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: 'synthetic-client', 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 report = { version: '1.0.0', projectId: 'synthetic-client', baselineRunIds: ['run-a'], targetRunIds: ['run-b'], sources: [{ sourceId: 'cis-cat', state: 'compared', reasons: [] }], changes: [{ sourceId: 'cis-cat', change: 'resolved', side: 'baseline', recordIds: ['a-1'], counterpartIds: ['b-1'], title: 'Synthetic rule', ruleId: '1.1', reasons: [] }], summary: { resolved: 1, unchanged: 0, 'value-changed': 0, unverified: 0, 'not-comparable': 0, 'not-applicable': 0, new: 0, regressed: 0, targetNotComparable: 0 }, score: { sources: [], excludedSources: [] }, scanner: {} } as unknown as TimeslotComparisonReport; function timeslotDataset(occurrence: string) { const sheet = makeSheet('Synthetic', [['Title', 'Status'], ['Other rule', 'Failed'], ['Synthetic rule', 'Passed']], '', 'synthetic.csv', { isProjectSheet: true }); sheet.analysis.records[0].findingOccurrenceId = `${occurrence}-other`; sheet.analysis.records[1].findingOccurrenceId = occurrence; return { sheet, dataset: { fileName: 'Synthetic client', sheets: [sheet], scanTool: 'Generic' } as ImportedDataset }; } let host: HTMLDivElement; let root: Root; let app: Record; function mockApp(occurrence: string, assessmentId: string) { const { sheet, dataset } = timeslotDataset(occurrence); app = { ...app, dataset, activeSheet: sheet, activeAssessmentIds: [assessmentId] }; vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType); return sheet; } beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); localStorage.clear(); resetEnginePreferencesForTests(); resetTimeslotComparisonRequestsForTests(); for (const engine of ['context', 'compliance', 'remediation', 'report'] as const) setEngineEnabled(engine, false); app = { activeProfile: genericProfile, reviews: {}, targetProjectId: 'synthetic-client', activeScanRunIds: [], facetFilters: {}, search: '', statusFilter: 'all', actionOnly: false, backendStatus: 'connected', status: '', setStatusFilter: vi.fn(), setFacetFilter: vi.fn(), setPathFindingIds: vi.fn(), resetFilters: vi.fn(), setView: vi.fn(), setActionOnly: vi.fn(), setActiveSheet: vi.fn(), setSelectedRecord: vi.fn(), openProject: vi.fn(async () => true) }; vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue({ observations: [], sections: [] } as never); vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never); 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, 'getTimeslotComparison').mockResolvedValue(report); vi.spyOn(AssessorApiClient.prototype, 'getTimeslotDomainComparison').mockResolvedValue({ version: '1.0.0', sources: ['cis-cat'], domains: [] } 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(); }); const settle = () => act(async () => { for (let index = 0; index < 8; index++) await Promise.resolve(); }); const button = (label: string) => [...host.querySelectorAll('button')].find(item => item.textContent?.trim() === label); it('opens the selected change in Work for timeslot B and, after opening it, for timeslot A', async () => { const sheetB = mockApp('b-1', 'assessment-b'); const openWork = vi.fn(); await act(async () => root.render()); await act(async () => button('Compare')!.click()); await settle(); expect(host.querySelector('.comparison-item.is-selected')?.textContent).toContain('Synthetic rule'); await act(async () => button('Open in Work')!.click()); // The resolved finding is Passed in B: Work opens exactly that record in the Reports view, whatever its status. expect(app.setStatusFilter).toHaveBeenLastCalledWith('all'); expect(app.setPathFindingIds).toHaveBeenLastCalledWith([sheetB.analysis.records[1].id]); expect(app.setSelectedRecord).toHaveBeenLastCalledWith(sheetB.analysis.records[1].id); expect(app.setView).toHaveBeenLastCalledWith('records'); expect(openWork).toHaveBeenCalledTimes(1); await act(async () => button('Open in timeslot A')!.click()); expect(app.openProject).toHaveBeenCalledWith('synthetic-client', ['run-a'], ['assessment-a']); expect(openWork).toHaveBeenCalledTimes(1); // Once timeslot A is loaded, its record of the same finding opens in Work. const sheetA = mockApp('a-1', 'assessment-a'); await act(async () => root.render()); await settle(); expect(app.setPathFindingIds).toHaveBeenLastCalledWith([sheetA.analysis.records[1].id]); expect(openWork).toHaveBeenCalledTimes(2); });