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
This commit is contained in:
dipatrik10
2026-09-28 23:15:04 +03:00
parent c6083b35e0
commit 97bf3cc50a
167 changed files with 31283 additions and 19033 deletions

View File

@@ -58,37 +58,59 @@ test('локальные стили используют общие шрифто
}
});
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('admin/js/views/classrooms.js'),
readFrontendFile('admin/js/views/groups.js'),
readFrontendFile('admin/js/views/users.js')
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="btn btn-sm btn-secondary btn-restore-/);
assert.match(source, /class(?:Name)?="btn btn-sm btn-secondary btn-restore-/);
});
const modalViews = await Promise.all([
'admin/views/academic-calendar.html',
'admin/views/classrooms.html',
'admin/views/groups.html',
'admin/views/schedule.html',
'admin/views/schedule-versions.html',
'admin/views/subjects.html',
'admin/views/university-structure.html',
'admin/settings/views/time-slots.html'
'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="[^"]*(?:modal-close|version-modal-close|settings-modal-close)[^"]*"[^>]*>/g),
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="[^"]*\bbtn\b[^"]*\bbtn-icon-md\b/);
assert.match(button, /class(?:Name)?="[^"]*\bbtn\b[^"]*\bbtn-icon-md\b/);
assert.match(button, /aria-label="Закрыть окно"/);
});
});