remove jest dependencies and old test files: clean migration to custom test framework

This commit is contained in:
Russell Ballestrini 2025-07-11 14:57:22 -04:00
parent 133daabd77
commit 527fbfad1b
9 changed files with 11 additions and 1847 deletions

View file

@ -10,8 +10,8 @@ help:
@echo " make check - Check syntax and linting with biome"
@echo " make format-check - Format and check in sequence"
@echo " make test - Run integration tests"
@echo " make test-jest - Run Jest unit tests for modals"
@echo " make test-jest-coverage - Run Jest tests with coverage report"
@echo " make test-simple - Run simple unit-style tests"
@echo " make test-modal - Run custom modal tests"
@echo " make validate-exports - Validate import/export consistency"
@echo " make verify-translations - Verify translation completeness"
@echo " make validate-translations - Validate all translation files"
@ -60,31 +60,15 @@ validate-exports:
echo "❌ Node.js not available"; \
fi
test-jest:
@echo "Running Jest unit tests for modals..."
@if command -v npm >/dev/null 2>&1; then \
if npm list jest >/dev/null 2>&1; then \
npm test; \
else \
echo "⚠️ Jest not installed - skipping Jest tests"; \
fi; \
test-modal:
@echo "Running custom modal tests..."
@if command -v node >/dev/null 2>&1; then \
node modal_tests.js; \
else \
echo "❌ npm not available"; \
echo "❌ Node.js not available"; \
fi
test-jest-coverage:
@echo "Running Jest tests with coverage..."
@if command -v npm >/dev/null 2>&1; then \
if npm list jest >/dev/null 2>&1; then \
npm run test:coverage; \
else \
echo "⚠️ Jest not installed - skipping Jest tests"; \
fi; \
else \
echo "❌ npm not available"; \
fi
validate-all: format-check validate-exports verify-translations test test-jest
validate-all: format-check validate-exports verify-translations test test-simple test-modal
# Development workflow
install:
@ -171,5 +155,5 @@ quick: format-check git-add
@echo "Quick validation complete - ready to commit"
# Full CI/CD cycle
ci: clean format-check validate-all validate-structure validate-translations test-jest-coverage check-sizes
ci: clean format-check validate-all validate-structure validate-translations check-sizes
@echo "CI pipeline complete"

View file

