// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, 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 { resetAssessmentContextRequestsForTests } from '../../src/features/context-layer/use-assessment-context'; import { emptyHeatmapContext } from '../assessment-context/context-heatmap-fixture'; import { SidebarSecurityDomains } from '../../src/features/sidebar/SidebarSecurityDomains'; import { filterAndSortRecords } from '../../src/features/search-filter/filter-records'; import { contextClusterIndex, CONTEXT_CLUSTER_FACET_ID } from '../../src/features/context-layer/context-cluster-facet'; import { BUSINESS_IMPACT_FACET_ID } from '../../src/features/search-filter/business-impact-controls'; import type { ImportedDataset } from '../../src/domain/dataset'; import { selectWorkFacets } from '../../src/features/search-filter/work-facets'; import { DynamicFilterControls } from '../../src/features/search-filter/DynamicFilterControls'; vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() })); vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => Synthetic assessment })); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); delete document.documentElement.dataset.theme; }); it.each(['dark', 'light'])('places four disjoint theme bars below Security Domains, omits unassigned and preserves Core with engines off (%s)', async theme => { document.documentElement.dataset.theme = theme; vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); localStorage.clear(); resetEnginePreferencesForTests(); resetAssessmentContextRequestsForTests(); const sheet = makeSheet('Synthetic', [['Name', 'Status'], ['One', 'Failed'], ['Two', 'Failed'], ['Unknown', 'Failed']], '', 'synthetic.csv', { isProjectSheet: true }); sheet.analysis.records.forEach((record, index) => { record.findingOccurrenceId = `occurrence-${index}`; }); const dataset = { fileName: 'Synthetic client', sheets: [sheet], scanTool: 'Generic' } as ImportedDataset; vi.mocked(useApp).mockReturnValue({ dataset, activeSheet: sheet, activeProfile: genericProfile, reviews: {}, targetProjectId: 'synthetic-client', activeAssessmentIds: ['synthetic-timeslot'], activeScanRunIds: ['synthetic-run'], facetFilters: {}, search: '', statusFilter: 'all', actionOnly: false, setStatusFilter: vi.fn(), setFacetFilter: vi.fn(), setPathFindingIds: vi.fn(), resetFilters: vi.fn(), setView: vi.fn(), setActionOnly: vi.fn(), setActiveSheet: vi.fn() } as unknown as ReturnType); vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue({ observations: [], sections: [] } as never); vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never); const context = emptyHeatmapContext(); const cluster = context.clusters.find(item => item.clusterId === 'adcs-escalation')!; const conditionId = cluster.conditions[0].conditionId; for (let index = 0; index < 2; index++) { context.observations.push({ id: `observation-${index}`, conditionId, occurrenceId: `occurrence-${index}`, resultProjectionId: `result-${index}`, tool: 'purple-knight', sourceRuleId: 'synthetic-rule', ruleText: 'Synthetic', outcome: 'fail', matchedBy: 'rule-id', assertion: 'observed-by-scanner' }); context.findings.push({ occurrenceId: `occurrence-${index}`, resultProjectionId: `result-${index}`, tool: 'purple-knight', sourceRuleId: 'synthetic-rule', observationIds: [], conditionIds: [conditionId], clusterIds: ['adcs-escalation'], declaredReferenceIds: [], cisControlAreaIds: [] }); } // A category attachment without published deviation evidence must not inflate the Business Impact filter. context.findings.push({ occurrenceId: 'occurrence-2', resultProjectionId: 'result-2', tool: 'purple-knight', sourceRuleId: 'unmapped', observationIds: [], conditionIds: [], clusterIds: ['adcs-escalation'], declaredReferenceIds: [], cisControlAreaIds: [] }); const request = vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(context); const before = JSON.stringify(dataset); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); const openWork = vi.fn(); try { await act(async () => root.render()); const chart = host.querySelector('.client-domains .client-business-impact')!; expect(chart).toBeTruthy(); expect(chart.querySelector('h3')?.textContent).toBe('Possible Business Impact'); // User decision 2026-10-04: four disjoint themes in a four-column row; financial and compliance are never bars. expect(chart.querySelectorAll('[role="listitem"]')).toHaveLength(4); expect([...chart.querySelectorAll('.client-business-impact-label')].map(item => item.textContent)).toEqual( ['Control & trust', 'Information & data', 'Operational continuity', 'Resilience & recovery']); expect([...chart.querySelectorAll('strong')].map(item => item.textContent)).toEqual(['2', '0', '0', '0']); expect(chart.textContent).not.toMatch(/Financial|Compliance/); expect(chart.textContent).not.toMatch(/unassigned|not assigned|nicht eindeutig/i); expect(chart.querySelector('[data-theme="control"] .client-business-impact-label')?.textContent).toBe('Control & trust'); await act(async () => (chart.querySelector('[data-theme="control"] button') as HTMLButtonElement).click()); const app = vi.mocked(useApp).mock.results.at(-1)!.value as ReturnType; const selected = { values: ['control'] }; expect(app.setFacetFilter).toHaveBeenLastCalledWith(BUSINESS_IMPACT_FACET_ID, selected); expect(app.setFacetFilter).not.toHaveBeenCalledWith(CONTEXT_CLUSTER_FACET_ID, expect.anything()); expect(app.setStatusFilter).toHaveBeenLastCalledWith('deviation'); expect(openWork).toHaveBeenCalledTimes(1); const matching = filterAndSortRecords(sheet, { dataset, activeProfile: genericProfile, reviews: {}, search: '', actionOnly: false, sortColumn: '', sortDirection: 'asc', statusFilter: 'deviation', facetFilters: { [BUSINESS_IMPACT_FACET_ID]: selected }, contextClusters: contextClusterIndex(context) }); expect(matching.map(record => record.findingOccurrenceId)).toEqual(['occurrence-0', 'occurrence-1']); const core = () => { const clone = host.cloneNode(true) as HTMLElement; clone.querySelector('.client-business-impact')?.remove(); clone.querySelector('.context-heatmap')?.remove(); clone.querySelector('.client-report-actions')?.remove(); clone.querySelector('.client-report-error')?.remove(); return clone.querySelector('.client-overview-main')?.innerHTML; }; const baseline = core(); await act(async () => setEngineEnabled('report', false)); expect(host.querySelector('.client-business-impact')).toBeNull(); expect(core()).toBe(baseline); await act(async () => setEngineEnabled('report', true)); expect(host.querySelectorAll('.client-business-impact-column')).toHaveLength(4); expect(request).toHaveBeenCalledTimes(1); await act(async () => setEngineEnabled('context', false)); expect(host.querySelector('.client-business-impact')).toBeNull(); expect(core()).toBe(baseline); expect(JSON.stringify(dataset)).toBe(before); await act(async () => setEngineEnabled('context', true)); await act(async () => root.render()); const sidebarTheme = host.querySelector('.sidebar-business-impact button') as HTMLButtonElement; expect(host.querySelectorAll('.sidebar-business-impact button')).toHaveLength(4); expect(sidebarTheme.textContent).toBe('Control & trust2'); await act(async () => sidebarTheme.click()); expect(app.setFacetFilter).toHaveBeenLastCalledWith(BUSINESS_IMPACT_FACET_ID, selected); app.facetFilters = { [BUSINESS_IMPACT_FACET_ID]: selected }; const workFilters = () => <>; await act(async () => root.render(workFilters())); expect(host.querySelector('select[aria-label="Business Impact"]')?.value).toBe('control'); expect(host.querySelector('select[aria-label="Context Cluster"]')?.value).toBe(''); const contextSelection = { values: ['adcs-escalation'] }; app.facetFilters = { [BUSINESS_IMPACT_FACET_ID]: selected, [CONTEXT_CLUSTER_FACET_ID]: contextSelection }; await act(async () => root.render(workFilters())); expect(host.querySelector('select[aria-label="Context Cluster"]')?.value).toBe('adcs-escalation'); const activeTheme = host.querySelector('.sidebar-business-impact button') as HTMLButtonElement; expect(activeTheme.getAttribute('aria-pressed')).toBe('true'); await act(async () => activeTheme.click()); expect(app.setFacetFilter).toHaveBeenLastCalledWith(BUSINESS_IMPACT_FACET_ID, null); expect(app.facetFilters[CONTEXT_CLUSTER_FACET_ID]).toBe(contextSelection); expect(app.setFacetFilter).not.toHaveBeenCalledWith(CONTEXT_CLUSTER_FACET_ID, expect.anything()); await act(async () => root.render()); await act(async () => (host.querySelector('[data-theme="control"] button') as HTMLButtonElement).click()); expect(app.setFacetFilter).toHaveBeenCalledWith(CONTEXT_CLUSTER_FACET_ID, contextSelection); expect(app.setFacetFilter).toHaveBeenLastCalledWith(BUSINESS_IMPACT_FACET_ID, selected); await act(async () => root.render()); await act(async () => setEngineEnabled('report', false)); expect(host.querySelector('.sidebar-business-impact')).toBeNull(); } finally { act(() => root.unmount()); host.remove(); } });