Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
117 lines
5.2 KiB
JavaScript
117 lines
5.2 KiB
JavaScript
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="Закрыть окно"/);
|
||
});
|
||
});
|