// @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(); }
});
});