- 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.
413 lines
No EOL
14 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
}); |