also moved some functions from VisProg outside VisProg.tsx into VisProgLogic.tsx so I can reuse it for the reset experiment function of monitor page Also fixed a small merge error in TriggerNodes.tsx ref: N25B-400
229 lines
6.7 KiB
TypeScript
229 lines
6.7 KiB
TypeScript
import { renderHook, act, cleanup } from '@testing-library/react';
|
|
import {
|
|
sendAPICall,
|
|
nextPhase,
|
|
resetPhase,
|
|
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('resetPhase sends correct params', async () => {
|
|
await resetPhase();
|
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({ body: JSON.stringify({ type: 'reset_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));
|
|
});
|
|
});
|
|
}); |