@ -1,26 +0,0 @@
export default {
testEnvironment: 'jsdom',
moduleFileExtensions: ['js', 'mjs'],
extensionsToTreatAsEsm: ['.js'],
transform: {},
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
testMatch: [
'<rootDir>/tests/**/*.test.js',
],
collectCoverageFrom: [
'src/**/*.js',
'!src/languages/**',
'!**/node_modules/**',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
testTimeout: 10000,
// Mock external dependencies
moduleNameMapping: {
'^https://cdn\\.jsdelivr\\.net/npm/marked/lib/marked\\.esm\\.js$': '<rootDir>/tests/__mocks__/marked.js',
'^https://cdnjs\\.cloudflare\\.com/ajax/libs/highlight\\.js/11\\.8\\.0/es/highlight\\.min\\.js$': '<rootDir>/tests/__mocks__/hljs.js',
},
globals: {
fetch: global.fetch,
},
};

View file

@ -4,16 +4,10 @@
"type": "module",
"main": "integration_tests.js",
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:modals": "jest tests/modals",
"test:verbose": "jest --verbose"
"test": "node integration_tests.js",
"test:simple": "node simple_tests.js"
},
"devDependencies": {
"@jest/globals": "^29.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jsdom": "^22.0.0"
},
"repository": {

View file

@ -1,6 +0,0 @@
// Mock for highlight.js library
export default {
highlightElement: jest.fn(),
highlightAll: jest.fn(),
highlight: jest.fn((lang, code) => ({ value: code })),
};

View file

@ -1,5 +0,0 @@
// Mock for marked library
export const marked = {
parse: jest.fn((content) => `<p>${content}</p>`),
setOptions: jest.fn(),
};

View file

@ -1,683 +0,0 @@
// Jest tests for Embed Modal (Main Chat 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',
TTS_API_URL: 'https://mock-tts-api.com',
setSystemMessageAppend: jest.fn(),
getSystemMessage: jest.fn(() => 'You are Hermes AI assistant.')
}));
jest.mock('../../src/ui-themes.js', () => ({
detectCurrentTheme: jest.fn(() => 'light'),
getThemeColors: jest.fn(() => ({
bg: '#ffffff',
text: '#000000',
accent: '#007bff',
border: '#dddddd',
actionBg: '#f8f9fa'
})),
initializeChunkFiveFont: jest.fn()
}));
jest.mock('../../src/ui-translations.js', () => ({
getUIText: jest.fn((key) => `mock-${key}`),
getUserLanguagePreference: jest.fn(() => 'en'),
setUserLanguagePreference: jest.fn()
}));
jest.mock('../../src/content.js', () => ({
extractWebpageContent: jest.fn(() => 'Mock webpage content for context.')
}));
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'
}
}));
// Import the modules under test
let openUncloseaiEmbeddedModal, toggleUncloseaiEmbeddedModal;
describe('Embed Modal (Main Chat Modal)', () => {
beforeEach(async () => {
// Reset all mocks
global.resetMocks();
// Clear any existing modals
const existingModal = document.getElementById('uncloseai-embedded-modal');
if (existingModal) {
existingModal.remove();
}
// Mock fetch for chat API calls
global.fetch.mockImplementation((url, options) => {
if (url.includes('chat/completions')) {
return Promise.resolve({
ok: true,
body: {
getReader: () => ({
read: jest.fn()
.mockResolvedValueOnce({
done: false,
value: new TextEncoder().encode('data: {"choices":[{"delta":{"content":"Hello! How can I help you today?"}}]}\n\n')
})
.mockResolvedValueOnce({
done: false,
value: new TextEncoder().encode('data: {"choices":[{"delta":{"content":" I\'m here to assist."}}]}\n\n')
})
.mockResolvedValueOnce({
done: true
})
})
}
});
}
if (url.includes('models')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
data: [
{ id: 'hermes-model-1', object: 'model' },
{ id: 'hermes-model-2', object: 'model' }
]
})
});
}
return Promise.reject(new Error('Unexpected URL'));
});
// Mock localStorage for conversation history
global.localStorage.getItem.mockImplementation((key) => {
if (key.includes('hermes-conversation-history')) {
return JSON.stringify([
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there!' }
]);
}
return null;
});
// Dynamically import the module to ensure fresh state
const module = await import('../../src/uncloseai-embed-modal.js');
openUncloseaiEmbeddedModal = module.openUncloseaiEmbeddedModal;
toggleUncloseaiEmbeddedModal = module.toggleUncloseaiEmbeddedModal;
});
describe('Modal Creation and Display', () => {
test('should create embed modal with correct structure', async () => {
await openUncloseaiEmbeddedModal();
const modal = document.getElementById('uncloseai-embedded-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();
const chatContainer = modal.querySelector('#hermes-chat-container, [data-chat-container]');
expect(chatContainer).toBeTruthy();
});
test('should open modal with showModal method', async () => {
const showModalSpy = jest.spyOn(HTMLDialogElement.prototype, 'showModal');
await openUncloseaiEmbeddedModal();
expect(showModalSpy).toHaveBeenCalled();
const modal = document.getElementById('uncloseai-embedded-modal');
expect(modal.open).toBe(true);
});
test('should set window flag when modal is open', async () => {
await openUncloseaiEmbeddedModal();
expect(window.uncloseaiEmbeddedModalOpen).toBe(true);
});
test('should prevent opening multiple embed modals', async () => {
await openUncloseaiEmbeddedModal();
const firstModal = document.getElementById('uncloseai-embedded-modal');
await openUncloseaiEmbeddedModal();
const modals = document.querySelectorAll('#uncloseai-embedded-modal');
expect(modals.length).toBe(1);
expect(firstModal).toBe(document.getElementById('uncloseai-embedded-modal'));
});
test('should create chat input and send button', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
expect(input).toBeTruthy();
expect(input.type).toBe('text');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
expect(sendButton).toBeTruthy();
});
});
describe('Model Loading and Selection', () => {
test('should load available models on startup', async () => {
await openUncloseaiEmbeddedModal();
// Wait for models to load
await new Promise(resolve => setTimeout(resolve, 100));
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('models'),
expect.any(Object)
);
});
test('should create model selection dropdown', async () => {
await openUncloseaiEmbeddedModal();
// Wait for models to load
await new Promise(resolve => setTimeout(resolve, 100));
const modelSelect = document.querySelector('#hermes-model-selection, select[data-model]');
expect(modelSelect).toBeTruthy();
});
test('should populate model options', async () => {
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
const modelSelect = document.querySelector('#hermes-model-selection, select[data-model]');
if (modelSelect) {
const options = modelSelect.querySelectorAll('option');
expect(options.length).toBeGreaterThan(0);
}
});
test('should handle model loading errors gracefully', async () => {
global.fetch.mockImplementationOnce(() => Promise.reject(new Error('Models API Error')));
expect(async () => {
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
});
describe('Conversation History', () => {
test('should load conversation history from localStorage', async () => {
await openUncloseaiEmbeddedModal();
// Wait for history to load
await new Promise(resolve => setTimeout(resolve, 100));
expect(global.localStorage.getItem).toHaveBeenCalledWith(
expect.stringContaining('hermes-conversation-history')
);
});
test('should display loaded conversation history', async () => {
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 150));
const chatContainer = document.querySelector('#hermes-chat-container, [data-chat-container]');
const messages = chatContainer.querySelectorAll('.message, [data-message]');
// Should have at least the intro message
expect(messages.length).toBeGreaterThan(0);
});
test('should save conversation history to localStorage on new messages', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message';
sendButton.click();
// Wait for message processing
await new Promise(resolve => setTimeout(resolve, 200));
expect(global.localStorage.setItem).toHaveBeenCalledWith(
expect.stringContaining('hermes-conversation-history'),
expect.any(String)
);
});
test('should handle empty conversation history gracefully', async () => {
global.localStorage.getItem.mockReturnValue(null);
expect(async () => {
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
});
describe('Chat Functionality', () => {
test('should send message when send button is clicked', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Hello, Hermes!';
sendButton.click();
// Wait for API call
await new Promise(resolve => setTimeout(resolve, 100));
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('chat/completions'),
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'Authorization': 'Bearer mock-api-key'
}),
body: expect.stringContaining('Hello, Hermes!')
})
);
});
test('should send message when Enter key is pressed', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
input.value = 'Test message via Enter';
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
input.dispatchEvent(enterEvent);
await new Promise(resolve => setTimeout(resolve, 100));
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('chat/completions'),
expect.any(Object)
);
});
test('should clear input after sending message', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message';
sendButton.click();
expect(input.value).toBe('');
});
test('should disable send button during message processing', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message';
sendButton.click();
expect(sendButton.disabled).toBe(true);
});
test('should display streaming AI response', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Hello';
sendButton.click();
// Wait for streaming response
await new Promise(resolve => setTimeout(resolve, 200));
const chatContainer = document.querySelector('#hermes-chat-container, [data-chat-container]');
const aiMessage = chatContainer.querySelector('[data-role="assistant"], .ai-message');
expect(aiMessage).toBeTruthy();
expect(aiMessage.textContent).toContain('Hello! How can I help you today?');
});
test('should handle empty message input gracefully', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = '';
sendButton.click();
// Should not make API call for empty message
expect(global.fetch).not.toHaveBeenCalledWith(
expect.stringContaining('chat/completions'),
expect.any(Object)
);
});
});
describe('Action Buttons', () => {
test('should create action buttons for TTS and Translation', async () => {
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
const actionButtons = document.querySelectorAll('button[data-action], .action-btn');
const buttonTexts = Array.from(actionButtons).map(btn => btn.textContent);
expect(buttonTexts.some(text => text.includes('mock-ttsAnything'))).toBeTruthy();
expect(buttonTexts.some(text => text.includes('mock-translationModal'))).toBeTruthy();
});
test('should handle TTS modal opening with fallback', async () => {
// Mock TTS modal not available on window
delete window.openTTSModal;
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
const ttsButton = Array.from(document.querySelectorAll('button')).find(
btn => btn.textContent.includes('mock-ttsAnything')
);
expect(async () => {
if (ttsButton) {
ttsButton.click();
await new Promise(resolve => setTimeout(resolve, 50));
}
}).not.toThrow();
});
test('should handle Translation modal opening with fallback', async () => {
// Mock Translation modal not available on window
delete window.openTranslateModal;
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
const translateButton = Array.from(document.querySelectorAll('button')).find(
btn => btn.textContent.includes('mock-translationModal')
);
expect(async () => {
if (translateButton) {
translateButton.click();
await new Promise(resolve => setTimeout(resolve, 50));
}
}).not.toThrow();
});
test('should create refresh models button', async () => {
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
const refreshButton = Array.from(document.querySelectorAll('button')).find(
btn => btn.textContent.includes('mock-refreshModels')
);
expect(refreshButton).toBeTruthy();
});
test('should create clear chat button', async () => {
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
const clearButton = Array.from(document.querySelectorAll('button')).find(
btn => btn.textContent.includes('mock-clearChat')
);
expect(clearButton).toBeTruthy();
});
});
describe('Message Actions', () => {
test('should add copy button to messages', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message';
sendButton.click();
await new Promise(resolve => setTimeout(resolve, 200));
const copyButtons = document.querySelectorAll('[data-copy], .copy-btn');
expect(copyButtons.length).toBeGreaterThan(0);
});
test('should add delete button to user messages', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message';
sendButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
const deleteButtons = document.querySelectorAll('[data-delete], .delete-btn');
expect(deleteButtons.length).toBeGreaterThan(0);
});
test('should copy message to clipboard when copy button is clicked', async () => {
// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve())
}
});
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message for copying';
sendButton.click();
await new Promise(resolve => setTimeout(resolve, 200));
const copyButton = document.querySelector('[data-copy], .copy-btn');
if (copyButton) {
copyButton.click();
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
expect.stringContaining('Test message for copying')
);
}
});
test('should delete message when delete button is clicked', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Message to delete';
sendButton.click();
await new Promise(resolve => setTimeout(resolve, 200));
const initialMessages = document.querySelectorAll('.message, [data-message]').length;
const deleteButton = document.querySelector('[data-delete], .delete-btn');
if (deleteButton) {
deleteButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
const finalMessages = document.querySelectorAll('.message, [data-message]').length;
expect(finalMessages).toBeLessThan(initialMessages);
}
});
});
describe('Toggle Functionality', () => {
test('should toggle modal open and closed', async () => {
// First toggle - should open
await toggleUncloseaiEmbeddedModal();
let modal = document.getElementById('uncloseai-embedded-modal');
expect(modal).toBeTruthy();
expect(window.uncloseaiEmbeddedModalOpen).toBe(true);
// Second toggle - should close
await toggleUncloseaiEmbeddedModal();
expect(window.uncloseaiEmbeddedModalOpen).toBe(false);
});
test('should close modal when close button is clicked', async () => {
await openUncloseaiEmbeddedModal();
const modal = document.getElementById('uncloseai-embedded-modal');
const closeButton = modal.querySelector('button[data-close], .close-btn');
const closeSpy = jest.spyOn(modal, 'close');
closeButton.click();
expect(closeSpy).toHaveBeenCalled();
});
test('should clean up modal state when closed', async () => {
await openUncloseaiEmbeddedModal();
const modal = document.getElementById('uncloseai-embedded-modal');
const closeButton = modal.querySelector('button[data-close], .close-btn');
closeButton.click();
await new Promise(resolve => setTimeout(resolve, 50));
expect(window.uncloseaiEmbeddedModalOpen).toBe(false);
expect(document.getElementById('uncloseai-embedded-modal')).toBeFalsy();
});
});
describe('Accessibility', () => {
test('should have proper ARIA attributes', async () => {
await openUncloseaiEmbeddedModal();
const modal = document.getElementById('uncloseai-embedded-modal');
expect(modal.getAttribute('role') || modal.tagName === 'DIALOG').toBeTruthy();
expect(modal.getAttribute('aria-modal')).toBe('true');
const input = modal.querySelector('input');
expect(input.getAttribute('aria-label')).toBeTruthy();
});
test('should focus input when modal opens', async () => {
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
expect(document.activeElement).toBe(input);
});
test('should handle keyboard navigation', async () => {
await openUncloseaiEmbeddedModal();
const modal = document.getElementById('uncloseai-embedded-modal');
const focusableElements = modal.querySelectorAll(
'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
expect(focusableElements.length).toBeGreaterThan(1);
});
});
describe('Error Handling', () => {
test('should handle chat API errors gracefully', async () => {
global.fetch.mockImplementationOnce(() => Promise.reject(new Error('Chat API Error')));
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message';
expect(async () => {
sendButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
test('should handle malformed API responses', async () => {
global.fetch.mockImplementationOnce(() => Promise.resolve({
ok: true,
body: {
getReader: () => ({
read: jest.fn().mockResolvedValue({
done: false,
value: new TextEncoder().encode('invalid json response')
})
})
}
}));
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message';
expect(async () => {
sendButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
test('should handle localStorage errors gracefully', async () => {
global.localStorage.getItem.mockImplementation(() => {
throw new Error('localStorage error');
});
expect(async () => {
await openUncloseaiEmbeddedModal();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
test('should handle network timeouts', async () => {
global.fetch.mockImplementationOnce(() =>
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Network timeout')), 50);
})
);
await openUncloseaiEmbeddedModal();
const input = document.querySelector('#hermes-user-input, input[data-user-input]');
const sendButton = document.querySelector('#hermes-send-btn, button[data-send]');
input.value = 'Test message';
expect(async () => {
sendButton.click();
await new Promise(resolve => setTimeout(resolve, 100));
}).not.toThrow();
});
});
});

View file

@ -1,573 +0,0 @@
// 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();
});
});
});

View file

@ -1,413 +0,0 @@
// 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();
});
});
});

