All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m58s
300 lines
12 KiB
TypeScript
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',
|
|
);
|
|
});
|
|
});
|
|
});
|