// @vitest-environment jsdom import { act, createElement } from 'react'; import { createRoot } from 'react-dom/client'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it, vi } from 'vitest'; import type { EnvironmentObservation, EnvironmentSnapshot } from '../../src/domain/environment-context'; import { operatingSystemGroup, operatingSystemInventory, operatingSystemEolCount } from '../../src/features/dashboard/operating-system-inventory'; import { OperatingSystemInventory } from '../../src/features/dashboard/OperatingSystemInventory'; function os(name: string, count: unknown, row: number, run = 'run'): EnvironmentObservation[] { return [['operating_system.name', name], ['operating_system.count', count]].map(([property, raw], column) => ({ version: 1, id: `${run}:${row}:${column}`, scanRunId: run, importFileId: `${run}:file`, tool: { id: 'pingcastle', name: 'PingCastle' }, subject: { type: 'computer-information.operating-systems', identity: name }, property: String(property), label: column ? 'Nb OS' : 'Operating system', originalValue: raw, normalizedValue: raw, valueType: 'string', state: 'observed', sectionKey: 'sectionComputerInformation', source: { rowIndex: row, columnIndex: column }, attributes: { tableName: 'Operating systems', sourceTableIndex: 0 }, mappingVersion: '1' })) as EnvironmentObservation[]; } function osStates(row: number, values: Record<'Active' | 'Inactive' | 'Enabled' | 'Disabled', unknown>, run = 'run'): EnvironmentObservation[] { return Object.entries(values).map(([label, raw], column) => ({ version: 1, id: `${run}:${row}:state:${column}`, scanRunId: run, importFileId: `${run}:file`, tool: { id: 'pingcastle', name: 'PingCastle' }, subject: { type: 'computer-information.operating-systems', identity: `row-${row}` }, property: `pingcastle.sectioncomputerinformation.operating.system.list.nb.${label.toLowerCase()}`, label: `Nb ${label}`, originalValue: raw, normalizedValue: raw, valueType: 'string', state: 'observed', sectionKey: 'sectionComputerInformation', source: { rowIndex: row, columnIndex: column + 2 }, attributes: { tableName: 'Operating systems', sourceTableIndex: 0 }, mappingVersion: '1' })) as EnvironmentObservation[]; } function snapshot(observations: EnvironmentObservation[]): EnvironmentSnapshot { return { version: 1, projectId: 'synthetic', scanRunIds: ['run'], generatedAt: '', sections: [], observations, coverage: { observed: observations.length, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 } }; } describe('operating system inventory', () => { it.each(['dark', 'light'])('omits EOL OS from the tile while preserving original Results evidence in %s', theme => { document.documentElement.dataset.theme = theme; (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1; }); const eol: EnvironmentObservation = { ...os('Windows 11', 2, 1)[1], id: 'eol', label: 'EOL OS', property: 'pingcastle.sectioncomputerinformation.account.analysis.eol.os', originalValue: '2', normalizedValue: 2, source: { objectPath: 'synthetic/account-analysis/eol', rowIndex: 1, columnIndex: 0 }, attributes: { tableName: 'Account analysis', sourceTableIndex: 1 } }; const source = snapshot([...os('Windows Server 2019', 1, 1), eol]); source.sections = [{ version: 1, id: 'computer', scanRunId: 'run', importFileId: 'run:file', tool: eol.tool, sectionKey: 'sectionComputerInformation', label: 'Computer Information', ordinal: 1, state: 'observed', tables: [{ name: 'Account analysis', sourceTableIndex: 1, headers: ['EOL OS'], rows: [['2']] }], source: { objectPath: 'synthetic/account-analysis' }, extractorVersion: '2.2.1', attributes: {} }]; const before = JSON.stringify(source); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { act(() => root.render(createElement(OperatingSystemInventory, { groups: operatingSystemInventory(source), snapshot: source }))); expect(container.textContent).not.toContain('EOL OS'); expect([...container.querySelectorAll('.client-env-facts dt')].map(node => node.textContent)) .toEqual(['Servers', 'Windows Server 2019']); expect(container.querySelector('.client-env-facts dd')?.textContent).toBe('1'); act(() => container.querySelector('[aria-label="Open Operating systems results"]')!.click()); expect(document.body.querySelector('.floating-data-window')?.textContent).toContain('EOL OS'); expect(document.body.querySelector('.floating-data-window')?.textContent).toContain('Account analysis'); expect(eol.source.objectPath).toBe('synthetic/account-analysis/eol'); expect(JSON.stringify(source)).toBe(before); } finally { act(() => root.unmount()); container.remove(); vi.unstubAllGlobals(); } }); it.each(['dark', 'light'])('keeps the empty tile free of an EOL placeholder in %s', theme => { document.documentElement.dataset.theme = theme; const markup = renderToStaticMarkup(createElement(OperatingSystemInventory, { groups: operatingSystemInventory() })); expect(markup).toContain('Operating systems'); expect(markup).toContain('Results'); expect(markup).toContain('
Inventory
—
'); expect(markup).not.toContain('EOL OS'); }); it('keeps missing, invalid and multiple EOL totals unknown and preserves explicit zero', () => { const eol = { ...os('Windows 11', 0, 1)[1], label: 'Nb EOL OS' }; expect(operatingSystemEolCount(snapshot([eol]))).toBe(0); expect(operatingSystemEolCount(snapshot(os('Windows XP', 99, 1)))).toBeUndefined(); for (const raw of ['', null, -1, 'invalid', true]) { expect(operatingSystemEolCount(snapshot([{ ...eol, originalValue: raw, normalizedValue: raw }]))).toBeUndefined(); } expect(operatingSystemEolCount(snapshot([eol, { ...eol, scanRunId: 'other' }]))).toBeUndefined(); expect(operatingSystemEolCount(snapshot([eol, { ...eol, originalValue: 1, normalizedValue: 1 }]))).toBeUndefined(); expect(operatingSystemEolCount(snapshot([{ ...eol, state: 'not-assessed' }]))).toBeUndefined(); expect(operatingSystemEolCount(snapshot([{ ...eol, sectionKey: 'sectionUserInformation' }]))).toBeUndefined(); }); it('splits server/client products, retains other systems and pairs exact native counts including zero', () => { const source = snapshot([...os('Windows Server 2022', 12, 1), ...os('Windows 11 24H2', '1,200', 2), ...os('Windows 10 22H2', 0, 3), ...os('Debian', 4, 4), ...os('OperatingSystem not set', 3, 5)]); const before = JSON.stringify(source); const groups = operatingSystemInventory(source); expect(groups.map(group => [group.label, group.total])).toEqual([['Servers', 12], ['Clients', 1200], ['Other / unclassified', 7]]); expect(groups[1].entries.map(entry => entry.count)).toEqual([1200, 0]); expect(renderToStaticMarkup(createElement(OperatingSystemInventory, { groups }))).toContain('1,200'); expect(JSON.stringify(source)).toBe(before); }); it('does not infer Linux roles, supports explicit roles and preserves conflicting classifications', () => { expect(operatingSystemGroup('Debian')).toBe('Other / unclassified'); expect(operatingSystemGroup('Debian', ['Server'])).toBe('Servers'); expect(operatingSystemGroup('Windows 11', ['Server', 'Workstation'])).toBe('Other / unclassified'); expect(operatingSystemGroup('Windows 2019')).toBe('Servers'); expect(operatingSystemGroup('Windows 2000')).toBe('Other / unclassified'); }); it('keeps missing, null, negative, boolean and malformed counts unknown rather than zero', () => { for (const raw of [undefined, null, '', -1, true, '1.5', '1,2', 'unknown']) { const group = operatingSystemInventory(snapshot(os('Windows 11', raw, 1)))[1]; expect(group.entries[0].count).toBeUndefined(); expect(group.total).toBeUndefined(); } expect(operatingSystemInventory().every(group => group.total === undefined)).toBe(true); }); it('never borrows counts from another report, table or source row', () => { const fields = os('Windows 11', 2, 1); fields[1].source.rowIndex = 2; const foreign = os('Windows Server 2022', 99, 1, 'foreign'); const groups = operatingSystemInventory(snapshot([...fields, ...foreign])); expect(groups[1].entries[0].count).toBeUndefined(); expect(groups[0].total).toBe(99); fields[1].source.rowIndex = 1; fields[1].attributes.sourceTableIndex = 2; expect(operatingSystemInventory(snapshot(fields))[1].total).toBeUndefined(); }); it('does not sum duplicate OS rows or overlapping inventories and invalidates on changed inputs', () => { const rows = [...os('Windows 11', 4, 1), ...os('Windows 11', 4, 2)]; expect(operatingSystemInventory(snapshot(rows))[1].total).toBeUndefined(); const groups = operatingSystemInventory(snapshot([...os('Windows 11', 4, 1), ...os('Windows 11', 7, 1, 'other')])); expect(groups[1].total).toBeUndefined(); expect(groups[1].entries.map(entry => entry.count)).toEqual([7, 4]); expect(renderToStaticMarkup(createElement(OperatingSystemInventory, { groups }))).not.toContain('overlapping counts are not added'); expect(operatingSystemInventory(snapshot(os('Windows 11', 8, 1)))[1].total).toBe(8); }); it('shows the complete quantified inventory total when another inventory reports names without counts', () => { const groups = operatingSystemInventory(snapshot([ ...os('Windows Server 2022', 40, 1), ...os('Windows Server 2025', 19, 2), ...os('Windows Server 2016', 10, 3), ...os('Windows Server 2019', 3, 4), ...os('Windows 2022', undefined, 1, 'other') ])); expect(groups[0].total).toBe(72); expect(groups[0].entries.find(entry => entry.name === 'Windows 2022')?.count).toBeUndefined(); }); it('shows PingCastle active, inactive, enabled and disabled counts from the exact OS source row', () => { const groups = operatingSystemInventory(snapshot([ ...os('Windows Server 2022', 12, 1), ...osStates(1, { Active: 10, Inactive: 2, Enabled: 9, Disabled: 3 }), ...os('Windows 11', 8, 2) ])); expect(groups[0].entries[0]).toMatchObject({ count: 12, active: 10, inactive: 2, enabled: 9, disabled: 3 }); expect(groups[1].entries[0]).toMatchObject({ count: 8, active: undefined, inactive: undefined, enabled: undefined, disabled: undefined }); (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1; }); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); act(() => root.render(createElement(OperatingSystemInventory, { groups }))); expect(container.querySelectorAll('.client-env-facts > div')).toHaveLength(4); act(() => container.querySelector('[aria-label="Open Operating systems results"]')?.click()); const table = document.body.querySelector('.floating-data-window .client-os-group table'); ['Active', 'Inactive', 'Enabled', 'Disabled'].forEach(label => expect(table?.textContent).toContain(label)); expect(table?.textContent).toContain('10'); expect(table?.textContent).toContain('3'); expect(document.body.querySelector('.floating-data-window')?.textContent).toContain('—'); act(() => root.unmount()); container.remove(); }); it('does not borrow or guess PingCastle state counts across source rows', () => { const groups = operatingSystemInventory(snapshot([ ...os('Windows Server 2022', 12, 1), ...osStates(2, { Active: 10, Inactive: 2, Enabled: 9, Disabled: 3 }) ])); expect(groups[0].entries[0]).toMatchObject({ count: 12, active: undefined, inactive: undefined, enabled: undefined, disabled: undefined }); }); it('opens the table in a movable window, maximizes it and restores focus after closing', () => { (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1; }); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); const groups = operatingSystemInventory(snapshot([ ...os('Windows Server 2022', 12, 1), ...os('Windows Server 2025', 4, 1, 'other') ])); act(() => root.render(createElement(OperatingSystemInventory, { groups }))); const trigger = container.querySelector('[aria-label="Open Operating systems results"]'); expect(trigger).not.toBeNull(); expect(trigger?.textContent).toBe('Results'); expect(trigger?.classList.contains('results-btn')).toBe(true); act(() => trigger?.click()); expect(trigger?.classList.contains('is-open')).toBe(true); const floating = document.body.querySelector('.floating-data-window'); expect(floating?.getAttribute('aria-modal')).toBe('false'); expect(floating?.textContent).toContain('Windows Server 2022'); expect(floating?.querySelector('.client-os-source')?.textContent).toBe('Inventory'); act(() => floating?.querySelector('.floating-data-window-head')?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))); expect(floating?.classList.contains('is-maximized')).toBe(true); expect(floating?.querySelector('[aria-label="Restore Operating systems window"]')).not.toBeNull(); act(() => floating?.querySelector('[aria-label="Close Operating systems window"]')?.click()); expect(document.body.querySelector('.floating-data-window')).toBeNull(); expect(document.activeElement).toBe(trigger); act(() => root.unmount()); container.remove(); vi.unstubAllGlobals(); }); });