// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { useApp } from '../../src/app/AppProvider'; import { installTableRowMarking } from '../../src/app/table-row-marking'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import type { WorkPackagesResponse } from '../../src/api/assessor-api-contract'; import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel'; import { makeSheet } from '../../src/importers/sheet-factory'; import { buildWorkPackages, scannerFindingKey } from '../../src/remediation/remediation-engine'; import { workRecordKey } from '../../src/remediation/work-package-index'; import { resetWorkPackageRequestsForTests } from '../../src/remediation/use-work-packages'; import { clientFactsSnapshot, clientFactsState } from '../fixtures/client-info-facts'; vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; }); vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => Synthetic assessment selector })); vi.mock('../../src/features/context-layer/use-context-cluster-index', () => ({ useContextClusterIndex: () => undefined })); function fixture(projectId = 'synthetic-project', assessmentId = 'synthetic-assessment') { const sheet = makeSheet('Synthetic', [['Name', 'Status', 'Severity', 'Source Rule ID', 'Source: Scan', 'Description', 'Remediation'], ['Synthetic admin A', 'Failed', 'High', 'MT.1014', 'Maester', 'Synthetic explanation A', 'Synthetic exact remediation A'], ['Synthetic admin B', 'Failed', 'High', 'CISA.MS.AAD.3.6', 'Maester', 'Synthetic explanation B', 'Synthetic exact remediation B'], ['Synthetic external control', 'Failed', 'Medium', 'MT.1056', 'Maester', 'Synthetic external explanation', 'Synthetic external remediation'], ['Synthetic unassigned control', 'Failed', 'Low', 'SYNTHETIC.UNASSIGNED', 'Maester', 'Synthetic unassigned explanation', 'Synthetic unassigned remediation'] ], '', 'synthetic.csv', { isProjectSheet: true }); sheet.analysis.records.forEach((record, index) => { record.findingOccurrenceId = `${projectId}-occ-${index}`; }); const findings = sheet.analysis.records.map(record => ({ key: scannerFindingKey(record.findingOccurrenceId!), occurrenceId: record.findingOccurrenceId, tool: 'maester', ruleId: record.fields[3].value, title: record.title, outcome: 'fail' })); const result = buildWorkPackages({ projectId, assessmentIds: [assessmentId], scanRunIds: ['synthetic-run'], findings, context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1, clusters: [{ clusterId: 'cloud-privileged-access-exposure', label: { en: 'Cloud Privileged Access Exposure' } }, { clusterId: 'cloud-external-access-exposure', label: { en: 'Cloud External Access Exposure' } }], findings: findings.slice(0, 3).map((finding, index) => ({ occurrenceId: finding.occurrenceId!, clusterIds: [index < 2 ? 'cloud-privileged-access-exposure' : 'cloud-external-access-exposure'], conditionIds: [] })), settings: [] } }); const app = { ...clientFactsState(), targetProjectId: projectId, activeAssessmentIds: [assessmentId], activeSheet: sheet, dataset: { fileName: `Synthetic ${projectId}`, sheets: [sheet], scanTool: 'Generic' }, search: 'retained Work search', statusFilter: 'passed', facetFilters: { severity: { values: ['High'] } }, setFacetFilter: vi.fn() }; return { app, result }; } function setup() { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); resetWorkPackageRequestsForTests(); setEngineEnabled('context', false); vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(clientFactsSnapshot); vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never); return vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext'); } afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); localStorage.clear(); resetEnginePreferencesForTests(); resetWorkPackageRequestsForTests(); }); describe.each(['light', 'dark'])('Client Info Work Packages (%s)', theme => { it('opens packages and original finding details without marking any Work Packages table row blue', async () => { setup(); document.documentElement.dataset.theme = theme; const { app, result } = fixture(); vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType); vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result }); const original = JSON.stringify(app.dataset); const openWork = vi.fn(); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); const stopMarking = installTableRowMarking(document); try { await act(async () => root.render()); await act(async () => [...host.querySelectorAll('[role="tab"]')].find(tab => tab.textContent === 'Work Packages')!.click()); const panel = host.querySelector('[data-group-scroll="client-info-work-packages"]')!; const packageButton = panel.querySelector('[data-work-package-select]')!; await act(async () => packageButton.click()); expect(packageButton.getAttribute('aria-expanded')).toBe('true'); const finding = panel.querySelector('.client-security-finding-toggle')!; await act(async () => finding.click()); expect(finding.getAttribute('aria-expanded')).toBe('true'); const details = panel.querySelector('.client-security-finding-detail')!; const record = app.activeSheet.analysis.records.find(record => record.title === finding.querySelector('strong')!.textContent)!; expect(details.textContent).toContain(record.fields[5].value); expect(details.textContent).toContain(record.fields[6].value); await act(async () => details.click()); expect(panel.querySelector('[data-row-marked="true"]')).toBeNull(); await act(async () => panel.querySelector('[data-work-package-unassigned-summary]')!.click()); expect(panel.querySelector('[data-work-package-unassigned-details]')?.textContent).toContain('Synthetic unassigned control'); expect(panel.querySelector('[data-row-marked="true"]')).toBeNull(); // Other tables retain the established reading marker. const table = document.createElement('table'); table.innerHTML = 'Other table'; host.append(table); table.querySelector('td')!.click(); expect(table.querySelector('tr')!.getAttribute('data-row-marked')).toBe('true'); expect(openWork).not.toHaveBeenCalled(); expect(JSON.stringify(app.dataset)).toBe(original); } finally { stopMarking(); await act(async () => root.unmount()); host.remove(); } }); it('keeps Security and Work Packages unchanged after all Work/Graph filters and the selected source sheet change', async () => { setup(); document.documentElement.dataset.theme = theme; const { app, result } = fixture(); vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType); const packages = vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result }); const openWork = vi.fn(); const original = JSON.stringify(app.dataset); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); const render = () => root.render(); const tab = (name: string) => [...host.querySelectorAll('[role="tab"]')].find(button => button.textContent === name)!; try { await act(async () => render()); await act(async () => tab('Security').click()); const security = host.querySelector('[data-group-scroll="client-info-security"]')!; await act(async () => security.querySelector('.client-security-group-toggle')!.click()); expect(security.querySelectorAll('.client-security-finding-toggle')).toHaveLength(4); expect(security.querySelector('.client-security-remediation strong')?.textContent).toBe('4'); const securityContent = security.textContent; await act(async () => tab('Work Packages').click()); const wp = host.querySelector('[data-group-scroll="client-info-work-packages"]')!; await act(async () => wp.querySelector('[data-work-package-select]')!.click()); const wpContent = wp.textContent; const subset = makeSheet('Selected source subset', [['Name', 'Status'], ['Only a source subset', 'Passed']], '', 'subset.csv'); const changed = { ...app, activeSheet: subset, activeProfile: { ...app.activeProfile, matchesStatus: () => false }, search: 'no matching title', statusFilter: 'passed', actionOnly: true, pathFindingIds: ['absent graph member'], facetFilters: { 'provenance:source-scan': { values: ['Absent scanner'] }, severity: { values: ['Critical'] }, 'context:cluster': { values: ['absent cluster'] }, 'report:business-impact': { values: ['absent impact'] }, 'mitre:technique': { values: ['T0000'] }, arbitrary: { values: ['absent'] } } }; vi.mocked(useApp).mockReturnValue(changed as unknown as ReturnType); await act(async () => render()); expect(wp.textContent).toBe(wpContent); expect(packages).toHaveBeenCalledTimes(1); await act(async () => tab('Security').click()); expect(security.textContent).toBe(securityContent); expect(security.querySelectorAll('.client-security-finding-toggle')).toHaveLength(4); expect(openWork).not.toHaveBeenCalled(); expect(app.setFacetFilter).not.toHaveBeenCalled(); expect(JSON.stringify(app.dataset)).toBe(original); expect(changed.facetFilters['provenance:source-scan'].values).toEqual(['Absent scanner']); } finally { await act(async () => root.unmount()); host.remove(); } }); it('opens the third tab lazily, retains filters and expands exact package findings and their original details locally', async () => { const context = setup(); document.documentElement.dataset.theme = theme; const { app, result } = fixture(); vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType); const packages = vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result }); const original = JSON.stringify({ app, result }); const openWork = vi.fn(); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); try { await act(async () => root.render()); const tabs = [...host.querySelectorAll('[role="tab"]')]; expect(tabs.map(tab => tab.textContent)).toEqual(['Overview', 'Security', 'Work Packages']); expect(packages).not.toHaveBeenCalled(); await act(async () => tabs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))); const panel = host.querySelector('[data-group-scroll="client-info-work-packages"]')!; expect(panel.hidden).toBe(false); expect(document.activeElement).toBe(tabs[2]); expect(packages).toHaveBeenCalledWith('synthetic-project', ['synthetic-assessment'], ['synthetic-run']); expect(context).not.toHaveBeenCalled(); expect(panel.querySelector('.floating-data-window')).toBeNull(); expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(result.packages.length); expect(panel.querySelector('[aria-label="Package totals"]')?.textContent).toContain('Findings4'); const filter = panel.querySelector('[aria-label="Filter Package"]')!; const selectedPackage = result.packages[0]; await act(async () => { filter.value = selectedPackage.id; filter.dispatchEvent(new Event('change', { bubbles: true })); }); expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(1); await act(async () => panel.querySelector('[data-sort-column="findings"]')!.click()); await act(async () => tabs[0].click()); await act(async () => tabs[2].click()); expect(filter.value).toBe(selectedPackage.id); expect(packages).toHaveBeenCalledTimes(1); expect(panel.querySelector('[aria-sort="descending"] button')?.textContent).toBe('Findings'); await act(async () => panel.querySelector(`[data-work-package-select="${selectedPackage.id}"]`)!.click()); const packageButton = panel.querySelector(`[data-work-package-select="${selectedPackage.id}"]`)!; const detailRow = panel.querySelector(`[data-work-package-details="${selectedPackage.id}"]`)!; expect(packageButton.getAttribute('aria-expanded')).toBe('true'); expect(detailRow.hidden).toBe(false); expect(detailRow.previousElementSibling?.getAttribute('data-work-package-row')).toBe(selectedPackage.id); const expected = app.activeSheet.analysis.records.filter(record => selectedPackage.findingKeys.includes(workRecordKey(record))); expect([...detailRow.querySelectorAll('.client-security-finding-toggle strong')].map(title => title.textContent)).toEqual(expected.map(record => record.title)); const finding = detailRow.querySelector('.client-security-finding-toggle')!; await act(async () => finding.click()); expect(finding.getAttribute('aria-expanded')).toBe('true'); expect(detailRow.textContent).toContain(expected[0].fields[5].value); expect(detailRow.textContent).toContain(expected[0].fields[6].value); await act(async () => tabs[0].click()); await act(async () => tabs[2].click()); expect(packageButton.getAttribute('aria-expanded')).toBe('true'); expect(finding.getAttribute('aria-expanded')).toBe('true'); await act(async () => packageButton.click()); expect(detailRow.hidden).toBe(true); expect(detailRow.querySelector('.client-security-finding-toggle')).toBeNull(); await act(async () => panel.querySelector('[data-work-package-unassigned-summary]')!.click()); expect(panel.querySelector('[data-work-package-unassigned-details]')?.textContent).toContain('Synthetic unassigned control'); expect(filter.value).toBe(''); expect(panel.querySelector('[title*="in Work"]')).toBeNull(); expect(app.setFacetFilter).not.toHaveBeenCalled(); expect(openWork).not.toHaveBeenCalled(); expect(JSON.stringify({ app, result })).toBe(original); } finally { await act(async () => root.unmount()); host.remove(); } }); it('clears the old scope while loading another client/assessment and suppresses late packages when Remediation is disabled', async () => { const context = setup(); document.documentElement.dataset.theme = theme; const first = fixture(), second = fixture('synthetic-other-client', 'synthetic-other-assessment'); vi.mocked(useApp).mockReturnValue(first.app as unknown as ReturnType); let finish!: (response: WorkPackagesResponse) => void; const packages = vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValueOnce({ status: 'ready', result: first.result }) .mockImplementationOnce(() => new Promise(resolve => { finish = resolve; })); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); const render = () => root.render( {}}/>); try { await act(async () => render()); await act(async () => [...host.querySelectorAll('[role="tab"]')].find(tab => tab.textContent === 'Work Packages')!.click()); expect(host.querySelectorAll('[data-work-package-row]')).toHaveLength(first.result.packages.length); vi.mocked(useApp).mockReturnValue(second.app as unknown as ReturnType); await act(async () => render()); expect(packages).toHaveBeenLastCalledWith('synthetic-other-client', ['synthetic-other-assessment'], ['synthetic-run']); expect(host.querySelector('[data-work-package-row]')).toBeNull(); expect(host.textContent).toContain('Loading work packages'); await act(async () => setEngineEnabled('remediation', false)); expect([...host.querySelectorAll('[role="tab"]')].map(tab => tab.textContent)).toEqual(['Overview', 'Security']); expect(host.querySelector('[role="tabpanel"]:not([hidden])')?.getAttribute('data-group-scroll')).toBe('client-info-overview'); await act(async () => finish({ status: 'ready', result: second.result })); expect(host.querySelector('[data-work-package-row]')).toBeNull(); expect(packages).toHaveBeenCalledTimes(2); expect(context).not.toHaveBeenCalled(); } finally { await act(async () => root.unmount()); host.remove(); } }); it('reads all exact package and same-change members from the project Work sheet even when another source sheet is selected', async () => { setup(); document.documentElement.dataset.theme = theme; const { app, result } = fixture(); const grouped = result.packages.find(p => p.findingKeys.length === 2)!; grouped.tasks = [{ id: 'synthetic-same-change', findingKeys: [...grouped.findingKeys], redundant: true }]; grouped.facts.tasks = 1; grouped.facts.redundantFindings = 2; const sourceSheet = makeSheet('Synthetic source subset', [['Name', 'Status'], ['A different source row', 'Passed']], '', 'source.csv'); app.dataset.sheets.push(sourceSheet); app.activeSheet = sourceSheet; vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType); vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result }); const original = JSON.stringify({ app, result }); const openWork = vi.fn(); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); try { await act(async () => root.render()); await act(async () => [...host.querySelectorAll('[role="tab"]')].find(tab => tab.textContent === 'Work Packages')!.click()); await act(async () => host.querySelector(`[data-work-package-select="${grouped.id}"]`)!.click()); const details = host.querySelector(`[data-work-package-details="${grouped.id}"]`)!; expect([...details.querySelectorAll('.client-security-finding-toggle strong')].map(title => title.textContent).sort()).toEqual(['Synthetic admin A', 'Synthetic admin B']); expect(details.textContent).not.toContain('A different source row'); await act(async () => host.querySelector('[data-work-package-related-summary]')!.click()); const related = host.querySelector('[data-work-package-related-summary]')!; const relatedDetails = document.getElementById(related.getAttribute('aria-controls')!)!; expect(relatedDetails.hidden).toBe(false); expect(relatedDetails.querySelectorAll('.client-security-finding-toggle')).toHaveLength(2); await act(async () => related.click()); expect(relatedDetails.hidden).toBe(true); expect(app.setFacetFilter).not.toHaveBeenCalled(); expect(openWork).not.toHaveBeenCalled(); expect(JSON.stringify({ app, result })).toBe(original); } finally { await act(async () => root.unmount()); host.remove(); } }); });