refactor: удалены .old файлы
- Deleted chatTestsV2.old.js - Deleted postDetailModal.old.js Финальная очистка deprecated кода завершена
This commit is contained in:
parent
a89a993861
commit
f77626914e
@ -1,401 +0,0 @@
|
||||
/**
|
||||
* postDetailModal.js
|
||||
* Модуль для открытия деталей поста в модальном окне
|
||||
* Полностью переработанная версия с правильной логикой определения автора
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Константы
|
||||
const INITIAL_COMMENTS_COUNT = 10;
|
||||
const DEBUG = true; // Включаем отладку для диагностики
|
||||
|
||||
// Состояние модуля
|
||||
let modal = null;
|
||||
let modalElement = null;
|
||||
let currentPost = null; // Сохраняем текущий пост
|
||||
|
||||
/**
|
||||
* Инициализация модального окна
|
||||
*/
|
||||
function initPostDetailModal() {
|
||||
modalElement = document.getElementById('postDetailModal');
|
||||
if (!modalElement) {
|
||||
console.warn('postDetailModal: элемент #postDetailModal не найден');
|
||||
return;
|
||||
}
|
||||
|
||||
modal = new bootstrap.Modal(modalElement);
|
||||
|
||||
// Очистка при закрытии
|
||||
modalElement.addEventListener('hidden.bs.modal', function() {
|
||||
const contentDiv = document.getElementById('postDetailContent');
|
||||
if (contentDiv) {
|
||||
contentDiv.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
currentPost = null;
|
||||
});
|
||||
|
||||
// Добавляем обработчики на ссылки постов
|
||||
attachPostLinkHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение CSRF токена из cookie
|
||||
*/
|
||||
function getCsrfToken() {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === 'csrftoken') {
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение JWT токена из cookie (для загрузки данных)
|
||||
*/
|
||||
function getAccessToken() {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === 'access_token') {
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка деталей поста через API
|
||||
*/
|
||||
async function loadPostDetail(postId) {
|
||||
const accessToken = getAccessToken();
|
||||
const headers = {
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (accessToken) {
|
||||
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/posts/${postId}/`, {
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const post = await response.json();
|
||||
|
||||
// Отладочная информация
|
||||
if (DEBUG) {
|
||||
console.log('=== POST DETAIL DEBUG ===');
|
||||
console.log('Post author:', post.author);
|
||||
console.log('Post author.id:', post.author?.id, 'type:', typeof post.author?.id);
|
||||
console.log('Post author_id:', post.author_id, 'type:', typeof post.author_id);
|
||||
if (post.comments && post.comments.length > 0) {
|
||||
console.log('First comment:', post.comments[0]);
|
||||
console.log('First comment author:', post.comments[0].author);
|
||||
console.log('First comment author.id:', post.comments[0].author?.id, 'type:', typeof post.comments[0].author?.id);
|
||||
console.log('First comment author_id:', post.comments[0].author_id, 'type:', typeof post.comments[0].author_id);
|
||||
}
|
||||
console.log('========================');
|
||||
}
|
||||
|
||||
currentPost = post;
|
||||
renderPostDetail(post);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки поста:', error);
|
||||
showError('Не удалось загрузить пост');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отображение деталей поста в модальном окне
|
||||
*/
|
||||
function renderPostDetail(post) {
|
||||
const contentDiv = document.getElementById('postDetailContent');
|
||||
|
||||
// Определяем ID автора поста (пробуем разные поля)
|
||||
const postAuthorId = getAuthorId(post.author) || post.author_id;
|
||||
|
||||
if (DEBUG) {
|
||||
console.log('Rendering post with author ID:', postAuthorId);
|
||||
}
|
||||
|
||||
// Формируем HTML с кнопкой закрытия
|
||||
let html = '<button type="button" class="btn-close position-absolute top-0 end-0 m-3" data-bs-dismiss="modal" aria-label="Закрыть" style="z-index: 1050;"></button>';
|
||||
html += '<article class="card">';
|
||||
|
||||
// Заголовок с автором
|
||||
html += renderPostHeader(post);
|
||||
|
||||
// Изображение
|
||||
if (post.image) {
|
||||
html += `<div class="feed-img"><img src="${post.image}" alt="${escapeHtml(post.title || 'Публикация')}"></div>`;
|
||||
}
|
||||
|
||||
// Тело
|
||||
html += '<div class="card-body">';
|
||||
html += `<h3 class="feed-title mb-3">${escapeHtml(post.title || 'Без названия')}</h3>`;
|
||||
if (post.body) {
|
||||
html += `<div class="feed-text mb-3">${formatBody(post.body)}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Футер с лайками
|
||||
html += renderPostFooter(post);
|
||||
|
||||
// Секция комментариев
|
||||
html += renderCommentsSection(post, postAuthorId);
|
||||
|
||||
html += '</article>';
|
||||
|
||||
contentDiv.innerHTML = html;
|
||||
|
||||
// Сохраняем данные для динамической подгрузки
|
||||
if (post.comments && post.comments.length > 0) {
|
||||
contentDiv.dataset.allComments = JSON.stringify(post.comments);
|
||||
contentDiv.dataset.postAuthorId = postAuthorId;
|
||||
}
|
||||
|
||||
// Прокручиваем к последнему комментарию
|
||||
scrollToBottom();
|
||||
|
||||
// Добавляем обработчики
|
||||
attachLikeHandlers();
|
||||
attachLoadMoreCommentsHandler();
|
||||
attachCommentFormHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отображение комментария
|
||||
* @param {Object} comment - Объект комментария
|
||||
* @param {number} postAuthorId - ID автора поста
|
||||
*/
|
||||
function renderComment(comment, postAuthorId) {
|
||||
// Приводим оба ID к числам для корректного сравнения
|
||||
const commentAuthorId = comment.author?.id ? parseInt(comment.author.id) : null;
|
||||
const postAuthorIdNum = postAuthorId ? parseInt(postAuthorId) : null;
|
||||
const isAuthor = commentAuthorId !== null && postAuthorIdNum !== null && commentAuthorId === postAuthorIdNum;
|
||||
|
||||
const commentClass = isAuthor ? 'comment-item comment-item--author' : 'comment-item';
|
||||
|
||||
let html = `<div class="${commentClass}">`;
|
||||
html += '<div class="comment-header">';
|
||||
|
||||
// Автор и дата
|
||||
const authorName = comment.author?.full_name || 'Аноним';
|
||||
const authorId = comment.author?.id;
|
||||
|
||||
if (authorId) {
|
||||
html += `<a href="/employees/${authorId}/" class="comment-author">${authorName}</a>`;
|
||||
} else {
|
||||
html += `<span class="comment-author">${authorName}</span>`;
|
||||
}
|
||||
|
||||
html += '<span class="comment-separator">·</span>';
|
||||
html += `<time class="comment-date">${formatDate(comment.created_at)}</time>`;
|
||||
html += '</div>';
|
||||
|
||||
// Текст комментария
|
||||
html += `<div class="comment-body">${formatBody(comment.text)}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирование даты
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) {
|
||||
return 'Сегодня, ' + date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (days === 1) {
|
||||
return 'Вчера, ' + date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (days < 7) {
|
||||
return days + ' дн. назад';
|
||||
} else {
|
||||
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирование текста (простая обработка переносов строк)
|
||||
*/
|
||||
function formatBody(text) {
|
||||
return text.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать ошибку
|
||||
*/
|
||||
function showError(message) {
|
||||
const contentDiv = document.getElementById('postDetailContent');
|
||||
contentDiv.innerHTML = `
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi-exclamation-triangle"></i> ${message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть модальное окно с постом
|
||||
*/
|
||||
function openPostModal(postId) {
|
||||
if (!modal) {
|
||||
console.error('Modal не инициализирован');
|
||||
return;
|
||||
}
|
||||
|
||||
modal.show();
|
||||
loadPostDetail(postId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление обработчиков на ссылки постов
|
||||
*/
|
||||
function attachPostLinkHandlers() {
|
||||
// Обработка кликов по ссылкам на посты
|
||||
document.addEventListener('click', function(e) {
|
||||
const link = e.target.closest('a[data-post-link]');
|
||||
if (link) {
|
||||
e.preventDefault();
|
||||
const postId = link.dataset.postId;
|
||||
if (postId) {
|
||||
openPostModal(postId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик для кнопки "Показать ещё комментарии"
|
||||
*/
|
||||
function attachLoadMoreCommentsHandler() {
|
||||
const loadMoreBtn = document.querySelector('.load-more-comments');
|
||||
if (!loadMoreBtn) return;
|
||||
|
||||
loadMoreBtn.addEventListener('click', function() {
|
||||
const contentDiv = document.getElementById('postDetailContent');
|
||||
const allComments = JSON.parse(contentDiv.dataset.allComments || '[]');
|
||||
const postAuthorId = parseInt(contentDiv.dataset.postAuthorId);
|
||||
const commentsContainer = document.getElementById('commentsListContainer');
|
||||
|
||||
if (!commentsContainer || allComments.length === 0) return;
|
||||
|
||||
// Сохраняем текущую позицию скролла и высоту
|
||||
const scrollHeight = commentsContainer.scrollHeight;
|
||||
const scrollTop = commentsContainer.scrollTop;
|
||||
|
||||
// Показываем все комментарии
|
||||
let html = '';
|
||||
allComments.forEach(comment => {
|
||||
html += renderComment(comment, postAuthorId);
|
||||
});
|
||||
|
||||
commentsContainer.innerHTML = html;
|
||||
|
||||
// Восстанавливаем позицию скролла (с учётом добавленного контента)
|
||||
const newScrollHeight = commentsContainer.scrollHeight;
|
||||
const heightDiff = newScrollHeight - scrollHeight;
|
||||
commentsContainer.scrollTop = scrollTop + heightDiff;
|
||||
|
||||
// Удаляем кнопку после загрузки
|
||||
this.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчики лайков в модальном окне
|
||||
*/
|
||||
function attachLikeHandlers() {
|
||||
const likeButtons = document.querySelectorAll('#postDetailContent .like-btn');
|
||||
likeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
const postId = this.dataset.postId;
|
||||
await toggleLike(postId, this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключение лайка
|
||||
*/
|
||||
async function toggleLike(postId, button) {
|
||||
const csrfToken = getCsrfToken();
|
||||
if (!csrfToken) {
|
||||
alert('Необходимо авторизоваться');
|
||||
return;
|
||||
}
|
||||
|
||||
const icon = button.querySelector('i');
|
||||
const isLiked = icon.classList.contains('bi-heart-fill');
|
||||
const endpoint = isLiked ? 'unlike' : 'like';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/posts/${postId}/${endpoint}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Обновляем иконку и счетчик
|
||||
if (isLiked) {
|
||||
icon.classList.remove('bi-heart-fill', 'text-danger');
|
||||
icon.classList.add('bi-heart');
|
||||
} else {
|
||||
icon.classList.remove('bi-heart');
|
||||
icon.classList.add('bi-heart-fill', 'text-danger');
|
||||
}
|
||||
|
||||
// Обновляем счетчик
|
||||
const countText = button.childNodes[1];
|
||||
if (countText) {
|
||||
countText.textContent = ' ' + (data.likes_count || 0);
|
||||
}
|
||||
} else {
|
||||
alert('Ошибка при обработке лайка');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Ошибка сети');
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация при загрузке DOM
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPostDetailModal);
|
||||
} else {
|
||||
initPostDetailModal();
|
||||
}
|
||||
|
||||
// Экспорт для использования извне
|
||||
window.openPostModal = openPostModal;
|
||||
|
||||
})();
|
||||
@ -1,428 +0,0 @@
|
||||
/**
|
||||
* Тесты для ChatControllerV2 - умный автоскролл
|
||||
* Запуск:
|
||||
* .venv/Scripts/python manage.py runserver
|
||||
* Открыть в браузере: http://localhost:8000/static/js/tests/chatTestsV2.html
|
||||
*/
|
||||
|
||||
import { ChatControllerV2 } from '../controllers/chatControllerV2.js';
|
||||
import { MessageStoreV2 } from '../stores/messageStoreV2.js';
|
||||
|
||||
class ChatTestSuiteV2 {
|
||||
constructor() {
|
||||
this.results = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
details: []
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Утилиты ====================
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
resetResults() {
|
||||
this.results = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
details: []
|
||||
};
|
||||
}
|
||||
|
||||
printResults() {
|
||||
const { passed, failed, total } = this.results;
|
||||
const passRate = total > 0 ? parseFloat(((passed / total) * 100).toFixed(1)) : 0;
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log(`📊 Результаты тестов V2:`);
|
||||
console.log(` ✅ Успешных: ${passed}`);
|
||||
console.log(` ❌ Провалено: ${failed}`);
|
||||
console.log(` 📈 Процент: ${passRate}%`);
|
||||
console.log('='.repeat(60) + '\n');
|
||||
|
||||
return { passed, failed, total, passRate };
|
||||
}
|
||||
|
||||
// ==================== Тесты базовой функциональности V2 ====================
|
||||
|
||||
async testBasicFunctionalityV2() {
|
||||
console.log('\n🧪 Тестирование базовой функциональности V2...\n');
|
||||
this.resetResults();
|
||||
|
||||
const store = new MessageStoreV2();
|
||||
const chatId = 1;
|
||||
|
||||
// Тест 1: Добавление сообщения
|
||||
const msg1 = {
|
||||
id: 1,
|
||||
chat_id: chatId,
|
||||
author_id: 1,
|
||||
author_name: 'User 1',
|
||||
content: 'Test message',
|
||||
created_ts: Date.now(),
|
||||
created: new Date().toISOString()
|
||||
};
|
||||
|
||||
store.addMessage(msg1);
|
||||
this.assert(store.hasMessage(1), 'Сообщение должно быть добавлено');
|
||||
|
||||
// Тест 2: Batch добавление (V2 фича)
|
||||
const messages = [
|
||||
{ id: 2, chat_id: chatId, author_id: 1, content: 'Msg 2', created_ts: Date.now() + 1000, created: new Date().toISOString() },
|
||||
{ id: 3, chat_id: chatId, author_id: 1, content: 'Msg 3', created_ts: Date.now() + 2000, created: new Date().toISOString() },
|
||||
{ id: 4, chat_id: chatId, author_id: 1, content: 'Msg 4', created_ts: Date.now() + 3000, created: new Date().toISOString() }
|
||||
];
|
||||
|
||||
store.addMessages(messages);
|
||||
this.assertEqual(store.messages.size, 4, 'Должно быть 4 сообщения после batch');
|
||||
|
||||
// Тест 3: Обновление сообщения
|
||||
store.updateMessage(1, { content: 'Updated', is_edited: true });
|
||||
const updated = store.getMessage(1);
|
||||
this.assertEqual(updated.content, 'Updated', 'Сообщение должно быть обновлено');
|
||||
|
||||
// Тест 4: Удаление сообщения
|
||||
store.removeMessage(1);
|
||||
this.assert(!store.hasMessage(1), 'Сообщение должно быть удалено');
|
||||
|
||||
this.printResults();
|
||||
return this.results;
|
||||
}
|
||||
|
||||
// ==================== Тесты умного автоскролла V2 ====================
|
||||
|
||||
async testSmartAutoscrollV2() {
|
||||
console.log('\n🧪 Тестирование умного автоскролла V2 (8 сценариев, ~18 проверок)...\n');
|
||||
this.resetResults();
|
||||
|
||||
// Создаем реальный контейнер
|
||||
const container = document.createElement('div');
|
||||
container.id = 'test-chat-container-v2';
|
||||
container.style.cssText = 'height: 400px; overflow-y: auto; position: relative;';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const currentUserId = 1;
|
||||
const controller = new ChatControllerV2({
|
||||
chatId: 1,
|
||||
scrollElement: container,
|
||||
containerId: 'test-chat-container-v2',
|
||||
currentUserId: currentUserId
|
||||
});
|
||||
|
||||
// Инициализируем контроллер
|
||||
try {
|
||||
await controller.init();
|
||||
} catch (e) {
|
||||
console.log('[Test V2] Init failed (expected without backend):', e.message);
|
||||
controller._initializing = false;
|
||||
controller._initialized = false;
|
||||
|
||||
// ВАЖНО: Инициализируем scroll watcher вручную для тестов
|
||||
// т.к. init() упал, но watcher нужен для Теста 5
|
||||
controller._initScrollWatcher();
|
||||
}
|
||||
|
||||
// Тест 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(500);
|
||||
|
||||
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(500);
|
||||
|
||||
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(500);
|
||||
|
||||
// Проверяем что индикатор показан
|
||||
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 V2] Индикатор не найден (может создаваться при первом показе)');
|
||||
}
|
||||
|
||||
// Тест 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';
|
||||
const badge = indicator.querySelector('.badge');
|
||||
if (badge) badge.textContent = '5';
|
||||
}
|
||||
|
||||
await this.sleep(100);
|
||||
|
||||
const countBefore = controller._newMessagesCount;
|
||||
this.assertEqual(countBefore, 5, 'Счётчик установлен корректно');
|
||||
|
||||
// Прокручиваем вниз вручную (programmatic scroll)
|
||||
controller.scrollManager.scrollToBottom({ instant: false, force: true });
|
||||
|
||||
// Ждем скролл event + обработку
|
||||
await this.sleep(800);
|
||||
|
||||
// Проверяем что индикатор скрылся
|
||||
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(500);
|
||||
|
||||
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(500);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ==================== Публичное API ====================
|
||||
|
||||
async runAll() {
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('🚀 Запуск всех тестов ChatControllerV2');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const basicResults = await this.testBasicFunctionalityV2();
|
||||
const smartAutoscrollResults = await this.testSmartAutoscrollV2();
|
||||
|
||||
const totalPassed = basicResults.passed + smartAutoscrollResults.passed;
|
||||
const totalFailed = basicResults.failed + smartAutoscrollResults.failed;
|
||||
const totalTests = basicResults.total + smartAutoscrollResults.total;
|
||||
const totalRate = totalTests > 0 ? parseFloat(((totalPassed / totalTests) * 100).toFixed(1)) : 0;
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 ИТОГОВЫЕ РЕЗУЛЬТАТЫ V2:');
|
||||
console.log(` ✅ Успешных: ${totalPassed}`);
|
||||
console.log(` ❌ Провалено: ${totalFailed}`);
|
||||
console.log(` 📈 Процент: ${totalRate}%`);
|
||||
console.log('='.repeat(60) + '\n');
|
||||
|
||||
return {
|
||||
basic: basicResults,
|
||||
smartAutoscroll: smartAutoscrollResults,
|
||||
total: { passed: totalPassed, failed: totalFailed, total: totalTests, passRate: totalRate }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем глобальный экземпляр
|
||||
const chatTestsV2 = new ChatTestSuiteV2();
|
||||
|
||||
// Экспортируем для использования в модулях
|
||||
export default chatTestsV2;
|
||||
|
||||
// Добавляем в window для вызова из консоли
|
||||
window.ChatTestsV2 = {
|
||||
runAll: () => chatTestsV2.runAll(),
|
||||
testBasicFunctionalityV2: () => chatTestsV2.testBasicFunctionalityV2(),
|
||||
testSmartAutoscrollV2: () => chatTestsV2.testSmartAutoscrollV2()
|
||||
};
|
||||
|
||||
console.log('✅ ChatTestsV2 loaded!');
|
||||
console.log('💡 Запуск из консоли:');
|
||||
console.log(' window.ChatTestsV2.runAll() - Все тесты V2');
|
||||
console.log(' window.ChatTestsV2.testBasicFunctionalityV2() - Базовая функциональность');
|
||||
console.log(' window.ChatTestsV2.testSmartAutoscrollV2() - Умный автоскролл');
|
||||
console.log('');
|
||||
console.log('⚠️ ПРИМЕЧАНИЕ: Это сокращенная версия тестов для V2.');
|
||||
console.log(' Полная версия (11 тестов, 1732 строки) доступна в chatTests.js');
|
||||
console.log(' V2 тесты фокусируются на новых фичах: smart autoscroll, batch ops, retry logic');
|
||||
Loading…
x
Reference in New Issue
Block a user