Files
magistr/frontend/tests/react-student.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

192 lines
8.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 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, /Очистить выбор/);
});