import { renderHook, act, cleanup } from '@testing-library/react'; import { sendAPICall, nextPhase, pauseExperiment, playExperiment, useExperimentLogger, useStatusLogger } from '../../../src/pages/MonitoringPage/MonitoringPageAPI'; // --- MOCK EVENT SOURCE SETUP --- // This mocks the browser's EventSource so we can manually 'push' messages to our hooks const mockInstances: MockEventSource[] = []; class MockEventSource { url: string; onmessage: ((event: MessageEvent) => void) | null = null; onerror: ((event: Event) => void) | null = null; // Added onerror support closed = false; constructor(url: string) { this.url = url; mockInstances.push(this); } sendMessage(data: string) { if (this.onmessage) { this.onmessage({ data } as MessageEvent); } } triggerError(err: any) { if (this.onerror) { this.onerror(err); } } close() { this.closed = true; } } // Mock global EventSource beforeAll(() => { (globalThis as any).EventSource = jest.fn((url: string) => new MockEventSource(url)); }); // Mock global fetch beforeEach(() => { globalThis.fetch = jest.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ reply: 'ok' }), }) ) as jest.Mock; }); // Cleanup after every test afterEach(() => { cleanup(); jest.restoreAllMocks(); mockInstances.length = 0; }); describe('MonitoringPageAPI', () => { describe('sendAPICall', () => { test('sends correct POST request', async () => { await sendAPICall('test_type', 'test_ctx'); expect(globalThis.fetch).toHaveBeenCalledWith( 'http://localhost:8000/button_pressed', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'test_type', context: 'test_ctx' }), }) ); }); test('appends endpoint if provided', async () => { await sendAPICall('t', 'c', '/extra'); expect(globalThis.fetch).toHaveBeenCalledWith( expect.stringContaining('/button_pressed/extra'), expect.any(Object) ); }); test('logs error on fetch network failure', async () => { (globalThis.fetch as jest.Mock).mockRejectedValue('Network error'); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); await sendAPICall('t', 'c'); expect(consoleSpy).toHaveBeenCalledWith('Failed to send api call:', 'Network error'); }); test('throws error if response is not ok', async () => { (globalThis.fetch as jest.Mock).mockResolvedValue({ ok: false }); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); await sendAPICall('t', 'c'); expect(consoleSpy).toHaveBeenCalledWith('Failed to send api call:', expect.any(Error)); }); }); describe('Helper Functions', () => { test('nextPhase sends correct params', async () => { await nextPhase(); expect(globalThis.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: JSON.stringify({ type: 'next_phase', context: '' }) }) ); }); test('pauseExperiment sends correct params', async () => { await pauseExperiment(); expect(globalThis.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: JSON.stringify({ type: 'pause', context: 'true' }) }) ); }); test('playExperiment sends correct params', async () => { await playExperiment(); expect(globalThis.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: JSON.stringify({ type: 'pause', context: 'false' }) }) ); }); }); describe('useExperimentLogger', () => { test('connects to SSE and receives messages', () => { const onUpdate = jest.fn(); // Hook must be rendered to start the effect renderHook(() => useExperimentLogger(onUpdate)); // Retrieve the mocked instance created by the hook const eventSource = mockInstances[0]; expect(eventSource.url).toContain('/experiment_stream'); // Simulate incoming message act(() => { eventSource.sendMessage(JSON.stringify({ type: 'phase_update', id: '1' })); }); expect(onUpdate).toHaveBeenCalledWith({ type: 'phase_update', id: '1' }); }); test('handles JSON parse errors in stream', () => { const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); renderHook(() => useExperimentLogger()); const eventSource = mockInstances[0]; act(() => { eventSource.sendMessage('invalid-json'); }); expect(consoleSpy).toHaveBeenCalledWith('Stream parse error:', expect.any(Error)); }); test('handles SSE connection error', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); renderHook(() => useExperimentLogger()); const eventSource = mockInstances[0]; act(() => { eventSource.triggerError('Connection lost'); }); expect(consoleSpy).toHaveBeenCalledWith('SSE Connection Error:', 'Connection lost'); expect(eventSource.closed).toBe(true); }); test('closes EventSource on unmount', () => { const { unmount } = renderHook(() => useExperimentLogger()); const eventSource = mockInstances[0]; const closeSpy = jest.spyOn(eventSource, 'close'); unmount(); expect(closeSpy).toHaveBeenCalled(); expect(eventSource.closed).toBe(true); }); }); describe('useStatusLogger', () => { test('connects to SSE and receives messages', () => { const onUpdate = jest.fn(); renderHook(() => useStatusLogger(onUpdate)); const eventSource = mockInstances[0]; expect(eventSource.url).toContain('/status_stream'); act(() => { eventSource.sendMessage(JSON.stringify({ some: 'data' })); }); expect(onUpdate).toHaveBeenCalledWith({ some: 'data' }); }); test('handles JSON parse errors', () => { const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); renderHook(() => useStatusLogger()); const eventSource = mockInstances[0]; act(() => { eventSource.sendMessage('bad-data'); }); expect(consoleSpy).toHaveBeenCalledWith('Status stream error:', expect.any(Error)); }); }); });