// @vitest-environment jsdom import { writeFileSync } from 'node:fs'; import { syntheticHeatmapContext } from '../assessment-context/context-heatmap-fixture'; import { resetAssessmentContextRequestsForTests } from '../../src/features/context-layer/use-assessment-context'; import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, expect, it, vi } from 'vitest'; import { useApp } from '../../src/app/AppProvider'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel'; import { makeSheet } from '../../src/importers/sheet-factory'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; import type { EnvironmentSnapshot } from '../../src/domain/environment-context'; vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() })); vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => Synthetic timeslot })); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); }); it.each(['dark', 'light'])('restores the previous Client Info layout, results, Core counts and heatmap drilldown in %s', async theme => { document.documentElement.dataset.theme = theme; vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); localStorage.clear(); resetEnginePreferencesForTests(); setEngineEnabled('context', false); const sheet = makeSheet('Synthetic', [['Name', 'Status'], ['Control', 'Failed']], '', 'synthetic.csv', { isProjectSheet: true }); vi.mocked(useApp).mockReturnValue({ dataset: { fileName: 'Synthetic client', sheets: [sheet], scanTool: 'Generic' }, activeSheet: sheet, activeProfile: genericProfile, reviews: {}, targetProjectId: 'synthetic-client', activeAssessmentIds: ['slot'], activeScanRunIds: ['run'], facetFilters: {}, 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); const tool = { id: 'synthetic', name: 'Synthetic scanner' }; const facts: Array<[string, string | number]> = [ ['domain.fqdn', 'synthetic.example'], ['domain.netbios_name', 'SYNTHETIC'], ['forest.name', 'synthetic.forest'], ['domain.count', 2], ['domain.controller_count', 3], ['domain.functional_level', 'Domain level'], ['forest.functional_level', 'Forest level'], ['forest.schema_version', 'Windows Server 2019'], ['domain.created_at', '2001-03-08'], ['entra.tenant_name', 'Synthetic tenant'] ]; const snapshot: EnvironmentSnapshot = { version: 1, projectId: 'synthetic-client', scanRunIds: ['run'], generatedAt: '2026-10-02T12:00:00Z', coverage: { observed: 10, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 }, observations: facts.map(([property, value]) => ({ version: 1, id: property, scanRunId: 'run', importFileId: 'file', tool, subject: { type: 'domain' }, property, label: property, originalValue: value, normalizedValue: value, valueType: typeof value === 'number' ? 'number' : 'string', state: 'observed', source: { rowIndex: 0 }, mappingVersion: '1', attributes: {} })), sections: [{ version: 1, id: 'native-domain', scanRunId: 'run', importFileId: 'file', tool, sectionKey: 'sectionDomainInformation', label: 'Domain source', ordinal: 1, state: 'observed', tables: [{ name: 'Domain source', headers: ['Original evidence'], rows: [['Synthetic retained source']] }], source: { rowIndex: 0 }, extractorVersion: '1', attributes: {} }] }; vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(snapshot); vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never); resetAssessmentContextRequestsForTests(); vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(syntheticHeatmapContext()); const onOpenWork = vi.fn(); const original = JSON.stringify(useApp().dataset); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); try { await act(async () => root.render()); const left = host.querySelector('.client-posture')!; const overview = host.querySelector('.client-overview-main')!; expect(overview.children).toHaveLength(2); const summary = overview.children[0]; expect(summary.classList.contains('client-overview-summary')).toBe(true); for (const selector of ['.client-overall', '.client-domains', '.client-priority-findings']) { expect(summary.querySelector(selector)).not.toBeNull(); } expect(overview.children[1].classList.contains('client-domain-information')).toBe(true); expect(summary.querySelector('.client-domain-information')).toBeNull(); expect(host.querySelector('.client-analysis-column')).toBeNull(); expect(host.querySelector('.client-priority-summary')).toBeNull(); const right = host.querySelector('.client-priority')!; // Data belongs to the summary tile, not the shell's separately painted sidebar. expect(right.matches('aside')).toBe(false); expect(left.querySelector('.client-overall')).not.toBeNull(); expect(left.querySelectorAll('.client-domains li')).toHaveLength(6); expect(right.querySelectorAll('.client-data-fact')).toHaveLength(6); expect(host.querySelectorAll('.client-environment-grid > section')).toHaveLength(6); const cloud = host.querySelector('[aria-label="Cloud Information"]')!; expect([...cloud.querySelectorAll('dd')].map(item => item.textContent)).toEqual(Array(4).fill('Not reported')); expect(host.querySelector('.client-environment-grid')?.parentElement).toBe(overview.parentElement); expect(host.querySelector('.client-risk-register')?.parentElement).toBe(overview.parentElement); const coreCounts = [...host.querySelectorAll('.client-domain-score, .client-domains b, .client-priority-context, .client-overall-ring, .client-overall-coverage')].map(el => el.textContent); await act(async () => setEngineEnabled('context', true)); expect(host.querySelectorAll('.context-heatmap__cell')).toHaveLength(22); expect(host.querySelector('.context-heatmap')?.parentElement).toBe(overview.parentElement); expect(left.querySelector('.context-heatmap')).toBeNull(); expect(right.querySelector('.context-heatmap')).toBeNull(); expect([...host.querySelectorAll('.client-domain-score, .client-domains b, .client-priority-context, .client-overall-ring, .client-overall-coverage')].map(el => el.textContent)).toEqual(coreCounts); if (process.env.ASSESSOR_LAYOUT_PREVIEW && theme === 'dark') { writeFileSync('tests/client-info-layout-preview.html', `Synthetic restored Client Info

Synthetic restored layout preview ยท no client data

${host.innerHTML}
`); } const domain = host.querySelector('.client-domain-information')!; for (const value of ['SYNTHETIC', 'Windows Server 2019', '2001-03-08', 'Synthetic tenant']) expect(domain.textContent).toContain(value); expect([...host.querySelectorAll('h3')].some(heading => heading.textContent === 'Directory')).toBe(false); const button = domain.querySelector('[aria-label="Open Domain Information results"]')!; expect(button).not.toBeNull(); await act(async () => button.click()); const popup = document.querySelector('[role="dialog"]')!; expect(popup).not.toBeNull(); expect([...popup.querySelectorAll('.client-env-facts dt')].map(item => item.textContent)).toEqual([ 'Domain', 'NetBIOS name', 'Forest', 'Domains', 'Domain controllers', 'Domain functional level', 'Forest functional level', 'DC OS', 'Schema version', 'Domain created', 'Entra ID' ]); for (const [, value] of facts) expect(popup.textContent).toContain(String(value)); expect(popup.textContent).toContain('Synthetic retained source'); expect(useApp().setFacetFilter).not.toHaveBeenCalled(); await act(async () => popup.querySelector('[aria-label="Close Domain Information window"]')?.click()); const cluster = host.querySelector('[data-cluster="cloud-privileged-access-exposure"]')!; await act(async () => cluster.click()); expect(useApp().setFacetFilter).toHaveBeenCalledWith('context:cluster', { values: ['cloud-privileged-access-exposure'] }); expect(onOpenWork).toHaveBeenCalledOnce(); expect(JSON.stringify(useApp().dataset)).toBe(original); } finally { act(() => root.unmount()); } });