// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { EnvironmentObservation, EnvironmentSnapshot, ScannerReportSection } from '../../src/domain/environment-context'; import { EnvironmentEvidenceTables } from '../../src/features/dashboard/EnvironmentEvidenceTables'; import { EnvironmentResultsButton } from '../../src/features/dashboard/EnvironmentResultsButton'; import type { EnvironmentEvidenceGroup } from '../../src/features/dashboard/environment-result-evidence'; import { environmentEvidenceForGroup, environmentSectionTitle, environmentTableSubsection, environmentTableTitle, partitionEnvironmentObservations } from '../../src/features/dashboard/environment-result-evidence'; const tool = { id: 'dummy', name: 'Dummy scanner' }; const section = (key: string, name: string, rows: (string | number)[][]): ScannerReportSection => ({ version: 1, id: key, scanRunId: 'dummy-run', importFileId: 'dummy-file', tool, sectionKey: key, label: name, ordinal: 1, state: 'observed', tables: [{ name, headers: ['Technical setting', 'Configured value', 'Extra evidence'], rows }], prose: `${name} source prose`, source: { rowIndex: 4 }, extractorVersion: 'dummy-1', attributes: { source: 'dummy' } }); const observation = (property: string, originalValue: string | number | boolean, normalizedValue?: string | number | boolean): EnvironmentObservation => ({ version: 1, id: property, scanRunId: 'dummy-run', importFileId: 'dummy-file', tool, subject: { type: 'domain', label: 'DUMMY' }, property, label: property, originalValue, normalizedValue, valueType: 'string', state: 'observed', sectionKey: 'report-overview', source: { objectPath: '$.dummy.value' }, mappingVersion: 'dummy-2', attributes: { role: 'test' } }); const snapshot: EnvironmentSnapshot = { version: 1, projectId: 'dummy-project', scanRunIds: ['dummy-run'], generatedAt: '2026-01-01T00:00:00Z', coverage: { observed: 1, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 }, sections: [ section('sectionPasswordPolicies', 'Password policies', [['Minimum length', 14, 'GPO A'], ['History', 24, 'GPO B']]), section('sectionPKI', 'Certificate templates', [['Template A', 'Enabled', 'Enrollment enabled']]), section('sectionComputerInformation', 'Operating systems', [['Windows Server', 7, 'Unsupported count']]), section('unknown-section', 'Unmapped report evidence', [['Unknown fact', 'retained', 'source-only']]) ], observations: [ observation('domain.count', 2, 3), observation('password_policy.minimum_length', 14), observation('pki.template_count', 1), observation('operating_system.name', 'Windows Server'), observation('domain.controller_count', 2), observation('entra.tenant_name', 'dummy tenant'), observation('unmapped.custom_property', 'keep me') ] }; afterEach(() => { vi.unstubAllGlobals(); document.body.replaceChildren(); }); describe('Environment Results source evidence', () => { it('opens Directory Results and native details without generated labels or legacy source-text controls', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const domain: ScannerReportSection = { ...section('sectionDomainInformation', 'Domain Information', []), tool: { id: 'pingcastle', name: 'PingCastle' }, prose: 'Domain Information legacy flattened text Close Close', detailLinks: [{ targetId: 'domain-extra', label: 'Additional domain details' }], tables: [ { name: 'Tabelle 35', title: 'Tabelle 35', sourceTableIndex: 34, headers: ['Domain', 'NetBIOS'], rows: [['synthetic.example', 'SYNTHETIC']], cellLinks: [{ rowIndex: 0, columnIndex: 0, targetId: 'domain-popup' }] }, { name: 'Tabelle 36', title: 'Table 36 of 40', presentation: 'modal', targetId: 'domain-popup', headers: ['Property', 'Value'], rows: [['Synthetic domain property', 'Retained popup value']] }, { name: 'Properties', title: 'Tabelle 37', presentation: 'linked-inline', targetId: 'domain-extra', headers: ['Property', 'Value'], rows: [['Synthetic extra property', 'Retained inline value']] } ] }; const infrastructure: ScannerReportSection = { ...section('sectionInfrastructure', 'Infrastructure', []), tables: [{ name: 'Azure AD Connect settings', title: 'Azure AD Connect settings · Tabelle 2', headers: ['Setting', 'Value'], rows: [['Synthetic configuration', 'Enabled']] }] }; const rowCount = { ...observation('domain.row_count', 1), sectionKey: domain.sectionKey, subject: { type: 'table', label: 'Tabelle 35' }, attributes: { sourceTableIndex: 34, subsectionLabel: 'Tabelle 35' } }; const data = { ...snapshot, sections: [domain, infrastructure], observations: [rowCount] }; const retained = JSON.stringify(data); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); const expectCleanHeadings = (element: Element) => { expect(element.textContent).not.toMatch(/Source text|Tabelle \d+|Table \d+|Close Close|legacy flattened text/); }; try { await act(async () => root.render( )); await act(async () => container.querySelector('button')!.click()); const results = document.querySelector('.floating-data-window')!; expectCleanHeadings(results); expect([...results.querySelectorAll('h4, h5')].map(node => node.textContent)) .toEqual(['Domain Information', 'Infrastructure', 'Azure AD Connect settings']); expect(results.querySelectorAll('table')).toHaveLength(2); expect(results.textContent).toContain('synthetic.example'); await act(async () => results.querySelector('.client-env-table-cell-link')!.click()); const popup = document.querySelector('.client-env-table-dialog')!; expectCleanHeadings(popup); expect(popup.querySelector('h2')?.textContent).toBe('Domain Information'); expect(popup.textContent).toContain('Retained popup value'); await act(async () => popup.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))); await act(async () => results.querySelector('.client-env-table-detail-link')!.click()); expectCleanHeadings(results); expect(results.textContent).toContain('Retained inline value'); expect(results.querySelectorAll('table')).toHaveLength(3); expect(JSON.stringify(data)).toBe(retained); } finally { await act(async () => root.unmount()); container.remove(); } }); it('filters generated names from every title source while keeping native headings', () => { const original = section('sectionDomainInformation', 'Domain Information', []); const table = { name: 'Tabelle 35', title: 'Table 35', sourceTableIndex: 34, headers: [], rows: [] }; const rowCount = { ...observation('domain.row_count', 1), sectionKey: original.sectionKey, subject: { type: 'table', label: 'Table 35' }, attributes: { sourceTableIndex: 34 } }; const labelSources: EnvironmentObservation['attributes'][] = [{ sourceTableIndex: 34 }, { sourceTableIndex: 34, subsectionLabel: 'Tabelle 35' }]; for (const attributes of labelSources) { const data = { ...snapshot, observations: [{ ...rowCount, attributes }] }; expect(environmentTableTitle(data, original, table)).toBe(''); expect(environmentTableTitle(data, original, { ...table, name: 'Domain controllers' })).toBe('Domain controllers'); expect(environmentTableTitle(data, original, { ...table, title: 'Native details' })).toBe('Native details'); } }); it.each([ ['Directory', 'directory', 'sectionDomainInformation', 'Domain information'], ['Resilience', 'resilience', 'sectionAnomalies', 'Backup'], ['Inventory', 'inventory', 'sectionAdminGroups', 'Admin groups list'], ['Password policy', 'password-policy', 'sectionPasswordPolicies', 'Password policies'], ['Certificates & PKI', 'certificates', 'sectionPKI', 'Certificate Templates'], ['Operating systems', 'operating-systems', 'sectionComputerInformation', 'Operating systems'], ['Overall Score', 'all', 'report-overview', 'Scanner summary'] ])('opens %s Results with every original table directly readable', async (title, group, sectionKey, tableName) => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const original = section(sectionKey, tableName, [['Synthetic first row', 14, 'Retained evidence']]); original.tables.push({ ...original.tables[0], rows: [['Synthetic second table', 24, 'Other evidence']] }); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render( )); expect(document.querySelector('[role="dialog"]')).toBeNull(); const trigger = container.querySelector('button')!; await act(async () => trigger.click()); const results = document.querySelector('.floating-data-window')!; expect(results.querySelectorAll('table')).toHaveLength(2); expect(results.querySelectorAll('tbody')[0].textContent).toContain('Synthetic first row'); expect(results.querySelectorAll('tbody')[1].textContent).toContain('Synthetic second table'); expect(results.textContent).not.toMatch(/Open table|Source text|Table \d+ of \d+/); expect(results.querySelector('details[open]')).toBeNull(); expect(Number.parseFloat(results.style.left) + Number.parseFloat(results.style.width)).toBeLessThanOrEqual(window.innerWidth - 8); await act(async () => [...results.querySelectorAll('button')] .find(button => button.getAttribute('aria-label') === `Close ${title} window`)!.click()); expect(document.querySelector('[role="dialog"]')).toBeNull(); expect(trigger.getAttribute('aria-expanded')).toBe('false'); } finally { await act(async () => root.unmount()); container.remove(); } }); it('keeps all PKI child tables in the correct report for both native link paths', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const makeSource = (file: string, value: string): ScannerReportSection[] => [ { ...section('sectionPKI', 'PKI', []), id: `${file}-parent`, importFileId: file, detailLinks: [{ targetId: 'native-child', label: `${value} details` }], tables: [ { name: 'Certificates', presentation: 'inline', headers: ['Certificate', 'Count'], rows: [[value, 2]], cellLinks: [{ rowIndex: 0, columnIndex: 1, targetId: 'native-child' }] } ] }, { ...section('native-child', 'Certificate detail', []), id: `${file}-child`, importFileId: file, tables: [{ name: 'Properties', presentation: 'inline', headers: ['Subject'], rows: [[`${value} subject`]] }, { name: 'Delegations', presentation: 'inline', headers: ['Principal'], rows: [[`${value} principal`]] }] } ]; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render( )); await act(async () => container.querySelector('button')!.click()); const results = document.querySelector('.floating-data-window')!; expect(results.querySelectorAll('table')).toHaveLength(2); await act(async () => results.querySelectorAll('.client-env-table-cell-link')[1].click()); const popup = document.querySelector('.client-env-table-dialog')!; expect(popup.querySelectorAll('table')).toHaveLength(2); expect(popup.textContent).toContain('Second subject'); expect(popup.textContent).toContain('Second principal'); expect(popup.textContent).not.toContain('First subject'); await act(async () => popup.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))); expect(document.querySelector('.client-env-table-dialog')).toBeNull(); expect(document.querySelector('.floating-data-window')).toBe(results); await act(async () => results.querySelectorAll('.client-env-table-detail-link')[0].click()); expect(results.querySelectorAll('table')).toHaveLength(4); expect(results.textContent).toContain('First subject'); expect(results.textContent).toContain('First principal'); expect(results.textContent).not.toContain('Second subject'); } finally { await act(async () => root.unmount()); container.remove(); } }); it('shows PingCastle base tables immediately, opens cell links as dialogs and outside links inline', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const adminSection: ScannerReportSection = { ...section('sectionAdminGroups', 'Admin Groups', []), tables: [ { name: 'Admin groups list', presentation: 'inline', headers: ['Group Name', 'Nb Admins'], rows: [['Enterprise Administrators', 1], ['Operator graph', 1], ['2', 2]], cellLinks: [{ rowIndex: 0, columnIndex: 0, targetId: 'modalEnterprise-Administrators' }, { rowIndex: 1, columnIndex: 0, targetId: 'mcg-Operators' }, { rowIndex: 2, columnIndex: 0, targetId: 'sectionAdminGroupsChild' }] }, { name: 'Admin groups detail', presentation: 'modal', targetId: 'modalEnterprise-Administrators', title: 'Enterprise Administrators', headers: ['SamAccountName'], rows: [['Administrator']], cellLinks: [{ rowIndex: 0, columnIndex: 0, targetId: 'modalMember' }] }, { name: 'Member detail', presentation: 'modal', targetId: 'modalMember', title: 'Administrator', headers: ['Account'], rows: [['Administrator detail']] }, { name: 'Admin groups detail', presentation: 'linked-inline', targetId: 'allprivileged', headers: ['SamAccountName'], rows: [['Administrator']] } ], detailLinks: [{ targetId: 'allprivileged', label: 'All users in Admins groups' }, { targetId: 'sectionAdminGroupsChild', label: 'Extra accounts' }], modalDetails: [{ targetId: 'mcg-Operators', title: 'Operator graph', prose: 'Graph legend.', interactiveGraph: true }] }; const childSection: ScannerReportSection = { ...section('sectionAdminGroupsChild', 'Extra accounts', []), parentSectionKey: 'sectionAdminGroups', tables: [{ name: 'Extra accounts', presentation: 'linked-inline', targetId: 'sectionAdminGroupsChild', headers: ['Account'], rows: [['Second account']] }] }; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); expect(container.querySelectorAll('.client-env-evidence-table')).toHaveLength(1); expect([...container.querySelectorAll('.client-env-evidence h3, .client-env-evidence h4, .client-env-evidence h5')] .map(heading => heading.textContent)).toEqual(['Admin Groups', 'Admin groups list']); expect(container.textContent).not.toContain('Original report sections'); expect(container.textContent).not.toContain('Original report table'); expect(container.textContent).not.toContain('Second account'); expect(container.textContent).toContain('Enterprise Administrators'); expect([...container.querySelectorAll('tbody td')].map(cell => cell.textContent)).not.toContain('Administrator'); await act(async () => container.querySelector('.client-env-table-cell-link')?.click()); const dialog = document.querySelector('.client-env-table-dialog')!; expect(dialog.querySelector('h2')?.textContent).toBe('Enterprise Administrators'); expect(dialog.textContent).toContain('Administrator'); await act(async () => dialog.querySelector('.client-env-table-cell-link')?.click()); expect(document.querySelector('.client-env-table-dialog h2')?.textContent).toBe('Administrator'); expect(document.querySelector('.client-env-table-dialog')?.textContent).toContain('Administrator detail'); await act(async () => document.querySelector('[aria-label="Close Administrator table"]')?.click()); expect(document.querySelector('.client-env-table-dialog h2')?.textContent).toBe('Enterprise Administrators'); await act(async () => document.querySelector('[aria-label="Close Enterprise Administrators table"]')?.click()); expect(document.querySelector('.client-env-table-dialog')).toBeNull(); await act(async () => container.querySelectorAll('.client-env-table-cell-link')[1]?.click()); expect(document.querySelector('.client-env-table-dialog')?.textContent).toContain('Graph legend.'); expect(document.querySelector('.client-env-table-dialog')?.textContent).toContain('original PingCastle report'); await act(async () => document.querySelector('[aria-label="Close Operator graph detail"]')?.click()); await act(async () => container.querySelectorAll('.client-env-table-cell-link')[2]?.click()); expect(document.querySelector('.client-env-table-dialog h2')?.textContent).toBe('Extra accounts'); expect(document.querySelector('.client-env-table-dialog')?.textContent).toContain('Second account'); await act(async () => document.querySelector('[aria-label="Close Extra accounts detail"]')?.click()); expect(container.textContent).not.toContain('Second account'); await act(async () => container.querySelector('.client-env-table-detail-link')?.click()); expect(container.querySelectorAll('.client-env-evidence-table')).toHaveLength(2); expect(container.textContent).toContain('Administrator'); expect(document.querySelector('.client-env-table-dialog')).toBeNull(); await act(async () => [...container.querySelectorAll('.client-env-table-detail-link')] .find(button => button.textContent === 'Extra accounts')?.click()); expect(container.textContent).toContain('Second account'); } finally { await act(async () => root.unmount()); container.remove(); } }); it('uses only the matching retained subsection heading and fails closed on conflicting evidence', () => { const userSection = { ...section('sectionUserInformation', 'Honey Pot', []), tables: [ { name: 'Account list', sourceTableIndex: 38, headers: ['Name'], rows: [['Dummy']] }, { name: 'Tabelle 40', sourceTableIndex: 39, headers: ['Nb User Accounts'], rows: [[590]] } ] }; const honeyPot = { ...observation('pingcastle.honeypot.name', 'Dummy'), sectionKey: userSection.sectionKey, attributes: { sourceTableIndex: 38, subsectionLabel: 'Honey Pot' } }; const accountAnalysis = { ...observation('pingcastle.account.count', 590), sectionKey: userSection.sectionKey, attributes: { sourceTableIndex: 39, subsectionLabel: 'Account analysis' } }; const matchingSnapshot = { ...snapshot, sections: [userSection], observations: [honeyPot, accountAnalysis] }; expect(environmentSectionTitle(matchingSnapshot, userSection)).toBe('User Information'); expect(environmentTableSubsection(matchingSnapshot, userSection, userSection.tables[0])).toBe('Honey Pot'); expect(environmentTableSubsection(matchingSnapshot, userSection, userSection.tables[1])).toBe('Account analysis'); expect(environmentTableSubsection({ ...matchingSnapshot, observations: [honeyPot, { ...accountAnalysis, importFileId: 'other-file' }] }, userSection, userSection.tables[1])).toBeUndefined(); expect(environmentTableSubsection({ ...matchingSnapshot, observations: [honeyPot, accountAnalysis, { ...accountAnalysis, id: 'conflict', attributes: { sourceTableIndex: 39, subsectionLabel: 'Different heading' } }] }, userSection, userSection.tables[1])).toBeUndefined(); }); it('uses the retained PingCastle heading instead of a technical section key', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const trusts = { ...section('sectionTrustsdetails', 'Discovered Domains', []), prose: 'Trusts details This section focuses on the relations that this domain has with other domains', tables: [{ name: 'Tabelle 1', presentation: 'inline' as const, headers: ['Trust Partner'], rows: [['example.local']] }] }; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); expect(container.querySelector('.client-env-evidence-section h4')?.textContent).toBe('Trusts details'); expect(container.querySelector('.client-env-evidence-prose')?.textContent).toBe('This section focuses on the relations that this domain has with other domains'); expect([...container.querySelectorAll('h4, h5')].some(heading => heading.textContent?.includes('Trustsdetails'))).toBe(false); } finally { await act(async () => root.unmount()); container.remove(); } }); it('keeps category evidence separate while Source Overview retains every original source section and observation', () => { expect(environmentEvidenceForGroup(snapshot, 'password-policy').sections.map(item => item.sectionKey)).toEqual(['sectionPasswordPolicies']); expect(environmentEvidenceForGroup(snapshot, 'password-policy').observations.map(item => item.property)).toEqual(['password_policy.minimum_length']); expect(environmentEvidenceForGroup(snapshot, 'certificates').sections.map(item => item.sectionKey)).toEqual(['sectionPKI']); expect(environmentEvidenceForGroup(snapshot, 'inventory').observations.map(item => item.property)).toContain('domain.controller_count'); expect(environmentEvidenceForGroup(snapshot, 'source-metrics').observations.map(item => item.property)).toContain('entra.tenant_name'); expect(environmentEvidenceForGroup(snapshot, 'operating-systems').sections[0].tables[0].rows).toEqual([['Windows Server', 7, 'Unsupported count']]); expect(environmentEvidenceForGroup(snapshot, 'all').sections).toHaveLength(snapshot.sections.length); expect(environmentEvidenceForGroup(snapshot, 'all').observations).toHaveLength(snapshot.observations.length); }); it('shows only original report values while retaining observation mapping internally', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const userSection = { ...section('sectionUserInformation', 'Honey Pot', []), tables: [ { name: 'Account list', sourceTableIndex: 38, headers: ['Name'], rows: [['Decoy']] }, { name: 'Tabelle 40', sourceTableIndex: 39, headers: ['Nb User Accounts'], rows: [[590]] } ] }; const linked = { ...observation('account.count', 590), sectionKey: userSection.sectionKey, attributes: { sourceTableIndex: 39, subsectionLabel: 'Account analysis' } }; const decoy = { ...observation('honeypot.name', 'Decoy'), sectionKey: userSection.sectionKey, attributes: { sourceTableIndex: 38, subsectionLabel: 'Honey Pot' } }; const unrelated = { ...observation('other.count', 10), sectionKey: userSection.sectionKey, importFileId: 'other-file', attributes: { sourceTableIndex: 39 } }; const ambiguous = { ...observation('section.count', 2), sectionKey: userSection.sectionKey }; const data = { ...snapshot, sections: [userSection], observations: [decoy, linked, unrelated, ambiguous] }; const partition = partitionEnvironmentObservations(data.sections, data.observations); expect(partition.bySection.get(userSection.id)?.tables[0]).toEqual([decoy]); expect(partition.bySection.get(userSection.id)?.tables[1]).toEqual([linked]); expect(partition.bySection.get(userSection.id)?.sectionObservations).toEqual([ambiguous]); expect(partition.unassigned).toEqual([unrelated]); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); const tables = [...container.querySelectorAll('.client-env-evidence-table')]; expect(tables).toHaveLength(2); expect(tables[0].querySelector('h5')?.textContent).toBe('Honey Pot'); expect(tables[1].querySelector('h5')?.textContent).toBe('Account analysis'); expect(tables[1].textContent).not.toContain('Original report table'); expect(tables[1].textContent).not.toContain('mapped observations'); expect(tables[0].textContent).not.toContain('account.count'); expect(tables[1].querySelector('table')?.textContent).toContain('590'); expect(tables[1].textContent).not.toContain('Source observations'); expect(tables[1].textContent).not.toContain('account.count'); expect(container.querySelector('.client-env-table-open')).toBeNull(); expect(container.querySelector('.client-env-evidence-section h4')?.textContent).toBe('User Information'); expect(container.textContent).not.toContain('Source observations'); expect(container.textContent).not.toContain('Source tool: Dummy scanner'); expect(container.textContent).not.toContain('other.count'); expect(container.textContent).not.toContain('section.count'); } finally { await act(async () => root.unmount()); container.remove(); } }); it('shows original table rows without technical source metadata', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); expect(container.querySelector('table')?.textContent).toContain('Minimum length'); expect(container.querySelector('table')?.textContent).toContain('History'); expect(container.querySelector('table')?.textContent).toContain('GPO B'); expect(container.querySelector('.client-env-evidence-prose')).toBeNull(); expect(snapshot.sections[0].prose).toBe('Password policies source prose'); expect(container.textContent).not.toContain('dummy-run'); expect(container.textContent).not.toContain('dummy-1'); expect(container.textContent).not.toContain('Enrollment enabled'); await act(async () => root.render()); expect(container.textContent).toContain('Unmapped report evidence'); expect(container.textContent).not.toContain('keep me'); expect(container.textContent).not.toContain('domain.count'); } finally { await act(async () => root.unmount()); container.remove(); } }); it('retains repeated native titles and complete tables without inventing ordinals or group names', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const repeated = { ...section('sectionAdminGroups', 'Admin Groups', []), tables: [ { name: 'Admin groups detail', headers: ['SamAccountName'], rows: [['Administrator']] }, { name: 'Admin groups detail', headers: ['SamAccountName'], rows: [['Other member']] } ] }; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); const tables = [...container.querySelectorAll('.client-env-evidence-table')]; expect(tables.map(table => table.querySelector('h5')?.textContent)).toEqual([ 'Admin groups detail', 'Admin groups detail' ]); expect(tables[1].querySelector('table')?.textContent).toContain('Other member'); expect(tables[1].textContent).not.toContain('Administrator'); expect(tables[0].querySelector('table')?.textContent).toContain('Administrator'); } finally { await act(async () => root.unmount()); container.remove(); } }); it('renders historical PingCastle Admin Groups as a readable overview with retained source tables', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const pingcastle = { id: 'pingcastle', name: 'PingCastle' }; const adminSection: ScannerReportSection = { ...section('sectionAdminGroups', 'Groups', []), tool: pingcastle, prose: 'Admin Groups This section is focused on groups critical for admin activities. Close Close All users in Admins groups [33] This feature is reserved for licensed customers. All delegations [209]', tables: [ { name: 'Admin groups list', sourceTableIndex: 54, headers: ['Group Name', 'Nb Admins'], rows: [['Domain Admins', 33]] }, { name: 'Admin groups detail', sourceTableIndex: 55, headers: ['SamAccountName'], rows: [['Administrator']] }, { name: 'Admin groups detail', sourceTableIndex: 56, headers: ['SamAccountName'], rows: [['Second administrator']] }, { name: 'Delegations list', sourceTableIndex: 62, headers: ['OU'], rows: [['Servers']] } ] }; const linked = [54, 55, 56].map(index => ({ ...observation(`pingcastle.admin.${index}`, 1), tool: pingcastle, sectionKey: 'sectionAdminGroups', attributes: { sourceTableIndex: index, subsectionLabel: 'Groups' } })); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); expect(container.querySelector('.client-env-evidence-section h4')?.textContent).toBe('Admin Groups'); expect(container.querySelector('.client-env-evidence-section > .client-env-evidence-prose')).toBeNull(); expect(container.querySelector('.client-env-evidence-source-text')).toBeNull(); expect([...container.querySelectorAll('.client-env-evidence-table h5')].map(node => node.textContent)).toEqual([ 'Admin groups list', 'Admin groups detail', 'Admin groups detail', 'Delegations list' ]); expect(container.querySelector('.client-env-table-inline-scroll')?.textContent).toContain('Domain Admins'); expect(container.querySelectorAll('.client-env-table-inline-scroll')).toHaveLength(4); expect(container.querySelectorAll('table')[2].textContent).toContain('Second administrator'); expect(container.textContent).not.toMatch(/Source text|Close Close/); expect(adminSection.prose).toContain('Close Close'); } finally { await act(async () => root.unmount()); container.remove(); } }); it('keeps repeated legacy PingCastle graph controls out of Inventory prose while retaining the source', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const graphSection: ScannerReportSection = { ...section('sectionControlPathsAnalysis', 'Control Paths Analysis', []), tool: { id: 'pingcastle', name: 'PingCastle' }, prose: 'Control Paths Analysis This section focuses on permissions issues. 0% Legend: u user m computer Close Close 0% Legend: u user m computer Close Close' }; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); expect(container.querySelector('.client-env-evidence-section > .client-env-evidence-prose')).toBeNull(); expect(container.querySelector('.client-env-evidence-source-text')).toBeNull(); expect(container.textContent).not.toMatch(/Source text|Close Close|Legend:/); expect(graphSection.prose).toContain('Close Close'); } finally { await act(async () => root.unmount()); container.remove(); } }); it('shows a named source table directly with search and complete pagination while preserving the original columns', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const longSnapshot = { ...snapshot, sections: [section('sectionPasswordPolicies', 'Long policies', Array.from({ length: 101 }, (_, index) => [`Rule ${index + 1}`, index, `Evidence ${index + 1}`]))], observations: [] }; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); const dialog = container.querySelector('.client-env-evidence-table')!; expect(dialog.querySelector('h5')?.textContent).toBe('Long policies'); expect([...dialog.querySelectorAll('thead th')].map(cell => cell.textContent)).toEqual([ 'Technical setting', 'Configured value', 'Extra evidence' ]); expect(dialog.textContent).toContain('Rule 25'); expect(dialog.textContent).not.toContain('Rule 26'); const next = [...dialog.querySelectorAll('button')].find(button => button.textContent === 'Next')!; for (let page = 0; page < 4; page++) await act(async () => next.click()); expect(dialog.textContent).toContain('Rule 101'); expect(dialog.textContent).toContain('Showing 101–101 of 101 rows'); const search = dialog.querySelector('input[type="search"]')!; await act(async () => { Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(search, 'Rule 42'); search.dispatchEvent(new Event('input', { bubbles: true })); }); expect(dialog.textContent).toContain('Rule 42'); expect(dialog.textContent).not.toContain('Rule 101'); expect(document.querySelector('.client-env-table-dialog')).toBeNull(); } finally { await act(async () => root.unmount()); container.remove(); } }); it('resizes a linked Admin Groups table popup from corners and keeps it inside the viewport', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const adminSection: ScannerReportSection = { ...section('sectionAdminGroups', 'Admin Groups', []), tables: [ { name: 'Admin groups list', presentation: 'inline', headers: ['Group Name'], rows: [['Domain Administrators']], cellLinks: [{ rowIndex: 0, columnIndex: 0, targetId: 'modalDomain-Administrators' }] }, { name: 'Admin groups detail', presentation: 'modal', targetId: 'modalDomain-Administrators', title: 'Domain Administrators', headers: ['SamAccountName', 'Enabled'], rows: [['Administrator', 'YES']] } ] }; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); const pointer = (type: string, x: number, y: number) => { const event = new MouseEvent(type, { bubbles: true, cancelable: true, clientX: x, clientY: y }); Object.defineProperty(event, 'pointerId', { value: 1 }); return event; }; try { await act(async () => root.render()); await act(async () => container.querySelector('.client-env-table-cell-link')?.click()); const dialog = document.querySelector('.client-env-table-dialog')!; expect(dialog.querySelector('h2')?.textContent).toBe('Domain Administrators'); expect(dialog.querySelectorAll('.results-window-resize')).toHaveLength(8); vi.spyOn(dialog, 'getBoundingClientRect').mockReturnValue({ left: 100, top: 100, width: 700, height: 400, right: 800, bottom: 500, x: 100, y: 100, toJSON: () => ({}) }); const corner = dialog.querySelector('.results-window-resize-se')!; corner.setPointerCapture = vi.fn(); corner.hasPointerCapture = vi.fn().mockReturnValue(true); corner.releasePointerCapture = vi.fn(); await act(async () => { corner.dispatchEvent(pointer('pointerdown', 800, 500)); corner.dispatchEvent(pointer('pointermove', 920, 600)); corner.dispatchEvent(pointer('pointerup', 920, 600)); }); expect(dialog.style.width).toBe('820px'); expect(dialog.style.height).toBe('500px'); expect(dialog.style.left).toBe('100px'); expect(dialog.style.top).toBe('100px'); const oppositeCorner = dialog.querySelector('.results-window-resize-nw')!; oppositeCorner.setPointerCapture = vi.fn(); oppositeCorner.hasPointerCapture = vi.fn().mockReturnValue(true); oppositeCorner.releasePointerCapture = vi.fn(); await act(async () => { oppositeCorner.dispatchEvent(pointer('pointerdown', 100, 100)); oppositeCorner.dispatchEvent(pointer('pointermove', 50, 60)); oppositeCorner.dispatchEvent(pointer('pointerup', 50, 60)); }); expect(dialog.style.left).toBe('50px'); expect(dialog.style.top).toBe('60px'); expect(dialog.style.width).toBe('750px'); expect(dialog.style.height).toBe('440px'); await act(async () => { corner.dispatchEvent(pointer('pointerdown', 800, 500)); corner.dispatchEvent(pointer('pointermove', 3000, 3000)); corner.dispatchEvent(pointer('pointerup', 3000, 3000)); }); expect(Number.parseInt(dialog.style.left) + Number.parseInt(dialog.style.width)).toBeLessThanOrEqual(window.innerWidth - 8); expect(Number.parseInt(dialog.style.top) + Number.parseInt(dialog.style.height)).toBeLessThanOrEqual(window.innerHeight - 8); expect([...dialog.querySelectorAll('thead th')].map(cell => cell.textContent)).toEqual(['SamAccountName', 'Enabled']); } finally { await act(async () => root.unmount()); container.remove(); } }); });