102 lines
7.0 KiB
TypeScript
102 lines
7.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { afterEach, expect, it, vi } from 'vitest';
|
|
import { useApp } from '../../src/app/AppProvider';
|
|
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
|
|
import { AssessorApiClient } from '../../src/api/assessor-api-client';
|
|
import { DashboardPanel, DashboardTopFact } from '../../src/features/dashboard/DashboardPanel';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
import { genericProfile } from '../../src/profiles/generic/generic-profile';
|
|
import type { EnvironmentSnapshot } from '../../src/domain/environment-context';
|
|
|
|
vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() }));
|
|
vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => <span>Synthetic timeslot</span> }));
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); });
|
|
|
|
it('opens an unknown fact without inventing a zero or source rows', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
|
try {
|
|
await act(async () => root.render(<DashboardTopFact label="User Accounts" glyph="user" evidenceGroup="user-accounts" />));
|
|
const trigger = host.querySelector<HTMLButtonElement>('button')!;
|
|
expect(trigger.querySelector('strong')!.textContent).toBe('—');
|
|
await act(async () => trigger.click());
|
|
const popup = document.querySelector('.floating-data-window')!;
|
|
expect(popup.querySelector('dd')!.textContent).toBe('—');
|
|
expect(popup.textContent).toContain('No retained Client Information available');
|
|
expect(popup.querySelector('table')).toBeNull();
|
|
} finally { act(() => root.unmount()); }
|
|
});
|
|
|
|
it.each(['dark', 'light'])('opens the six actual Data facts with their own retained results and preserves Work in %s', async theme => {
|
|
document.documentElement.dataset.theme = theme;
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} });
|
|
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1; });
|
|
localStorage.clear(); resetEnginePreferencesForTests(); setEngineEnabled('context', false);
|
|
const sheet = makeSheet('Synthetic', [['Name', 'Status'], ['Control', 'Failed']], '', 'synthetic.csv', { isProjectSheet: true });
|
|
vi.mocked(useApp).mockReturnValue({ dataset: { fileName: 'Synthetic client', sheets: [sheet], scanTool: 'Generic' },
|
|
activeSheet: sheet, activeProfile: genericProfile, reviews: {}, targetProjectId: 'synthetic-client',
|
|
activeAssessmentIds: ['slot'], activeScanRunIds: ['run'], facetFilters: {},
|
|
setStatusFilter: vi.fn(), setFacetFilter: vi.fn(), setPathFindingIds: vi.fn(), resetFilters: vi.fn(),
|
|
setView: vi.fn(), setActionOnly: vi.fn(), setActiveSheet: vi.fn()
|
|
} as unknown as ReturnType<typeof useApp>);
|
|
const tool = { id: 'synthetic', name: 'Synthetic scanner' };
|
|
const facts: Array<[string, string | number]> = [
|
|
['domain.count', 1], ['domain.fqdn', 'synthetic.example'], ['entra.tenant_name', 'Synthetic tenant'],
|
|
['domain.controller_count', 3], ['gpo.count', 277]
|
|
];
|
|
const snapshot: EnvironmentSnapshot = { version: 1, projectId: 'synthetic-client', scanRunIds: ['run'],
|
|
generatedAt: '2026-10-02T12:00:00Z', coverage: { observed: 5, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 },
|
|
observations: facts.map(([property, value]) => ({ version: 1, id: property, scanRunId: 'run', importFileId: 'file', tool,
|
|
subject: { type: 'domain' }, property, label: property, originalValue: value, normalizedValue: value,
|
|
valueType: typeof value === 'number' ? 'number' : 'string', state: 'observed', source: { rowIndex: 0 }, mappingVersion: '1', attributes: {} })),
|
|
sections: [
|
|
['sectionUserInformation', 'User source', 'Nb user accounts', 590, 'Synthetic users'],
|
|
['sectionComputerInformation', 'Computer source', 'Nb computer accounts', 341, 'Synthetic computers'],
|
|
['sectionDomainControllers', 'Controller source', 'DC count', 3, 'Synthetic controllers'],
|
|
['sectionGPO', 'GPO source', 'GPO count', 277, 'Synthetic policies']
|
|
].map(([key, label, header, count, marker]) => ({ version: 1, id: String(key), scanRunId: 'run', importFileId: 'file', tool,
|
|
sectionKey: String(key), label: String(label), ordinal: 1, state: 'observed',
|
|
tables: [{ name: String(label), headers: [String(header), 'Original evidence'], rows: [[count, marker]] }],
|
|
source: { rowIndex: 0 }, extractorVersion: '1', attributes: {} }))
|
|
};
|
|
const original = JSON.stringify(snapshot);
|
|
const request = vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(snapshot);
|
|
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [
|
|
{ artifactId: 'backup-a', fileName: 'Synthetic backup.zip', processingStatus: 'complete', gpoCount: 277, updatedAt: '2026-10-02T12:00:00Z' }
|
|
] } as never);
|
|
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
|
const onOpenWork = vi.fn();
|
|
try {
|
|
await act(async () => root.render(<DashboardPanel onOpenWork={onOpenWork} />));
|
|
expect(host.querySelectorAll('button.client-data-fact')).toHaveLength(6);
|
|
for (const [label, count, evidence] of [
|
|
['AD Domains', '1', 'synthetic.example'], ['Entra Tenant', '1', 'Synthetic tenant'],
|
|
['User Accounts', '590', 'Synthetic users'], ['Computer Objects', '341', 'Synthetic computers'],
|
|
['Domain Controllers', '3', 'Synthetic controllers'], ['GPOs', '277', 'Synthetic backup.zip']
|
|
]) {
|
|
const trigger = host.querySelector<HTMLButtonElement>(`[aria-label="Open ${label} results"]`)!;
|
|
expect(trigger.querySelector('strong')!.textContent).toBe(count);
|
|
expect(trigger.getAttribute('aria-haspopup')).toBe('dialog');
|
|
await act(async () => trigger.click());
|
|
const popup = document.querySelector<HTMLElement>('.floating-data-window')!;
|
|
expect(popup.querySelector('h2')!.textContent).toBe(label);
|
|
expect(popup.textContent).toContain(evidence);
|
|
if (label === 'User Accounts') expect(popup.textContent).not.toContain('Synthetic computers');
|
|
if (label === 'GPOs') expect(popup.textContent).toContain('All processed client backups');
|
|
expect(trigger.getAttribute('aria-expanded')).toBe('true');
|
|
await act(async () => window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })));
|
|
expect(document.querySelector('.floating-data-window')).toBeNull();
|
|
expect(trigger.getAttribute('aria-expanded')).toBe('false');
|
|
expect(document.activeElement).toBe(trigger);
|
|
}
|
|
expect(request).toHaveBeenCalledOnce();
|
|
expect(onOpenWork).not.toHaveBeenCalled();
|
|
expect(useApp().resetFilters).not.toHaveBeenCalled();
|
|
expect(useApp().setFacetFilter).not.toHaveBeenCalled();
|
|
expect(JSON.stringify(snapshot)).toBe(original);
|
|
} finally { act(() => root.unmount()); }
|
|
});
|