Files
magistr/frontend/tests/admin-ui-regressions.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

172 lines
9.4 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';
import {
scheduleSlotActionLabel,
shouldMarkRuleFormDirty
} from '../react/admin/schedule-service.js';
const readFrontendFile = relativePath => readFile(
new URL(`../${relativePath}`, import.meta.url),
'utf8'
);
test('одиночный слот очищается, а несколько слотов удаляются', () => {
assert.equal(scheduleSlotActionLabel(1), 'Очистить');
assert.equal(scheduleSlotActionLabel(2), 'Удалить');
assert.equal(scheduleSlotActionLabel(5), 'Удалить');
});
test('конструктор считает изменениями только действия пользователя', () => {
assert.equal(shouldMarkRuleFormDirty({ programmatic: true, type: 'change' }), false);
assert.equal(shouldMarkRuleFormDirty({ type: 'input', fromAsyncCombobox: true }), false);
assert.equal(shouldMarkRuleFormDirty({ type: 'change' }), true);
assert.equal(shouldMarkRuleFormDirty({ type: 'input' }), true);
});
test('поисковые списки просмотра расписаний загружаются свёрнутыми', async () => {
const [scheduleViewTab, combobox] = await Promise.all([
readFrontendFile('react/admin/tabs/ScheduleViewTab.jsx'),
readFrontendFile('react/shared/ui/AsyncCombobox.jsx')
]);
assert.match(scheduleViewTab, /<AsyncCombobox/);
assert.doesNotMatch(scheduleViewTab, /search\(''\)/);
assert.match(combobox, /const \[open, setOpen\] = useState\(false\)/);
assert.match(combobox, /onFocus=\{handleFocus\}/);
});
test('в общих заголовках админ-панели и настроек нет бургер-меню', async () => {
const files = await Promise.all([
readFrontendFile('admin/index.html'),
readFrontendFile('react/admin/AdminApp.jsx'),
readFrontendFile('admin/settings/index.html'),
readFrontendFile('react/settings/SettingsApp.jsx')
]);
files.forEach(content => assert.doesNotMatch(content, /menu-toggle/));
});
test('заголовок вкладки соответствует конструктору правил', async () => {
const adminService = await readFrontendFile('react/admin/admin-service.js');
assert.match(adminService, /schedule:\s*\{\s*title:\s*'Конструктор правил'/);
});
test('основная кнопка показа находится в заголовке блока параметров', async () => {
const scheduleViewTab = await readFrontendFile('react/admin/tabs/ScheduleViewTab.jsx');
const controlHeaderIndex = scheduleViewTab.indexOf('schedule-view-control-header');
const controlGridIndex = scheduleViewTab.indexOf('schedule-view-control-grid');
const loadButtonIndex = scheduleViewTab.indexOf('id="schedule-view-load"');
assert.ok(controlHeaderIndex >= 0);
assert.ok(loadButtonIndex > controlHeaderIndex);
assert.ok(loadButtonIndex < controlGridIndex);
assert.equal(scheduleViewTab.match(/id="schedule-view-load"/g)?.length, 1);
});
test('пустое состояние расписания содержит собственную кнопку обновления', async () => {
const [scheduleViewService, scheduleViewTab] = await Promise.all([
readFrontendFile('react/admin/schedule-view-service.js'),
readFrontendFile('react/admin/tabs/ScheduleViewTab.jsx')
]);
assert.match(scheduleViewService, /SCHEDULE_REFRESH_PROMPT\s*=\s*'Нажмите «Показать», чтобы обновить расписание'/);
assert.match(scheduleViewTab, /data-schedule-view-reload/);
assert.match(scheduleViewTab, /data-schedule-view-reload=""[\s\S]*?void loadData\(\)/);
assert.match(scheduleViewTab, /actionable: message === SCHEDULE_REFRESH_PROMPT/);
assert.match(scheduleViewTab, /renderStateRow\(tableState\.message,\s*tableState\.actionable\)/);
});
test('действия слотов имеют размер соседнего поля', async () => {
const [components, scheduleTab] = await Promise.all([
readFrontendFile('admin/css/components.css'),
readFrontendFile('react/admin/tabs/ScheduleTab.jsx')
]);
assert.match(components, /\.schedule-slot-remove\s*\{[^}]*min-width:\s*125px[^}]*min-height:\s*44px/s);
assert.match(scheduleTab, /className=\{`btn btn-md \$\{actionClass\} schedule-slot-remove`\}/);
});
test('все варианты системных кнопок имеют видимую обводку', async () => {
const components = await readFrontendFile('admin/css/components.css');
assert.match(components, /\.btn\s*\{[^}]*border:\s*1px solid var\(--button-secondary-border\)/s);
assert.match(components, /\.btn-primary\s*\{[^}]*border-color:\s*var\(--button-primary-border\)/s);
assert.match(components, /\.btn-ghost\s*\{[^}]*border-color:\s*var\(--button-ghost-border\)/s);
assert.match(components, /\.btn-danger\s*\{[^}]*border-color:\s*var\(--button-danger-border\)/s);
});
test('подсказка календарной сетки получает позицию напрямую от курсора', async () => {
const calendarTab = await readFrontendFile('react/admin/tabs/AcademicCalendarTab.jsx');
assert.match(calendarTab, /translate3d\(\$\{left\}px, \$\{top\}px, 0\) scale\(1\)/);
assert.match(calendarTab, /\{ duration: 1, fill: 'forwards' \}/);
assert.doesNotMatch(calendarTab, /\.style\.(?:left|top)\s*=/);
});
test('светлая тема задаёт контрастные пункты и заголовки левого меню', async () => {
const [layout, workspace, adminService] = await Promise.all([
readFrontendFile('admin/css/layout.css'),
readFrontendFile('react/admin/AdminApp.jsx'),
readFrontendFile('react/admin/admin-service.js')
]);
assert.match(layout, /\[data-theme="light"\] \.sidebar \.nav-item\s*\{[^}]*color:\s*#334155/s);
assert.match(layout, /\[data-theme="light"\] \.sidebar-section-title\s*\{[^}]*color:\s*#64748b !important/s);
assert.match(workspace, /sidebar-section-title/);
assert.match(adminService, /title: 'Расписание'/);
});
test('таблица календарных графиков не содержит отдельный столбец названия', async () => {
const calendarTab = await readFrontendFile('react/admin/tabs/AcademicCalendarTab.jsx');
const bodyIndex = calendarTab.indexOf('id="academic-calendars-tbody"');
const tableStart = calendarTab.lastIndexOf('<table>', bodyIndex);
const tableEnd = calendarTab.indexOf('</table>', bodyIndex);
const calendarTable = calendarTab.slice(tableStart, tableEnd);
assert.doesNotMatch(calendarTable, /<th>Название<\/th>/);
assert.match(calendarTable, /colSpan=\{6\}/);
assert.doesNotMatch(calendarTab, /title:\s*buildCalendarTitleFromForm/);
});
test('конструктор правил показывает готовую компоновку без скачков колонок', async () => {
const [scheduleTab, components] = await Promise.all([
readFrontendFile('react/admin/tabs/ScheduleTab.jsx'),
readFrontendFile('admin/css/components.css')
]);
assert.match(scheduleTab, /id="schedule-initial-state"/);
assert.match(scheduleTab, /schedule-initial-content\$\{initialPhase === 'loading' \? ' is-loading' : ''\}/);
assert.match(scheduleTab, /<table id="schedule-table">\s*<colgroup>/s);
assert.match(scheduleTab, /finally\s*\{[\s\S]*?setInitialPhase\('ready'\)/);
assert.match(components, /#schedule-table\s*\{[^}]*table-layout:\s*fixed/s);
assert.match(components, /#schedule-table tbody tr\s*\{[^}]*animation:\s*none/s);
});
test('форма учебного года расположена после рабочих блоков календарного графика', async () => {
const calendarTab = await readFrontendFile('react/admin/tabs/AcademicCalendarTab.jsx');
const calendarsRegistryIndex = calendarTab.indexOf('id="academic-calendars-tbody"');
const academicYearBuilderIndex = calendarTab.indexOf('academic-year-builder-card');
const managePanelEndIndex = calendarTab.indexOf('</section>', academicYearBuilderIndex);
assert.ok(calendarsRegistryIndex >= 0);
assert.ok(academicYearBuilderIndex > calendarsRegistryIndex);
assert.ok(managePanelEndIndex > academicYearBuilderIndex);
});
test('поисковые и диалоговые списки имеют размытие и светлую тему', async () => {
const [components, absences, absenceTab] = await Promise.all([
readFrontendFile('admin/css/components.css'),
readFrontendFile('admin/css/teacher-absences.css'),
readFrontendFile('react/admin/tabs/TeacherAbsencesTab.jsx')
]);
assert.match(absenceTab, /AsyncCombobox/);
assert.match(absenceTab, /placeholder="Найдите преподавателя по ФИО"/);
assert.match(components, /\.async-combobox-menu\s*\{[^}]*backdrop-filter:\s*blur\(20px\)/s);
assert.match(components, /\[data-theme="light"\] \.async-combobox-menu\s*\{[^}]*background:\s*rgba\(255, 255, 255, 0\.96\)/s);
assert.match(absences, /\.teacher-dialogue-toolbar \.custom-select-menu\s*\{[^}]*backdrop-filter:\s*blur\(20px\)/s);
assert.match(absences, /\[data-theme="light"\] \.teacher-dialogue-toolbar \.custom-select-menu/);
});