feat: The Big One UI
This commit is contained in:
committed by
Pim Hutting
parent
f9e0eb95f8
commit
82785dc8cb
220
test/pages/monitoringPage/MonitoringPageAPI.test.ts
Normal file
220
test/pages/monitoringPage/MonitoringPageAPI.test.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user