// @vitest-environment jsdom import { act, type ReactNode } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import type { AiOverviewResponse } from '../../src/compliance/ai-overview'; import { useAiOverview } from '../../src/compliance/ui/AiOverviewPanel'; import { activitySnapshot, clearFinishedActivities } from '../../src/platform/activity-store'; afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); function deferred() { let resolve!: (value: T) => void; const promise = new Promise(next => { resolve = next; }); return { promise, resolve }; } function response(projectId: string): AiOverviewResponse { return { version: 1, projectId, scope: { assessmentIds: [`assessment-${projectId}`], scanRunIds: [] }, generationEnabled: true, state: 'empty', stale: false }; } function HookProbe({ projectId }: { projectId: string }): ReactNode { const controller = useAiOverview(projectId, [`assessment-${projectId}`]); return {controller.response?.projectId ?? 'empty'}; } function GenerateProbe({ onReady }: { onReady: (generate: () => Promise) => void }): ReactNode { const controller = useAiOverview('project-a', ['assessment-project-a']); onReady(controller.generate); return {controller.response?.state ?? 'none'}; } it('keeps one Compliance Engine activity open while the backend generates the overview', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); clearFinishedActivities(); const generationActivity = () => activitySnapshot().filter(row => row.label === 'Generating AI overview'); const running: AiOverviewResponse = { ...response('project-a'), state: 'running', pollAfterMs: 10 }; const status = vi.spyOn(AssessorApiClient.prototype, 'getAiOverview').mockResolvedValue(response('project-a')); vi.spyOn(AssessorApiClient.prototype, 'generateAiOverview').mockResolvedValue(running); let generate!: () => Promise; const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render( { generate = next; }} />)); expect(generationActivity()).toEqual([]); // The start request returns at once; the activity must outlive it and every status poll. status.mockImplementation(async () => ({ ...running })); await act(async () => { await generate(); }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)); }); expect(generationActivity()).toMatchObject([{ area: 'Compliance Engine', state: 'running' }]); status.mockResolvedValue({ ...response('project-a'), state: 'succeeded' }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)); }); expect(container.textContent).toBe('succeeded'); expect(generationActivity()).toMatchObject([{ area: 'Compliance Engine', state: 'completed' }]); } finally { await act(async () => root.unmount()); container.remove(); } }); it('does not show a previous project response when it resolves after the active project', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const first = deferred(); const second = deferred(); const request = vi.spyOn(AssessorApiClient.prototype, 'getAiOverview').mockImplementation(projectId => projectId === 'project-a' ? first.promise : second.promise); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); try { await act(async () => root.render()); await act(async () => root.render()); expect(request).toHaveBeenCalledTimes(2); await act(async () => { second.resolve(response('project-b')); await Promise.resolve(); }); expect(container.textContent).toBe('project-b'); await act(async () => { first.resolve(response('project-a')); await Promise.resolve(); }); expect(container.textContent).toBe('project-b'); } finally { await act(async () => root.unmount()); container.remove(); } });