uncloseai.com/tests/modals/translate-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

573 lines
No EOL
20 KiB
JavaScript

// Jest tests for Translate Modal functionality
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
// Mock the dependencies before importing the module
jest.mock('../../src/config.js', () => ({
API_KEY: 'mock-api-key'
}));
jest.mock('../../src/ui-themes.js', () => ({
detectCurrentTheme: jest.fn(() => 'light'),
getThemeColors: jest.fn(() => ({
bg: '#ffffff',
text: '#000000',
accent: '#007bff',
border: '#dddddd'
}))
}));
jest.mock('../../src/ui-translations.js', () => ({
getUIText: jest.fn((key) => `mock-${key}`),
getUserLanguagePreference: jest.fn(() => 'en')
}));
jest.mock('../../src/language-detection.js', () => ({
detectPageLanguage: jest.fn(() => Promise.resolve('en'))
}));
jest.mock('../../src/translation.js', () => ({
NATIVE_LANGUAGE_NAMES: {
'en': 'English',
'es': 'Español',
'fr': 'Français',
'de': 'Deutsch',
'zh': '中文',
'ja': '日本語',
'ko': '한국어',
'ar': 'العربية',
'hi': 'हिन्दी',
'pt': 'Português'
}
}));
// Import the module under test
let openTranslateModal;
describe('Translate Modal', () => {
beforeEach(async () => {
// Reset all mocks
global.resetMocks();
// Clear any existing modals
const existingModal = document.getElementById('translate-modal');
if (existingModal) {
existingModal.remove();
}
// Mock fetch for translation API calls
global.fetch.mockImplementation((url, options) => {
if (url.includes('chat/completions') || url.includes('translate')) {
return Promise.resolve({
ok: true,
body: {
getReader: () => ({
read: jest.fn()
.mockResolvedValueOnce({
done: false,
value: new TextEncoder().encode('data: {"choices":[{"delta":{"content":"Translated text here"}}]}\n\n')
})
.mockResolvedValueOnce({
done: true
})
})
}
});
}
return Promise.reject(new Error('Unexpected URL'));
});
// Dynamically import the module to ensure fresh state
const module = await import('../../src/translate-modal.js');
openTranslateModal = module.openTranslateModal;
});
describe('Modal Creation and Display', () => {
test('should create translate modal with correct structure', async () => {
await openTranslateModal('Hello world', 'en');
const modal = document.getElementById('translate-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 openTranslateModal('Test content', 'en');
expect(showModalSpy).toHaveBeenCalled();
const modal = document.getElementById('translate-modal');
expect(modal.open).toBe(true);
});
test('should prevent opening multiple translate modals', async () => {
await openTranslateModal('First content', 'en');
const firstModal = document.getElementById('translate-modal');
await openTranslateModal('Second content', 'fr');
const modals = document.querySelectorAll('#translate-modal');
expect(modals.length).toBe(1);
expect(firstModal).toBe(document.getElementById('translate-modal'));
});
test('should handle empty or null content gracefully', async () => {
await openTranslateModal('', 'en');
let modal = document.getElementById('translate-modal');
expect(modal).toBeTruthy();
await openTranslateModal(null, 'en');
modal = document.getElementById('translate-modal');
expect(modal).toBeTruthy();
await openTranslateModal(undefined, 'en');
modal = document.getElementById('translate-modal');
expect(modal).toBeTruthy();
});
test('should display original text in textarea', async () => {
const originalText = 'Hello, this is a test message for translation.';
await openTranslateModal(originalText, 'en');
const originalTextArea = document.querySelector('#original-text, textarea[data-original]');
expect(originalTextArea).toBeTruthy();
expect(originalTextArea.value).toBe(originalText);
});
});
describe('Language Selection', () => {
test('should create source language dropdown', async () => {
await openTranslateModal('Test content', 'en');
const sourceSelect = document.querySelector('#source-language, select[data-source]');
expect(sourceSelect).toBeTruthy();
expect(sourceSelect.tagName).toBe('SELECT');
});
test('should create target language dropdown', async () => {
await openTranslateModal('Test content', 'en');
const targetSelect = document.querySelector('#target-language, select[data-target]');
expect(targetSelect).toBeTruthy();
expect(targetSelect.tagName).toBe('SELECT');
});
test('should populate language options correctly', async () => {
await openTranslateModal('Test content', 'en');
const sourceSelect = document.querySelector('#source-language, select[data-source]');
const targetSelect = document.querySelector('#target-language, select[data-target]');
const sourceOptions = sourceSelect.querySelectorAll('option');
const targetOptions = targetSelect.querySelectorAll('option');
expect(sourceOptions.length).toBeGreaterThan(5);
expect(targetOptions.length).toBeGreaterThan(5);
// Check for common languages
const sourceValues = Array.from(sourceOptions).map(opt => opt.value);
const targetValues = Array.from(targetOptions).map(opt => opt.value);
expect(sourceValues).toContain('en');
expect(sourceValues).toContain('es');
expect(targetValues).toContain('fr');
expect(targetValues).toContain('de');
});
test('should set detected source language automatically', async () => {
await openTranslateModal('Test content', 'es');
const sourceSelect = document.querySelector('#source-language, select[data-source]');
expect(sourceSelect.value).toBe('es');
});
test('should update language selection on change', async () => {
await openTranslateModal('Test content', 'en');
const targetSelect = document.querySelector('#target-language, select[data-target]');
targetSelect.value = 'fr';
targetSelect.dispatchEvent(new Event('change'));
expect(targetSelect.value).toBe('fr');
});
test('should have language swap functionality', async () => {
await openTranslateModal('Test content', 'en');
const sourceSelect = document.querySelector('#source-language, select[data-source]');
const targetSelect = document.querySelector('#target-language, select[data-target]');
const swapButton = document.querySelector('#swap-languages, button[data-swap]');
sourceSelect.value = 'en';
targetSelect.value = 'fr';
if (swapButton) {
swapButton.click();
expect(sourceSelect.value).toBe('fr');
expect(targetSelect.value).toBe('en');
}
});
});
describe('Translation Process', () => {
test('should initiate translation when translate button is clicked', async () => {
await openTranslateModal('Hello world', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
expect(translateButton).toBeTruthy();
translateButton.click();
// Wait for async operation
await new Promise(resolve => setTimeout(resolve, 100));
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'Authorization': 'Bearer mock-api-key'
}),
body: expect.stringContaining('Hello world')
})
);
});
test('should disable translate button during translation', async () => {
await openTranslateModal('Test content', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
const originalText = translateButton.textContent;
translateButton.click();
expect(translateButton.disabled).toBe(true);
expect(translateButton.textContent).not.toBe(originalText);
});
test('should display translation results', async () => {
await openTranslateModal('Hello world', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
translateButton.click();
// Wait for translation to complete
await new Promise(resolve => setTimeout(resolve, 200));
const resultArea = document.querySelector('#translation-result, textarea[data-result]');
expect(resultArea).toBeTruthy();
expect(resultArea.value).toContain('Translated text');
});
test('should show error message on translation failure', async () => {
global.fetch.mockRejectedValueOnce(new Error('Translation API Error'));
await openTranslateModal('Test content', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
translateButton.click();
// Wait for error handling
await new Promise(resolve => setTimeout(resolve, 100));
// Check for error message
const errorElement = document.querySelector('.error-message, [data-error]');
const hasError = errorElement || translateButton.textContent.includes('Error') || translateButton.textContent.includes('Failed');
expect(hasError).toBeTruthy();
});
test('should handle streaming translation response', async () => {
await openTranslateModal('Test content', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
translateButton.click();
// Wait for streaming to complete
await new Promise(resolve => setTimeout(resolve, 150));
const resultArea = document.querySelector('#translation-result, textarea[data-result]');
expect(resultArea.value.length).toBeGreaterThan(0);
});
});
describe('Copy Functionality', () => {
test('should create copy button for translation result', async () => {
await openTranslateModal('Hello world', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
translateButton.click();
// Wait for translation
await new Promise(resolve => setTimeout(resolve, 200));
const copyButton = document.querySelector('#copy-translation, button[data-copy]');
expect(copyButton).toBeTruthy();
});
test('should copy translation to clipboard when copy button is clicked', async () => {
// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve())
}
});
await openTranslateModal('Hello world', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
translateButton.click();
await new Promise(resolve => setTimeout(resolve, 200));
const copyButton = document.querySelector('#copy-translation, button[data-copy]');
if (copyButton) {
copyButton.click();
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
expect.stringContaining('Translated text')
);
}
});
test('should show copy success feedback', async () => {
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve())
}
});
await openTranslateModal('Hello world', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
translateButton.click();
await new Promise(resolve => setTimeout(resolve, 200));
const copyButton = document.querySelector('#copy-translation, button[data-copy]');
if (copyButton) {
const originalText = copyButton.textContent;
copyButton.click();
// Should show feedback briefly
expect(copyButton.textContent).not.toBe(originalText);
// Should restore original text after timeout
setTimeout(() => {
expect(copyButton.textContent).toBe(originalText);
}, 2000);
}
});
});
describe('Text Editing', () => {
test('should allow editing original text', async () => {
await openTranslateModal('Original text', 'en');
const originalTextArea = document.querySelector('#original-text, textarea[data-original]');
originalTextArea.value = 'Modified text';
originalTextArea.dispatchEvent(new Event('input'));
expect(originalTextArea.value).toBe('Modified text');
});
test('should retranslate when original text is modified and translate button is clicked', async () => {
await openTranslateModal('Original text', 'en');
const originalTextArea = document.querySelector('#original-text, textarea[data-original]');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
originalTextArea.value = 'Modified text for translation';
originalTextArea.dispatchEvent(new Event('input'));
translateButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: expect.stringContaining('Modified text for translation')
})
);
});
test('should handle very long text input', async () => {
const longText = 'A'.repeat(5000);
await openTranslateModal(longText, 'en');
const originalTextArea = document.querySelector('#original-text, textarea[data-original]');
expect(originalTextArea.value).toBe(longText);
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
expect(() => translateButton.click()).not.toThrow();
});
});
describe('Modal Closing', () => {
test('should close modal when close button is clicked', async () => {
await openTranslateModal('Test content', 'en');
const modal = document.getElementById('translate-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 openTranslateModal('Test content', 'en');
const modal = document.getElementById('translate-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('translate-modal')).toBeFalsy();
});
test('should handle escape key to close modal', async () => {
await openTranslateModal('Test content', 'en');
const modal = document.getElementById('translate-modal');
const closeSpy = jest.spyOn(modal, 'close');
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
document.dispatchEvent(escapeEvent);
expect(closeSpy).toHaveBeenCalled();
});
});
describe('Accessibility', () => {
test('should have proper ARIA attributes', async () => {
await openTranslateModal('Test content', 'en');
const modal = document.getElementById('translate-modal');
// Check for accessibility attributes
expect(modal.getAttribute('role') || modal.tagName === 'DIALOG').toBeTruthy();
expect(modal.getAttribute('aria-modal')).toBe('true');
const textareas = modal.querySelectorAll('textarea');
textareas.forEach(textarea => {
expect(textarea.getAttribute('aria-label')).toBeTruthy();
});
});
test('should have proper keyboard navigation', async () => {
await openTranslateModal('Test content', 'en');
const modal = document.getElementById('translate-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 have proper labels for form controls', async () => {
await openTranslateModal('Test content', 'en');
const selects = document.querySelectorAll('select');
const textareas = document.querySelectorAll('textarea');
selects.forEach(select => {
const hasLabel = select.getAttribute('aria-label') ||
document.querySelector(`label[for="${select.id}"]`) ||
select.closest('label');
expect(hasLabel).toBeTruthy();
});
textareas.forEach(textarea => {
const hasLabel = textarea.getAttribute('aria-label') ||
document.querySelector(`label[for="${textarea.id}"]`) ||
textarea.closest('label');
expect(hasLabel).toBeTruthy();
});
});
});
describe('Error Handling', () => {
test('should handle network errors gracefully', async () => {
global.fetch.mockRejectedValueOnce(new Error('Network error'));
expect(async () => {
await openTranslateModal('Test content', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
translateButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
test('should handle invalid language codes', async () => {
await openTranslateModal('Test content', 'invalid-lang');
const sourceSelect = document.querySelector('#source-language, select[data-source]');
// Should fallback to a valid language or show error
expect(sourceSelect.value).not.toBe('invalid-lang');
});
test('should handle API rate limiting', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 429,
statusText: 'Too Many Requests'
});
await openTranslateModal('Test content', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
expect(async () => {
translateButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
test('should handle malformed API response', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
body: {
getReader: () => ({
read: jest.fn().mockResolvedValue({
done: false,
value: new TextEncoder().encode('invalid json response')
})
})
}
});
await openTranslateModal('Test content', 'en');
const translateButton = document.querySelector('#translate-btn, button[data-translate]');
expect(async () => {
translateButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
});
});