Files
magistr/frontend/tests/ui-consistency.test.mjs
dipatrik10 97bf3cc50a feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента
полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md).

- 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой
- Удалены legacy-views и переходный слой LegacyTabView (45 файлов)
- Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки
- CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf)
- Фиксы: lazy-импорт именованных экспортов (краш после авторизации),
  растяжение #admin-root/#settings-root на всю ширину,
  возврат subjects/lessonTypes из справочников (краш просмотра расписаний),
  отказ от заведомо широкого поиска расписания без выбранной цели,
  центрирование карточки логина
- Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину
- Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
2026-09-28 23:15:04 +03:00

117 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const readFrontendFile = relativePath => readFile(
new URL(`../${relativePath}`, import.meta.url),
'utf8'
);
test('все пользовательские оболочки завершают каскад единой визуальной основой', async () => {
const entrypoints = [
['index.html', 'ui-foundation.css'],
['admin/index.html', '../ui-foundation.css'],
['admin/settings/index.html', '../../ui-foundation.css'],
['teacher/index.html', '../ui-foundation.css'],
['student/index.html', '../ui-foundation.css']
];
for (const [file, expectedHref] of entrypoints) {
const html = await readFrontendFile(file);
const stylesheets = Array.from(
html.matchAll(/<link\b[^>]*rel="stylesheet"[^>]*href="([^"]+)"[^>]*>/g),
match => match[1]
);
assert.equal(stylesheets.at(-1), expectedHref, file);
}
});
test('единая визуальная основа фиксирует типографику, размеры и доступный фокус', async () => {
const foundation = await readFrontendFile('ui-foundation.css');
assert.match(foundation, /--ui-font-sans:/);
assert.match(foundation, /--ui-control-sm:\s*32px/);
assert.match(foundation, /--ui-control-md:\s*40px/);
assert.match(foundation, /--ui-control-lg:\s*48px/);
assert.match(foundation, /\.btn-sm\s*\{[^}]*min-height:\s*var\(--ui-control-sm\)/s);
assert.match(foundation, /\.btn-md\s*\{[^}]*min-height:\s*var\(--ui-control-md\)/s);
assert.match(foundation, /\.btn-lg\s*\{[^}]*min-height:\s*var\(--ui-control-lg\)/s);
assert.match(foundation, /:focus-visible\s*\{[^}]*outline:/s);
assert.match(foundation, /prefers-reduced-motion:\s*reduce/);
});
test('локальные стили используют общие шрифтовые токены', async () => {
const files = [
'style.css',
'student/style.css',
'teacher/style.css',
'admin/css/main.css',
'admin/settings/css/main.css',
'admin/css/schedule-quality.css',
'admin/css/schedule-versions.css'
];
for (const file of files) {
const css = await readFrontendFile(file);
assert.doesNotMatch(css, /font-family:\s*(?:['"]?Inter|['"]?Segoe UI|Georgia|['"]Times New Roman)/, file);
}
});
test('корневые контейнеры админ-оболочек растягиваются на всю ширину окна', async () => {
// body в admin/css/main.css и admin/settings/css/main.css — flex-контейнер:
// без растяжения корневой div сжимается до max-content контента, и узкие
// страницы (карточки, формы) не занимают всю ширину монитора, а дашборд
// «растягивается» только после загрузки данных. flex-basis 0 обязателен:
// intrinsic-замер max-content раздувает вложенные flex-фильтры
// (панель загруженности аудиторий) до ~1 млн px горизонтального скролла.
const cases = [
['admin/css/layout.css', '#admin-root'],
['admin/settings/css/layout.css', '#settings-root']
];
for (const [file, selector] of cases) {
const css = await readFrontendFile(file);
const escaped = selector.replace('#', '\\#');
assert.match(
css,
new RegExp(`${escaped}\\s*\\{[^}]*flex:\\s*1 1 0%\\s*;[^}]*min-width:\\s*0`, 's'),
`${file}: ${selector} должен растягиваться ровно на ширину окна (flex: 1 1 0%; min-width: 0)`
);
}
});
test('архивные действия и кнопки закрытия используют системные размеры', async () => {
const actionSources = await Promise.all([
readFrontendFile('react/admin/tabs/ClassroomsTab.jsx'),
readFrontendFile('react/admin/tabs/GroupsTab.jsx'),
readFrontendFile('react/admin/tabs/UsersTab.jsx')
]);
actionSources.forEach(source => {
assert.match(source, /class(?:Name)?="btn btn-sm btn-secondary btn-restore-/);
});
const modalViews = await Promise.all([
'react/admin/tabs/AcademicCalendarTab.jsx',
'react/admin/tabs/ClassroomsTab.jsx',
'react/admin/tabs/GroupsTab.jsx',
'react/admin/tabs/ScheduleTab.jsx',
'react/admin/tabs/ScheduleVersionsTab.jsx',
'react/admin/tabs/SubjectsTab.jsx',
'react/admin/tabs/UniversityStructureTab.jsx'
].map(readFrontendFile));
const closeButtons = modalViews.flatMap(html => Array.from(
html.matchAll(/<button\b[^>]*class(?:Name)?="[^"]*(?:modal-close|version-modal-close|settings-modal-close)[^"]*"[^>]*>/g),
match => match[0]
));
assert.ok(closeButtons.length > 0);
closeButtons.forEach(button => {
assert.match(button, /type="button"/);
assert.match(button, /class(?:Name)?="[^"]*\bbtn\b[^"]*\bbtn-icon-md\b/);
assert.match(button, /aria-label="Закрыть окно"/);
});
});