uncloseai.com/tests/modals/tts-modal.test.js
Russell Ballestrini c721b88473 fix: add fallback handling for window modal functions in embed modal
- Add safety checks for window.openTTSModal and window.openTranslateModal availability
- Implement fallback dynamic imports if window functions not available
- Prevents "window.openTTSModal is not a function" and "window.openTranslateModal is not a function" errors
- Provides graceful error handling with user feedback if modal imports fail
- Resolves loading order dependency issues between ui.js and embed modal

This ensures modal buttons work regardless of script loading order.
2025-07-03 18:51:48 -04:00

413 lines
No EOL
14 KiB
JavaScript

// Jest tests for TTS Modal functionality
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
// Mock the dependencies before importing the module
jest.mock('../../src/config.js', () => ({
TTS_API_URL: 'https://mock-tts-api.com',
API_KEY: 'mock-api-key'
}));
jest.mock('../../src/ui-themes.js', () => ({
detectCurrentTheme: jest.fn(() => 'light'),
getThemeColors: jest.fn(() => ({
bg: '#ffffff',
text: '#000000',
accent: '#007bff'
}))
}));
jest.mock('../../src/ui-translations.js', () => ({
getUIText: jest.fn((key) => `mock-${key}`),
getUserLanguagePreference: jest.fn(() => 'en')
}));
// Import the module under test
let openTTSModal;
describe('TTS Modal', () => {
beforeEach(async () => {
// Reset all mocks
global.resetMocks();
// Clear any existing modals
const existingModal = document.getElementById('tts-modal');
if (existingModal) {
existingModal.remove();
}
// Mock fetch for TTS API calls
global.fetch.mockImplementation((url) => {
if (url.includes('tts')) {
return Promise.resolve({
ok: true,
blob: () => Promise.resolve(new Blob(['mock audio data'], { type: 'audio/mpeg' }))
});
}
return Promise.reject(new Error('Unexpected URL'));
});
// Dynamically import the module to ensure fresh state
const module = await import('../../src/tts-modal.js');
openTTSModal = module.openTTSModal;
});
describe('Modal Creation and Display', () => {
test('should create TTS modal with correct structure', async () => {
await openTTSModal('Test content for TTS');
const modal = document.getElementById('tts-modal');
expect(modal).toBeTruthy();
expect(modal.tagName).toBe('DIALOG');
// Check modal structure
const article = modal.querySelector('article');
expect(article).toBeTruthy();
const header = modal.querySelector('header');
expect(header).toBeTruthy();
expect(header.textContent).toContain('mock-'); // Should contain mocked UI text
const closeButton = modal.querySelector('button[data-close]');
expect(closeButton).toBeTruthy();
});
test('should open modal with showModal method', async () => {
const showModalSpy = jest.spyOn(HTMLDialogElement.prototype, 'showModal');
await openTTSModal('Test content');
expect(showModalSpy).toHaveBeenCalled();
const modal = document.getElementById('tts-modal');
expect(modal.open).toBe(true);
});
test('should prevent opening multiple TTS modals', async () => {
await openTTSModal('First content');
const firstModal = document.getElementById('tts-modal');
await openTTSModal('Second content');
const modals = document.querySelectorAll('#tts-modal');
expect(modals.length).toBe(1);
expect(firstModal).toBe(document.getElementById('tts-modal'));
});
test('should handle empty or null content gracefully', async () => {
await openTTSModal('');
let modal = document.getElementById('tts-modal');
expect(modal).toBeTruthy();
await openTTSModal(null);
modal = document.getElementById('tts-modal');
expect(modal).toBeTruthy();
await openTTSModal(undefined);
modal = document.getElementById('tts-modal');
expect(modal).toBeTruthy();
});
});
describe('Voice Selection', () => {
test('should create voice selection dropdown', async () => {
await openTTSModal('Test content');
const voiceSelect = document.querySelector('#tts-voice-select');
expect(voiceSelect).toBeTruthy();
expect(voiceSelect.tagName).toBe('SELECT');
});
test('should populate voice options', async () => {
await openTTSModal('Test content');
const voiceSelect = document.querySelector('#tts-voice-select');
const options = voiceSelect.querySelectorAll('option');
expect(options.length).toBeGreaterThan(0);
// Check for expected voice options
const optionValues = Array.from(options).map(opt => opt.value);
expect(optionValues).toContain('alloy');
expect(optionValues).toContain('echo');
expect(optionValues).toContain('fable');
});
test('should update selected voice on change', async () => {
await openTTSModal('Test content');
const voiceSelect = document.querySelector('#tts-voice-select');
voiceSelect.value = 'nova';
voiceSelect.dispatchEvent(new Event('change'));
expect(voiceSelect.value).toBe('nova');
});
});
describe('Speed Control', () => {
test('should create speed control slider', async () => {
await openTTSModal('Test content');
const speedSlider = document.querySelector('#tts-speed-slider');
expect(speedSlider).toBeTruthy();
expect(speedSlider.type).toBe('range');
});
test('should have correct speed range', async () => {
await openTTSModal('Test content');
const speedSlider = document.querySelector('#tts-speed-slider');
expect(speedSlider.min).toBe('0.5');
expect(speedSlider.max).toBe('2.0');
expect(speedSlider.step).toBe('0.1');
expect(speedSlider.value).toBe('1.0');
});
test('should update speed display when slider changes', async () => {
await openTTSModal('Test content');
const speedSlider = document.querySelector('#tts-speed-slider');
const speedDisplay = document.querySelector('#speed-display');
speedSlider.value = '1.5';
speedSlider.dispatchEvent(new Event('input'));
expect(speedDisplay.textContent).toContain('1.5');
});
});
describe('Audio Generation', () => {
test('should generate audio when generate button is clicked', async () => {
await openTTSModal('Test content for audio generation');
const generateButton = document.querySelector('#generate-tts-btn');
expect(generateButton).toBeTruthy();
generateButton.click();
// Wait for async operation
await new Promise(resolve => setTimeout(resolve, 100));
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('tts'),
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'Authorization': 'Bearer mock-api-key'
}),
body: expect.stringContaining('Test content for audio generation')
})
);
});
test('should disable generate button during generation', async () => {
await openTTSModal('Test content');
const generateButton = document.querySelector('#generate-tts-btn');
const originalText = generateButton.textContent;
generateButton.click();
expect(generateButton.disabled).toBe(true);
expect(generateButton.textContent).not.toBe(originalText);
});
test('should show error message on API failure', async () => {
global.fetch.mockRejectedValueOnce(new Error('API Error'));
await openTTSModal('Test content');
const generateButton = document.querySelector('#generate-tts-btn');
generateButton.click();
// Wait for error handling
await new Promise(resolve => setTimeout(resolve, 100));
// Check for error message (implementation dependent)
const errorElement = document.querySelector('.error-message, [data-error]');
expect(errorElement || generateButton.textContent.includes('Error')).toBeTruthy();
});
test('should create audio player after successful generation', async () => {
await openTTSModal('Test content');
const generateButton = document.querySelector('#generate-tts-btn');
generateButton.click();
// Wait for async operation
await new Promise(resolve => setTimeout(resolve, 100));
const audioPlayer = document.querySelector('audio');
expect(audioPlayer).toBeTruthy();
expect(audioPlayer.controls).toBe(true);
});
});
describe('Download Functionality', () => {
test('should create download button after audio generation', async () => {
await openTTSModal('Test content');
const generateButton = document.querySelector('#generate-tts-btn');
generateButton.click();
// Wait for async operation
await new Promise(resolve => setTimeout(resolve, 100));
const downloadButton = document.querySelector('#download-tts-btn, [data-download]');
expect(downloadButton).toBeTruthy();
});
test('should trigger download when download button is clicked', async () => {
// Mock URL.createObjectURL
global.URL.createObjectURL = jest.fn(() => 'blob:mock-url');
global.URL.revokeObjectURL = jest.fn();
await openTTSModal('Test content');
const generateButton = document.querySelector('#generate-tts-btn');
generateButton.click();
// Wait for generation
await new Promise(resolve => setTimeout(resolve, 100));
const downloadButton = document.querySelector('#download-tts-btn, [data-download]');
if (downloadButton) {
downloadButton.click();
expect(global.URL.createObjectURL).toHaveBeenCalled();
}
});
});
describe('Modal Closing', () => {
test('should close modal when close button is clicked', async () => {
await openTTSModal('Test content');
const modal = document.getElementById('tts-modal');
const closeButton = modal.querySelector('button[data-close], .close-btn');
const closeSpy = jest.spyOn(modal, 'close');
closeButton.click();
expect(closeSpy).toHaveBeenCalled();
});
test('should remove modal from DOM when closed', async () => {
await openTTSModal('Test content');
const modal = document.getElementById('tts-modal');
const closeButton = modal.querySelector('button[data-close], .close-btn');
closeButton.click();
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 50));
expect(document.getElementById('tts-modal')).toBeFalsy();
});
test('should clean up audio resources when modal is closed', async () => {
await openTTSModal('Test content');
// Generate audio first
const generateButton = document.querySelector('#generate-tts-btn');
generateButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
const modal = document.getElementById('tts-modal');
const audio = modal.querySelector('audio');
if (audio) {
const pauseSpy = jest.spyOn(audio, 'pause');
const closeButton = modal.querySelector('button[data-close], .close-btn');
closeButton.click();
expect(pauseSpy).toHaveBeenCalled();
}
});
});
describe('Accessibility', () => {
test('should have proper ARIA attributes', async () => {
await openTTSModal('Test content');
const modal = document.getElementById('tts-modal');
// Check for accessibility attributes
expect(modal.getAttribute('role') || modal.tagName === 'DIALOG').toBeTruthy();
expect(modal.getAttribute('aria-modal')).toBe('true');
const closeButton = modal.querySelector('button[data-close], .close-btn');
expect(closeButton.getAttribute('aria-label')).toBeTruthy();
});
test('should have proper keyboard navigation', async () => {
await openTTSModal('Test content');
const modal = document.getElementById('tts-modal');
const focusableElements = modal.querySelectorAll(
'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
expect(focusableElements.length).toBeGreaterThan(0);
// First focusable element should receive focus
expect(document.activeElement).toBe(focusableElements[0]);
});
test('should handle escape key to close modal', async () => {
await openTTSModal('Test content');
const modal = document.getElementById('tts-modal');
const closeSpy = jest.spyOn(modal, 'close');
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
document.dispatchEvent(escapeEvent);
expect(closeSpy).toHaveBeenCalled();
});
});
describe('Error Handling', () => {
test('should handle missing TTS API gracefully', async () => {
global.fetch.mockRejectedValueOnce(new Error('Network error'));
expect(async () => {
await openTTSModal('Test content');
const generateButton = document.querySelector('#generate-tts-btn');
generateButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
test('should handle invalid voice selection', async () => {
await openTTSModal('Test content');
const voiceSelect = document.querySelector('#tts-voice-select');
voiceSelect.value = 'invalid-voice';
voiceSelect.dispatchEvent(new Event('change'));
const generateButton = document.querySelector('#generate-tts-btn');
expect(() => generateButton.click()).not.toThrow();
});
test('should handle malformed API response', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error'
});
await openTTSModal('Test content');
const generateButton = document.querySelector('#generate-tts-btn');
expect(async () => {
generateButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
});
});