feat: integrate pixel teacher presence and resilient classroom preview
This commit is contained in:
122
tests/unit/use-work-preview.test.tsx
Normal file
122
tests/unit/use-work-preview.test.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user