168 lines
5.0 KiB
TypeScript
168 lines
5.0 KiB
TypeScript
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(<Robot />);
|
|
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(<Robot />);
|
|
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(<Robot />);
|
|
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(<Robot />);
|
|
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(<Robot />);
|
|
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(<Robot />);
|
|
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(<Robot />);
|
|
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(<Robot />);
|
|
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(<Robot />);
|
|
const eventSource = mockInstances[0];
|
|
const closeSpy = jest.spyOn(eventSource, 'close');
|
|
|
|
unmount();
|
|
expect(closeSpy).toHaveBeenCalled();
|
|
expect(eventSource.closed).toBe(true);
|
|
});
|
|
});
|