89 lines
5.5 KiB
TypeScript
89 lines
5.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import { afterEach, beforeEach, 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 { resetAssessmentContextRequestsForTests, useAssessmentContext } from '../../src/features/context-layer/use-assessment-context';
|
|
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
|
|
vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() }));
|
|
let host: HTMLDivElement;
|
|
let root: Root;
|
|
function workspace(id = 'synthetic') {
|
|
return { dataset: { fileName: 'Synthetic', scanTool: 'Generic', sheets: [makeSheet('Synthetic', [['Title', 'Status'], ['Synthetic finding', 'Failed']], '', 'synthetic.csv')] },
|
|
assessmentIds: [id + '-slot'], models: [{ project: { id, name: 'Synthetic' }, assessment: { id: id + '-slot' }, reviews: [] }] } as unknown as Awaited<ReturnType<typeof loadCanonicalProjectWorkspace>>;
|
|
}
|
|
function Consumer() {
|
|
const app = useApp();
|
|
const context = useAssessmentContext(true, app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds, app.dataset);
|
|
return <output data-context>{context.status}</output>;
|
|
}
|
|
function Harness() {
|
|
const app = useApp();
|
|
return <><button onClick={() => void app.openProject('synthetic', [], ['synthetic-slot'])}>Open</button>
|
|
<button onClick={() => void app.openProject('second', [], ['second-slot'])}>Second</button>
|
|
<output data-loaded>{app.dataset ? app.projectId : 'waiting'}</output><output data-status>{app.status}</output>
|
|
<Consumer/><Consumer/></>;
|
|
}
|
|
beforeEach(async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
localStorage.clear(); resetEnginePreferencesForTests(); resetAssessmentContextRequestsForTests();
|
|
vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never);
|
|
host = document.createElement('div'); document.body.append(host); root = createRoot(host);
|
|
});
|
|
afterEach(async () => { await act(async () => root.unmount()); host.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); });
|
|
const render = () => act(async () => root.render(<AppProvider><Harness/></AppProvider>));
|
|
const click = (index = 0) => act(async () => host.querySelectorAll('button')[index].click());
|
|
|
|
it('opens the client only after one shared Context request and never starts with the empty dataset', async () => {
|
|
let load!: (value: ReturnType<typeof workspace>) => void;
|
|
vi.mocked(loadCanonicalProjectWorkspace).mockReturnValue(new Promise(resolve => { load = resolve; }));
|
|
let finish!: (value: never) => void;
|
|
const request = vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockReturnValue(new Promise(resolve => { finish = resolve; }));
|
|
await render(); await click();
|
|
expect(request).not.toHaveBeenCalled();
|
|
await act(async () => load(workspace()));
|
|
expect(request).toHaveBeenCalledTimes(1);
|
|
expect(host.querySelector('[data-loaded]')?.textContent).toBe('waiting');
|
|
await act(async () => finish({} as never));
|
|
expect(host.querySelector('[data-loaded]')?.textContent).toBe('synthetic');
|
|
expect([...host.querySelectorAll('[data-context]')].map(node => node.textContent)).toEqual(['ready', 'ready']);
|
|
expect(request).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('keeps Core accessible after a Context failure, exposes the error, and retries only on reopening', async () => {
|
|
vi.mocked(loadCanonicalProjectWorkspace).mockImplementation(async () => workspace());
|
|
const request = vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockRejectedValueOnce(new Error('Synthetic context failure')).mockResolvedValue({} as never);
|
|
await render(); await click();
|
|
expect(host.querySelector('[data-loaded]')?.textContent).toBe('synthetic');
|
|
expect(host.querySelector('[data-status]')?.textContent).toContain('Context could not be loaded');
|
|
expect(host.querySelector('[data-context]')?.textContent).toBe('error');
|
|
expect(request).toHaveBeenCalledTimes(1);
|
|
await click();
|
|
expect(request).toHaveBeenCalledTimes(2);
|
|
expect(host.querySelector('[data-context]')?.textContent).toBe('ready');
|
|
});
|
|
|
|
it('does not request Context when its engine is off', async () => {
|
|
setEngineEnabled('context', false);
|
|
vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue(workspace());
|
|
const request = vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext');
|
|
await render(); await click();
|
|
expect(host.querySelector('[data-loaded]')?.textContent).toBe('synthetic');
|
|
expect(request).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not publish an older client when its Context finishes after a client switch', async () => {
|
|
vi.mocked(loadCanonicalProjectWorkspace).mockImplementation(async (_api, id) => workspace(id));
|
|
let finish!: (value: never) => void;
|
|
const request = vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockReturnValueOnce(new Promise(resolve => { finish = resolve; })).mockResolvedValue({} as never);
|
|
await render(); await click(); await click(1);
|
|
expect(host.querySelector('[data-loaded]')?.textContent).toBe('second');
|
|
await act(async () => finish({} as never));
|
|
expect(host.querySelector('[data-loaded]')?.textContent).toBe('second');
|
|
expect(request).toHaveBeenCalledTimes(2);
|
|
});
|