Corp/backend/static/js/tests/chatStylesAudit.js
Игорь Надеин d5be62b34c refactor: восстановлены старые стили пузырей сообщений, аватаров и разделителей (pre-redesign)
- Восстановлен мягкий цвет пузырей: color-mix(92%) вместо жесткого var(--bs-primary)
- Аватары: border-radius 9999px + grid layout для идеальных кругов
- Разделители: правильный spacing, font-weight 700, pointer-events, color-mix прозрачность
- Добавлены оптимизации скролла: rootMargin 300px, limit 50 сообщений (Telegram-стиль)
- Добавлены новые модули: messageLoader, scrollManager, chatUtils
- Рефакторинг логики загрузки сообщений для предотвращения дубликатов
- Создана документация по архитектуре и тестированию
2026-01-12 17:21:41 +03:00

496 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Аудит CSS стилей на странице чата
* Проверяет какие стили используются, а какие нет
*
* Запуск из консоли браузера:
*
* 1. Загрузить скрипт:
* const script = document.createElement('script');
* script.src = '/static/js/tests/chatStylesAudit.js';
* script.type = 'module';
* document.head.appendChild(script);
*
* 2. Запустить аудит:
* window.ChatStylesAudit.runFullAudit()
* window.ChatStylesAudit.checkChatDetailStyles()
* window.ChatStylesAudit.checkMessageStyles()
* window.ChatStylesAudit.findUnusedStyles()
*/
class ChatStylesAudit {
constructor() {
this.results = {
total: 0,
used: 0,
unused: 0,
details: []
};
// CSS файлы для проверки
this.cssFiles = [
'/static/css/components/chat-detail.css',
'/static/css/message-reactions.css',
'/static/css/message-context-menu.css',
'/static/css/message-selection.css',
'/static/css/components/chat-polls.css'
];
}
/**
* Получает все CSS правила из stylesheet
*/
getCSSRules(stylesheet) {
try {
return Array.from(stylesheet.cssRules || stylesheet.rules || []);
} catch (e) {
console.warn('Не удалось получить правила из stylesheet:', e);
return [];
}
}
/**
* Извлекает селекторы из CSS правила
*/
extractSelectors(rule) {
const selectors = [];
if (rule.selectorText) {
// Разбиваем составные селекторы
const parts = rule.selectorText.split(',').map(s => s.trim());
selectors.push(...parts);
}
// Рекурсивно обрабатываем медиа-запросы
if (rule.cssRules) {
for (const subRule of this.getCSSRules(rule)) {
selectors.push(...this.extractSelectors(subRule));
}
}
return selectors;
}
/**
* Проверяет используется ли селектор в DOM
*/
isSelectorUsed(selector) {
try {
// Пропускаем псевдо-элементы и псевдо-классы для точной проверки
const cleanSelector = selector
.replace(/::(before|after|placeholder|selection)/g, '')
.replace(/:(hover|focus|active|visited|link|first-child|last-child|nth-child\([^)]+\)|not\([^)]+\))/g, '')
.trim();
if (!cleanSelector || cleanSelector === '') {
return { used: false, reason: 'empty_after_cleanup' };
}
const elements = document.querySelectorAll(cleanSelector);
return {
used: elements.length > 0,
count: elements.length,
selector: cleanSelector
};
} catch (e) {
return { used: false, reason: 'invalid_selector', error: e.message };
}
}
/**
* Сканирует все стили связанные с чатом
*/
async scanChatStyles() {
console.log('🔍 Сканирование CSS стилей чата...\n');
const allSelectors = new Map(); // selector -> { used, files, count }
// Проходим по всем загруженным stylesheets
for (const stylesheet of document.styleSheets) {
try {
const href = stylesheet.href || 'inline';
// Проверяем только файлы связанные с чатом
const isChatRelated = this.cssFiles.some(file => href.includes(file)) ||
href.includes('chat') ||
href.includes('message');
if (!isChatRelated && stylesheet.href) continue;
const rules = this.getCSSRules(stylesheet);
for (const rule of rules) {
const selectors = this.extractSelectors(rule);
for (const selector of selectors) {
if (!allSelectors.has(selector)) {
const usage = this.isSelectorUsed(selector);
allSelectors.set(selector, {
original: selector,
used: usage.used,
count: usage.count || 0,
files: [href],
reason: usage.reason
});
} else {
const existing = allSelectors.get(selector);
if (!existing.files.includes(href)) {
existing.files.push(href);
}
}
}
}
} catch (e) {
console.warn('Ошибка при обработке stylesheet:', e);
}
}
return allSelectors;
}
/**
* Проверяет конкретный CSS файл
*/
async checkSpecificFile(filePath) {
console.log(`📄 Проверка файла: ${filePath}\n`);
const results = {
file: filePath,
total: 0,
used: 0,
unused: 0,
selectors: []
};
for (const stylesheet of document.styleSheets) {
if (stylesheet.href && stylesheet.href.includes(filePath)) {
const rules = this.getCSSRules(stylesheet);
for (const rule of rules) {
const selectors = this.extractSelectors(rule);
for (const selector of selectors) {
const usage = this.isSelectorUsed(selector);
results.total++;
if (usage.used) {
results.used++;
} else {
results.unused++;
}
results.selectors.push({
selector,
used: usage.used,
count: usage.count || 0,
reason: usage.reason
});
}
}
}
}
return results;
}
/**
* Полный аудит всех стилей
*/
async runFullAudit() {
console.log('\n' + '='.repeat(80));
console.log('🎨 ПОЛНЫЙ АУДИТ CSS СТИЛЕЙ ЧАТА');
console.log('='.repeat(80) + '\n');
const allSelectors = await this.scanChatStyles();
const used = [];
const unused = [];
for (const [selector, data] of allSelectors) {
if (data.used) {
used.push({ selector, ...data });
} else {
unused.push({ selector, ...data });
}
}
// Сортируем по количеству использований
used.sort((a, b) => b.count - a.count);
unused.sort((a, b) => a.selector.localeCompare(b.selector));
console.log('📊 СТАТИСТИКА:');
console.log('─'.repeat(80));
console.log(`Всего селекторов: ${allSelectors.size}`);
console.log(`✅ Используется: ${used.length} (${(used.length / allSelectors.size * 100).toFixed(1)}%)`);
console.log(`❌ Не используется: ${unused.length} (${(unused.length / allSelectors.size * 100).toFixed(1)}%)`);
console.log('─'.repeat(80) + '\n');
// ТОП-10 самых используемых стилей
console.log('🏆 ТОП-10 САМЫХ ИСПОЛЬЗУЕМЫХ СТИЛЕЙ:');
console.log('─'.repeat(80));
used.slice(0, 10).forEach((item, i) => {
console.log(`${i + 1}. ${item.selector}`);
console.log(` 📍 Использований: ${item.count}`);
console.log(` 📁 Файлы: ${item.files.join(', ')}\n`);
});
// Неиспользуемые стили
if (unused.length > 0) {
console.log('\n❌ НЕИСПОЛЬЗУЕМЫЕ СТИЛИ:');
console.log('─'.repeat(80));
// Группируем по файлам
const byFile = {};
unused.forEach(item => {
item.files.forEach(file => {
if (!byFile[file]) byFile[file] = [];
byFile[file].push(item.selector);
});
});
for (const [file, selectors] of Object.entries(byFile)) {
const fileName = file.split('/').pop();
console.log(`\n📄 ${fileName} (${selectors.length} неиспользуемых):`);
selectors.slice(0, 20).forEach(sel => {
console.log(` • ${sel}`);
});
if (selectors.length > 20) {
console.log(` ... и ещё ${selectors.length - 20}`);
}
}
}
console.log('\n' + '='.repeat(80));
console.log('✅ АУДИТ ЗАВЕРШЁН');
console.log('='.repeat(80) + '\n');
// Сохраняем результаты
this.lastAudit = {
timestamp: new Date().toISOString(),
total: allSelectors.size,
used: used.length,
unused: unused.length,
usedSelectors: used,
unusedSelectors: unused
};
return this.lastAudit;
}
/**
* Проверяет стили chat-detail.css
*/
async checkChatDetailStyles() {
console.log('\n📋 АУДИТ: chat-detail.css\n');
const result = await this.checkSpecificFile('chat-detail.css');
this.printFileResults(result);
return result;
}
/**
* Проверяет стили сообщений
*/
async checkMessageStyles() {
console.log('\n📋 АУДИТ: Стили сообщений\n');
const files = [
'message-reactions.css',
'message-context-menu.css',
'message-selection.css'
];
const results = [];
for (const file of files) {
const result = await this.checkSpecificFile(file);
results.push(result);
}
// Общая статистика
const total = results.reduce((sum, r) => sum + r.total, 0);
const used = results.reduce((sum, r) => sum + r.used, 0);
const unused = results.reduce((sum, r) => sum + r.unused, 0);
console.log('\n' + '='.repeat(60));
console.log('📊 ОБЩАЯ СТАТИСТИКА ПО СТИЛЯМ СООБЩЕНИЙ:');
console.log('='.repeat(60));
console.log(`Всего: ${total}`);
console.log(`✅ Используется: ${used} (${(used / total * 100).toFixed(1)}%)`);
console.log(`❌ Не используется: ${unused} (${(unused / total * 100).toFixed(1)}%)`);
console.log('='.repeat(60) + '\n');
return results;
}
/**
* Находит дубликаты стилей
*/
findDuplicateStyles() {
console.log('\n🔎 ПОИСК ДУБЛИРУЮЩИХСЯ СТИЛЕЙ...\n');
const styleMap = new Map(); // CSS text -> selectors[]
for (const stylesheet of document.styleSheets) {
try {
const rules = this.getCSSRules(stylesheet);
for (const rule of rules) {
if (rule.style && rule.selectorText) {
const cssText = rule.style.cssText;
if (!styleMap.has(cssText)) {
styleMap.set(cssText, []);
}
styleMap.get(cssText).push(rule.selectorText);
}
}
} catch (e) {
// Skip
}
}
const duplicates = [];
for (const [cssText, selectors] of styleMap) {
if (selectors.length > 1) {
duplicates.push({ cssText, selectors, count: selectors.length });
}
}
duplicates.sort((a, b) => b.count - a.count);
console.log(`Найдено ${duplicates.length} групп дублирующихся стилей:\n`);
duplicates.slice(0, 10).forEach((dup, i) => {
console.log(`${i + 1}. ${dup.count} дубликатов:`);
console.log(` Стили: ${dup.cssText.substring(0, 100)}${dup.cssText.length > 100 ? '...' : ''}`);
console.log(` Селекторы:`);
dup.selectors.forEach(sel => console.log(` • ${sel}`));
console.log('');
});
return duplicates;
}
/**
* Выводит результаты для файла
*/
printFileResults(result) {
console.log('─'.repeat(60));
console.log(`📄 Файл: ${result.file}`);
console.log('─'.repeat(60));
console.log(`Всего селекторов: ${result.total}`);
console.log(`✅ Используется: ${result.used} (${(result.used / result.total * 100).toFixed(1)}%)`);
console.log(`❌ Не используется: ${result.unused} (${(result.unused / result.total * 100).toFixed(1)}%)`);
console.log('─'.repeat(60));
if (result.unused > 0) {
const unusedList = result.selectors.filter(s => !s.used);
console.log(`\n❌ Неиспользуемые селекторы (${unusedList.length}):`);
unusedList.slice(0, 20).forEach(s => {
console.log(` • ${s.selector}`);
});
if (unusedList.length > 20) {
console.log(` ... и ещё ${unusedList.length - 20}`);
}
}
console.log('\n');
}
/**
* Анализирует специфичность селекторов
*/
analyzeSpecificity() {
console.log('\n⚖️ АНАЛИЗ СПЕЦИФИЧНОСТИ СЕЛЕКТОРОВ...\n');
const specificityMap = new Map();
for (const stylesheet of document.styleSheets) {
try {
const rules = this.getCSSRules(stylesheet);
for (const rule of rules) {
if (rule.selectorText) {
const selectors = rule.selectorText.split(',').map(s => s.trim());
for (const selector of selectors) {
const spec = this.calculateSpecificity(selector);
const key = spec.join('-');
if (!specificityMap.has(key)) {
specificityMap.set(key, []);
}
specificityMap.get(key).push(selector);
}
}
}
} catch (e) {
// Skip
}
}
const entries = Array.from(specificityMap.entries())
.map(([key, selectors]) => ({
specificity: key,
count: selectors.length,
examples: selectors.slice(0, 3)
}))
.sort((a, b) => b.count - a.count);
console.log('📊 Распределение по специфичности:\n');
entries.slice(0, 15).forEach((entry, i) => {
console.log(`${i + 1}. [${entry.specificity}] - ${entry.count} селекторов`);
entry.examples.forEach(ex => console.log(` ${ex}`));
console.log('');
});
return entries;
}
/**
* Вычисляет специфичность селектора
*/
calculateSpecificity(selector) {
let ids = 0;
let classes = 0;
let elements = 0;
// Подсчет ID
ids = (selector.match(/#/g) || []).length;
// Подсчет классов, атрибутов, псевдоклассов
classes = (selector.match(/\./g) || []).length;
classes += (selector.match(/\[/g) || []).length;
classes += (selector.match(/:/g) || []).length;
// Подсчет элементов и псевдоэлементов
elements = (selector.match(/\b[a-z]+\b/g) || []).length;
elements += (selector.match(/::/g) || []).length;
return [ids, classes, elements];
}
/**
* Экспортирует результаты в JSON
*/
exportResults() {
if (!this.lastAudit) {
console.error('❌ Сначала запустите runFullAudit()');
return;
}
const json = JSON.stringify(this.lastAudit, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chat-styles-audit-${Date.now()}.json`;
a.click();
console.log('✅ Результаты экспортированы в JSON');
}
}
// Экспортируем в глобальную область
window.ChatStylesAudit = new ChatStylesAudit();
export default ChatStylesAudit;