View file

@ -1,108 +0,0 @@
// Jest test setup for modal testing
import { JSDOM } from 'jsdom';
// Set up DOM environment
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'https://example.com',
pretendToBeVisual: true,
resources: 'usable'
});
global.document = dom.window.document;
global.window = dom.window;
global.navigator = dom.window.navigator;
global.HTMLElement = dom.window.HTMLElement;
global.Element = dom.window.Element;
// Mock localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
// Mock fetch
global.fetch = jest.fn();
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Mock dialog element if not available
if (typeof HTMLDialogElement === 'undefined') {
global.HTMLDialogElement = class HTMLDialogElement extends HTMLElement {
constructor() {
super();
this.open = false;
}
show() {
this.open = true;
}
showModal() {
this.open = true;
}
close() {
this.open = false;
}
};
// Register as a custom element
if (customElements && !customElements.get('dialog')) {
customElements.define('dialog', HTMLDialogElement);
}
}
// Global test helpers
global.createMockModal = () => {
const modal = document.createElement('dialog');
modal.id = 'test-modal';
document.body.appendChild(modal);
return modal;
};
global.createMockEvent = (type, properties = {}) => {
const event = new Event(type, { bubbles: true, cancelable: true });
Object.assign(event, properties);
return event;
};
// Cleanup function to reset mocks between tests
global.resetMocks = () => {
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
localStorageMock.removeItem.mockClear();
localStorageMock.clear.mockClear();
global.fetch.mockClear();
// Clear DOM
document.body.innerHTML = '';
document.head.innerHTML = '';
// Reset window properties
delete window.UNCLOSEAI_CUSTOM_STYLING;
delete window.uncloseaiEmbeddedModalOpen;
delete window.openTTSModal;
delete window.openTranslateModal;
delete window.toggleUncloseaiEmbeddedModal;
};
// Run cleanup before each test
beforeEach(() => {
resetMocks();
});