178 lines
10 KiB
TypeScript
178 lines
10 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act, useState } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { AppProvider, useApp } from '../../src/app/AppProvider';
|
|
import { AssessorApiClient } from '../../src/api/assessor-api-client';
|
|
import { loadCanonicalProjectWorkspace } from '../../src/features/project-import/load-canonical-project-workspace';
|
|
import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel';
|
|
import { SearchFilterBar, SheetAndSearchControls } from '../../src/features/search-filter/SearchFilterBar';
|
|
import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
import type { ImportedDataset } from '../../src/domain/dataset';
|
|
import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model';
|
|
|
|
vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() }));
|
|
vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => <span>Synthetic timeslot</span> }));
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); localStorage.clear(); });
|
|
|
|
function fixture() {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never);
|
|
vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockRejectedValue(new Error('Synthetic context unavailable'));
|
|
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never);
|
|
const rows = [['Title', 'Category', 'Status', 'Source: Scan', 'Diagnostics'],
|
|
['Synthetic review one', 'GPOs', 'Review required', 'GPOs', 'policy-evaluation-baseline-incomplete'],
|
|
['Synthetic review two', 'GPOs', 'Review required', 'GPOs', 'policy-evaluation-customer-absence-unproven'],
|
|
['Synthetic duplicate', 'GPOs', 'Duplicates', 'GPOs', '']];
|
|
const gpo = makeSheet('GPOs', rows, '', 'Synthetic', { toolId: 'group-policy' });
|
|
const aggregate = makeSheet('All Records', [...rows, ['Synthetic scanner', 'Scanner', 'Fail', 'Maester', '']], '', 'Synthetic', { isProjectSheet: true });
|
|
const dataset = { fileName: 'Synthetic', profileId: 'generic', scanTool: 'Multiple Tools', fileSize: 0,
|
|
lastModified: 0, format: 'Project', rawText: '', sheets: [aggregate, gpo] } as ImportedDataset;
|
|
vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ dataset, assessmentIds: ['slot'], models: [{
|
|
project: { id: 'synthetic', name: 'Synthetic' }, assessment: { id: 'slot' }, reviews: []
|
|
} as unknown as CanonicalAssessmentModel] });
|
|
return { dataset, gpo };
|
|
}
|
|
|
|
function Harness() {
|
|
const app = useApp();
|
|
currentApp = app;
|
|
const [area, setArea] = useState('dashboard');
|
|
const records = useFilteredRecords();
|
|
return <>
|
|
<button onClick={() => void app.openProject('synthetic', ['run'], ['slot'])}>Open synthetic client</button>
|
|
<button onClick={() => setArea('work')}>Work</button>
|
|
<button onClick={() => {
|
|
app.setSearch('no matching source row');
|
|
app.setStatusFilter('passed');
|
|
app.setFacetFilter('category', { values: ['GPOs'] });
|
|
app.setPathFindingIds(['stale-record']);
|
|
app.setActionOnly(true);
|
|
}}>Apply stale Work filters</button>
|
|
<output data-testid="count">{records.length}</output>
|
|
{area === 'dashboard' ? <DashboardPanel onOpenWork={() => setArea('work')}/>
|
|
: <><SheetAndSearchControls/><SearchFilterBar/><ul data-testid="records">{records.map(record => <li key={record.id}>{record.title}</li>)}</ul></>}
|
|
</>;
|
|
}
|
|
|
|
let currentApp: ReturnType<typeof useApp>;
|
|
|
|
describe('GPO source navigation with the real application state provider', () => {
|
|
it('resets stale Work filters when another Source: Scan is selected and shows its default results', async () => {
|
|
fixture();
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
const click = (label: string) => act(async () => {
|
|
[...container.querySelectorAll('button')].find(button => button.textContent === label)!.click();
|
|
});
|
|
const selectSource = (value: string) => act(async () => {
|
|
const control = container.querySelector<HTMLSelectElement>('select[aria-label="Source: Scan"]')!;
|
|
control.value = value;
|
|
control.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
try {
|
|
await act(async () => root.render(<AppProvider><Harness/></AppProvider>));
|
|
await click('Open synthetic client');
|
|
await click('Work');
|
|
await selectSource('gpos');
|
|
await click('Apply stale Work filters');
|
|
expect(container.querySelector('[data-testid="count"]')?.textContent).toBe('0');
|
|
const source = container.querySelector<HTMLSelectElement>('select[aria-label="Source: Scan"]')!;
|
|
const scannerOption = [...source.options].find(option => option.textContent?.startsWith('Maester'))!;
|
|
await selectSource(scannerOption.value);
|
|
expect(currentApp.search).toBe('');
|
|
expect(currentApp.statusFilter).toBe('deviation');
|
|
expect(currentApp.actionOnly).toBe(false);
|
|
expect(currentApp.pathFindingIds).toEqual([]);
|
|
expect(currentApp.facetFilters).toEqual({ 'provenance:source-scan': { values: [scannerOption.value] } });
|
|
expect(container.querySelector('[data-testid="count"]')?.textContent).toBe('1');
|
|
expect(container.querySelector('[data-testid="records"]')?.textContent).toContain('Synthetic scanner');
|
|
await click('Apply stale Work filters');
|
|
await selectSource('');
|
|
expect(currentApp.search).toBe('');
|
|
expect(currentApp.statusFilter).toBe('deviation');
|
|
expect(currentApp.actionOnly).toBe(false);
|
|
expect(currentApp.pathFindingIds).toEqual([]);
|
|
expect(currentApp.facetFilters).toEqual({});
|
|
expect(container.querySelector('[data-testid="records"]')?.textContent).toContain('Synthetic scanner');
|
|
} finally { await act(async () => root.unmount()); container.remove(); }
|
|
});
|
|
|
|
it('shows the full GPO filters only while Source: Scan is set to GPOs', async () => {
|
|
fixture();
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
const labels = () => [...container.querySelectorAll<HTMLSelectElement>('.dynamic-facet-select')]
|
|
.map(control => control.getAttribute('aria-label'));
|
|
const selectSource = (value: string) => act(async () => {
|
|
const control = container.querySelector<HTMLSelectElement>('select[aria-label="Source: Scan"]')!;
|
|
control.value = value;
|
|
control.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
try {
|
|
await act(async () => root.render(<AppProvider><Harness/></AppProvider>));
|
|
await act(async () => [...container.querySelectorAll('button')]
|
|
.find(button => button.textContent === 'Open synthetic client')!.click());
|
|
await act(async () => [...container.querySelectorAll('button')]
|
|
.find(button => button.textContent === 'Work')!.click());
|
|
|
|
await selectSource('gpos');
|
|
expect(labels()).toContain('Diagnostics');
|
|
expect(labels()).toContain('Category');
|
|
expect(labels()).toContain('Security Domain');
|
|
|
|
await selectSource('');
|
|
expect(container.querySelector<HTMLSelectElement>('select[aria-label="Source: Scan"]')?.value).toBe('');
|
|
expect(labels()).toContain('Category');
|
|
expect(labels()).not.toContain('Diagnostics');
|
|
} finally { await act(async () => root.unmount()); container.remove(); }
|
|
});
|
|
|
|
it.each(['dashboard source then Work', 'Work source', 'GPO sheet', 'GPO review domain'] as const)('uses Deviation by default through %s while preserving explicit incoming drilldowns', async route => {
|
|
const { gpo } = fixture();
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
const click = (label: string) => act(async () => {
|
|
[...container.querySelectorAll('button')].find(button => button.textContent === label)!.click();
|
|
});
|
|
const select = (selector: string, value: string) => act(async () => {
|
|
const control = container.querySelector<HTMLSelectElement>(selector)!;
|
|
control.value = value;
|
|
control.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
try {
|
|
await act(async () => root.render(<AppProvider><Harness/></AppProvider>));
|
|
await click('Open synthetic client');
|
|
if (route === 'dashboard source then Work' || route === 'GPO review domain') {
|
|
await select('.client-overview-source select', 'gpos');
|
|
// The GPO & Baseline domain surfaces its two review-required records; the
|
|
// review descriptor lives on the amount's title per dashboard-layout.
|
|
expect(container.querySelector('.client-domains [title="Review required"]')?.textContent).toBe('2');
|
|
if (route === 'GPO review domain') await act(async () => container.querySelector<HTMLButtonElement>('.client-domains button')!.click());
|
|
else await click('Work');
|
|
} else {
|
|
await click('Work');
|
|
if (route === 'Work source') await select('select[aria-label="Source: Scan"]', 'gpos');
|
|
else {
|
|
const sheet = container.querySelector<HTMLSelectElement>('.sheet-select')!;
|
|
await act(async () => sheet.dispatchEvent(new Event('focusin', { bubbles: true })));
|
|
await select('.sheet-select', gpo.id);
|
|
}
|
|
}
|
|
const explicitReviewDrilldown = route === 'GPO review domain';
|
|
expect(container.querySelector<HTMLSelectElement>('select[aria-label="Result Status"]')?.value)
|
|
.toBe(explicitReviewDrilldown ? 'all' : 'deviation');
|
|
expect(container.querySelector('[data-testid="count"]')?.textContent).toBe(explicitReviewDrilldown ? '2' : '0');
|
|
if (!explicitReviewDrilldown) await select('select[aria-label="Result Status"]', 'all');
|
|
expect(container.querySelector('[data-testid="records"]')?.textContent).toContain('Synthetic review one');
|
|
expect(container.querySelector('[data-testid="records"]')?.textContent).not.toContain('Synthetic scanner');
|
|
await select('select[aria-label="Result Status"]', 'passed');
|
|
expect(container.querySelector<HTMLSelectElement>('select[aria-label="Result Status"]')?.value).toBe('passed');
|
|
expect(container.querySelector('[data-testid="count"]')?.textContent).toBe('0');
|
|
await select('select[aria-label="Result Status"]', 'all');
|
|
expect(container.querySelector('[data-testid="count"]')?.textContent).toBe(explicitReviewDrilldown ? '2' : '3');
|
|
} finally { await act(async () => root.unmount()); container.remove(); }
|
|
});
|
|
});
|