// @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>; } function Consumer() { const app = useApp(); const context = useAssessmentContext(true, app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds, app.dataset); return {context.status}; } function Harness() { const app = useApp(); return <> {app.dataset ? app.projectId : 'waiting'}{app.status} ; } 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()); 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) => 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); });