// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import { render, screen, act, cleanup, fireEvent } from '@testing-library/react'; import Robot from '../../../src/pages/Robot/Robot'; // Mock EventSource const mockInstances: MockEventSource[] = []; class MockEventSource { url: string; onmessage: ((event: MessageEvent) => void) | null = null; closed = false; constructor(url: string) { this.url = url; mockInstances.push(this); } sendMessage(data: string) { this.onmessage?.({ data } as MessageEvent); } close() { this.closed = true; } } // Mock global EventSource beforeAll(() => { (globalThis as any).EventSource = jest.fn((url: string) => new MockEventSource(url)); }); // Mock fetch beforeEach(() => { globalThis.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ reply: 'ok' }), }) ) as jest.Mock; }); // Cleanup afterEach(() => { cleanup(); jest.restoreAllMocks(); mockInstances.length = 0; }); describe('Robot', () => { test('renders initial state', () => { render(); expect(screen.getByText('Robot interaction')).toBeInTheDocument(); expect(screen.getByText('Force robot speech')).toBeInTheDocument(); expect(screen.getByText('Listening 🔴')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Enter a message')).toBeInTheDocument(); }); test('sends message via button', async () => { render(); const input = screen.getByPlaceholderText('Enter a message'); const button = screen.getByText('Speak'); fireEvent.change(input, { target: { value: 'Hello' } }); await act(async () => fireEvent.click(button)); expect(globalThis.fetch).toHaveBeenCalledWith( 'http://localhost:8000/message', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Hello' }), }) ); }); test('sends message via Enter key', async () => { render(); const input = screen.getByPlaceholderText('Enter a message'); fireEvent.change(input, { target: { value: 'Hi Enter' } }); await act(async () => fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 }) ); expect(globalThis.fetch).toHaveBeenCalledWith( 'http://localhost:8000/message', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Hi Enter' }), }) ); expect((input as HTMLInputElement).value).toBe(''); }); test('handles fetch errors', async () => { globalThis.fetch = jest.fn(() => Promise.reject('Network error')) as jest.Mock; const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); render(); const input = screen.getByPlaceholderText('Enter a message'); const button = screen.getByText('Speak'); fireEvent.change(input, { target: { value: 'Error test' } }); await act(async () => fireEvent.click(button)); expect(consoleSpy).toHaveBeenCalledWith( 'Error sending message: ', 'Network error' ); }); test('updates conversation on SSE', async () => { render(); const eventSource = mockInstances[0]; await act(async () => { eventSource.sendMessage(JSON.stringify({ voice_active: true })); eventSource.sendMessage(JSON.stringify({ speech: 'User says hi' })); eventSource.sendMessage(JSON.stringify({ llm_response: 'Assistant replies' })); }); expect(screen.getByText('Listening 🟢')).toBeInTheDocument(); expect(screen.getByText('User says hi')).toBeInTheDocument(); expect(screen.getByText('Assistant replies')).toBeInTheDocument(); }); test('handles invalid SSE JSON', async () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); render(); const eventSource = mockInstances[0]; await act(async () => eventSource.sendMessage('bad-json')); expect(logSpy).toHaveBeenCalledWith('Unparsable SSE message:', 'bad-json'); }); test('resets conversation with Reset button', async () => { render(); const eventSource = mockInstances[0]; await act(async () => eventSource.sendMessage(JSON.stringify({ speech: 'Hello' })) ); expect(screen.getByText('Hello')).toBeInTheDocument(); fireEvent.click(screen.getByText('Reset')); expect(screen.queryByText('Hello')).not.toBeInTheDocument(); }); test('toggles conversationIndex with Stop/Start button', () => { render(); const stopButton = screen.getByText('Stop'); fireEvent.click(stopButton); expect(screen.getByText('Start')).toBeInTheDocument(); fireEvent.click(screen.getByText('Start')); expect(screen.getByText('Stop')).toBeInTheDocument(); }); test('closes EventSource on unmount', () => { const { unmount } = render(); const eventSource = mockInstances[0]; const closeSpy = jest.spyOn(eventSource, 'close'); unmount(); expect(closeSpy).toHaveBeenCalled(); expect(eventSource.closed).toBe(true); }); });