remove jest dependencies and old test files: clean migration to custom test framework
This commit is contained in:
parent
133daabd77
commit
527fbfad1b
9 changed files with 11 additions and 1847 deletions
34
Makefile
34
Makefile
|
|
@ -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"
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
10
package.json
10
package.json
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
// Mock for highlight.js library
|
||||
export default {
|
||||
highlightElement: jest.fn(),
|
||||
highlightAll: jest.fn(),
|
||||
highlight: jest.fn((lang, code) => ({ value: code })),
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// Mock for marked library
|
||||
export const marked = {
|
||||
parse: jest.fn((content) => `<p>${content}</p>`),
|
||||
setOptions: jest.fn(),
|
||||
};
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
108
tests/setup.js
108
tests/setup.js
|
|
@ -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();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue