123 lines
6.6 KiB
TypeScript
123 lines
6.6 KiB
TypeScript
import { act, renderHook } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { useWorkPreview } from '@/pages/Chat/use-work-preview';
|
|
const ensure = vi.hoisted(() => vi.fn());
|
|
vi.mock('@/lib/coding-work-preview', () => ({ ensureWorkPreview: ensure }));
|
|
beforeEach(() => { vi.useFakeTimers(); ensure.mockReset(); });
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
describe('work preview request lifecycle', () => {
|
|
it('resumes a manually opened work page and ignores the older startup response', async () => {
|
|
let finish!: (value: unknown) => void;
|
|
ensure.mockImplementationOnce(() => new Promise((resolve) => { finish = resolve; })).mockResolvedValue({ status: 'ready' });
|
|
const { result } = renderHook(() => useWorkPreview({ projectId: 'p1', enabled: true, onResult: vi.fn() }));
|
|
await act(async () => result.current.showBrowser());
|
|
expect(result.current.result?.status).toBe('ready');
|
|
await act(async () => window.dispatchEvent(new Event('focus')));
|
|
expect(ensure).toHaveBeenCalledTimes(2);
|
|
await act(async () => finish({ status: 'failed', message: 'old startup failure' }));
|
|
expect(result.current.result?.status).toBe('ready');
|
|
expect(new Set(ensure.mock.calls.map((call) => call[1])).size).toBe(1);
|
|
});
|
|
|
|
it('does not let the initial queued placeholder overwrite a manual open', async () => {
|
|
ensure.mockReturnValue(new Promise(() => {}));
|
|
const { result } = renderHook(() => useWorkPreview({ projectId: 'p1', enabled: true, onResult: vi.fn() }));
|
|
act(() => result.current.showBrowser());
|
|
await act(async () => {});
|
|
expect(result.current.result?.status).toBe('ready');
|
|
});
|
|
|
|
it('bounds transport retries and keeps one request identity, including explicit reconnect', async () => {
|
|
ensure.mockRejectedValue(new Error('network disconnected'));
|
|
const { result } = renderHook(() => useWorkPreview({ projectId: 'p1', enabled: true, onResult: vi.fn() }));
|
|
await act(async () => {});
|
|
await act(async () => vi.advanceTimersByTimeAsync(60_000));
|
|
expect(ensure).toHaveBeenCalledTimes(3);
|
|
expect(result.current.result).toMatchObject({ status: 'failed', errorCode: 'NETWORK' });
|
|
ensure.mockResolvedValue({ status: 'ready' });
|
|
await act(async () => result.current.retry());
|
|
expect(result.current.result?.status).toBe('ready');
|
|
expect(new Set(ensure.mock.calls.map((call) => call[1])).size).toBe(1);
|
|
});
|
|
|
|
it('does not automatically retry login errors or refresh them as a new action', async () => {
|
|
ensure.mockRejectedValue(new Error('401 unauthorized'));
|
|
const { result } = renderHook(() => useWorkPreview({ projectId: 'p1', enabled: true, onResult: vi.fn() }));
|
|
await act(async () => {});
|
|
await act(async () => vi.advanceTimersByTimeAsync(60_000));
|
|
expect(ensure).toHaveBeenCalledTimes(1);
|
|
expect(result.current.result).toMatchObject({ status: 'failed', errorCode: 'AUTH_INVALID' });
|
|
});
|
|
it('polls one action identity, preserves it across tab switches, and stops when ready', async () => {
|
|
ensure.mockResolvedValue({ status: 'starting' });
|
|
const onResult = vi.fn();
|
|
const { result, rerender } = renderHook(({ enabled }) => useWorkPreview({ projectId: 'p1', conversationId: 'c1', enabled, onResult }), { initialProps: { enabled: false } });
|
|
expect(ensure).not.toHaveBeenCalled();
|
|
await act(async () => rerender({ enabled: true }));
|
|
const id = ensure.mock.calls[0][1];
|
|
expect(result.current.result?.status).toBe('starting');
|
|
await act(async () => vi.advanceTimersByTimeAsync(2_000));
|
|
expect(ensure).toHaveBeenLastCalledWith('p1', id, 'c1');
|
|
await act(async () => rerender({ enabled: false }));
|
|
await act(async () => vi.advanceTimersByTimeAsync(10_000));
|
|
expect(ensure).toHaveBeenCalledTimes(2);
|
|
ensure.mockResolvedValue({ status: 'ready' });
|
|
await act(async () => rerender({ enabled: true }));
|
|
expect(ensure).toHaveBeenLastCalledWith('p1', id, 'c1');
|
|
await act(async () => vi.advanceTimersByTimeAsync(10_000));
|
|
expect(ensure).toHaveBeenCalledTimes(3);
|
|
expect(result.current.result?.status).toBe('ready');
|
|
});
|
|
it('does not retry failure automatically and creates a new identity only on retry', async () => {
|
|
ensure.mockResolvedValue({ status: 'failed' });
|
|
const { result } = renderHook(() => useWorkPreview({ projectId: 'p1', enabled: true, onResult: vi.fn() }));
|
|
await act(async () => {});
|
|
const id = ensure.mock.calls[0][1];
|
|
await act(async () => vi.advanceTimersByTimeAsync(10_000));
|
|
expect(ensure).toHaveBeenCalledTimes(1);
|
|
await act(async () => result.current.retry());
|
|
expect(ensure.mock.calls[1][1]).not.toBe(id);
|
|
});
|
|
it('ignores a late result from a previous project', async () => {
|
|
let finish!: (value: unknown) => void;
|
|
ensure.mockImplementationOnce(() => new Promise((resolve) => { finish = resolve; })).mockResolvedValue({ status: 'waiting' });
|
|
const onResult = vi.fn();
|
|
const { result, rerender } = renderHook(({ projectId }) => useWorkPreview({ projectId, enabled: true, onResult }), { initialProps: { projectId: 'p1' } });
|
|
await act(async () => rerender({ projectId: 'p2' }));
|
|
await act(async () => finish({ status: 'ready' }));
|
|
expect(result.current.result?.status).toBe('waiting');
|
|
expect(onResult).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('restores a ready preview on foreground focus with the same action identity', async () => {
|
|
ensure.mockResolvedValue({ status: 'ready' });
|
|
const onResult = vi.fn();
|
|
const { unmount } = renderHook(() => useWorkPreview({ projectId: 'p1', enabled: true, onResult }));
|
|
await act(async () => {});
|
|
const requestId = ensure.mock.calls[0][1];
|
|
await act(async () => window.dispatchEvent(new Event('focus')));
|
|
expect(ensure).toHaveBeenCalledTimes(2);
|
|
expect(ensure.mock.calls[1][1]).toBe(requestId);
|
|
unmount();
|
|
await act(async () => window.dispatchEvent(new Event('focus')));
|
|
expect(ensure).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('pauses checks while hidden and resumes once without duplicate focus requests', async () => {
|
|
ensure.mockResolvedValue({ status: 'starting' });
|
|
const visibility = vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('hidden');
|
|
const { unmount } = renderHook(() => useWorkPreview({ projectId: 'p1', enabled: true, onResult: vi.fn() }));
|
|
await act(async () => {});
|
|
expect(ensure).not.toHaveBeenCalled();
|
|
visibility.mockReturnValue('visible');
|
|
await act(async () => {
|
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
expect(ensure).toHaveBeenCalledTimes(1);
|
|
unmount();
|
|
visibility.mockRestore();
|
|
});
|
|
});
|