chore: added tests for monitoringpage
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
This commit is contained in:
293
test/pages/monitoringPage/MonitoringPage.test.tsx
Normal file
293
test/pages/monitoringPage/MonitoringPage.test.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import MonitoringPage from '../../../src/pages/MonitoringPage/MonitoringPage';
|
||||
import useProgramStore from '../../../src/utils/programStore';
|
||||
import * as MonitoringAPI from '../../../src/pages/MonitoringPage/MonitoringPageAPI';
|
||||
import * as VisProg from '../../../src/pages/VisProgPage/VisProgLogic';
|
||||
|
||||
// --- Mocks ---
|
||||
|
||||
// Mock the Zustand store
|
||||
jest.mock('../../../src/utils/programStore', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the API layer including hooks
|
||||
jest.mock('../../../src/pages/MonitoringPage/MonitoringPageAPI', () => ({
|
||||
nextPhase: jest.fn(),
|
||||
resetPhase: jest.fn(),
|
||||
pauseExperiment: jest.fn(),
|
||||
playExperiment: jest.fn(),
|
||||
// We mock these to capture the callbacks and trigger them manually in tests
|
||||
useExperimentLogger: jest.fn(),
|
||||
useStatusLogger: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock VisProg functionality
|
||||
jest.mock('../../../src/pages/VisProgPage/VisProgLogic', () => ({
|
||||
graphReducer: jest.fn(),
|
||||
runProgramm: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock Child Components to reduce noise (optional, but keeps unit test focused)
|
||||
// For this test, we will allow them to render to test data passing,
|
||||
// but we mock RobotConnected as it has its own side effects
|
||||
jest.mock('../../../src/pages/MonitoringPage/MonitoringPageComponents', () => {
|
||||
const original = jest.requireActual('../../../src/pages/MonitoringPage/MonitoringPageComponents');
|
||||
return {
|
||||
...original,
|
||||
RobotConnected: () => <div data-testid="robot-connected-mock">Robot Status</div>,
|
||||
};
|
||||
});
|
||||
|
||||
describe('MonitoringPage', () => {
|
||||
// Capture stream callbacks
|
||||
let streamUpdateCallback: (data: any) => void;
|
||||
let statusUpdateCallback: (data: any) => void;
|
||||
|
||||
// Setup default store state
|
||||
const mockGetPhaseIds = jest.fn();
|
||||
const mockGetPhaseNames = jest.fn();
|
||||
const mockGetNorms = jest.fn();
|
||||
const mockGetGoals = jest.fn();
|
||||
const mockGetTriggers = jest.fn();
|
||||
const mockSetProgramState = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Default Store Implementation
|
||||
(useProgramStore as unknown as jest.Mock).mockImplementation((selector) => {
|
||||
const state = {
|
||||
getPhaseIds: mockGetPhaseIds,
|
||||
getPhaseNames: mockGetPhaseNames,
|
||||
getNormsInPhase: mockGetNorms,
|
||||
getGoalsInPhase: mockGetGoals,
|
||||
getTriggersInPhase: mockGetTriggers,
|
||||
setProgramState: mockSetProgramState,
|
||||
};
|
||||
return selector(state);
|
||||
});
|
||||
|
||||
// Capture the hook callbacks
|
||||
(MonitoringAPI.useExperimentLogger as jest.Mock).mockImplementation((cb) => {
|
||||
streamUpdateCallback = cb;
|
||||
});
|
||||
(MonitoringAPI.useStatusLogger as jest.Mock).mockImplementation((cb) => {
|
||||
statusUpdateCallback = cb;
|
||||
});
|
||||
|
||||
// Default mock return values
|
||||
mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']);
|
||||
mockGetPhaseNames.mockReturnValue(['Intro', 'Main']);
|
||||
mockGetGoals.mockReturnValue([{ id: 'g1', name: 'Goal 1' }, { id: 'g2', name: 'Goal 2' }]);
|
||||
mockGetTriggers.mockReturnValue([{ id: 't1', name: 'Trigger 1' }]);
|
||||
mockGetNorms.mockReturnValue([
|
||||
{ id: 'n1', norm: 'Norm 1', condition: null },
|
||||
{ id: 'cn1', norm: 'Cond Norm 1', condition: 'some-cond' }
|
||||
]);
|
||||
});
|
||||
|
||||
test('renders "No program loaded" when phaseIds are empty', () => {
|
||||
mockGetPhaseIds.mockReturnValue([]);
|
||||
render(<MonitoringPage />);
|
||||
expect(screen.getByText('No program loaded.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the dashboard with initial state', () => {
|
||||
render(<MonitoringPage />);
|
||||
|
||||
// Check Header
|
||||
expect(screen.getByText('Phase 1:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Intro')).toBeInTheDocument();
|
||||
|
||||
// Check Lists
|
||||
expect(screen.getByText(/Goal 1/)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Trigger 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Norm 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cond Norm 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Control Buttons', () => {
|
||||
test('Pause calls API and updates UI', async () => {
|
||||
render(<MonitoringPage />);
|
||||
const pauseBtn = screen.getByText('❚❚');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(pauseBtn);
|
||||
});
|
||||
|
||||
expect(MonitoringAPI.pauseExperiment).toHaveBeenCalled();
|
||||
// Ensure local state toggled (we check if play button is now inactive style or pause active)
|
||||
});
|
||||
|
||||
test('Play calls API and updates UI', async () => {
|
||||
render(<MonitoringPage />);
|
||||
const playBtn = screen.getByText('▶');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(playBtn);
|
||||
});
|
||||
|
||||
expect(MonitoringAPI.playExperiment).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Next Phase calls API', async () => {
|
||||
render(<MonitoringPage />);
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('⏭'));
|
||||
});
|
||||
expect(MonitoringAPI.nextPhase).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Reset Experiment calls logic and resets state', async () => {
|
||||
render(<MonitoringPage />);
|
||||
|
||||
// Mock graph reducer return
|
||||
(VisProg.graphReducer as jest.Mock).mockReturnValue([{ id: 'new-phase' }]);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('⟲'));
|
||||
});
|
||||
|
||||
expect(VisProg.graphReducer).toHaveBeenCalled();
|
||||
expect(mockSetProgramState).toHaveBeenCalledWith({ phases: [{ id: 'new-phase' }] });
|
||||
expect(VisProg.runProgramm).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Reset Experiment handles errors gracefully', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
(VisProg.runProgramm as jest.Mock).mockRejectedValue(new Error('Fail'));
|
||||
|
||||
render(<MonitoringPage />);
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('⟲'));
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Failed to reset program:', expect.any(Error));
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stream Updates (useExperimentLogger)', () => {
|
||||
test('Handles phase_update to next phase', () => {
|
||||
render(<MonitoringPage />);
|
||||
|
||||
expect(screen.getByText('Intro')).toBeInTheDocument(); // Phase 0
|
||||
|
||||
act(() => {
|
||||
streamUpdateCallback({ type: 'phase_update', id: 'phase-2' });
|
||||
});
|
||||
|
||||
expect(screen.getByText('Main')).toBeInTheDocument(); // Phase 1
|
||||
});
|
||||
|
||||
test('Handles phase_update to "end"', () => {
|
||||
render(<MonitoringPage />);
|
||||
|
||||
act(() => {
|
||||
streamUpdateCallback({ type: 'phase_update', id: 'end' });
|
||||
});
|
||||
|
||||
expect(screen.getByText('Experiment finished')).toBeInTheDocument();
|
||||
expect(screen.getByText('All phases have been successfully completed.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Handles phase_update with unknown ID gracefully', () => {
|
||||
render(<MonitoringPage />);
|
||||
act(() => {
|
||||
streamUpdateCallback({ type: 'phase_update', id: 'unknown-phase' });
|
||||
});
|
||||
// Should remain on current phase
|
||||
expect(screen.getByText('Intro')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Handles goal_update: advances index and marks previous as achieved', () => {
|
||||
render(<MonitoringPage />);
|
||||
|
||||
// Initial: Goal 1 (index 0) is current.
|
||||
// Send update for Goal 2 (index 1).
|
||||
act(() => {
|
||||
streamUpdateCallback({ type: 'goal_update', id: 'g2' });
|
||||
});
|
||||
|
||||
// Goal 1 should now be marked achieved (passed via activeIds)
|
||||
// Goal 2 should be current.
|
||||
|
||||
// We can inspect the "StatusList" props implicitly by checking styling or indicators if not mocked,
|
||||
// but since we render the full component, we check the class/text.
|
||||
// Goal 1 should have checkmark (override logic puts checkmark for activeIds)
|
||||
// The implementation details of StatusList show ✔️ for activeIds.
|
||||
|
||||
const items = screen.getAllByRole('listitem');
|
||||
// Helper to find checkmarks within items
|
||||
expect(items[0]).toHaveTextContent('Goal 1');
|
||||
// After update, g1 is active (achieved), g2 is current
|
||||
// logic: loop i < gIndex (1). activeIds['g1'] = true.
|
||||
});
|
||||
|
||||
test('Handles goal_update with unknown ID', () => {
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
render(<MonitoringPage />);
|
||||
act(() => {
|
||||
streamUpdateCallback({ type: 'goal_update', id: 'unknown-goal' });
|
||||
});
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Goal unknown-goal not found'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('Handles trigger_update', () => {
|
||||
render(<MonitoringPage />);
|
||||
|
||||
// Trigger 1 initially not achieved
|
||||
act(() => {
|
||||
streamUpdateCallback({ type: 'trigger_update', id: 't1', achieved: true });
|
||||
});
|
||||
|
||||
// StatusList logic: if activeId is true, show ✔️
|
||||
// We look for visual confirmation or check logic
|
||||
const triggerList = screen.getByText('Triggers').parentElement;
|
||||
expect(triggerList).toHaveTextContent('✔️'); // Assuming 't1' is the only trigger
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status Updates (useStatusLogger)', () => {
|
||||
test('Handles cond_norms_state_update', () => {
|
||||
render(<MonitoringPage />);
|
||||
|
||||
// Initial state: activeIds empty.
|
||||
act(() => {
|
||||
statusUpdateCallback({
|
||||
type: 'cond_norms_state_update',
|
||||
norms: [{ id: 'cn1', active: true }]
|
||||
});
|
||||
});
|
||||
|
||||
// Conditional Norm 1 should now be active
|
||||
const cnList = screen.getByText('Conditional Norms').parentElement;
|
||||
expect(cnList).toHaveTextContent('✔️');
|
||||
});
|
||||
|
||||
test('Ignores status update if no changes detected', () => {
|
||||
render(<MonitoringPage />);
|
||||
// First update
|
||||
act(() => {
|
||||
statusUpdateCallback({ type: 'cond_norms_state_update', norms: [{ id: 'cn1', active: true }] });
|
||||
});
|
||||
|
||||
// Second identical update - strictly checking if this causes a rerender is hard in RTL,
|
||||
// but we ensure no errors and state remains consistent.
|
||||
act(() => {
|
||||
statusUpdateCallback({ type: 'cond_norms_state_update', norms: [{ id: 'cn1', active: true }] });
|
||||
});
|
||||
|
||||
const cnList = screen.getByText('Conditional Norms').parentElement;
|
||||
expect(cnList).toHaveTextContent('✔️');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
229
test/pages/monitoringPage/MonitoringPageAPI.test.ts
Normal file
229
test/pages/monitoringPage/MonitoringPageAPI.test.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Corrected Imports
|
||||
import {
|
||||
GestureControls,
|
||||
SpeechPresets,
|
||||
DirectSpeechInput,
|
||||
StatusList,
|
||||
RobotConnected
|
||||
} from '../../../src/pages/MonitoringPage/MonitoringPageComponents';
|
||||
|
||||
import * as MonitoringAPI from '../../../src/pages/MonitoringPage/MonitoringPageAPI';
|
||||
|
||||
// Mock the API Call function with the correct path
|
||||
jest.mock('../../../src/pages/MonitoringPage/MonitoringPageAPI', () => ({
|
||||
sendAPICall: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('MonitoringPageComponents', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GestureControls', () => {
|
||||
test('renders and sends gesture command', () => {
|
||||
render(<GestureControls />);
|
||||
|
||||
// Change selection
|
||||
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'animations/Stand/Gestures/Thinking_8' } });
|
||||
|
||||
// Click button
|
||||
fireEvent.click(screen.getByText('Actuate'));
|
||||
|
||||
expect(MonitoringAPI.sendAPICall).toHaveBeenCalledWith('gesture', 'animations/Stand/Gestures/Thinking_8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SpeechPresets', () => {
|
||||
test('renders buttons and sends speech command', () => {
|
||||
render(<SpeechPresets />);
|
||||
|
||||
const btn = screen.getByText('"Hello, I\'m Pepper"');
|
||||
fireEvent.click(btn);
|
||||
|
||||
expect(MonitoringAPI.sendAPICall).toHaveBeenCalledWith('speech', "Hello, I'm Pepper");
|
||||
});
|
||||
});
|
||||
|
||||
describe('DirectSpeechInput', () => {
|
||||
test('inputs text and sends on button click', () => {
|
||||
render(<DirectSpeechInput />);
|
||||
const input = screen.getByPlaceholderText('Type message...');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'Custom text' } });
|
||||
fireEvent.click(screen.getByText('Send'));
|
||||
|
||||
expect(MonitoringAPI.sendAPICall).toHaveBeenCalledWith('speech', 'Custom text');
|
||||
expect(input).toHaveValue(''); // Should clear
|
||||
});
|
||||
|
||||
test('sends on Enter key', () => {
|
||||
render(<DirectSpeechInput />);
|
||||
const input = screen.getByPlaceholderText('Type message...');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'Enter text' } });
|
||||
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
expect(MonitoringAPI.sendAPICall).toHaveBeenCalledWith('speech', 'Enter text');
|
||||
});
|
||||
|
||||
test('does not send empty text', () => {
|
||||
render(<DirectSpeechInput />);
|
||||
fireEvent.click(screen.getByText('Send'));
|
||||
expect(MonitoringAPI.sendAPICall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('StatusList', () => {
|
||||
const mockSet = jest.fn();
|
||||
const items = [
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' }
|
||||
];
|
||||
|
||||
test('renders list items', () => {
|
||||
render(<StatusList title="Test List" items={items} type="goal" activeIds={{}} />);
|
||||
expect(screen.getByText('Test List')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Goals: click override on inactive item calls API', () => {
|
||||
render(
|
||||
<StatusList
|
||||
title="Goals"
|
||||
items={items}
|
||||
type="goal"
|
||||
activeIds={{}}
|
||||
setActiveIds={mockSet}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click the X (inactive)
|
||||
const indicator = screen.getAllByText('❌')[0];
|
||||
fireEvent.click(indicator);
|
||||
|
||||
expect(MonitoringAPI.sendAPICall).toHaveBeenCalledWith('override', '1');
|
||||
expect(mockSet).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Conditional Norms: click override on ACTIVE item unachieves', () => {
|
||||
render(
|
||||
<StatusList
|
||||
title="CN"
|
||||
items={items}
|
||||
type="cond_norm"
|
||||
activeIds={{ '1': true }}
|
||||
/>
|
||||
);
|
||||
|
||||
const indicator = screen.getByText('✔️'); // It is active
|
||||
fireEvent.click(indicator);
|
||||
|
||||
expect(MonitoringAPI.sendAPICall).toHaveBeenCalledWith('override_unachieve', '1');
|
||||
});
|
||||
|
||||
test('Current Goal highlighting', () => {
|
||||
render(
|
||||
<StatusList
|
||||
title="Goals"
|
||||
items={items}
|
||||
type="goal"
|
||||
activeIds={{}}
|
||||
currentGoalIndex={0}
|
||||
/>
|
||||
);
|
||||
// Using regex to handle the "(Current)" text
|
||||
expect(screen.getByText(/Item 1/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/(Current)/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RobotConnected', () => {
|
||||
let mockEventSource: any;
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'EventSource', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(() => ({
|
||||
close: jest.fn(),
|
||||
onmessage: null,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockEventSource = new window.EventSource('url');
|
||||
(window.EventSource as unknown as jest.Mock).mockClear();
|
||||
(window.EventSource as unknown as jest.Mock).mockImplementation(() => mockEventSource);
|
||||
});
|
||||
|
||||
test('displays disconnected initially', () => {
|
||||
render(<RobotConnected />);
|
||||
expect(screen.getByText('● Robot is disconnected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('updates to connected when SSE receives true', async () => {
|
||||
render(<RobotConnected />);
|
||||
|
||||
act(() => {
|
||||
if(mockEventSource.onmessage) {
|
||||
mockEventSource.onmessage({ data: 'true' } as MessageEvent);
|
||||
}
|
||||
});
|
||||
|
||||
expect(await screen.findByText('● Robot is connected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('handles invalid JSON gracefully', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
render(<RobotConnected />);
|
||||
|
||||
act(() => {
|
||||
if(mockEventSource.onmessage) {
|
||||
mockEventSource.onmessage({ data: 'invalid-json' } as MessageEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Should catch error and log it, state remains disconnected
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Ping message not in correct format:', 'invalid-json');
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('logs error if state update fails (inner catch block)', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// 1. Force useState to return a setter that throws an error
|
||||
const mockThrowingSetter = jest.fn(() => { throw new Error('Forced State Error'); });
|
||||
|
||||
// We use mockImplementation to return [currentState, throwingSetter]
|
||||
const useStateSpy = jest.spyOn(React, 'useState')
|
||||
.mockImplementation(() => [null, mockThrowingSetter]);
|
||||
|
||||
render(<RobotConnected />);
|
||||
|
||||
// 2. Trigger the event with VALID JSON ("true")
|
||||
// This passes the first JSON.parse try/catch,
|
||||
// but fails when calling setConnected(true) because of our mock.
|
||||
await act(async () => {
|
||||
if (mockEventSource.onmessage) {
|
||||
mockEventSource.onmessage({ data: 'true' } as MessageEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Verify the specific error log from line 205
|
||||
expect(consoleSpy).toHaveBeenCalledWith("couldnt extract connected from incoming ping data");
|
||||
|
||||
// Cleanup spies
|
||||
useStateSpy.mockRestore();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,47 +137,46 @@ describe('TriggerNode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
//doesnt work anymore, but I have no idea how to fix it
|
||||
// describe('TriggerConnects Function', () => {
|
||||
// it('should correctly remove a goal from the triggers plan after it has been disconnected', () => {
|
||||
// // first, define the goal node and trigger node.
|
||||
// const goal: Node = {
|
||||
// id: 'g-1',
|
||||
// type: 'goal',
|
||||
// position: { x: 0, y: 0 },
|
||||
// data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), name: 'Goal 1' },
|
||||
// };
|
||||
describe('TriggerConnects Function', () => {
|
||||
it('should correctly remove a goal from the triggers plan after it has been disconnected', () => {
|
||||
// first, define the goal node and trigger node.
|
||||
const goal: Node = {
|
||||
id: 'g-1',
|
||||
type: 'goal',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), name: 'Goal 1' },
|
||||
};
|
||||
|
||||
// const trigger: Node<TriggerNodeData> = {
|
||||
// id: 'trigger-1',
|
||||
// type: 'trigger',
|
||||
// position: { x: 0, y: 0 },
|
||||
// data: { ...JSON.parse(JSON.stringify(TriggerNodeDefaults)) },
|
||||
// };
|
||||
const trigger: Node<TriggerNodeData> = {
|
||||
id: 'trigger-1',
|
||||
type: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { ...JSON.parse(JSON.stringify(TriggerNodeDefaults)) },
|
||||
};
|
||||
|
||||
// // set initial store
|
||||
// useFlowStore.setState({ nodes: [goal, trigger], edges: [] });
|
||||
// set initial store
|
||||
useFlowStore.setState({ nodes: [goal, trigger], edges: [] });
|
||||
|
||||
// // then, connect the goal to the trigger.
|
||||
// act(() => {
|
||||
// useFlowStore.getState().onConnect({ source: 'g-1', target: 'trigger-1', sourceHandle: null, targetHandle: null });
|
||||
// });
|
||||
// // expect the goal id to be part of a goal step of the plan.
|
||||
// let updatedTrigger = useFlowStore.getState().nodes.find((n) => n.id === 'trigger-1');
|
||||
// expect(updatedTrigger?.data.plan).toBeDefined();
|
||||
// const plan = updatedTrigger?.data.plan as any;
|
||||
// expect(plan.steps.find((s: any) => s.id === 'g-1')).toBeDefined();
|
||||
// then, connect the goal to the trigger.
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect({ source: 'g-1', target: 'trigger-1', sourceHandle: null, targetHandle: null });
|
||||
});
|
||||
// expect the goal id to be part of a goal step of the plan.
|
||||
let updatedTrigger = useFlowStore.getState().nodes.find((n) => n.id === 'trigger-1');
|
||||
expect(updatedTrigger?.data.plan).toBeDefined();
|
||||
const plan = updatedTrigger?.data.plan as any;
|
||||
expect(plan.steps.find((s: any) => s.id === 'g-1')).toBeDefined();
|
||||
|
||||
// // then, disconnect the goal from the trigger.
|
||||
// act(() => {
|
||||
// useFlowStore.getState().onEdgesDelete([{ id: 'g-1-trigger-1', source: 'g-1', target: 'trigger-1' } as any]);
|
||||
// });
|
||||
// then, disconnect the goal from the trigger.
|
||||
act(() => {
|
||||
useFlowStore.getState().onEdgesDelete([{ id: 'g-1-trigger-1', source: 'g-1', target: 'trigger-1' } as any]);
|
||||
});
|
||||
|
||||
// // finally, expect the goal id to NOT be part of the goal step of the plan.
|
||||
// updatedTrigger = useFlowStore.getState().nodes.find((n) => n.id === 'trigger-1');
|
||||
// const planAfter = updatedTrigger?.data.plan as any;
|
||||
// const stillHas = planAfter?.steps?.find((s: any) => s.id === 'g-1');
|
||||
// expect(stillHas).toBeUndefined();
|
||||
// });
|
||||
// });
|
||||
// finally, expect the goal id to NOT be part of the goal step of the plan.
|
||||
updatedTrigger = useFlowStore.getState().nodes.find((n) => n.id === 'trigger-1');
|
||||
const planAfter = updatedTrigger?.data.plan as any;
|
||||
const stillHas = planAfter?.steps?.find((s: any) => s.id === 'g-1');
|
||||
expect(stillHas).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user