// @vitest-environment jsdom import { act, useState } from 'react'; import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow'; import { readFileSync } from 'node:fs'; import { createRoot, type Root } from 'react-dom/client'; import { beforeEach, afterEach, expect, it, vi } from 'vitest'; import { useApp } from '../../src/app/AppProvider'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; import { SearchFilterBar } from '../../src/features/search-filter/SearchFilterBar'; import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records'; import { FindingReader } from '../../src/features/finding-reader/FindingReader'; import { RelatedFindingsProvider } from '../../src/features/related-findings/RelatedFindings'; import { ActionlistControls } from '../../src/features/actionlist/ActionlistControls'; import { SidebarSecurityDomains } from '../../src/features/sidebar/SidebarSecurityDomains'; import { useContextClusterIndex } from '../../src/features/context-layer/use-context-cluster-index'; import { makeSheet } from '../../src/importers/sheet-factory'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; import { buildWorkPackages, scannerFindingKey, UNASSIGNED_WORK_PACKAGE, WORK_PACKAGE_FACET_ID, type WorkPackage } from '../../src/remediation/remediation-engine'; import { resetWorkPackageRequestsForTests } from '../../src/remediation/use-work-packages'; import { workPackageOverview } from '../../src/remediation/work-package-overview'; import { workPackageIndex } from '../../src/remediation/work-package-index'; import { ContextHeatmap } from '../../src/features/context-layer/ContextHeatmap'; import { contextClusterIndex } from '../../src/features/context-layer/context-cluster-facet'; import { emptyHeatmapContext } from '../assessment-context/context-heatmap-fixture'; vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; }); vi.mock('../../src/features/context-layer/use-context-cluster-index', () => ({ useContextClusterIndex: vi.fn(() => undefined) })); let host: HTMLDivElement; let root: Root; let app: ReturnType; let result: ReturnType; it('keeps only the table head sticky when scrolling package row headings', async () => { const style = document.createElement('style'); style.textContent = ['base.css', 'components.css'].map(file => readFileSync(`src/styles/${file}`, 'utf8')).join('\n'); document.head.append(style); try { await act(async () => root.render(<>)); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const scroll = document.querySelector('.work-packages__scroll')!; scroll.scrollTop = 300; await act(async () => scroll.dispatchEvent(new Event('scroll'))); expect(getComputedStyle(scroll.querySelector('thead')!).position).toBe('sticky'); for (const heading of scroll.querySelectorAll('tbody th')) { expect(getComputedStyle(heading).position).toBe('static'); expect(getComputedStyle(heading).zIndex).toBe('auto'); } } finally { style.remove(); } }); function View() { const records = useFilteredRecords(); // Work shows the selected record in the Reader, otherwise the first visible one. const selected = records.find(record => record.id === useApp().selectedRecordId) ?? records[0]; return <>{records.length}{selected && {}} onNext={() => {}} onReview={() => {}}/>}; } it.each(['light', 'dark'])('places yellow-outlined Work Packages before CSV and keeps its outline when opened (%s)', async theme => { document.documentElement.dataset.theme = theme; const style = document.createElement('style'); // JSDOM does not resolve custom colour properties; resolve this shared token for its CSS cascade. const accent = readFileSync('src/styles/ui-tokens.css', 'utf8').match(/--button-accent:\s*([^;]+);/)![1]; style.textContent = ['base.css', 'components.css'].map(file => readFileSync(`src/styles/${file}`, 'utf8')).join('\n') .replaceAll('var(--button-accent)', accent); const expected = document.createElement('span'); expected.style.borderColor = accent; document.head.append(style); try { await act(async () => root.render()); const row = host.querySelector('.header-export-row')!; expect([...row.children].filter(node => node.tagName === 'BUTTON').map(node => node.textContent)) .toEqual(['Work Packages', 'CSV', 'MD', 'HTML']); const button = row.querySelector('[data-work-packages-open]')!; expect(getComputedStyle(button).borderColor).toBe(expected.style.borderColor); await act(async () => button.click()); expect(button.getAttribute('aria-pressed')).toBe('true'); expect(getComputedStyle(button).borderColor).toBe(expected.style.borderColor); await act(async () => button.click()); expect(document.querySelector('[data-work-packages-window]')).toBeNull(); expect(button.getAttribute('aria-pressed')).toBe('false'); await act(async () => button.click()); expect(document.querySelector('[data-work-packages-window]')).not.toBeNull(); await act(async () => document.querySelector('[aria-label="Close Work packages window"]')!.click()); expect(button.getAttribute('aria-pressed')).toBe('false'); expect(getComputedStyle(button).borderColor).toBe(expected.style.borderColor); } finally { style.remove(); delete document.documentElement.dataset.theme; } }); beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); resetWorkPackageRequestsForTests(); setEngineEnabled('context', false); vi.mocked(useContextClusterIndex).mockReturnValue(undefined); const sheet = makeSheet('Synthetic', [['Title','Status','Severity','Source Rule ID','Result Message','Source: Scan'], ['Admin sign-in A','Failed','High','MT.1014','Message A','Synthetic'], ['Admin sign-in B','Failed','High','CISA.MS.AAD.3.6','Message B','Synthetic'], ['Azure hierarchy','Failed','High','MT.1056','Message C','Synthetic']], '', 'synthetic.csv', { isProjectSheet: true }); sheet.analysis.records.forEach((r, i) => { r.findingOccurrenceId = `occ-${i}`; }); const fs = sheet.analysis.records.map(r => ({ key: scannerFindingKey(r.findingOccurrenceId!), occurrenceId: r.findingOccurrenceId, tool: 'maester', ruleId: r.fields[3].value, title: r.title, outcome: 'fail' })); result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings: fs, 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' } }], // The third finding belongs to another cluster, so the package boundary yields one two-finding package. findings: fs.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [i < 2 ? 'cloud-privileged-access-exposure' : 'cloud-external-access-exposure'], conditionIds: [] })), settings: [] } }); app = { activeSheet: sheet, activeProfile: genericProfile, dataset: { fileName: 'Synthetic', sheets: [sheet] }, targetProjectId: 'synthetic', activeAssessmentIds: ['a'], activeScanRunIds: ['s'], reviews: {}, search: '', statusFilter: 'all', facetFilters: {}, sortColumn: '', sortDirection: 'asc', pathFindingIds: [], actionOnly: false, selectedRecordId: '', setFacetFilter: (id: string, filter: unknown) => { app = { ...app, facetFilters: { ...app.facetFilters } }; if (filter) app.facetFilters[id] = filter as never; else delete app.facetFilters[id]; vi.mocked(useApp).mockReturnValue(app); root.render(<>); }, setSelectedRecord: (id: string) => { app = { ...app, selectedRecordId: id }; vi.mocked(useApp).mockReturnValue(app); root.render(<>); }, setStatusFilter: vi.fn(), setActiveSheet: vi.fn(), resetFilters: vi.fn(), setActionOnly: vi.fn() } as unknown as ReturnType; vi.mocked(useApp).mockReturnValue(app); vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result }); host = document.createElement('div'); document.body.append(host); root = createRoot(host); }); it.each(['light', 'dark'])('minimizes below Source: Scan and restores the retained Work Packages table (%s)', async theme => { document.documentElement.dataset.theme = theme; const originalWidth = window.innerWidth; Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1400 }); try { await act(async () => root.render(<>)); const anchor = host.querySelector('[aria-label="Source: Scan"]')!; expect(anchor).not.toBeNull(); vi.spyOn(anchor, 'getBoundingClientRect').mockReturnValue({ bottom: 96 } as DOMRect); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const panel = document.querySelector('.floating-data-window')!; const filter = panel.querySelector('[aria-label="Filter Package"]')!; await act(async () => { filter.value = filter.options[1].value; filter.dispatchEvent(new Event('change', { bubbles: true })); }); await act(async () => panel.querySelector('[data-sort-column="findings"]')!.click()); const before = panel.querySelector('tbody')!.innerHTML; const selectedFilter = filter.value; const minimize = () => panel.querySelector('[aria-label="Minimize Work packages window"]')!.click(); await act(async () => minimize()); expect(panel.classList.contains('is-minimized')).toBe(true); expect(panel.style.left).toBe('1064px'); expect(panel.style.top).toBe('104px'); expect(panel.style.width).toBe('320px'); expect(panel.querySelector('.floating-data-window-body')!.hidden).toBe(true); expect(panel.querySelector('.results-window-resize')).toBeNull(); expect(panel.querySelector('.floating-data-window-actions')!.lastElementChild!.getAttribute('aria-label')).toBe('Close Work packages window'); await act(async () => panel.querySelector('[aria-label="Open Work packages"]')!.click()); expect(panel.classList.contains('is-minimized')).toBe(false); expect(panel.style.left).toBe('554px'); expect(panel.style.width).toBe('830px'); expect(panel.querySelector('.floating-data-window-body')!.hidden).toBe(false); expect(filter.value).toBe(selectedFilter); expect(panel.querySelector('tbody')!.innerHTML).toBe(before); await act(async () => panel.querySelector('[aria-label="Maximize Work packages window"]')!.click()); await act(async () => minimize()); expect(panel.classList.contains('is-maximized')).toBe(false); expect(panel.classList.contains('is-minimized')).toBe(true); await act(async () => panel.querySelector('[aria-label="Restore Work packages window"]')!.click()); expect(panel.style.left).toBe('554px'); expect(filter.value).toBe(selectedFilter); await act(async () => minimize()); await act(async () => panel.querySelector('[aria-label="Close Work packages window"]')!.click()); expect(document.querySelector('.floating-data-window')).toBeNull(); } finally { Object.defineProperty(window, 'innerWidth', { configurable: true, value: originalWidth }); delete document.documentElement.dataset.theme; } }); afterEach(async () => { await act(async () => root.unmount()); host.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); resetEnginePreferencesForTests(); }); it.each(['light', 'dark'])('opens Results above Work Packages and brings either clicked window forward while Filters stays above both (%s)', async theme => { vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); document.documentElement.dataset.theme = theme; function Windows() { const [shown, setShown] = useState(false); return <> {shown && {}} onClose={() => setShown(false)}/>}; } await act(async () => root.render()); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const packages = document.querySelector('.floating-data-window')!; await act(async () => [...host.querySelectorAll('button')].find(b => b.textContent === 'Results')!.click()); const results = host.querySelector('.results-window')!; expect(Number(results.style.zIndex)).toBeGreaterThan(Number(packages.style.zIndex)); const activate = async (element: Element) => act(async () => element.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }))); for (let index = 0; index < 3; index++) { await activate(packages); expect(Number(packages.style.zIndex)).toBeGreaterThan(Number(results.style.zIndex)); await activate(results); expect(Number(results.style.zIndex)).toBeGreaterThan(Number(packages.style.zIndex)); } await act(async () => document.querySelector('[aria-label="Detach filters"]')!.click()); const filterLayer = Number(readFileSync('src/styles/ui-tokens.css', 'utf8').match(/--z-floating-filters:\s*(\d+)/)![1]); expect(filterLayer).toBeGreaterThan(Number(results.style.zIndex)); expect(filterLayer).toBeGreaterThan(Number(packages.style.zIndex)); expect(document.querySelector('.work-filter-panel.is-floating')).not.toBeNull(); delete document.documentElement.dataset.theme; }); it.each(['light', 'dark'])('shows legacy AP snapshots as WP in every package control and preserves exact selection IDs (%s)', async theme => { document.documentElement.dataset.theme = theme; result.packages.forEach(p => { p.code = p.code.replace(/^WP-/, 'AP-'); }); const before = JSON.stringify(result); await act(async () => root.render(<>)); expect(host.querySelector('[data-work-package-id]')!.textContent).toContain('WP-01'); const dropdown = host.querySelector('[aria-label="Work Package"]')!; expect(dropdown.textContent).toContain('WP-01'); expect(dropdown.textContent).not.toContain('AP-'); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const first = document.querySelector('[data-work-package-select]')!; expect(first.textContent).toBe('WP-01'); expect(document.querySelector('[data-work-packages-window]')!.textContent).not.toContain('AP-'); await act(async () => first.click()); expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [result.packages[0].id] }); expect(JSON.stringify(result)).toBe(before); delete document.documentElement.dataset.theme; }); it.each(['light', 'dark'])('uses the actual heatmap positions in groups, both filters and the Reader without renumbering filtered groups (%s)', async theme => { document.documentElement.dataset.theme = theme; setEngineEnabled('context', true); let context = emptyHeatmapContext(); vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(context); context.clusters.forEach(cluster => { cluster.satisfied = 1; }); const sheet = makeSheet('Synthetic heatmap order', [['Title', 'Status', 'Severity', 'Source Rule ID'], ...Array.from({ length: 10 }, (_, i) => [`Synthetic ${i}`, 'Failed', 'High', `order-${i}`])], '', 'order.csv', { isProjectSheet: true }); const findings = sheet.analysis.records.map((record, i) => { record.findingOccurrenceId = `order-${i}`; const clusterId = i < 9 ? 'cloud-privileged-access-exposure' : 'credential-relay-exposure'; const conditionId = context.clusters.find(cluster => cluster.clusterId === clusterId)!.conditions[0].conditionId; context.observations.push({ id: record.id, occurrenceId: record.findingOccurrenceId, resultProjectionId: record.id, conditionId, tool: 'maester', sourceRuleId: `order-${i}`, ruleText: record.title, outcome: 'fail', matchedBy: 'rule-id', assertion: 'observed-by-scanner' } as never); context.findings.push({ occurrenceId: record.findingOccurrenceId, resultProjectionId: record.id, tool: 'maester', sourceRuleId: `order-${i}`, observationIds: [record.id], conditionIds: [], clusterIds: [clusterId], declaredReferenceIds: [], cisControlAreaIds: [] }); return { key: scannerFindingKey(record.findingOccurrenceId), occurrenceId: record.findingOccurrenceId, tool: 'maester', ruleId: `order-${i}`, title: record.title, outcome: 'fail' }; }); result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings, context }); vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result }); vi.mocked(useContextClusterIndex).mockReturnValue(contextClusterIndex(context)); const render = () => root.render(<>); app = { ...app, activeSheet: sheet, dataset: { ...app.dataset!, fileName: 'Synthetic order', sheets: [sheet] }, setFacetFilter: (id, filter) => { app = { ...app, facetFilters: { ...app.facetFilters } }; if (filter) app.facetFilters[id] = filter; else delete app.facetFilters[id]; vi.mocked(useApp).mockReturnValue(app); render(); } }; vi.mocked(useApp).mockReturnValue(app); const original = JSON.stringify({ context, result, records: sheet.analysis.records }); await act(async () => render()); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const heatmapLabels = new Map([...host.querySelectorAll('[data-cluster]')].map(cell => [cell.dataset.cluster!, `${cell.querySelector('.context-heatmap__number')!.textContent} · ${cell.querySelector('strong')!.textContent}`])); expect(heatmapLabels.get('cloud-privileged-access-exposure')).toBe('01 · Cloud Privileged Access Exposure'); expect(heatmapLabels.get('credential-relay-exposure')).toBe('05 · Credential Relay Exposure'); const groups = [...document.querySelectorAll('[data-work-package-cluster]')]; expect(groups.map(group => group.textContent)).toEqual(['01 · Cloud Privileged Access Exposure', '05 · Credential Relay Exposure']); for (const group of groups) expect(group.textContent).toBe(heatmapLabels.get(group.dataset.workPackageCluster!)); expect([...document.querySelectorAll('[data-work-package-select]')].map(button => button.textContent)).toEqual(['WP-01', 'WP-02', 'WP-01']); expect([...host.querySelectorAll('.work-package-cluster-head > span:first-child')].map(node => node.textContent)).toEqual(groups.map(group => group.textContent)); for (const facetLabel of ['Context Cluster', 'Work Package']) { const labels = [...host.querySelector(`select[aria-label="${facetLabel}"]`)!.options].slice(1).map(option => option.textContent); expect(labels[0]).toMatch(/^01 · Cloud Privileged Access Exposure/); expect(labels.some(label => label?.startsWith('05 · Credential Relay Exposure'))).toBe(true); } expect(host.querySelector('[data-work-package-id]')!.textContent).toContain('01 · Cloud Privileged Access Exposure → WP-01'); const relay = result.packages.find(p => p.clusterId === 'credential-relay-exposure')!; await act(async () => document.querySelector(`[data-work-package-select="${relay.id}"]`)!.click()); expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [relay.id] }); expect(host.querySelector('[data-record-count]')!.textContent).toBe('1'); expect(host.querySelector('[data-work-package-id]')!.textContent).toContain('05 · Credential Relay Exposure → WP-01'); expect([...document.querySelectorAll('[data-work-package-cluster]')].map(group => group.textContent)).toEqual(groups.map(group => group.textContent)); const tableFilter = document.querySelector('[aria-label="Filter Package"]')!; await act(async () => { tableFilter.value = relay.id; tableFilter.dispatchEvent(new Event('change', { bubbles: true })); }); expect([...document.querySelectorAll('[data-work-package-cluster]')].map(group => group.textContent)).toEqual(['05 · Credential Relay Exposure']); expect(JSON.stringify({ context, result, records: sheet.analysis.records })).toBe(original); // A different published tie order must update every visible position, retaining the selected package ID. context = { ...context, clusters: [...context.clusters].reverse() }; vi.mocked(useContextClusterIndex).mockReturnValue(contextClusterIndex(context)); await act(async () => render()); const relayTile = host.querySelector('[data-cluster="credential-relay-exposure"]')!; const relayHeatmapLabel = `${relayTile.querySelector('.context-heatmap__number')!.textContent} · ${relayTile.querySelector('strong')!.textContent}`; expect(relayHeatmapLabel).not.toBe('05 · Credential Relay Exposure'); expect(document.querySelector('[data-work-package-cluster]')!.textContent).toBe(relayHeatmapLabel); expect(tableFilter.value).toBe(relay.id); expect(host.querySelector('[data-work-package-id]')!.textContent).toContain(`${relayHeatmapLabel} → WP-01`); delete document.documentElement.dataset.theme; }); it.each(['dark', 'light'])('selects the real Work Package filter, displays the flag beside Source ID and removes Result Message in %s', async theme => { document.documentElement.dataset.theme = theme; await act(async () => { root.render(<>); await new Promise(resolve => setTimeout(resolve, 0)); }); expect(host.querySelector('[data-record-count]')?.textContent).toBe('3'); expect(host.querySelector('select[aria-label="Result Message"]')).toBeNull(); const select = host.querySelector('select[aria-label="Work Package"]')!; const selected = result.packages.find(p => p.findingKeys.length === 2)!; expect(select.options.namedItem(selected.id)).toBeNull(); expect([...select.options].find(o => o.value === selected.id)?.textContent).toContain('· 2'); await act(async () => { select.value = selected.id; select.dispatchEvent(new Event('change', { bubbles: true })); }); expect(host.querySelector('[data-record-count]')?.textContent).toBe('2'); const source = host.querySelector('[data-source-rule-id]')!; expect(source.closest('.reader-summary')).not.toBeNull(); expect(source.querySelector('h4')?.textContent).toBe('Source Rule ID'); expect(source.querySelector('code.mono')).not.toBeNull(); expect(host.querySelector('.reader-heading [data-source-rule-id]')).toBeNull(); expect(host.querySelector('.finding-badges [data-work-package-id]')).toBeNull(); const link = source.querySelector('a[data-work-package-id]')!; expect(link.textContent).toBe(`Cloud Privileged Access Exposure → ${selected.code}`); expect(link.classList.contains('status-badge')).toBe(false); const badges = host.querySelector('.finding-badges')!; expect(badges.firstElementChild?.className).toBe('outcome-mark'); expect(badges.querySelector('.outcome-mark')?.getAttribute('data-outcome')).toBe('fail'); expect(badges.querySelector('.severity-badge.severity-high')?.textContent).toBe('High'); await act(async () => app.setFacetFilter(WORK_PACKAGE_FACET_ID, null)); expect(host.querySelector('[data-record-count]')?.textContent).toBe('3'); await act(async () => host.querySelector('a[data-work-package-id]')!.click()); expect(host.querySelector('[data-record-count]')?.textContent).toBe('2'); expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [selected.id] }); expect(AssessorApiClient.prototype.getWorkPackages).toHaveBeenCalledTimes(1); await act(async () => setEngineEnabled('remediation', false)); expect(host.querySelector('select[aria-label="Work Package"]')).toBeNull(); expect(host.querySelector('[data-work-package-id]')).toBeNull(); expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toBeUndefined(); expect(host.querySelector('[data-record-count]')?.textContent).toBe('3'); delete document.documentElement.dataset.theme; }); it('suppresses late output after disabling the engine and makes no new request', async () => { let resolve!: (value: { status: 'ready'; result: typeof result }) => void; vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockReturnValue(new Promise(r => { resolve = r; })); await act(async () => root.render()); await act(async () => setEngineEnabled('remediation', false)); await act(async () => resolve({ status: 'ready', result })); expect(host.querySelector('[data-work-package-id]')).toBeNull(); expect(host.querySelector('[data-record-count]')?.textContent).toBe('3'); expect(AssessorApiClient.prototype.getWorkPackages).toHaveBeenCalledTimes(1); }); it('offers a shared non-Cloud package and selects every associated finding through the real Work filter', async () => { const fs = app.activeSheet!.analysis.records.map((r, i) => ({ key: scannerFindingKey(r.findingOccurrenceId!), occurrenceId: r.findingOccurrenceId, tool: i === 0 ? 'pingcastle' : 'purple-knight', ruleId: `synthetic-${i}`, title: r.title, outcome: 'fail' })); result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings: fs, context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1, clusters: [{ clusterId: 'ad-privileged-account-exposure', label: { en: 'AD Privileged Account Exposure' } }, { clusterId: 'ad-persistence-indicators', label: { en: 'AD Persistence Indicators' } }], findings: fs.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [i < 2 ? 'ad-privileged-account-exposure' : 'ad-persistence-indicators'], conditionIds: [i < 2 ? 'ad-admins-not-protected-users' : 'schema-admins-not-empty'] })), settings: [] } }); vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result }); await act(async () => root.render()); const select = host.querySelector('select[aria-label="Work Package"]')!; const selected = result.packages.find(p => p.findingKeys.length === 2)!; expect([...select.options].find(o => o.value === selected.id)?.textContent).toContain('· 2'); await act(async () => { select.value = selected.id; select.dispatchEvent(new Event('change', { bubbles: true })); }); expect(host.querySelector('[data-record-count]')?.textContent).toBe('2'); expect(host.querySelector('[data-work-package-id]')?.textContent).toBe(`AD Privileged Account Exposure → ${selected.code}`); }); it('keeps a selected package from showing unrelated findings while a changed scope loads', async () => { await act(async () => root.render()); const select = host.querySelector('select[aria-label="Work Package"]')!; const selected = result.packages.find(p => p.findingKeys.length === 2)!; await act(async () => { select.value = selected.id; select.dispatchEvent(new Event('change', { bubbles: true })); }); expect(host.querySelector('[data-record-count]')?.textContent).toBe('2'); vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockReturnValue(new Promise(() => {})); app = { ...app, dataset: { ...app.dataset! } }; vi.mocked(useApp).mockReturnValue(app); await act(async () => root.render()); expect(host.querySelector('[data-record-count]')?.textContent).toBe('0'); expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toBeDefined(); }); it.each(['dark', 'light'])('opens the sortable package list from the Work toolbar, selects a package and shows its redundant task in the Reader in %s', async theme => { document.documentElement.dataset.theme = theme; const fs = app.activeSheet!.analysis.records.map((r, i) => ({ key: scannerFindingKey(r.findingOccurrenceId!), occurrenceId: r.findingOccurrenceId, tool: i === 0 ? 'pingcastle' : 'purple-knight', ruleId: r.fields[3].value, title: r.title, outcome: 'fail', severity: i === 0 ? 'high' : 'critical' })); // Both sources report the same specific condition once: one redundant task. The third finding sits in another cluster. result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings: fs, context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1, clusters: [{ clusterId: 'ad-privileged-account-exposure', label: { en: 'AD Privileged Account Exposure' } }, { clusterId: 'ad-persistence-indicators', label: { en: 'AD Persistence Indicators' } }], findings: fs.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [i < 2 ? 'ad-privileged-account-exposure' : 'ad-persistence-indicators'], conditionIds: [i < 2 ? 'ad-admins-not-protected-users' : 'ad-stale-admincount'] })), settings: [] } }); vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result }); await act(async () => { root.render(<>); await new Promise(resolve => setTimeout(resolve, 0)); }); const redundant = result.packages.find(p => p.facts.redundantFindings === 2)!; expect(redundant.tasks).toHaveLength(1); expect(host.querySelector('[data-work-packages-open]')?.getAttribute('aria-pressed')).toBe('false'); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); expect(host.querySelector('[data-work-packages-open]')?.getAttribute('aria-pressed')).toBe('true'); expect(document.querySelector('.floating-data-window-head.is-marked')?.textContent).toContain('Work packages'); await act(async () => document.querySelector('[aria-label="Close Work packages window"]')!.click()); expect(host.querySelector('[data-work-packages-open]')?.getAttribute('aria-pressed')).toBe('false'); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const window = document.querySelector('[data-work-packages-window]')!; expect(window.querySelector('th[aria-sort="ascending"] [data-sort-column="package"]')).not.toBeNull(); expect(window.querySelector(`[data-work-package-select="${redundant.id}"]`)?.textContent).toBe(redundant.code); expect(window.querySelector(`[data-work-package-select="${redundant.id}"]`)?.getAttribute('aria-label')).toBe(`AD Privileged Account Exposure → ${redundant.code}`); expect(window.querySelectorAll('[data-work-package-row]')).toHaveLength(2); expect(window.querySelectorAll('[data-work-package-unassigned]')).toHaveLength(1); expect(window.querySelector('[data-work-package-select-unassigned]')!.disabled).toBe(true); await act(async () => window.querySelector('[data-sort-column="findings"]')!.click()); expect(window.querySelector('th[aria-sort="descending"] [data-sort-column="findings"]')).not.toBeNull(); expect(window.querySelector('[data-work-package-row]')?.getAttribute('data-work-package-row')).toBe(result.packages.find(p => p.clusterId === 'ad-persistence-indicators')!.id); expect(window.querySelector('[data-sort-column="severity"]')).toBeNull(); await act(async () => window.querySelector(`[data-work-package-select="${redundant.id}"]`)!.click()); expect(document.querySelector('[data-work-packages-window]')).not.toBeNull(); expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [redundant.id] }); await act(async () => root.render(<>)); expect(host.querySelector('[data-work-packages-open]')?.getAttribute('aria-pressed')).toBe('true'); expect(host.querySelector('[data-record-count]')?.textContent).toBe('2'); const detail = host.querySelector('[data-work-package-task]')!; expect(detail.textContent).toContain('One of 1 task'); expect(detail.textContent).not.toMatch(/Task \d+ of/); expect(host.querySelector('[data-work-package-redundant]')?.textContent).toBe(' · Same change as Synthetic CISA.MS.AAD.3.6'); expect(host.querySelector('a[data-work-package-id]')?.textContent).toBe(`AD Privileged Account Exposure → ${redundant.code}`); await act(async () => setEngineEnabled('remediation', false)); expect(host.querySelector('[data-work-packages-open]')).toBeNull(); expect(host.querySelector('[data-work-package-task]')).toBeNull(); delete document.documentElement.dataset.theme; }); it.each(['dark', 'light'])('offers selectable values without Measure, finding-severity or Critical/High filters (%s)', async theme => { document.documentElement.dataset.theme = theme; await act(async () => root.render(<>)); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const panel = document.querySelector('[data-work-packages-window]')!; const before = JSON.stringify(result); const filter = async (label: string, value: string) => act(async () => { const input = panel.querySelector(`select[aria-label="Filter ${label}"]`)!; value = [...input.options].find(option => option.textContent === value)?.value ?? value; expect([...input.options].some(option => option.value === value)).toBe(true); input.value = value; input.dispatchEvent(new Event('change', { bubbles: true })); }); expect(panel.querySelector('input')).toBeNull(); expect(panel.querySelector('[aria-label="Filter Measure"]')).toBeNull(); expect(panel.querySelector('[aria-label="Filter Highest finding severity"]')).toBeNull(); expect(panel.querySelector('[aria-label="Filter Critical/High"]')).toBeNull(); expect(panel.querySelector('[data-sort-column="severity"]')).toBeNull(); expect(panel.querySelector('[data-sort-column="criticalHigh"]')).toBeNull(); const inputs = [...panel.querySelectorAll('.work-packages__filter')]; expect(inputs).toHaveLength(8); for (const input of inputs) { const label = input.getAttribute('aria-label')!.replace('Filter ', ''); await filter(label, input.options[1].value); expect(panel.querySelector('tbody tr')).not.toBeNull(); expect(host.querySelector('[data-record-count]')?.textContent).toBe('3'); await filter(label, ''); expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(2); } await filter('Tasks', '2'); expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(1); await filter('Package', 'Cloud External Access Exposure → WP-01'); expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(0); await filter('Tasks', ''); expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(1); expect(panel.querySelector('[data-work-package-cluster]')?.textContent).toContain('Cloud External Access Exposure'); await act(async () => [...panel.querySelectorAll('button')].find(b => b.textContent === 'Clear column filters')!.click()); expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(2); expect(panel.querySelector('[data-work-package-unassigned]')).not.toBeNull(); expect(panel.querySelector('[data-work-package-filter-count]')).toBeNull(); expect(JSON.stringify(result)).toBe(before); expect(app.facetFilters).toEqual({}); delete document.documentElement.dataset.theme; }); it.each(['dark', 'light'])('keeps numbered groups in published heatmap order through filtering and column sorting (%s)', async theme => { document.documentElement.dataset.theme = theme; const clusters = [ ['credential-relay-exposure', 'Credential Relay Exposure', '01'], ['cloud-privileged-access-exposure', 'Cloud Privileged Access Exposure', '02'], ['cloud-sign-in-protection-gaps', 'Cloud Sign-in Protection Gaps', '03'], ['malicious-code-execution-exposure', 'Malicious Code Execution Exposure', '12'] ]; const sheet = makeSheet('Synthetic order', [['Title', 'Status', 'Severity', 'Source Rule ID'], ...Array.from({ length: 12 }, (_, i) => [`Finding ${i}`, 'Failed', i === 11 ? 'Critical' : 'High', `synthetic-${i}`])], '', 'order.csv', { isProjectSheet: true }); sheet.analysis.records.forEach((r, i) => { r.findingOccurrenceId = `order-${i}`; }); const findings = sheet.analysis.records.map((r, i) => ({ key: scannerFindingKey(r.findingOccurrenceId!), occurrenceId: r.findingOccurrenceId, tool: 'maester', ruleId: `synthetic-${i}`, title: r.title, outcome: 'fail', severity: i === 11 ? 'critical' : 'high' })); result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings, context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1, clusters: clusters.map(([clusterId, label]) => ({ clusterId, label: { en: label } })), findings: findings.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [clusters[i < 9 ? 0 : i - 8][0]], conditionIds: [] })), settings: [] } }); app = { ...app, activeSheet: sheet, dataset: { ...app.dataset!, fileName: 'Synthetic order', sheets: [sheet] } }; vi.mocked(useApp).mockReturnValue(app); vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result }); vi.mocked(useContextClusterIndex).mockReturnValue({ publication: { clusters: [clusters[1], clusters[3], clusters[0], clusters[2]].map(([id, label], i) => ({ id, label, hasEvidence: true, severity: i < 2 ? 'critical' : 'high' })) }, clusters: [], facet: { id: 'context:cluster', label: 'Context Cluster', kind: 'category', source: 'derived', values: [], valuesFor: () => [] } } as never); await act(async () => root.render()); const open = async () => act(async () => host.querySelector('[data-work-packages-open]')!.click()); const labels = () => [...document.querySelectorAll('[data-work-package-select]')].map(el => el.getAttribute('aria-label')); const expected = [ '01 · Cloud Privileged Access Exposure → WP-01', '02 · Malicious Code Execution Exposure → WP-01', '03 · Credential Relay Exposure → WP-01', '03 · Credential Relay Exposure → WP-02', '04 · Cloud Sign-in Protection Gaps → WP-01' ]; await open(); expect(labels()).toEqual(expected); expect([...document.querySelectorAll('[data-work-package-cluster]')].map(el => el.textContent)).toEqual([ '01 · Cloud Privileged Access Exposure', '02 · Malicious Code Execution Exposure', '03 · Credential Relay Exposure', '04 · Cloud Sign-in Protection Gaps']); expect([...document.querySelectorAll('[data-work-package-select]')].map(el => el.textContent)).toEqual(['WP-01', 'WP-01', 'WP-01', 'WP-02', 'WP-01']); const filter = document.querySelector('[aria-label="Filter Package"]')!; await act(async () => { filter.value = [...filter.options].find(option => option.textContent === expected[1])!.value; filter.dispatchEvent(new Event('change', { bubbles: true })); }); expect(labels()).toEqual([expected[1]]); await act(async () => { filter.value = ''; filter.dispatchEvent(new Event('change', { bubbles: true })); }); expect(labels()).toEqual(expected); await act(async () => document.querySelector('[data-sort-column="contextSeverity"]')!.click()); expect(labels()).toEqual(expected); await act(async () => document.querySelector('[data-sort-column="package"]')!.click()); expect(labels()).toEqual(expected); expect(document.querySelector('.work-packages__table tbody tr:last-child')?.hasAttribute('data-work-package-unassigned')).toBe(true); await act(async () => document.querySelector('[aria-label="Close Work packages window"]')!.click()); await open(); expect(labels()).toEqual(expected); delete document.documentElement.dataset.theme; }); it('updates an open package overview after Context preparation and dataset refresh, suppressing an older late response', async () => { const contextRequest = vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue({ findings: [], settings: [], clusters: [], coverage: { uncataloguedWithoutRecognition: 0, scannerFindings: 0 }, engine: { version: 'synthetic' }, catalog: { version: 'synthetic' }, repositoryRevision: 2 } as never); await act(async () => setEngineEnabled('context', true)); const render = () => root.render(<>); await act(async () => render()); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); expect(document.querySelectorAll('[data-work-package-row]')).toHaveLength(2); let resolveOld!: (response: { status: 'ready'; result: typeof result }) => void; let resolveNew!: typeof resolveOld; vi.mocked(AssessorApiClient.prototype.getWorkPackages) .mockReturnValueOnce(new Promise(resolve => { resolveOld = resolve; })) .mockReturnValueOnce(new Promise(resolve => { resolveNew = resolve; })); app = { ...app, dataset: { ...app.dataset! } }; vi.mocked(useApp).mockReturnValue(app); await act(async () => render()); expect(document.querySelector('[data-work-packages-window]')).toBeNull(); app = { ...app, dataset: { ...app.dataset! } }; vi.mocked(useApp).mockReturnValue(app); await act(async () => render()); await act(async () => resolveOld({ status: 'ready', result })); expect(document.querySelector('[data-work-packages-window]')).toBeNull(); const p = result.packages.find(p => p.findingKeys.length === 1)!; const next = { ...result, repositoryRevision: 2, packages: [{ ...p, title: 'Refreshed published measure' }], unassignedFindingKeys: result.packages.filter(other => other.id !== p.id).flatMap(other => other.findingKeys) }; await act(async () => resolveNew({ status: 'ready', result: next })); expect(document.querySelectorAll('[data-work-package-row]')).toHaveLength(1); expect(document.querySelector('[data-work-packages-window]')?.textContent).toContain('Refreshed published measure'); expect(document.querySelector('[data-work-package-unassigned-summary]')?.textContent).toBe('2'); expect(contextRequest).toHaveBeenCalledTimes(3); expect(AssessorApiClient.prototype.getWorkPackages).toHaveBeenCalledTimes(3); await act(async () => setEngineEnabled('remediation', false)); expect(document.querySelector('[data-work-packages-window]')).toBeNull(); expect(host.querySelector('[data-work-packages-open]')).toBeNull(); }); it.each(['dark', 'light'])('keeps the full published package contents and compact totals across Work search, status, package filters and reopening (%s)', async theme => { document.documentElement.dataset.theme = theme; const sheet = makeSheet('Synthetic reconciliation', [['Title', 'Status', 'Severity', 'Source Rule ID', 'Source: Scan'], ...Array.from({ length: 274 }, (_, i) => [i === 0 ? 'Only matched declaration' : i < 258 ? `Assigned ${i}` : `Unassigned ${i - 258}`, 'Failed', 'High', `synthetic-${i}`, 'Synthetic'])], '', 'synthetic.csv', { isProjectSheet: true }); sheet.analysis.records.forEach((record, i) => { record.findingOccurrenceId = `reconcile-${i}`; }); const keys = sheet.analysis.records.map(record => scannerFindingKey(record.findingOccurrenceId!)); let nextFinding = 0; const packages: WorkPackage[] = Array.from({ length: 49 }, (_, packageIndex) => { const tasks = Array.from({ length: packageIndex < 7 ? 6 : 5 }, (_, taskIndex) => { const findingKeys = keys.slice(nextFinding, nextFinding + (packageIndex === 0 ? 2 : 1)); nextFinding += findingKeys.length; return { id: `task-${packageIndex}-${taskIndex}`, findingKeys, redundant: findingKeys.length > 1 }; }); const findingKeys = tasks.flatMap(task => task.findingKeys); return { id: `package-${packageIndex}`, code: `WP-${String(packageIndex + 1).padStart(2, '0')}`, clusterId: 'ad-privileged-account-exposure', clusterLabel: 'Synthetic cluster', platform: 'active-directory', title: 'Synthetic published change', tasks, findingKeys, measures: [], method: 'individual', reason: 'Synthetic published task identities', facts: { tasks: tasks.length, findings: findingKeys.length, redundantFindings: packageIndex === 0 ? 12 : 0, highestSeverity: 'high', criticalOrHighFindings: findingKeys.length, conditionsClosed: 0, conditionsTouched: 0, sources: ['Synthetic'] } }; }); expect(nextFinding).toBe(258); result = { ...result, packages, unassignedFindingKeys: keys.slice(258) }; app = { ...app, activeSheet: sheet, dataset: { ...app.dataset!, fileName: 'Synthetic reconciliation', sheets: [sheet] }, statusFilter: 'deviation', facetFilters: {}, search: '', reviews: {} }; vi.mocked(useApp).mockReturnValue(app); vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result }); const originalRecords = JSON.stringify(sheet.analysis.records), originalPackages = JSON.stringify(result); const openOverview = async () => { await act(async () => { root.render(<>); await new Promise(resolve => setTimeout(resolve, 0)); }); if (!document.querySelector('[data-work-packages-window]')) await act(async () => host.querySelector('[data-work-packages-open]')!.click()); return document.querySelector('[data-work-packages-window]')!; }; let window = await openOverview(); expect(host.querySelector('[data-record-count]')!.textContent).toBe('274'); const facts = () => Object.fromEntries([...window.querySelectorAll('.work-packages__facts > div')].map(cell => [cell.querySelector('dt')!.textContent, cell.querySelector('dd')!.textContent])); const expectedFacts = { Findings: '274', Assigned: '258', Unassigned: '16', Packages: '49', Tasks: '252', 'Redundant findings': '12' }; const originalTable = window.querySelector('tbody')!.innerHTML; const unchanged = () => { expect(facts()).toEqual(expectedFacts); expect(window.querySelectorAll('[data-work-package-row]')).toHaveLength(49); expect(window.querySelector('tbody')!.innerHTML).toBe(originalTable); }; unchanged(); app = { ...app, search: 'Unassigned' }; vi.mocked(useApp).mockReturnValue(app); await act(async () => root.render(<>)); expect(host.querySelector('[data-record-count]')!.textContent).toBe('16'); unchanged(); await act(async () => window.querySelector(theme === 'dark' ? '[data-work-package-select-unassigned]' : '[data-work-package-unassigned-summary]')!.click()); expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [UNASSIGNED_WORK_PACKAGE] }); expect(app.statusFilter).toBe('deviation'); expect(app.search).toBe('Unassigned'); expect(document.querySelector('[data-work-packages-window]')).not.toBeNull(); window = await openOverview(); unchanged(); await act(async () => document.querySelector('[aria-label="Close Work packages window"]')!.click()); app = { ...app, facetFilters: {}, search: 'Only matched declaration' }; vi.mocked(useApp).mockReturnValue(app); window = await openOverview(); expect(host.querySelector('[data-record-count]')!.textContent).toBe('1'); unchanged(); app = { ...app, statusFilter: 'passed', search: 'no matching finding' }; vi.mocked(useApp).mockReturnValue(app); await act(async () => root.render(<>)); expect(host.querySelector('[data-record-count]')!.textContent).toBe('0'); unchanged(); expect(JSON.stringify(sheet.analysis.records)).toBe(originalRecords); expect(JSON.stringify(result)).toBe(originalPackages); delete document.documentElement.dataset.theme; }); it('keeps records outside the published package snapshot separately visible in the reconciliation', () => { const records = app.activeSheet!.analysis.records; const outside = { ...records[0], id: 'outside-snapshot', findingOccurrenceId: 'outside-snapshot' }; const before = JSON.stringify(result); const overview = workPackageOverview([...records, outside], workPackageIndex(result)); expect(overview.records).toBe(4); expect(overview.assigned).toBe(3); expect(overview.unassigned).toBe(0); expect(overview.outsideSnapshot).toBe(1); expect(overview.assigned + overview.unassigned + overview.outsideSnapshot).toBe(overview.records); expect(JSON.stringify(result)).toBe(before); }); it('opens the redundant finding of the same task with only the package filter changed, and reports a finding hidden by other Work filters', async () => { const fs = app.activeSheet!.analysis.records.map((r, i) => ({ key: scannerFindingKey(r.findingOccurrenceId!), occurrenceId: r.findingOccurrenceId, tool: i === 0 ? 'pingcastle' : 'purple-knight', ruleId: r.fields[3].value, title: r.title, outcome: 'fail' })); result = buildWorkPackages({ projectId: 'synthetic', assessmentIds: ['a'], scanRunIds: ['s'], findings: fs, context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1, clusters: [{ clusterId: 'ad-privileged-account-exposure', label: { en: 'AD Privileged Account Exposure' } }, { clusterId: 'ad-persistence-indicators', label: { en: 'AD Persistence Indicators' } }], findings: fs.map((f, i) => ({ occurrenceId: f.occurrenceId!, clusterIds: [i < 2 ? 'ad-privileged-account-exposure' : 'ad-persistence-indicators'], conditionIds: [i < 2 ? 'ad-admins-not-protected-users' : 'ad-stale-admincount'] })), settings: [] } }); vi.mocked(AssessorApiClient.prototype.getWorkPackages).mockResolvedValue({ status: 'ready', result }); const [first, second] = app.activeSheet!.analysis.records; const redundant = result.packages.find(p => p.facts.redundantFindings === 2)!; await act(async () => { root.render(<>); await new Promise(resolve => setTimeout(resolve, 0)); }); // Visible case: no package filter yet; the click sets it and selects the other finding. const link = host.querySelector(`[data-work-package-finding="${second.id}"]`)!; expect(link.textContent).toBe('Synthetic CISA.MS.AAD.3.6'); await act(async () => link.click()); expect(app.facetFilters[WORK_PACKAGE_FACET_ID]).toEqual({ values: [redundant.id] }); expect(app.selectedRecordId).toBe(second.id); expect(host.querySelector('[data-record-count]')?.textContent).toBe('2'); expect(host.querySelector(`[data-work-package-finding="${first.id}"]`)?.textContent).toBe('Synthetic MT.1014'); // Hidden case: a search keeps only the current finding; the click reports it and changes nothing. app = { ...app, search: 'Admin sign-in B', facetFilters: {}, selectedRecordId: '' }; vi.mocked(useApp).mockReturnValue(app); await act(async () => root.render()); expect(host.querySelector('[data-record-count]')?.textContent).toBe('1'); await act(async () => host.querySelector(`[data-work-package-finding="${first.id}"]`)!.click()); expect(host.querySelector('[data-work-package-hidden]')?.textContent).toBe(' · Synthetic MT.1014 is hidden by the current Work filters.'); expect(app.facetFilters).toEqual({}); expect(app.search).toBe('Admin sign-in B'); expect(app.selectedRecordId).toBe(''); }); it.each(['dark', 'light'])('matches Context severity between Filters and Packages, including GPO and live updates (%s)', async theme => { document.documentElement.dataset.theme = theme; const p = result.packages.find(p => p.findingKeys.length === 2)!; p.platform = 'windows-host'; p.facts.highestSeverity = undefined; const cluster = { id: p.clusterId, severity: 'critical', hasEvidence: true }; const index = { publication: { clusters: [cluster] }, clusters: [], facet: { id: 'context:cluster', label: 'Context Cluster', kind: 'category', source: 'derived', values: [], valuesFor: () => [] } } as unknown as NonNullable>; vi.mocked(useContextClusterIndex).mockReturnValue(index); const render = () => root.render(<>); await act(async () => render()); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const row = () => document.querySelector('[data-work-package-row="' + p.id + '"]')!; expect(document.querySelector('[data-work-package-row]')?.getAttribute('data-work-package-row')).toBe(p.id); expect(row().querySelector('[data-context-severity="critical"]')?.textContent).toBe('Critical'); expect(document.querySelector('.work-package-cluster-colour.severity-critical')).not.toBeNull(); expect(document.querySelector('[data-sort-column="severity"]')).toBeNull(); cluster.severity = 'medium'; vi.mocked(useContextClusterIndex).mockReturnValue({ ...index }); await act(async () => render()); expect(row().querySelector('[data-context-severity="medium"]')?.textContent).toBe('Medium'); expect(document.querySelector('.work-package-cluster-colour.severity-medium')).not.toBeNull(); cluster.hasEvidence = false; await act(async () => render()); expect(row().querySelector('[data-context-severity="undetermined"]')?.textContent).toBe('Undetermined'); expect(document.querySelector('.work-package-cluster-colour.severity-undetermined')).not.toBeNull(); expect(p.facts.highestSeverity).toBeUndefined(); delete document.documentElement.dataset.theme; }); it.each(['light', 'dark'])('opens all same-change evidence in Work from the summary and parks Packages with matching cluster squares (%s)', async theme => { document.documentElement.dataset.theme = theme; Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1400 }); Object.defineProperty(window, 'innerHeight', { configurable: true, value: 1200 }); const p = result.packages.find(p => p.findingKeys.length === 2)!; p.tasks = [{ ...p.tasks[0], findingKeys: p.findingKeys, redundant: true }]; p.facts.tasks = 1; p.facts.redundantFindings = 2; app = { ...app, activeSheetId: app.activeSheet!.id, workScopeId: 'synthetic', search: 'Azure hierarchy', actionOnly: false, applyViewBookmark: saved => { app = { ...app, ...saved }; vi.mocked(useApp).mockReturnValue(app); } }; vi.mocked(useApp).mockReturnValue(app); const render = () => root.render(); await act(async () => { render(); await new Promise(resolve => setTimeout(resolve, 0)); }); expect(host.querySelector('[data-record-count]')!.textContent).toBe('1'); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); const panel = document.querySelector('.floating-data-window')!; expect(panel.style.width).toBe('830px'); expect(panel.style.height).toBe('1080px'); expect(panel.style.left).toBe('554px'); expect(panel.style.top).toBe('60px'); await act(async () => panel.querySelector('[aria-label="Maximize Work packages window"]')!.click()); expect(panel.classList.contains('is-maximized')).toBe(true); Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1600 }); await act(async () => panel.querySelector('[aria-label="Restore Work packages window"]')!.click()); expect(panel.style.left).toBe('754px'); expect(panel.style.width).toBe('830px'); await act(async () => panel.querySelector('.floating-data-window-head')!.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))); expect(panel.classList.contains('is-maximized')).toBe(true); await act(async () => panel.querySelector('.floating-data-window-head')!.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))); expect(panel.style.left).toBe('754px'); expect(panel.querySelectorAll('[data-work-package-colour]')).toHaveLength(0); for (const row of panel.querySelectorAll('[data-work-package-row]')) expect(row.querySelector('.work-package-cluster-colour')).toBeNull(); expect(panel.querySelectorAll('.work-packages__cluster .work-package-cluster-colour')).toHaveLength(new Set(result.packages.map(p => p.clusterId)).size); const before = JSON.stringify(result); const link = panel.querySelector('[data-work-package-related-summary]')!; expect(link.textContent).toBe('2'); expect(link.disabled).toBe(false); await act(async () => { link.click(); render(); }); expect(document.querySelector('[data-work-packages-window]')).not.toBeNull(); expect(panel.style.left).toBe('754px'); expect(app.pathFindingIds).toHaveLength(2); expect(app.search).toBe(''); expect(app.facetFilters).toEqual({}); expect(app.statusFilter).toBe('all'); expect(app.view).toBe('records'); expect(app.actionOnly).toBe(false); expect(host.querySelector('[data-record-count]')!.textContent).toBe('2'); expect(JSON.stringify(result)).toBe(before); await act(async () => panel.querySelector('[aria-label="Close Work packages window"]')!.click()); expect(document.querySelector('[data-work-packages-window]')).toBeNull(); await act(async () => host.querySelector('[data-work-packages-open]')!.click()); expect(document.querySelector('.floating-data-window')!.style.left).toBe('754px'); delete document.documentElement.dataset.theme; });