Assessor_2.0/tests/app/client-context-loading.test...

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);
});