94 lines
3.7 KiB
TypeScript
94 lines
3.7 KiB
TypeScript
// @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/domain/ai-overview';
|
|
import { useAiOverview } from '../../src/features/dashboard/AiOverviewPanel';
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>(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 <output>{controller.response?.projectId ?? 'empty'}</output>;
|
|
}
|
|
|
|
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<AiOverviewResponse>();
|
|
const second = deferred<AiOverviewResponse>();
|
|
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(<HookProbe projectId="project-a" />));
|
|
await act(async () => root.render(<HookProbe projectId="project-b" />));
|
|
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();
|
|
}
|
|
});
|
|
|
|
function LoadingProbe({ projectId }: { projectId: string }): ReactNode {
|
|
const controller = useAiOverview(projectId, [`assessment-${projectId}`]);
|
|
return <output>{controller.loading ? 'loading' : controller.response?.projectId ?? 'empty'}</output>;
|
|
}
|
|
|
|
it('shows the last response immediately after Client Info remounts and still revalidates it', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
const revalidation = deferred<AiOverviewResponse>();
|
|
const request = vi.spyOn(AssessorApiClient.prototype, 'getAiOverview')
|
|
.mockResolvedValueOnce(response('project-remount'))
|
|
.mockReturnValueOnce(revalidation.promise);
|
|
const container = document.createElement('div');
|
|
document.body.append(container);
|
|
const first = createRoot(container);
|
|
await act(async () => first.render(<LoadingProbe projectId="project-remount" />));
|
|
expect(container.textContent).toBe('project-remount');
|
|
await act(async () => first.unmount());
|
|
|
|
// Returning from another area mounts a fresh hook for the same timeslot.
|
|
const second = createRoot(container);
|
|
try {
|
|
await act(async () => second.render(<LoadingProbe projectId="project-remount" />));
|
|
expect(container.textContent).toBe('project-remount');
|
|
expect(request).toHaveBeenCalledTimes(2);
|
|
await act(async () => { revalidation.resolve({ ...response('project-remount'), stale: true }); await Promise.resolve(); });
|
|
expect(container.textContent).toBe('project-remount');
|
|
} finally {
|
|
await act(async () => second.unmount());
|
|
container.remove();
|
|
}
|
|
});
|