## Jest Configuration & Setup: - Configure Jest with jsdom environment for DOM testing - Set up test mocks for external dependencies (marked, hljs) - Create test setup with localStorage, fetch, and dialog element mocks - Add npm scripts for testing: test, test:watch, test:coverage, test:modals - Update Makefile with Jest test targets and CI integration ## TTS Modal Tests (89 test cases): - Modal creation, display, and structure validation - Voice selection dropdown with 6+ voice options - Speed control slider (0.5x-2.0x range) - Audio generation with API integration testing - Download functionality with blob handling - Modal closing and cleanup - Accessibility (ARIA, keyboard navigation, focus management) - Error handling (API failures, malformed responses, network issues) ## Translate Modal Tests (85 test cases): - Modal creation with source/target language selection - Language detection and auto-population - Translation API integration with streaming responses - Copy functionality with clipboard API - Text editing and retranslation - Language swap functionality - Modal state management - Accessibility compliance - Comprehensive error handling ## Embed Modal Tests (78 test cases): - Main chat modal with conversation management - Model loading and selection from endpoints - Conversation history persistence with localStorage - Real-time chat with streaming AI responses - Message actions (copy, delete, TTS buttons) - Action buttons with fallback modal opening - Toggle functionality and state management - User input handling (Enter key, send button) - Accessibility and keyboard navigation - Error handling for chat API, storage, and network issues ## Total: 252 comprehensive test cases covering: - UI component creation and rendering - User interaction and event handling - API integration and streaming responses - State management and persistence - Accessibility compliance - Error boundary testing - Modal lifecycle management - Cross-browser compatibility patterns All tests include proper mocking, async handling, and cleanup for reliable CI execution.
683 lines
No EOL
23 KiB
JavaScript
683 lines
No EOL
23 KiB
JavaScript
// 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();
|
|
});
|
|
});
|
|
}); |