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