Files
flyer-crawler.projectium.com/src/pages/VoiceLabPage.test.tsx
Torben Sorensen 9fd15f3a50
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m58s
unit test auto-provider refactor
2026-01-02 11:33:11 -08:00

300 lines
12 KiB
TypeScript

// src/pages/VoiceLabPage.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { VoiceLabPage } from './VoiceLabPage';
import * as aiApiClient from '../services/aiApiClient';
import { notifyError } from '../services/notificationService';
import { logger } from '../services/logger.client';
// Extensive logging for debugging
const LOG_PREFIX = '[TEST DEBUG]';
// The aiApiClient, notificationService, and logger are mocked globally.
// We can get a typed reference to the aiApiClient for individual test overrides.
const mockedAiApiClient = vi.mocked(aiApiClient);
// Define mock at module level so it can be referenced in the implementation
const mockAudioPlay = vi.fn(() => {
console.log(`${LOG_PREFIX} mockAudioPlay executed`);
return Promise.resolve();
});
describe('VoiceLabPage', () => {
beforeEach(() => {
console.log(`${LOG_PREFIX} beforeEach: Cleaning mocks and setting up Audio mock`);
vi.clearAllMocks();
mockAudioPlay.mockClear();
// Mock the global Audio constructor
// We use a robust mocking strategy to ensure it overrides JSDOM's Audio
const AudioMock = vi.fn(function (url: string) {
console.log(`${LOG_PREFIX} Audio constructor called with URL:`, url);
return {
play: mockAudioPlay,
pause: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
src: url,
};
});
// Stub global Audio
vi.stubGlobal('Audio', AudioMock);
// Forcefully overwrite window.Audio using defineProperty to bypass potential JSDOM read-only restrictions
Object.defineProperty(window, 'Audio', {
writable: true,
value: AudioMock,
});
});
afterEach(() => {
console.log(`${LOG_PREFIX} afterEach: unstubbing globals`);
vi.unstubAllGlobals();
});
it('should render the initial state correctly', () => {
console.log(`${LOG_PREFIX} Test: render initial state`);
render(<VoiceLabPage />);
expect(screen.getByRole('heading', { name: /admin voice lab/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /text-to-speech generation/i })).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue(
'Hello! This is a test of the text-to-speech generation.',
);
expect(screen.getByRole('button', { name: /generate & play/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /replay/i })).not.toBeInTheDocument();
console.log(`${LOG_PREFIX} Test: render initial state passed`);
});
describe('Text-to-Speech Generation', () => {
it('should call generateSpeechFromText and play audio on success', async () => {
console.log(`${LOG_PREFIX} Test: generateSpeechFromText success flow`);
const mockBase64Audio = 'mock-audio-data';
mockedAiApiClient.generateSpeechFromText.mockResolvedValue({
json: async () => {
console.log(`${LOG_PREFIX} Mock response.json() called`);
return mockBase64Audio;
},
} as Response);
render(<VoiceLabPage />);
const generateButton = screen.getByRole('button', { name: /generate & play/i });
console.log(`${LOG_PREFIX} Clicking generate button`);
fireEvent.click(generateButton);
// Check for loading state
expect(generateButton).toBeDisabled();
await waitFor(() => {
console.log(`${LOG_PREFIX} Waiting for generateSpeechFromText call`);
expect(mockedAiApiClient.generateSpeechFromText).toHaveBeenCalledWith(
'Hello! This is a test of the text-to-speech generation.',
);
});
// Wait specifically for the audio constructor call
await waitFor(() => {
console.log(`${LOG_PREFIX} Waiting for Audio constructor call`);
expect(global.Audio).toHaveBeenCalledWith(`data:audio/mpeg;base64,${mockBase64Audio}`);
});
// Then check play
await waitFor(() => {
console.log(`${LOG_PREFIX} Waiting for mockAudioPlay call`);
// Debugging helper: if play wasn't called, check if error was notified
if (mockAudioPlay.mock.calls.length === 0) {
const errorCalls = vi.mocked(notifyError).mock.calls;
if (errorCalls.length > 0) {
console.error(
`${LOG_PREFIX} Unexpected: notifyError was called during success test:`,
errorCalls,
);
} else {
// If notifyError wasn't called, verify if Audio constructor was actually called as expected
console.log(`${LOG_PREFIX} mockAudioPlay not called yet...`);
}
}
expect(mockAudioPlay).toHaveBeenCalled();
});
// Check that loading state is gone and replay button is visible
await waitFor(() => {
console.log(`${LOG_PREFIX} Waiting for UI update (button enabled, replay visible)`);
expect(screen.getByRole('button', { name: /generate & play/i })).not.toBeDisabled();
expect(screen.getByRole('button', { name: /replay/i })).toBeInTheDocument();
});
console.log(`${LOG_PREFIX} Test: generateSpeechFromText success flow passed`);
});
it('should show an error notification if text is empty', async () => {
console.log(`${LOG_PREFIX} Test: empty text validation`);
render(<VoiceLabPage />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: ' ' } }); // Empty text
console.log(`${LOG_PREFIX} Clicking generate button with empty text`);
fireEvent.click(screen.getByRole('button', { name: /generate & play/i }));
await waitFor(() => {
expect(notifyError).toHaveBeenCalledWith('Please enter some text to generate speech.');
});
expect(mockedAiApiClient.generateSpeechFromText).not.toHaveBeenCalled();
console.log(`${LOG_PREFIX} Test: empty text validation passed`);
});
it('should show an error notification if API call fails', async () => {
console.log(`${LOG_PREFIX} Test: API failure`);
mockedAiApiClient.generateSpeechFromText.mockRejectedValue(new Error('AI service is down'));
render(<VoiceLabPage />);
console.log(`${LOG_PREFIX} Clicking generate button (expecting failure)`);
fireEvent.click(screen.getByRole('button', { name: /generate & play/i }));
await waitFor(() => {
expect(notifyError).toHaveBeenCalledWith('Speech generation failed: AI service is down');
});
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Error) },
'Failed to generate speech:',
);
});
it('should show an error if API returns no audio data', async () => {
console.log(`${LOG_PREFIX} Test: No audio data returned`);
mockedAiApiClient.generateSpeechFromText.mockResolvedValue({
json: async () => null, // Simulate falsy response
} as Response);
render(<VoiceLabPage />);
fireEvent.click(screen.getByRole('button', { name: /generate & play/i }));
await waitFor(() => {
expect(notifyError).toHaveBeenCalledWith('The AI did not return any audio data.');
});
console.log(`${LOG_PREFIX} Test: No audio data returned passed`);
});
it('should handle non-Error objects in catch block', async () => {
console.log(`${LOG_PREFIX} Test: Non-error object rejection`);
mockedAiApiClient.generateSpeechFromText.mockRejectedValue('A simple string error');
render(<VoiceLabPage />);
fireEvent.click(screen.getByRole('button', { name: /generate & play/i }));
await waitFor(() => {
expect(notifyError).toHaveBeenCalledWith(
'Speech generation failed: An unknown error occurred.',
);
});
expect(logger.error).toHaveBeenCalledWith(
{ err: 'A simple string error' },
'Failed to generate speech:',
);
});
it('should allow replaying the generated audio', async () => {
console.log(`${LOG_PREFIX} Test: Replay functionality`);
mockedAiApiClient.generateSpeechFromText.mockResolvedValue({
json: async () => 'mock-audio-data',
} as Response);
render(<VoiceLabPage />);
const generateButton = screen.getByRole('button', { name: /generate & play/i });
console.log(`${LOG_PREFIX} Clicking generate button for replay test`);
fireEvent.click(generateButton);
// Wait for the replay button to appear
console.log(`${LOG_PREFIX} Waiting for replay button to appear`);
const replayButton = await screen.findByTestId('replay-button');
console.log(`${LOG_PREFIX} Replay button found`);
// Verify initial play happened
await waitFor(() => {
console.log(`${LOG_PREFIX} Verifying initial play count: 1`);
expect(mockAudioPlay).toHaveBeenCalledTimes(1);
});
// Click the replay button
console.log(`${LOG_PREFIX} Clicking replay button`);
fireEvent.click(replayButton);
// Verify that play was called a second time
await waitFor(() => {
console.log(
`${LOG_PREFIX} Verifying play count reaches 2. Current calls: ${mockAudioPlay.mock.calls.length}`,
);
expect(mockAudioPlay).toHaveBeenCalledTimes(2);
});
console.log(`${LOG_PREFIX} Test: Replay functionality passed`);
});
});
describe('Real-time Voice Session', () => {
it('should call startVoiceSession and show an error notification', async () => {
console.log(`${LOG_PREFIX} Test: Real-time voice session error`);
// The startVoiceSession function is a stub that will throw an error.
// We can mock it to control the error message for the test.
mockedAiApiClient.startVoiceSession.mockImplementation(() => {
console.log(`${LOG_PREFIX} mocked startVoiceSession called`);
throw new Error('WebSocket proxy not implemented.');
});
render(<VoiceLabPage />);
fireEvent.click(screen.getByRole('button', { name: /attempt to start session/i }));
await waitFor(() => {
expect(mockedAiApiClient.startVoiceSession).toHaveBeenCalled();
expect(notifyError).toHaveBeenCalledWith(
'Could not start voice session: WebSocket proxy not implemented.',
);
});
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Error) },
'Failed to start voice session:',
);
});
it('should handle unknown errors in startVoiceSession', async () => {
mockedAiApiClient.startVoiceSession.mockImplementation(() => {
throw 'Unknown session error';
});
render(<VoiceLabPage />);
fireEvent.click(screen.getByRole('button', { name: /attempt to start session/i }));
await waitFor(() => {
expect(notifyError).toHaveBeenCalledWith(
'Could not start voice session: An unknown error occurred.',
);
});
expect(logger.error).toHaveBeenCalledWith(
{ err: 'Unknown session error' },
'Failed to start voice session:',
);
});
it('should handle successful startVoiceSession and log messages', async () => {
mockedAiApiClient.startVoiceSession.mockImplementation(async (config) => {
if (config && config.onmessage) {
// Cast to `any` as we don't need a full LiveServerMessage object for this test
config.onmessage('Test message from socket' as any);
}
});
render(<VoiceLabPage />);
fireEvent.click(screen.getByRole('button', { name: /attempt to start session/i }));
await waitFor(() => {
expect(mockedAiApiClient.startVoiceSession).toHaveBeenCalled();
});
expect(logger.info).toHaveBeenCalledWith(
'Received voice session message:',
'Test message from socket',
);
});
});
});