Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
192 lines
8.4 KiB
JavaScript
192 lines
8.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
|
||
import {
|
||
addDays,
|
||
buildLessonChips,
|
||
buildWeekStatusMessage,
|
||
fetchGroupOptions,
|
||
fetchSchedule,
|
||
formatWeekLabel,
|
||
groupLessonsByDate,
|
||
groupOptionDescription,
|
||
parseIsoDate,
|
||
resolveSelectedGroup,
|
||
startOfWeek,
|
||
toIsoDate,
|
||
weekDates,
|
||
weekEndIso
|
||
} from '../react/student/student-service.js';
|
||
import {
|
||
renderAsyncCombobox,
|
||
renderScheduleOverview,
|
||
renderWeekGrid,
|
||
weekStartOf
|
||
} from '../dist/tests/react-student-renderer.mjs';
|
||
|
||
test('неделя студента начинается с понедельника, включая воскресенье', () => {
|
||
assert.equal(toIsoDate(startOfWeek(parseIsoDate('2026-09-30'))), '2026-09-28');
|
||
assert.equal(toIsoDate(startOfWeek(parseIsoDate('2026-10-04'))), '2026-09-28');
|
||
assert.equal(toIsoDate(startOfWeek(parseIsoDate('2026-10-10'))), '2026-10-05');
|
||
});
|
||
|
||
test('недельная сетка содержит семь дней и подпись границ недели', () => {
|
||
const weekStart = startOfWeek(parseIsoDate('2026-09-28'));
|
||
const days = weekDates(weekStart);
|
||
|
||
assert.equal(days.length, 7);
|
||
assert.equal(toIsoDate(days[0]), '2026-09-28');
|
||
assert.equal(toIsoDate(days[6]), '2026-10-04');
|
||
assert.equal(weekEndIso(weekStart), '2026-10-04');
|
||
assert.equal(formatWeekLabel(weekStart), '28.09.2026 - 04.10.2026');
|
||
});
|
||
|
||
test('занятия группируются по датам без потери пустых дней', () => {
|
||
const byDate = groupLessonsByDate([
|
||
{ date: '2026-09-28', subjectName: 'Лекции' },
|
||
{ date: '2026-09-28', subjectName: 'Практика' },
|
||
{ date: '2026-09-30', subjectName: 'Лаборатория' }
|
||
]);
|
||
|
||
assert.equal(byDate.get('2026-09-28').length, 2);
|
||
assert.equal(byDate.get('2026-09-30').length, 1);
|
||
assert.equal(byDate.get('2026-10-01'), undefined);
|
||
});
|
||
|
||
test('чипы занятия содержат тип, подгруппы, формат, преподавателя, аудиторию и группы', () => {
|
||
const chips = buildLessonChips({
|
||
classroomName: '301',
|
||
groupNames: ['ИС-41', 'ИС-42'],
|
||
lessonFormat: 'Очно',
|
||
lessonTypeName: 'Лекция',
|
||
subgroupNames: ['1 подгруппа'],
|
||
teacherName: 'Иванов И. И.'
|
||
});
|
||
|
||
assert.deepEqual(chips, ['Лекция', '1 подгруппа', 'Очно', 'Иванов И. И.', '301', 'ИС-41, ИС-42']);
|
||
});
|
||
|
||
test('подгруппа-строка используется, когда массив подгрупп пуст', () => {
|
||
const chips = buildLessonChips({
|
||
lessonTypeName: 'Практика',
|
||
subgroupNames: [],
|
||
subgroupName: '2 подгруппа',
|
||
teacherName: 'Петрова П. П.'
|
||
});
|
||
|
||
assert.deepEqual(chips, ['Практика', '2 подгруппа', 'Петрова П. П.']);
|
||
});
|
||
|
||
test('статус недели различает найденные занятия и пустую неделю', () => {
|
||
assert.equal(buildWeekStatusMessage([{ date: '2026-09-28' }]), 'Найдено занятий: 1');
|
||
assert.equal(buildWeekStatusMessage([]), 'На этой неделе занятий нет');
|
||
});
|
||
|
||
test('выбор группы сохраняет сохранённый ID и падает обратно на первую группу', () => {
|
||
const groups = [{ id: 1, name: 'ИС-41' }, { id: 7, name: 'ИС-42' }];
|
||
|
||
assert.equal(resolveSelectedGroup(groups, '7').name, 'ИС-42');
|
||
assert.equal(resolveSelectedGroup(groups, '999').name, 'ИС-41');
|
||
assert.equal(resolveSelectedGroup([], '7'), null);
|
||
});
|
||
|
||
test('описание группы собирает код, профиль и курс через точку', () => {
|
||
assert.equal(
|
||
groupOptionDescription({ course: 3, specialtyCode: '09.03.03', specialtyProfileName: 'Информационные системы' }),
|
||
'09.03.03 · Информационные системы · 3 курс'
|
||
);
|
||
assert.equal(groupOptionDescription({ specialtyCode: '09.03.03' }), '09.03.03');
|
||
});
|
||
|
||
test('сервис студента сохраняет прежние контракты API', async () => {
|
||
const calls = [];
|
||
const client = {
|
||
get: async (endpoint, options) => {
|
||
calls.push({ endpoint, options });
|
||
if (endpoint.startsWith('/api/groups/options')) return [{ id: 5, name: 'ИС-41' }];
|
||
return [{ date: '2026-09-28', subjectName: 'Лекция' }];
|
||
}
|
||
};
|
||
|
||
const groups = await fetchGroupOptions({ query: 'ИС', selectedId: '5' }, client);
|
||
const lessons = await fetchSchedule({ groupId: 5, startDate: '2026-09-28', endDate: '2026-10-04' }, client);
|
||
|
||
assert.deepEqual(groups, [{ id: 5, name: 'ИС-41' }]);
|
||
assert.deepEqual(lessons, [{ date: '2026-09-28', subjectName: 'Лекция' }]);
|
||
assert.match(calls[0].endpoint, /^\/api\/groups\/options\?query=%D0%98%D0%A1&limit=20&selectedId=5$/);
|
||
assert.equal(calls[0].options.errorMessage, 'Не удалось загрузить группы');
|
||
assert.equal(calls[1].endpoint, '/api/schedule?groupId=5&startDate=2026-09-28&endDate=2026-10-04');
|
||
assert.equal(calls[1].options.errorMessage, 'Не удалось загрузить расписание');
|
||
});
|
||
|
||
test('сетка недели рендерит дни, занятия и пустые дни с русскими текстами', () => {
|
||
const markup = renderWeekGrid({
|
||
lessons: [
|
||
{
|
||
classroomName: '301',
|
||
date: '2026-09-28',
|
||
endTime: '10:10',
|
||
lessonFormat: 'Очно',
|
||
lessonTypeName: 'Лекция',
|
||
startTime: '08:30',
|
||
subjectName: 'Базы данных',
|
||
teacherName: 'Иванов И. И.'
|
||
}
|
||
],
|
||
weekStart: weekStartOf('2026-09-30')
|
||
});
|
||
|
||
assert.match(markup, /Понедельник/);
|
||
assert.match(markup, /Базы данных/);
|
||
assert.match(markup, /08:30 - 10:10/);
|
||
assert.match(markup, /class="chip"/);
|
||
assert.match(markup, /Занятий нет/);
|
||
assert.match(markup, /aria-live="polite"/);
|
||
});
|
||
|
||
test('обзор ближайших занятий выделяет сегодня и следующую пару', () => {
|
||
const now = new Date('2026-09-28T09:00:00');
|
||
const markup = renderScheduleOverview({
|
||
lessons: [
|
||
{ date: '2026-09-28', startTime: '08:30', endTime: '10:10', subjectName: 'Базы данных', teacherName: 'Иванов И. И.', classroomName: '301' },
|
||
{ date: '2026-09-30', startTime: '12:00', endTime: '13:30', subjectName: 'Тестирование', teacherName: 'Петрова П. П.', classroomName: '205' }
|
||
],
|
||
now,
|
||
onGoToday: () => {}
|
||
});
|
||
|
||
assert.match(markup, /Быстрый обзор/);
|
||
assert.match(markup, /1 занятие/);
|
||
assert.match(markup, /Следующая пара/);
|
||
assert.match(markup, /Идёт сейчас/);
|
||
assert.match(markup, /Открыть текущую неделю/);
|
||
});
|
||
|
||
test('обзор без ближайших занятий показывает пустые состояния', () => {
|
||
const markup = renderScheduleOverview({ lessons: [], now: new Date('2026-09-28T09:00:00') });
|
||
|
||
assert.match(markup, /Свободный день/);
|
||
assert.match(markup, /Пока ничего не запланировано/);
|
||
assert.match(markup, /В ближайшие 14 дней занятий нет\./);
|
||
});
|
||
|
||
test('поисковый список групп рендерится с доступной семантикой combobox', () => {
|
||
const markup = renderAsyncCombobox({
|
||
allowClear: false,
|
||
ariaLabel: 'Группа',
|
||
placeholder: 'Найдите группу по названию или специальности',
|
||
selectedLabel: 'ИС-41',
|
||
selectedValue: '5',
|
||
load: () => Promise.resolve([]),
|
||
onSelect: () => {}
|
||
});
|
||
|
||
assert.match(markup, /role="combobox"/);
|
||
assert.match(markup, /aria-expanded="false"/);
|
||
assert.match(markup, /aria-autocomplete="list"/);
|
||
assert.match(markup, /aria-label="Группа"/);
|
||
assert.match(markup, /value="ИС-41"/);
|
||
assert.doesNotMatch(markup, /async-combobox-clear/);
|
||
assert.doesNotMatch(markup, /Очистить выбор/);
|
||
});
|