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