/** * Тесты для системы чата * Запуск из консоли браузера: * * 1. Загрузить скрипт: * const script = document.createElement('script'); * script.src = '/static/js/tests/chatTests.js'; * script.type = 'module'; * document.head.appendChild(script); * * 2. Или скопировать весь код в консоль * * 3. Запустить тесты: * window.ChatTests.runAll() // Все тесты * window.ChatTests.testMessageStore() // Отдельный модуль * window.ChatTests.testMessageLoader() * window.ChatTests.testScrollManager() * window.ChatTests.testMessageRenderer() * window.ChatTests.testChatController() */ import MessageStore from '../stores/messageStore.js'; import MessageLoader from '../loaders/messageLoader.js'; import ScrollManager from '../managers/scrollManager.js'; import MessageRendererV2 from '../renderers/messageRendererV2.js'; import ChatController from '../controllers/chatController.js'; class ChatTestSuite { constructor() { this.results = { passed: 0, failed: 0, total: 0, details: [] }; } // ==================== Утилиты ==================== /** * Генерирует реалистичное сообщение в формате бэкэнда * @param {Object} overrides - Поля для переопределения * @returns {Object} Сообщение в формате serialize_message() */ createRealisticMessage(overrides = {}) { const defaults = { id: Math.floor(Math.random() * 10000), content: 'Тестовое сообщение', author_id: 1, author_name: 'Тестовый пользователь', author_url: '/employees/1/', avatar: '/media/users/avatars/default.jpg', created: '12.01.2026 16:00', created_ts: Date.now(), is_edited: false, edited_at: null, is_deleted: false, is_pinned: false, is_forwarded: false, is_system: false, has_attachments: false, reactions_summary: {}, // Опциональные поля reply_to: null, forwarded_from: null, attachments: [], poll: null }; return { ...defaults, ...overrides }; } assert(condition, message) { this.results.total++; if (condition) { this.results.passed++; console.log(`✅ PASS: ${message}`); this.results.details.push({ status: 'PASS', message }); } else { this.results.failed++; console.error(`❌ FAIL: ${message}`); this.results.details.push({ status: 'FAIL', message }); } } assertEqual(actual, expected, message) { const condition = actual === expected; const fullMessage = `${message} (expected: ${expected}, got: ${actual})`; this.assert(condition, fullMessage); } assertNotNull(value, message) { this.assert(value !== null && value !== undefined, message); } async sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Проверяет, видимо ли сообщение в контейнере * @private */ _isMessageVisible(container, messageId) { const messageEl = container.querySelector(`[data-message-id="${messageId}"]`); if (!messageEl) return false; const rect = messageEl.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); // Проверяем что хотя бы часть сообщения видна return rect.bottom > containerRect.top && rect.top < containerRect.bottom; } resetResults() { this.results = { passed: 0, failed: 0, total: 0, details: [] }; } printResults() { console.log('\n' + '='.repeat(60)); console.log('📊 РЕЗУЛЬТАТЫ ТЕСТОВ'); console.log('='.repeat(60)); console.log(`Всего тестов: ${this.results.total}`); console.log(`✅ Успешно: ${this.results.passed}`); console.log(`❌ Провалено: ${this.results.failed}`); console.log(`📈 Процент успеха: ${(this.results.passed / this.results.total * 100).toFixed(1)}%`); console.log('='.repeat(60) + '\n'); if (this.results.failed > 0) { console.log('❌ Проваленные тесты:'); this.results.details .filter(d => d.status === 'FAIL') .forEach(d => console.log(` - ${d.message}`)); } } // ==================== Тесты MessageStore ==================== async testMessageStore() { console.log('\n🧪 Тестирование MessageStore...\n'); this.resetResults(); const store = new MessageStore(); const chatId = 1; // Тест 1: Добавление одного сообщения с реальной структурой const msg1 = this.createRealisticMessage({ id: 100, chat_id: chatId, author_id: 1, author_name: 'Иван Иванов', content: 'Тестовое сообщение 1', created: '12.01.2026 10:00', created_ts: new Date('2024-01-01T10:00:00Z').getTime() }); const added = store.addMessage(msg1); this.assertEqual(added, 1, 'Добавление одного сообщения должно вернуть 1'); this.assertEqual(store.getMessagesForChat(chatId).length, 1, 'В чате должно быть 1 сообщение'); // Тест 2: Попытка добавить дубликат const duplicate = store.addMessage(msg1); this.assertEqual(duplicate, 0, 'Добавление дубликата должно вернуть 0'); this.assertEqual(store.getMessagesForChat(chatId).length, 1, 'Количество сообщений не должно измениться'); // Тест 3: Массовое добавление с реальной структурой const messages = [ this.createRealisticMessage({ id: 101, chat_id: chatId, author_id: 1, author_name: 'Иван Иванов', content: 'Сообщение 2', created_ts: new Date('2024-01-01T10:01:00Z').getTime() }), this.createRealisticMessage({ id: 102, chat_id: chatId, author_id: 2, author_name: 'Пётр Петров', content: 'Сообщение 3', created_ts: new Date('2024-01-01T10:02:00Z').getTime() }), this.createRealisticMessage({ id: 103, chat_id: chatId, author_id: 1, author_name: 'Иван Иванов', content: 'Сообщение 4', created_ts: new Date('2024-01-01T10:03:00Z').getTime() }) ]; const addedCount = store.addMessages(messages); this.assertEqual(addedCount, 3, 'Добавление 3 новых сообщений должно вернуть 3'); this.assertEqual(store.getMessagesForChat(chatId).length, 4, 'Всего должно быть 4 сообщения'); // Тест 4: Сообщение с reply_to const msgWithReply = this.createRealisticMessage({ id: 104, chat_id: chatId, author_id: 2, author_name: 'Пётр Петров', content: 'Ответ на сообщение', created_ts: new Date('2024-01-01T10:04:00Z').getTime(), reply_to: { id: 100, content: 'Тестовое сообщение 1', author_name: 'Иван Иванов' } }); store.addMessage(msgWithReply); const savedMsg = store.getMessagesForChat(chatId).find(m => m.id === 104); this.assertNotNull(savedMsg.reply_to, 'Сообщение с reply_to должно сохранить это поле'); this.assertEqual(savedMsg.reply_to.id, 100, 'reply_to.id должен совпадать'); // Тест 5: Сообщение с вложениями const msgWithAttachment = this.createRealisticMessage({ id: 105, chat_id: chatId, author_id: 1, author_name: 'Иван Иванов', content: 'Сообщение с файлом', created_ts: new Date('2024-01-01T10:05:00Z').getTime(), has_attachments: true, attachments: [{ id: 1, file_name: 'document.pdf', file_type: 'application/pdf', file_url: '/media/attachments/document.pdf', file_size: 1024000, mime_type: 'application/pdf', thumbnail: null }] }); store.addMessage(msgWithAttachment); const savedMsgAtt = store.getMessagesForChat(chatId).find(m => m.id === 105); this.assert(savedMsgAtt.has_attachments, 'has_attachments должен быть true'); this.assertEqual(savedMsgAtt.attachments.length, 1, 'Должно быть 1 вложение'); // Тест 6: Сообщение с reactions const msgWithReactions = this.createRealisticMessage({ id: 106, chat_id: chatId, author_id: 1, author_name: 'Иван Иванов', content: 'Сообщение с реакциями', created_ts: new Date('2024-01-01T10:06:00Z').getTime(), reactions_summary: { '👍': { count: 3, users: [1, 2, 3], user_names: ['User1', 'User2', 'User3'] }, '❤️': { count: 1, users: [2], user_names: ['User2'] } } }); store.addMessage(msgWithReactions); const savedMsgReact = store.getMessagesForChat(chatId).find(m => m.id === 106); this.assertNotNull(savedMsgReact.reactions_summary, 'reactions_summary должен существовать'); this.assertEqual(Object.keys(savedMsgReact.reactions_summary).length, 2, 'Должно быть 2 типа реакций'); // Тест 7: Получение сообщения по ID const retrieved = store.getMessage(100); this.assertNotNull(retrieved, 'Сообщение должно быть найдено'); this.assertEqual(retrieved.content, 'Тестовое сообщение 1', 'Содержимое должно совпадать'); // Тест 8: Проверка наличия сообщения this.assert(store.hasMessage(100), 'hasMessage(100) должен вернуть true'); this.assert(!store.hasMessage(999), 'hasMessage(999) должен вернуть false'); // Тест 9: Получение старейшего сообщения const oldest = store.getOldestMessage(chatId); this.assertEqual(oldest.id, 100, 'Старейшее сообщение должно иметь ID 100'); // Тест 10: Получение новейшего сообщения const newest = store.getNewestMessage(chatId); this.assertEqual(newest.id, 106, 'Новейшее сообщение должно иметь ID 106'); // Тест 8: Обновление сообщения store.updateMessage(100, { content: 'Updated content', is_edited: true }); const updated = store.getMessage(100); this.assertEqual(updated.content, 'Updated content', 'Содержимое должно быть обновлено'); this.assert(updated.is_edited, 'Флаг is_edited должен быть true'); // Тест 11: Удаление сообщения store.removeMessage(100); this.assert(!store.hasMessage(100), 'Сообщение 100 должно быть удалено'); // Было добавлено: 4 (тесты 1-3) + 3 (тесты 4-6) = 7 сообщений, удалили 1 = 6 this.assertEqual(store.getMessagesForChat(chatId).length, 6, 'Должно остаться 6 сообщений'); // Тест 12: Очистка чата store.clearChat(chatId); this.assertEqual(store.getMessagesForChat(chatId).length, 0, 'Чат должен быть пустым'); // Тест 13: Оптимистичное сообщение const tempId = `temp_${Date.now()}`; const optimisticMsg = this.createRealisticMessage({ id: tempId, chat_id: chatId, author_id: 1, author_name: 'Тестовый пользователь', content: 'Оптимистичное сообщение', created_ts: Date.now(), is_optimistic: true }); store.addOptimisticMessage(tempId, optimisticMsg); this.assert(store.optimisticMessages.has(tempId), 'Оптимистичное сообщение должно быть сохранено'); // Подтверждение оптимистичного сообщения const confirmed = this.createRealisticMessage({ id: 200, chat_id: chatId, author_id: 1, author_name: 'Тестовый пользователь', content: 'Оптимистичное сообщение', created_ts: optimisticMsg.created_ts }); store.confirmOptimisticMessage(tempId, confirmed); this.assert(!store.optimisticMessages.has(tempId), 'Оптимистичное сообщение должно быть удалено'); this.assert(store.hasMessage(200), 'Подтвержденное сообщение должно существовать'); // Тест 14: События Store let eventFired = false; const listener = () => { eventFired = true; }; store.on('message_added', listener); store.addMessage(this.createRealisticMessage({ id: 201, chat_id: chatId, author_id: 1, author_name: 'Тестовый пользователь', content: 'Тест события', created_ts: Date.now() })); await this.sleep(10); // Даем время на обработку события this.assert(eventFired, 'Событие message_added должно быть вызвано'); store.off('message_added', listener); this.printResults(); return this.results; } // ==================== Тесты MessageLoader ==================== async testMessageLoader() { console.log('\n🧪 Тестирование MessageLoader...\n'); this.resetResults(); const store = new MessageStore(); const loader = new MessageLoader({ store }); // Эти тесты требуют реального API, поэтому проверяем структуру // Тест 1: Инициализация this.assertNotNull(loader.store, 'Store должен быть инициализирован'); this.assertNotNull(loader.loadingState, 'LoadingState должен быть инициализирован'); this.assertNotNull(loader.chatState, 'ChatState должен быть инициализирован'); // Тест 2: Получение состояния загрузки const state = loader._getLoadingState(1); this.assert(state.hasOwnProperty('initial'), 'Состояние должно иметь поле initial'); this.assert(state.hasOwnProperty('history'), 'Состояние должно иметь поле history'); this.assert(state.hasOwnProperty('status'), 'Состояние должно иметь поле status'); // Тест 3: Получение состояния чата const chatState = loader._getChatState(1); this.assert(chatState.hasOwnProperty('hasMore'), 'Состояние чата должно иметь поле hasMore'); this.assert(chatState.hasOwnProperty('oldestId'), 'Состояние чата должно иметь поле oldestId'); // Тест 4: Обновление состояния чата loader._updateChatState(1, { hasMore: false, oldestId: 100 }); const updatedState = loader._getChatState(1); this.assertEqual(updatedState.hasMore, false, 'hasMore должен быть обновлен'); this.assertEqual(updatedState.oldestId, 100, 'oldestId должен быть обновлен'); // Тест 5: Проверка hasMoreHistory this.assertEqual(loader.hasMoreHistory(1), false, 'hasMoreHistory должен вернуть false'); loader._updateChatState(1, { hasMore: true }); this.assertEqual(loader.hasMoreHistory(1), true, 'hasMoreHistory должен вернуть true'); // Тест 6: Методы WebSocket обработчиков существуют this.assert(typeof loader.handleNewMessage === 'function', 'handleNewMessage должен быть функцией'); this.assert(typeof loader.handleMessageEdited === 'function', 'handleMessageEdited должен быть функцией'); this.assert(typeof loader.handleMessageRemoved === 'function', 'handleMessageRemoved должен быть функцией'); this.assert(typeof loader.handleReactionAdded === 'function', 'handleReactionAdded должен быть функцией'); this.assert(typeof loader.handleReactionRemoved === 'function', 'handleReactionRemoved должен быть функцией'); // Тест 7: Тестируем handleNewMessage с mock данными const testMsg = { id: 300, chat_id: 1, sender: { id: 1, username: 'test' }, content: 'Test WS message', created_at: new Date().toISOString() }; loader.currentChatId = 1; loader.handleNewMessage(testMsg); this.assert(store.hasMessage(300), 'Сообщение из WebSocket должно быть добавлено в Store'); // Тест 8: Тестируем handleMessageEdited loader.handleMessageEdited({ message: { ...testMsg, content: 'Edited content', is_edited: true } }); const edited = store.getMessage(300); this.assertEqual(edited.content, 'Edited content', 'Сообщение должно быть отредактировано'); // Тест 9: Тестируем handleMessageRemoved loader.handleMessageRemoved({ message_id: 300 }); this.assert(!store.hasMessage(300), 'Сообщение должно быть удалено'); this.printResults(); return this.results; } // ==================== Тесты ScrollManager ==================== async testScrollManager() { console.log('\n🧪 Тестирование ScrollManager...\n'); this.resetResults(); // Создаем mock элементы const container = document.createElement('div'); container.style.height = '500px'; container.style.overflow = 'auto'; document.body.appendChild(container); const store = new MessageStore(); const loader = new MessageLoader({ store }); const renderer = new MessageRendererV2({ store, containerId: 'test-container', currentUserId: 1 }); const manager = new ScrollManager({ scrollElement: container, messageLoader: loader, messageRenderer: renderer, messageStore: store, chatId: 1 }); // Тест 1: Инициализация this.assertNotNull(manager.scrollEl, 'ScrollElement должен быть инициализирован'); this.assertNotNull(manager.loader, 'Loader должен быть инициализирован'); // Тест 2: Методы существуют this.assert(typeof manager.scrollToBottom === 'function', 'scrollToBottom должен быть функцией'); this.assert(typeof manager.scrollToMessage === 'function', 'scrollToMessage должен быть функцией'); this.assert(typeof manager.saveScrollPosition === 'function', 'saveScrollPosition должен быть функцией'); this.assert(typeof manager.restoreScrollPosition === 'function', 'restoreScrollPosition должен быть функцией'); this.assert(typeof manager.setupIntersectionObserver === 'function', 'setupIntersectionObserver должен быть функцией'); // Тест 3: Сохранение и восстановление позиции container.scrollTop = 100; const savedPos = manager.saveScrollPosition(); this.assertNotNull(savedPos, 'Позиция должна быть сохранена'); container.scrollTop = 0; manager.restoreScrollPosition(savedPos); // Note: в тестах может не работать точное восстановление из-за отсутствия реального контента // Тест 4: scrollToBottom const contentDiv = document.createElement('div'); contentDiv.style.height = '1000px'; container.appendChild(contentDiv); manager.scrollToBottom(); await this.sleep(100); // Примечание: в тестах scrollTop может не работать корректно без реального контента this.assert(true, 'scrollToBottom выполнен без ошибок'); // Тест 5: setupIntersectionObserver // Добавляем mock сообщение в DOM для observer'а (должен быть класс .msg) const messageDiv = document.createElement('div'); messageDiv.className = 'msg'; messageDiv.setAttribute('data-message-id', '1'); container.appendChild(messageDiv); manager.setupIntersectionObserver(); this.assertNotNull(manager.historyObserver, 'IntersectionObserver должен быть создан когда есть сообщения'); // Cleanup manager.destroy(); document.body.removeChild(container); this.printResults(); return this.results; } // ==================== Тесты MessageRenderer ==================== async testMessageRenderer() { console.log('\n🧪 Тестирование MessageRenderer...\n'); this.resetResults(); // Создаем mock элементы const container = document.createElement('div'); container.id = 'messages-list'; document.body.appendChild(container); const store = new MessageStore(); const currentUser = { id: 1, username: 'testuser' }; const renderer = new MessageRendererV2({ store, containerId: 'messages-list', currentUserId: currentUser.id }); // Тест 1: Инициализация this.assertNotNull(renderer.containerId, 'Container ID должен быть инициализирован'); this.assertNotNull(renderer.store, 'Store должен быть инициализирован'); this.assertEqual(renderer.currentUserId, 1, 'CurrentUserId должен быть установлен'); // Тест 2: Рендеринг сообщения const testMsg = { id: 400, chat_id: 1, sender: { id: 2, username: 'other', display_name: 'Other User' }, content: 'Test render message', created_at: '2024-01-01T10:00:00Z', is_read: false }; store.addMessage(testMsg); renderer.render(1); await this.sleep(100); const msgElement = container.querySelector('[data-message-id="400"]'); this.assertNotNull(msgElement, 'Элемент сообщения должен быть отрендерен'); // Тест 3: Обновление сообщения store.updateMessage(400, { content: 'Updated render content', is_edited: true }); renderer.render(1); await this.sleep(100); const updatedElement = container.querySelector('[data-message-id="400"]'); this.assert(updatedElement.textContent.includes('Updated render content'), 'Содержимое должно быть обновлено'); // Тест 4: Рендеринг собственного сообщения const ownMsg = { id: 401, chat_id: 1, sender: { id: 1, username: 'testuser', display_name: 'Test User' }, content: 'My message', created_at: '2024-01-01T10:01:00Z' }; store.addMessage(ownMsg); renderer.render(1); await this.sleep(100); const ownElement = container.querySelector('[data-message-id="401"]'); this.assertNotNull(ownElement, 'Собственное сообщение должно быть отрендерено'); // Тест 5: Удаление сообщения store.removeMessage(400); renderer.render(1); await this.sleep(100); const removedElement = container.querySelector('[data-message-id="400"]'); this.assert(!removedElement, 'Удаленное сообщение не должно отображаться'); // Тест 6: Очистка контейнера renderer.clear(); this.assertEqual(container.children.length, 0, 'Контейнер должен быть пустым'); // Cleanup document.body.removeChild(container); this.printResults(); return this.results; } // ==================== Тесты ChatController ==================== async testChatController() { console.log('\n🧪 Тестирование ChatController...\n'); this.resetResults(); // Создаем mock элементы const container = document.createElement('div'); container.id = 'messages-list'; container.style.height = '500px'; container.style.overflow = 'auto'; document.body.appendChild(container); const currentUser = { id: 1, username: 'testuser' }; const controller = new ChatController({ chatId: 1, scrollElement: container, containerId: 'messages-list', currentUserId: currentUser.id }); // Тест 1: Инициализация this.assertNotNull(controller.store, 'Store должен быть создан'); this.assertNotNull(controller.loader, 'Loader должен быть создан'); this.assertNotNull(controller.renderer, 'Renderer должен быть создан'); this.assertNotNull(controller.scrollManager, 'ScrollManager должен быть создан'); // Тест 2: Методы существуют this.assert(typeof controller.init === 'function', 'init должен быть функцией'); this.assert(typeof controller.sendMessage === 'function', 'sendMessage должен быть функцией'); this.assert(typeof controller.destroy === 'function', 'destroy должен быть функцией'); // Тест 3: Event listeners let eventReceived = false; const handler = () => { eventReceived = true; }; controller.store.on('message_added', handler); controller.store.addMessage({ id: 500, chat_id: 1, sender: { id: 1 }, content: 'Test controller message', created_at: new Date().toISOString() }); await this.sleep(10); this.assert(eventReceived, 'Event handler должен получить событие'); // Тест 4: Получение текущего чата controller.currentChatId = 1; this.assertEqual(controller.currentChatId, 1, 'CurrentChatId должен быть установлен'); // Тест 5: Отправка оптимистичного сообщения (mock) const tempId = controller.loader.sendMessageOptimistically(1, 'Optimistic test'); this.assertNotNull(tempId, 'Временный ID должен быть возвращен'); this.assert(tempId.startsWith('temp_'), 'Временный ID должен начинаться с temp_'); // Cleanup controller.destroy(); document.body.removeChild(container); this.printResults(); return this.results; } // ==================== Интеграционные тесты ==================== async testIntegration() { console.log('\n🧪 Интеграционные тесты...\n'); this.resetResults(); // Создаем полную систему const container = document.createElement('div'); container.id = 'messages-list'; container.style.height = '500px'; container.style.overflow = 'auto'; document.body.appendChild(container); const chatId = 1; const currentUser = { id: 1, username: 'testuser' }; const controller = new ChatController({ chatId: chatId, scrollElement: container, containerId: 'messages-list', currentUserId: currentUser.id }); // Тест 1: Полный цикл сообщения console.log('\n--- Тест 1: Полный цикл сообщения ---'); // Добавляем сообщение в Store const msg1 = { id: 600, chat_id: chatId, sender: { id: 2, username: 'other', display_name: 'Other' }, content: 'Integration test message', created_at: new Date().toISOString() }; controller.store.addMessage(msg1); controller.renderer.render(chatId); await this.sleep(100); let element = container.querySelector('[data-message-id="600"]'); this.assertNotNull(element, 'Сообщение должно быть отрендерено'); // Редактируем сообщение controller.store.updateMessage(600, { content: 'Edited integration message', is_edited: true }); controller.renderer.render(chatId); await this.sleep(100); element = container.querySelector('[data-message-id="600"]'); this.assert(element.textContent.includes('Edited integration message'), 'Сообщение должно быть обновлено'); // Удаляем сообщение controller.store.removeMessage(600); controller.renderer.render(chatId); await this.sleep(100); element = container.querySelector('[data-message-id="600"]'); this.assert(!element, 'Сообщение должно быть удалено'); // Тест 2: Множественные сообщения console.log('\n--- Тест 2: Множественные сообщения ---'); const messages = []; for (let i = 0; i < 10; i++) { messages.push({ id: 700 + i, chat_id: chatId, sender: { id: i % 2 === 0 ? 1 : 2, username: `user${i % 2}` }, content: `Message ${i}`, created_at: new Date(Date.now() + i * 1000).toISOString() }); } controller.store.addMessages(messages); controller.renderer.render(chatId); await this.sleep(200); const renderedMessages = container.querySelectorAll('[data-message-id]'); this.assertEqual(renderedMessages.length, 10, 'Должно быть отрендерено 10 сообщений'); // Тест 3: Оптимистичная отправка console.log('\n--- Тест 3: Оптимистичная отправка ---'); const tempId = controller.loader.sendMessageOptimistically(chatId, 'Optimistic integration'); this.assert(controller.store.optimisticMessages.has(tempId), 'Оптимистичное сообщение должно быть в Store'); // Симулируем подтверждение const confirmed = { id: 800, chat_id: chatId, sender: currentUser, content: 'Optimistic integration', created_at: new Date().toISOString() }; controller.store.confirmOptimisticMessage(tempId, confirmed); this.assert(!controller.store.optimisticMessages.has(tempId), 'Оптимистичное сообщение должно быть удалено'); this.assert(controller.store.hasMessage(800), 'Подтвержденное сообщение должно существовать'); // Тест 4: События Store → Controller console.log('\n--- Тест 4: События Store → Controller ---'); // Проверяем, что Controller получает события от Store let eventReceived = false; const originalHandler = controller._handleStoreUpdate.bind(controller); controller._handleStoreUpdate = function(...args) { eventReceived = true; return originalHandler(...args); }; controller.store.addMessage({ id: 900, chat_id: chatId, sender: { id: 1 }, content: 'Event test', created_at: new Date().toISOString() }); await this.sleep(100); this.assert(eventReceived, 'Controller должен получать события от Store'); // Cleanup controller.destroy(); document.body.removeChild(container); this.printResults(); return this.results; } // ==================== Тесты производительности ==================== async testPerformance() { console.log('\n🧪 Тесты производительности...\n'); this.resetResults(); const store = new MessageStore(); const chatId = 1; // Тест 1: Массовое добавление сообщений console.log('\n--- Тест 1: Добавление 1000 сообщений ---'); const messages = []; for (let i = 0; i < 1000; i++) { messages.push({ id: 1000 + i, chat_id: chatId, sender: { id: i % 10 + 1, username: `user${i % 10}` }, content: `Performance test message ${i}`, created_at: new Date(Date.now() + i * 1000).toISOString() }); } const start1 = performance.now(); store.addMessages(messages); const duration1 = performance.now() - start1; console.log(`⏱️ Добавлено 1000 сообщений за ${duration1.toFixed(2)}ms`); this.assert(duration1 < 100, 'Добавление 1000 сообщений должно занять < 100ms'); this.assertEqual(store.getMessagesForChat(chatId).length, 1000, 'Должно быть 1000 сообщений'); // Тест 2: Поиск сообщений console.log('\n--- Тест 2: Поиск 100 сообщений ---'); const start2 = performance.now(); for (let i = 0; i < 100; i++) { store.getMessage(1000 + Math.floor(Math.random() * 1000)); } const duration2 = performance.now() - start2; console.log(`⏱️ 100 поисков выполнено за ${duration2.toFixed(2)}ms`); this.assert(duration2 < 10, 'Поиск должен быть быстрым (< 10ms для 100 запросов)'); // Тест 3: Рендеринг console.log('\n--- Тест 3: Рендеринг 1000 сообщений ---'); const container = document.createElement('div'); container.id = 'perf-messages-list'; document.body.appendChild(container); const renderer = new MessageRendererV2({ store, containerId: 'perf-messages-list', currentUserId: 1 }); const start3 = performance.now(); renderer.render(chatId); const duration3 = performance.now() - start3; console.log(`⏱️ Рендеринг 1000 сообщений занял ${duration3.toFixed(2)}ms`); this.assert(duration3 < 500, 'Рендеринг должен быть достаточно быстрым (< 500ms)'); document.body.removeChild(container); // Тест 4: Обновление сообщений console.log('\n--- Тест 4: Обновление 100 сообщений ---'); const start4 = performance.now(); for (let i = 0; i < 100; i++) { store.updateMessage(1000 + i, { content: `Updated ${i}`, is_edited: true }); } const duration4 = performance.now() - start4; console.log(`⏱️ Обновление 100 сообщений заняло ${duration4.toFixed(2)}ms`); this.assert(duration4 < 50, 'Обновление должно быть быстрым (< 50ms)'); this.printResults(); return this.results; } // ==================== Расширенные тесты ==================== async testAdvanced() { console.log('\n🧪 Расширенные тесты системы чата...\n'); this.resetResults(); const store = new MessageStore(); const chatId = 1; // Тест 1: Множественная дозагрузка истории console.log('\n--- Тест 1: Множественная дозагрузка ---'); // Добавляем начальные сообщения (ID 100-119) const initialMessages = []; for (let i = 0; i < 20; i++) { initialMessages.push({ id: 100 + i, chat_id: chatId, sender: { id: 1, username: 'user1' }, content: `Message ${100 + i}`, created_at: new Date(Date.now() - (19 - i) * 60000).toISOString() }); } store.addMessages(initialMessages); // Первая дозагрузка (ID 80-99) const history1 = []; for (let i = 0; i < 20; i++) { history1.push({ id: 80 + i, chat_id: chatId, sender: { id: 1, username: 'user1' }, content: `History ${80 + i}`, created_at: new Date(Date.now() - (39 - i) * 60000).toISOString() }); } const added1 = store.addMessages(history1); this.assertEqual(added1, 20, 'Первая дозагрузка: добавлено 20 сообщений'); this.assertEqual(store.getMessagesForChat(chatId).length, 40, 'Всего должно быть 40 сообщений'); // Вторая дозагрузка (ID 60-79) const history2 = []; for (let i = 0; i < 20; i++) { history2.push({ id: 60 + i, chat_id: chatId, sender: { id: 1, username: 'user1' }, content: `History ${60 + i}`, created_at: new Date(Date.now() - (59 - i) * 60000).toISOString() }); } const added2 = store.addMessages(history2); this.assertEqual(added2, 20, 'Вторая дозагрузка: добавлено 20 сообщений'); this.assertEqual(store.getMessagesForChat(chatId).length, 60, 'Всего должно быть 60 сообщений'); // Тест 2: Порядок сообщений после множественных дозагрузок console.log('\n--- Тест 2: Порядок сообщений ---'); const allMessages = store.getMessagesForChat(chatId); // Отладка: показываем первые и последние 3 сообщения console.log('Первые 3 сообщения:', allMessages.slice(0, 3).map(m => ({ id: m.id, ts: m.created_ts }))); console.log('Последние 3 сообщения:', allMessages.slice(-3).map(m => ({ id: m.id, ts: m.created_ts }))); // Проверяем сортировку по timestamp (не по ID!) let correctOrder = true; for (let i = 1; i < allMessages.length; i++) { if (allMessages[i].created_ts < allMessages[i-1].created_ts) { correctOrder = false; console.log(`❌ Нарушение порядка на позиции ${i}: ${allMessages[i-1].id}(ts:${allMessages[i-1].created_ts}) > ${allMessages[i].id}(ts:${allMessages[i].created_ts})`); break; } } this.assert(correctOrder, 'Сообщения должны быть отсортированы по timestamp'); const oldest = store.getOldestMessage(chatId); const newest = store.getNewestMessage(chatId); console.log('Oldest message:', oldest ? { id: oldest.id, ts: oldest.created_ts } : null); console.log('Newest message:', newest ? { id: newest.id, ts: newest.created_ts } : null); this.assertEqual(oldest.id, 60, 'Старейшее сообщение должно иметь ID 60'); this.assertEqual(newest.id, 119, 'Новейшее сообщение должно иметь ID 119'); // Тест 3: Дубликаты при дозагрузке console.log('\n--- Тест 3: Дубликаты при дозагрузке ---'); // Пытаемся добавить уже существующие сообщения const duplicates = [ { ...initialMessages[0] }, { ...initialMessages[10] }, { ...history1[5] } ]; const addedDupes = store.addMessages(duplicates); this.assertEqual(addedDupes, 0, 'Дубликаты не должны быть добавлены'); this.assertEqual(store.getMessagesForChat(chatId).length, 60, 'Количество сообщений не должно измениться'); // Тест 4: Смешанная дозагрузка (новые + дубликаты) console.log('\n--- Тест 4: Смешанная дозагрузка ---'); const mixed = [ { ...initialMessages[0] }, // дубликат { id: 50, chat_id: chatId, sender: { id: 1 }, content: 'New 50', created_at: new Date(Date.now() - 69 * 60000).toISOString() }, { ...history1[5] }, // дубликат { id: 51, chat_id: chatId, sender: { id: 1 }, content: 'New 51', created_at: new Date(Date.now() - 68 * 60000).toISOString() }, { id: 52, chat_id: chatId, sender: { id: 1 }, content: 'New 52', created_at: new Date(Date.now() - 67 * 60000).toISOString() } ]; const addedMixed = store.addMessages(mixed); this.assertEqual(addedMixed, 3, 'Должно быть добавлено 3 новых сообщения из 5'); this.assertEqual(store.getMessagesForChat(chatId).length, 63, 'Всего должно быть 63 сообщения'); // Тест 5: Разделители дней console.log('\n--- Тест 5: Разделители дней ---'); store.clearChat(chatId); // Добавляем сообщения за разные дни const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const dayBefore = new Date(today); dayBefore.setDate(dayBefore.getDate() - 2); const multiDayMessages = [ { id: 200, chat_id: chatId, sender: { id: 1 }, content: 'Msg 1', created_at: dayBefore.toISOString() }, { id: 201, chat_id: chatId, sender: { id: 1 }, content: 'Msg 2', created_at: dayBefore.toISOString() }, { id: 202, chat_id: chatId, sender: { id: 1 }, content: 'Msg 3', created_at: yesterday.toISOString() }, { id: 203, chat_id: chatId, sender: { id: 1 }, content: 'Msg 4', created_at: yesterday.toISOString() }, { id: 204, chat_id: chatId, sender: { id: 1 }, content: 'Msg 5', created_at: today.toISOString() }, { id: 205, chat_id: chatId, sender: { id: 1 }, content: 'Msg 6', created_at: today.toISOString() } ]; store.addMessages(multiDayMessages); const itemsWithDividers = store.getMessagesWithDividers(chatId); const dividers = itemsWithDividers.filter(item => item.type === 'day-divider'); this.assertEqual(dividers.length, 3, 'Должно быть 3 разделителя дней'); const messages = itemsWithDividers.filter(item => item.type === 'message'); this.assertEqual(messages.length, 6, 'Должно быть 6 сообщений'); // Тест 6: Добавление сообщений в разном порядке console.log('\n--- Тест 6: Добавление в разном порядке ---'); store.clearChat(chatId); // Добавляем сообщения не по порядку const unordered = [ { id: 305, chat_id: chatId, sender: { id: 1 }, content: 'Msg 5', created_at: new Date(Date.now() + 4000).toISOString() }, { id: 302, chat_id: chatId, sender: { id: 1 }, content: 'Msg 2', created_at: new Date(Date.now() + 1000).toISOString() }, { id: 301, chat_id: chatId, sender: { id: 1 }, content: 'Msg 1', created_at: new Date(Date.now()).toISOString() }, { id: 304, chat_id: chatId, sender: { id: 1 }, content: 'Msg 4', created_at: new Date(Date.now() + 3000).toISOString() }, { id: 303, chat_id: chatId, sender: { id: 1 }, content: 'Msg 3', created_at: new Date(Date.now() + 2000).toISOString() } ]; store.addMessages(unordered); const sorted = store.getMessagesForChat(chatId); console.log('Sorted messages:', sorted.map(m => ({ id: m.id, ts: m.created_ts }))); this.assertEqual(sorted[0].id, 301, 'Первое сообщение должно иметь ID 301'); this.assertEqual(sorted[4].id, 305, 'Последнее сообщение должно иметь ID 305'); // Проверяем сортировку по timestamp let isSorted = true; for (let i = 1; i < sorted.length; i++) { if (sorted[i].created_ts < sorted[i-1].created_ts) { isSorted = false; console.log(`❌ Нарушение порядка: ${sorted[i-1].id}(ts:${sorted[i-1].created_ts}) > ${sorted[i].id}(ts:${sorted[i].created_ts})`); break; } } this.assert(isSorted, 'Сообщения должны быть автоматически отсортированы по timestamp'); // Тест 7: Граничные случаи console.log('\n--- Тест 7: Граничные случаи ---'); // Пустой массив const emptyAdded = store.addMessages([]); this.assertEqual(emptyAdded, 0, 'Пустой массив должен вернуть 0'); // null/undefined const nullAdded = store.addMessages(null); this.assertEqual(nullAdded, 0, 'null должен вернуть 0'); // Невалидные сообщения const invalid = [ { id: 400, chat_id: chatId, sender: { id: 1 }, content: 'Valid', created_at: new Date().toISOString() }, { chat_id: chatId, sender: { id: 1 }, content: 'No ID' }, // нет ID { id: null, chat_id: chatId, sender: { id: 1 }, content: 'Null ID' }, // null ID { id: 401, chat_id: chatId, sender: { id: 1 }, content: 'Valid 2', created_at: new Date().toISOString() } ]; const validAdded = store.addMessages(invalid); this.assertEqual(validAdded, 2, 'Должно быть добавлено только 2 валидных сообщения'); // Тест 8: Поиск несуществующих сообщений console.log('\n--- Тест 8: Поиск несуществующих ---'); const notFound = store.getMessage(99999); this.assert(notFound === null, 'Несуществующее сообщение должно вернуть null'); this.assert(!store.hasMessage(99999), 'hasMessage для несуществующего должен вернуть false'); // Тест 9: Обновление несуществующего сообщения console.log('\n--- Тест 9: Обновление несуществующего ---'); const updateResult = store.updateMessage(99999, { content: 'Updated' }); this.assert(!updateResult, 'Обновление несуществующего должно вернуть false'); // Тест 10: Удаление несуществующего сообщения console.log('\n--- Тест 10: Удаление несуществующего ---'); const removeResult = store.removeMessage(99999); this.assert(!removeResult, 'Удаление несуществующего должно вернуть false'); this.printResults(); return this.results; } // ==================== Тесты стабильности скролла ==================== async testScrollStability() { console.log('\n🧪 Тесты стабильности скролла...\n'); this.resetResults(); // Создаем реальный контейнер с прокруткой const container = document.createElement('div'); container.id = 'scroll-test-container'; container.style.cssText = 'height: 300px; overflow-y: auto; position: relative;'; document.body.appendChild(container); const store = new MessageStore(); const loader = new MessageLoader({ store }); const renderer = new MessageRendererV2({ store, containerId: 'scroll-test-container', currentUserId: 1 }); const manager = new ScrollManager({ scrollElement: container, messageLoader: loader, messageRenderer: renderer, messageStore: store, chatId: 1 }); const chatId = 1; // Тест 1: Добавление начальных сообщений console.log('\n--- Тест 1: Начальная загрузка ---'); const initialMessages = []; for (let i = 0; i < 20; i++) { initialMessages.push({ id: 100 + i, chat_id: chatId, sender: { id: 1, username: 'user1', display_name: 'User 1' }, content: `Initial message ${100 + i}`, created_at: new Date(Date.now() - (19 - i) * 60000).toISOString() }); } store.addMessages(initialMessages); renderer.render(chatId); await this.sleep(100); // Прокручиваем к началу (чтобы имитировать чтение старых) container.scrollTop = 100; const initialScrollTop = container.scrollTop; const initialScrollHeight = container.scrollHeight; this.assert(initialScrollTop > 0, 'Начальная позиция скролла должна быть > 0'); this.assert(initialScrollHeight > container.clientHeight, 'Контент должен быть больше видимой области'); console.log('Initial state:', { scrollTop: initialScrollTop, scrollHeight: initialScrollHeight, clientHeight: container.clientHeight }); // Тест 2: Симуляция дозагрузки истории console.log('\n--- Тест 2: Дозагрузка истории ---'); // Добавляем старые сообщения (имитация loadHistory) const historyMessages = []; for (let i = 0; i < 10; i++) { historyMessages.push({ id: 90 + i, chat_id: chatId, sender: { id: 1, username: 'user1', display_name: 'User 1' }, content: `History message ${90 + i}`, created_at: new Date(Date.now() - (29 - i) * 60000).toISOString() }); } // ПРАВИЛЬНЫЙ подход (как в Telegram): сохраняем offsetTop якоря const anchorBefore = manager._getFirstVisibleMessage(); const anchorIdBefore = anchorBefore ? anchorBefore.dataset.messageId : null; const anchorOffsetTopBefore = anchorBefore ? anchorBefore.offsetTop : 0; const viewportOffsetBefore = container.scrollTop; const anchorRelativePos = anchorOffsetTopBefore - viewportOffsetBefore; console.log('Before history load:', { anchorId: anchorIdBefore, anchorOffsetTop: anchorOffsetTopBefore, viewportOffset: viewportOffsetBefore, anchorRelativePos }); // Добавляем в Store и ре-рендерим store.addMessages(historyMessages); container.style.visibility = 'hidden'; renderer.render(chatId); await this.sleep(50); // Восстанавливаем позицию: находим якорь и ставим его на ту же позицию относительно viewport if (anchorIdBefore) { const anchorAfter = container.querySelector(`[data-message-id="${anchorIdBefore}"]`); if (anchorAfter) { const newAnchorOffsetTop = anchorAfter.offsetTop; container.scrollTop = newAnchorOffsetTop - anchorRelativePos; console.log('After history load:', { anchorId: anchorIdBefore, oldOffsetTop: anchorOffsetTopBefore, newOffsetTop: newAnchorOffsetTop, delta: newAnchorOffsetTop - anchorOffsetTopBefore, newScrollTop: container.scrollTop }); } } container.style.visibility = ''; await this.sleep(50); // Проверяем что якорное сообщение все еще видимо if (anchorIdBefore) { const anchorStillVisible = this._isMessageVisible(container, anchorIdBefore); this.assert(anchorStillVisible, `Якорное сообщение ${anchorIdBefore} должно остаться видимым`); // Проверяем что якорь находится примерно в той же позиции относительно viewport const anchorAfterCheck = container.querySelector(`[data-message-id="${anchorIdBefore}"]`); if (anchorAfterCheck) { const newRelativePos = anchorAfterCheck.offsetTop - container.scrollTop; const posDiff = Math.abs(newRelativePos - anchorRelativePos); console.log('Position check:', { originalRelativePos: anchorRelativePos, newRelativePos, diff: posDiff }); this.assert(posDiff < 5, `Якорь должен остаться в той же позиции viewport (diff: ${posDiff}px)`); } } this.assert(container.scrollHeight > initialScrollHeight, 'Высота контента должна увеличиться'); // Тест 3: Проверка что скролл не прыгает к низу console.log('\n--- Тест 3: Скролл не прыгает к низу ---'); const maxScrollTop = container.scrollHeight - container.clientHeight; this.assert(container.scrollTop < maxScrollTop - 50, 'Скролл не должен быть в самом низу'); // Тест 4: Множественная дозагрузка (реалистичный сценарий) console.log('\n--- Тест 4: Множественная дозагрузка ---'); // Сохраняем начальное якорное сообщение для проверки в конце const initialAnchor = manager._getFirstVisibleMessage(); const initialAnchorId = initialAnchor ? initialAnchor.dataset.messageId : null; console.log('Starting multiple loads with anchor:', initialAnchorId); // Имитируем 5 последовательных дозагрузок истории for (let batch = 0; batch < 5; batch++) { console.log(`\n Batch ${batch + 1}/5:`); // Сохраняем якорь ПЕРЕД каждой дозагрузкой const anchor = manager._getFirstVisibleMessage(); if (!anchor) { console.warn(' No anchor found, skipping batch'); continue; } const anchorId = anchor.dataset.messageId; const anchorOffsetTop = anchor.offsetTop; const viewportOffset = container.scrollTop; const anchorRelativePos = anchorOffsetTop - viewportOffset; console.log(' Before:', { anchorId, anchorOffsetTop, viewportOffset, anchorRelativePos }); // Добавляем 10 старых сообщений const batchMessages = []; const startId = 80 - batch * 10; for (let i = 0; i < 10; i++) { const msgId = startId + i; batchMessages.push({ id: msgId, chat_id: chatId, sender: { id: 1, username: 'user1', display_name: 'User 1' }, content: `Batch ${batch} message ${msgId}`, created_at: new Date(Date.now() - (100 + batch * 10 - i) * 60000).toISOString() }); } // Добавляем в Store и ре-рендерим store.addMessages(batchMessages); container.style.visibility = 'hidden'; renderer.render(chatId); await this.sleep(20); // ПРАВИЛЬНОЕ восстановление: находим якорь и восстанавливаем его позицию const anchorAfter = container.querySelector(`[data-message-id="${anchorId}"]`); if (anchorAfter) { const newOffsetTop = anchorAfter.offsetTop; container.scrollTop = newOffsetTop - anchorRelativePos; console.log(' After:', { newOffsetTop, delta: newOffsetTop - anchorOffsetTop, newScrollTop: container.scrollTop }); // Проверяем что якорь остался видимым const stillVisible = this._isMessageVisible(container, anchorId); this.assert(stillVisible, `Batch ${batch + 1}: Якорь ${anchorId} должен остаться видимым`); } container.style.visibility = ''; await this.sleep(20); } // Проверяем что начальный якорь все еще видим (или близко) if (initialAnchorId) { const initialStillVisible = this._isMessageVisible(container, initialAnchorId); console.log(`\nInitial anchor ${initialAnchorId} still visible:`, initialStillVisible); // Допускаем что начальный якорь может уйти за границу после 5 дозагрузок // Главное что каждая дозагрузка сохраняет СВОЙ якорь } console.log('\nFinal state:', { totalMessages: store.getMessagesForChat(chatId).length, scrollTop: container.scrollTop, scrollHeight: container.scrollHeight, clientHeight: container.clientHeight }); // Проверяем что добавлено много сообщений this.assert(store.getMessagesForChat(chatId).length >= 80, 'Должно быть добавлено много сообщений'); // Тест 5: Большой объем данных (стресс-тест) console.log('\n--- Тест 5: Стресс-тест с большим количеством сообщений ---'); store.clearChat(chatId); // Добавляем 100 сообщений const manyMessages = []; for (let i = 0; i < 100; i++) { manyMessages.push({ id: 1000 + i, chat_id: chatId, sender: { id: i % 5 + 1, username: `user${i % 5}`, display_name: `User ${i % 5}` }, content: `Stress test message ${1000 + i}`, created_at: new Date(Date.now() - (99 - i) * 60000).toISOString() }); } store.addMessages(manyMessages); renderer.render(chatId); await this.sleep(100); // Скроллим в середину container.scrollTop = container.scrollHeight / 2; await this.sleep(50); const stressAnchor = manager._getFirstVisibleMessage(); const stressAnchorId = stressAnchor ? stressAnchor.dataset.messageId : null; const stressAnchorOffsetTop = stressAnchor ? stressAnchor.offsetTop : 0; const stressViewportOffset = container.scrollTop; const stressRelativePos = stressAnchorOffsetTop - stressViewportOffset; console.log('Stress test before:', { totalMessages: 100, anchorId: stressAnchorId, scrollTop: container.scrollTop }); // Добавляем еще 50 старых сообщений const stressHistory = []; for (let i = 0; i < 50; i++) { stressHistory.push({ id: 950 + i, chat_id: chatId, sender: { id: 1, username: 'user1', display_name: 'User 1' }, content: `Stress history ${950 + i}`, created_at: new Date(Date.now() - (149 - i) * 60000).toISOString() }); } store.addMessages(stressHistory); container.style.visibility = 'hidden'; renderer.render(chatId); await this.sleep(50); // Восстанавливаем якорь if (stressAnchorId) { const stressAnchorAfter = container.querySelector(`[data-message-id="${stressAnchorId}"]`); if (stressAnchorAfter) { container.scrollTop = stressAnchorAfter.offsetTop - stressRelativePos; } } container.style.visibility = ''; await this.sleep(50); console.log('Stress test after:', { totalMessages: store.getMessagesForChat(chatId).length, scrollTop: container.scrollTop }); // Проверяем что якорь остался видимым даже при большом объеме if (stressAnchorId) { const stressVisible = this._isMessageVisible(container, stressAnchorId); this.assert(stressVisible, `Стресс-тест: якорь ${stressAnchorId} должен остаться видимым даже при 150 сообщениях`); } this.assertEqual(store.getMessagesForChat(chatId).length, 150, 'Должно быть 150 сообщений после стресс-теста'); // Cleanup manager.destroy(); document.body.removeChild(container); this.printResults(); return this.results; } // ==================== Тесты умного автоскролла ==================== async testSmartAutoscroll() { console.log('\n🧪 Тестирование умного автоскролла и индикатора новых сообщений...\n'); this.resetResults(); // Создаем реальный контейнер const container = document.createElement('div'); container.id = 'test-chat-container'; container.style.cssText = 'height: 400px; overflow-y: auto; position: relative;'; document.body.appendChild(container); const currentUserId = 1; const controller = new ChatController({ chatId: 1, scrollElement: container, containerId: 'test-chat-container', currentUserId: currentUserId }); // Инициализируем контроллер try { await controller.init(); } catch (e) { console.log('[Test] Init failed (expected without backend):', e.message); // ВАЖНО: Сбрасываем флаг инициализации для тестов controller.isInitializing = false; } // Тест 1: Своё сообщение - всегда скроллим вниз console.log('\n--- Тест 1: Своё сообщение скроллит вниз ---'); // Добавляем несколько сообщений для высоты for (let i = 0; i < 20; i++) { controller.store.addMessage({ id: i + 1, chat_id: 1, author_id: 2, author_name: 'Other User', content: `Message ${i + 1}`, created_ts: Date.now() + i * 1000, created: new Date(Date.now() + i * 1000).toISOString() }); } controller.renderer.render(1); await this.sleep(100); // Прокручиваем вверх (читаем историю) container.scrollTop = 50; await this.sleep(50); const wasAtBottom = controller.scrollManager.isNearBottom(); this.assert(!wasAtBottom, 'До отправки своего сообщения должны быть НЕ внизу'); // Добавляем своё сообщение controller.store.addMessage({ id: 100, chat_id: 1, author_id: currentUserId, author_name: 'Me', content: 'My message', created_ts: Date.now() + 30000, created: new Date(Date.now() + 30000).toISOString() }); await this.sleep(200); const isNowAtBottom = controller.scrollManager.isNearBottom(); this.assert(isNowAtBottom, 'После своего сообщения должны быть внизу (автоскролл)'); // Тест 2: Чужое сообщение + внизу - скроллим console.log('\n--- Тест 2: Чужое сообщение + внизу = скролл ---'); // Убеждаемся что внизу controller.scrollManager.scrollToBottom({ instant: true, force: true }); await this.sleep(100); const wasAtBottomBefore = controller.scrollManager.isNearBottom(); this.assert(wasAtBottomBefore, 'Должны быть внизу перед получением чужого сообщения'); // Добавляем чужое сообщение controller.store.addMessage({ id: 101, chat_id: 1, author_id: 2, author_name: 'Other User', content: 'Other message', created_ts: Date.now() + 31000, created: new Date(Date.now() + 31000).toISOString() }); await this.sleep(200); const stillAtBottom = controller.scrollManager.isNearBottom(); this.assert(stillAtBottom, 'Должны остаться внизу после чужого сообщения (автоскролл)'); // Тест 3: Чужое сообщение + читаем историю - показываем индикатор console.log('\n--- Тест 3: Чужое сообщение + история = индикатор ---'); // Прокручиваем вверх (читаем историю) container.scrollTop = 100; await this.sleep(100); const readingHistory = !controller.scrollManager.isNearBottom(); this.assert(readingHistory, 'Должны читать историю (не внизу)'); // Сбрасываем счётчик индикатора controller.newMessagesCount = 0; // Добавляем чужое сообщение controller.store.addMessage({ id: 102, chat_id: 1, author_id: 2, author_name: 'Other User', content: 'Another message', created_ts: Date.now() + 32000, created: new Date(Date.now() + 32000).toISOString() }); await this.sleep(200); // Проверяем что индикатор показан this.assert(controller.newMessagesCount > 0, 'Счётчик новых сообщений должен увеличиться'); const indicator = document.getElementById('new-messages-btn'); if (indicator) { const isVisible = indicator.style.display !== 'none'; this.assert(isVisible, 'Индикатор новых сообщений должен быть видимым'); } else { console.warn('[Test] Индикатор не найден (может создаваться при первом показе)'); } // Тест 4: Клик по индикатору - скролл вниз и скрытие console.log('\n--- Тест 4: Клик по индикатору ---'); if (indicator && controller.newMessagesCount > 0) { // Кликаем по индикатору indicator.click(); // Ждём завершения анимации скролла await this.sleep(500); // Проверяем что прокрутили вниз const isAtBottomNow = controller.scrollManager.isNearBottom(); this.assert(isAtBottomNow, 'После клика по индикатору должны быть внизу'); // Проверяем что счётчик сброшен this.assertEqual(controller.newMessagesCount, 0, 'Счётчик должен быть сброшен'); // Проверяем что индикатор скрыт const isHiddenNow = indicator.style.display === 'none'; this.assert(isHiddenNow, 'Индикатор должен быть скрыт'); } // Тест 5: Ручной скролл вниз - скрытие индикатора console.log('\n--- Тест 5: Ручной скролл вниз ---'); // Прокручиваем вверх container.scrollTop = 50; controller.newMessagesCount = 5; // Устанавливаем счётчик вручную if (indicator) { indicator.style.display = 'flex'; } await this.sleep(100); // Прокручиваем вниз вручную controller.scrollManager.scrollToBottom({ instant: false, force: true }); // Увеличиваем ожидание для гарантированного срабатывания scroll watcher await this.sleep(500); // Проверяем что индикатор скрылся this.assertEqual(controller.newMessagesCount, 0, 'Счётчик должен сброситься при ручном скролле'); // Тест 6: Оптимистичное сообщение - скроллим console.log('\n--- Тест 6: Оптимистичное сообщение ---'); // Прокручиваем вверх container.scrollTop = 80; await this.sleep(100); const notAtBottomBefore = !controller.scrollManager.isNearBottom(); this.assert(notAtBottomBefore, 'Должны быть не внизу перед оптимистичным сообщением'); // Добавляем оптимистичное сообщение (своё) controller.store.addMessage({ id: 'temp_123', chat_id: 1, author_id: currentUserId, author_name: 'Me', content: 'Optimistic message', created_ts: Date.now() + 33000, created: new Date(Date.now() + 33000).toISOString(), is_optimistic: true }, true); // true = optimistic await this.sleep(200); const atBottomAfter = controller.scrollManager.isNearBottom(); this.assert(atBottomAfter, 'Оптимистичное сообщение должно скроллить вниз'); // Тест 7: Методы индикатора существуют console.log('\n--- Тест 7: API индикатора ---'); this.assert(typeof controller._showNewMessagesIndicator === 'function', '_showNewMessagesIndicator должен быть функцией'); this.assert(typeof controller._hideNewMessagesIndicator === 'function', '_hideNewMessagesIndicator должен быть функцией'); this.assert(typeof controller._findOrCreateNewMessagesButton === 'function', '_findOrCreateNewMessagesButton должен быть функцией'); this.assert(typeof controller._initScrollWatcher === 'function', '_initScrollWatcher должен быть функцией'); // Тест 8: Счётчик новых сообщений инкрементируется console.log('\n--- Тест 8: Счётчик сообщений ---'); container.scrollTop = 100; controller.newMessagesCount = 0; await this.sleep(100); // Добавляем 3 чужих сообщения подряд for (let i = 0; i < 3; i++) { controller.store.addMessage({ id: 200 + i, chat_id: 1, author_id: 2, author_name: 'Other User', content: `Batch message ${i}`, created_ts: Date.now() + 40000 + i * 100, created: new Date(Date.now() + 40000 + i * 100).toISOString() }); await this.sleep(50); } await this.sleep(200); this.assertEqual(controller.newMessagesCount, 3, 'Счётчик должен показывать 3 новых сообщения'); // Cleanup controller.destroy(); if (indicator && indicator.parentElement) { indicator.parentElement.removeChild(indicator); } document.body.removeChild(container); this.printResults(); return this.results; } // ==================== Запуск всех тестов ==================== async runAll() { console.log('\n' + '='.repeat(60)); console.log('🚀 ЗАПУСК ВСЕХ ТЕСТОВ СИСТЕМЫ ЧАТА'); console.log('='.repeat(60)); const allResults = { passed: 0, failed: 0, total: 0 }; try { // Юнит-тесты const storeResults = await this.testMessageStore(); const loaderResults = await this.testMessageLoader(); const scrollManagerResults = await this.testScrollManager(); const rendererResults = await this.testMessageRenderer(); const controllerResults = await this.testChatController(); // Интеграционные тесты const integrationResults = await this.testIntegration(); // Тесты производительности const perfResults = await this.testPerformance(); // Расширенные тесты const advancedResults = await this.testAdvanced(); // Тесты стабильности скролла const scrollStabilityResults = await this.testScrollStability(); // Тесты умного автоскролла const smartAutoscrollResults = await this.testSmartAutoscroll(); // Суммируем результаты [storeResults, loaderResults, scrollManagerResults, rendererResults, controllerResults, integrationResults, perfResults, advancedResults, scrollStabilityResults, smartAutoscrollResults].forEach(result => { allResults.passed += result.passed; allResults.failed += result.failed; allResults.total += result.total; }); console.log('\n' + '='.repeat(60)); console.log('🏁 ИТОГОВЫЕ РЕЗУЛЬТАТЫ'); console.log('='.repeat(60)); console.log(`Всего тестов: ${allResults.total}`); console.log(`✅ Успешно: ${allResults.passed}`); console.log(`❌ Провалено: ${allResults.failed}`); console.log(`📈 Процент успеха: ${(allResults.passed / allResults.total * 100).toFixed(1)}%`); console.log('='.repeat(60) + '\n'); return allResults; } catch (error) { console.error('❌ Критическая ошибка при выполнении тестов:', error); throw error; } } } // Экспортируем для использования в консоли const chatTests = new ChatTestSuite(); window.ChatTests = { runAll: () => chatTests.runAll(), testMessageStore: () => chatTests.testMessageStore(), testMessageLoader: () => chatTests.testMessageLoader(), testScrollManager: () => chatTests.testScrollManager(), testMessageRenderer: () => chatTests.testMessageRenderer(), testChatController: () => chatTests.testChatController(), testIntegration: () => chatTests.testIntegration(), testPerformance: () => chatTests.testPerformance(), testAdvanced: () => chatTests.testAdvanced(), testScrollStability: () => chatTests.testScrollStability(), testSmartAutoscroll: () => chatTests.testSmartAutoscroll() }; console.log('✅ ChatTests загружен! Используйте:'); console.log(' window.ChatTests.runAll() - Все тесты'); console.log(' window.ChatTests.testMessageStore() - Тесты MessageStore'); console.log(' window.ChatTests.testMessageLoader() - Тесты MessageLoader'); console.log(' window.ChatTests.testScrollManager() - Тесты ScrollManager'); console.log(' window.ChatTests.testMessageRenderer() - Тесты MessageRenderer'); console.log(' window.ChatTests.testChatController() - Тесты ChatController'); console.log(' window.ChatTests.testIntegration() - Интеграционные тесты'); console.log(' window.ChatTests.testPerformance() - Тесты производительности'); console.log(' window.ChatTests.testAdvanced() - Расширенные тесты'); console.log(' window.ChatTests.testScrollStability() - Тесты стабильности скролла'); console.log(' window.ChatTests.testSmartAutoscroll() - Тесты умного автоскролла'); export default chatTests;