Много разных изменений
This commit is contained in:
@@ -2,7 +2,10 @@ import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
import { scheduleSlotActionLabel } from '../admin/js/views/schedule.js';
|
||||
import {
|
||||
scheduleSlotActionLabel,
|
||||
shouldMarkRuleFormDirty
|
||||
} from '../admin/js/views/schedule.js';
|
||||
|
||||
const readFrontendFile = relativePath => readFile(
|
||||
new URL(`../${relativePath}`, import.meta.url),
|
||||
@@ -15,6 +18,20 @@ test('одиночный слот очищается, а несколько сл
|
||||
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 scheduleView = await readFrontendFile('admin/js/views/schedule-view.js');
|
||||
|
||||
assert.match(scheduleView, /combo\.preload\(''\)/);
|
||||
assert.doesNotMatch(scheduleView, /combo\.search\(''\)/);
|
||||
});
|
||||
|
||||
test('в общих заголовках админ-панели и настроек нет бургер-меню', async () => {
|
||||
const files = await Promise.all([
|
||||
readFrontendFile('admin/index.html'),
|
||||
@@ -104,3 +121,43 @@ test('таблица календарных графиков не содержи
|
||||
assert.match(calendarTable, /colspan="6"/);
|
||||
assert.doesNotMatch(calendarScript, /title:\s*buildCalendarTitleFromForm/);
|
||||
});
|
||||
|
||||
test('конструктор правил показывает готовую компоновку без скачков колонок', async () => {
|
||||
const [scheduleView, scheduleScript, components] = await Promise.all([
|
||||
readFrontendFile('admin/views/schedule.html'),
|
||||
readFrontendFile('admin/js/views/schedule.js'),
|
||||
readFrontendFile('admin/css/components.css')
|
||||
]);
|
||||
|
||||
assert.match(scheduleView, /id="schedule-initial-state"/);
|
||||
assert.match(scheduleView, /id="schedule-initial-content"[^>]*class="schedule-initial-content is-loading"|class="schedule-initial-content is-loading"[^>]*id="schedule-initial-content"/);
|
||||
assert.match(scheduleView, /<table id="schedule-table">\s*<colgroup>/s);
|
||||
assert.match(scheduleScript, /finally\s*\{[\s\S]*initialContent\?\.classList\.remove\('is-loading'\)/);
|
||||
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 calendarView = await readFrontendFile('admin/views/academic-calendar.html');
|
||||
const calendarsRegistryIndex = calendarView.indexOf('id="academic-calendars-tbody"');
|
||||
const academicYearBuilderIndex = calendarView.indexOf('academic-year-builder-card');
|
||||
const managePanelEndIndex = calendarView.indexOf('</section>', academicYearBuilderIndex);
|
||||
|
||||
assert.ok(calendarsRegistryIndex >= 0);
|
||||
assert.ok(academicYearBuilderIndex > calendarsRegistryIndex);
|
||||
assert.ok(managePanelEndIndex > academicYearBuilderIndex);
|
||||
});
|
||||
|
||||
test('поисковые и диалоговые списки имеют размытие и светлую тему', async () => {
|
||||
const [components, absences, absenceView] = await Promise.all([
|
||||
readFrontendFile('admin/css/components.css'),
|
||||
readFrontendFile('admin/css/teacher-absences.css'),
|
||||
readFrontendFile('admin/views/teacher-absences.html')
|
||||
]);
|
||||
|
||||
assert.match(absenceView, /id="teacher-absence-teacher"[^>]*data-native-select="true"/);
|
||||
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/);
|
||||
});
|
||||
|
||||
29
frontend/tests/schedule-overview.test.mjs
Normal file
29
frontend/tests/schedule-overview.test.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { buildScheduleOverviewModel } from '../schedule-overview.js';
|
||||
|
||||
test('обзор расписания выделяет занятия сегодня и ближайшую пару', () => {
|
||||
const now = new Date(2026, 7, 19, 10, 15, 0);
|
||||
const lessons = [
|
||||
{ date: '2026-08-20', startTime: '09:00:00', endTime: '10:30:00', subjectName: 'Завтра' },
|
||||
{ date: '2026-08-19', startTime: '08:30:00', endTime: '10:00:00', subjectName: 'Завершилась' },
|
||||
{ date: '2026-08-19', startTime: '11:00:00', endTime: '12:30:00', subjectName: 'Ближайшая' }
|
||||
];
|
||||
|
||||
const model = buildScheduleOverviewModel(lessons, now);
|
||||
|
||||
assert.equal(model.todayIso, '2026-08-19');
|
||||
assert.deepEqual(model.todayLessons.map(item => item.subjectName), ['Завершилась', 'Ближайшая']);
|
||||
assert.equal(model.nextLesson.subjectName, 'Ближайшая');
|
||||
});
|
||||
|
||||
test('текущее занятие считается ближайшим до времени окончания', () => {
|
||||
const now = new Date(2026, 7, 19, 10, 15, 0);
|
||||
const model = buildScheduleOverviewModel([
|
||||
{ date: '2026-08-19', startTime: '10:00', endTime: '11:30', subjectName: 'Идёт сейчас' },
|
||||
{ date: '2026-08-19', startTime: '12:00', endTime: '13:30', subjectName: 'Следующее' }
|
||||
], now);
|
||||
|
||||
assert.equal(model.nextLesson.subjectName, 'Идёт сейчас');
|
||||
});
|
||||
@@ -32,14 +32,17 @@ test('конструктор по умолчанию открывает опуб
|
||||
assert.equal(chooseScheduleVersion(versions, 10, 4).id, 2);
|
||||
});
|
||||
|
||||
test('контур версий подключён к SPA, а конструктор работает с выбранной версией и создаёт черновик из неё', async () => {
|
||||
const [main, roles, constructor, quality, index, view] = await Promise.all([
|
||||
test('контур версий подключён к SPA, а профильные вкладки имеют локальный выбор периода', async () => {
|
||||
const [main, roles, constructor, quality, index, view, qualityView, versionsView, scheduleView] = await Promise.all([
|
||||
readFile(new URL('../admin/js/main.js', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../admin/js/role-capabilities.js', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../admin/js/views/schedule.js', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../admin/js/views/schedule-quality.js', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../admin/index.html', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../admin/views/schedule.html', import.meta.url), 'utf8')
|
||||
readFile(new URL('../admin/views/schedule.html', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../admin/views/schedule-quality.html', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../admin/views/schedule-versions.html', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../admin/views/schedule-view.html', import.meta.url), 'utf8')
|
||||
]);
|
||||
|
||||
assert.match(main, /'schedule-versions'/);
|
||||
@@ -52,5 +55,12 @@ test('контур версий подключён к SPA, а конструкт
|
||||
assert.match(constructor, /function localIsoDate\(date\)/);
|
||||
assert.match(constructor, /data-slot-id/);
|
||||
assert.match(view, /id="schedule-version-create-draft"/);
|
||||
assert.match(view, /id="schedule-version-year"/);
|
||||
assert.match(view, /id="schedule-version-semester"/);
|
||||
assert.match(qualityView, /id="quality-year"/);
|
||||
assert.match(qualityView, /id="quality-version"/);
|
||||
assert.match(versionsView, /id="version-year"/);
|
||||
assert.match(scheduleView, /id="schedule-view-year"/);
|
||||
assert.doesNotMatch(index, /id="workspace-context"/);
|
||||
assert.match(quality, /versionId=.*encodeURIComponent\(versionId\)/);
|
||||
});
|
||||
|
||||
@@ -12,9 +12,15 @@ import {
|
||||
defaultSchedulePeriod,
|
||||
preferredScheduleSemester
|
||||
} from '../admin/js/schedule-period.js';
|
||||
import {
|
||||
academicYearsFromSemesters,
|
||||
resolveAcademicPeriod,
|
||||
semestersForAcademicYear
|
||||
} from '../admin/js/academic-period.js';
|
||||
|
||||
const autumnSemester = {
|
||||
id: 11,
|
||||
academicYearId: 5,
|
||||
academicYearTitle: '2025/2026',
|
||||
semesterType: 'autumn',
|
||||
startDate: '2025-09-01',
|
||||
@@ -23,6 +29,7 @@ const autumnSemester = {
|
||||
|
||||
const springSemester = {
|
||||
id: 12,
|
||||
academicYearId: 5,
|
||||
academicYearTitle: '2025/2026',
|
||||
semesterType: 'spring',
|
||||
startDate: '2026-02-09',
|
||||
@@ -77,6 +84,7 @@ test('кафедральное расписание не отбрасывает
|
||||
test('между семестрами выбирается ближайший будущий период с занятиями', () => {
|
||||
const nextAutumnSemester = {
|
||||
id: 13,
|
||||
academicYearId: 6,
|
||||
academicYearTitle: '2026/2027',
|
||||
semesterType: 'autumn',
|
||||
startDate: '2026-09-01',
|
||||
@@ -91,3 +99,32 @@ test('между семестрами выбирается ближайший б
|
||||
endDate: '2026-09-14'
|
||||
});
|
||||
});
|
||||
|
||||
test('локальные селекты сначала ограничивают семестры выбранным учебным годом', () => {
|
||||
const nextAutumnSemester = {
|
||||
id: 13,
|
||||
academicYearId: 6,
|
||||
academicYearTitle: '2026/2027',
|
||||
semesterType: 'autumn',
|
||||
startDate: '2026-09-01',
|
||||
endDate: '2027-01-31'
|
||||
};
|
||||
const semesters = [nextAutumnSemester, springSemester, autumnSemester];
|
||||
|
||||
assert.deepEqual(academicYearsFromSemesters(semesters), [
|
||||
{ id: '6', title: '2026/2027' },
|
||||
{ id: '5', title: '2025/2026' }
|
||||
]);
|
||||
assert.deepEqual(
|
||||
semestersForAcademicYear(semesters, 5).map(semester => semester.id),
|
||||
[12, 11]
|
||||
);
|
||||
assert.deepEqual(resolveAcademicPeriod(semesters, {
|
||||
academicYearId: 5,
|
||||
semesterId: 12,
|
||||
date: '2026-08-12'
|
||||
}), {
|
||||
academicYearId: '5',
|
||||
semesterId: '12'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@ import test from 'node:test';
|
||||
|
||||
import {
|
||||
decisionOptions,
|
||||
parseDecisionOption
|
||||
parseDecisionOption,
|
||||
teacherAbsenceTeacherOptionsUrl
|
||||
} from '../admin/js/views/teacher-absences.js';
|
||||
|
||||
test('значение мастера преобразуется в payload точечного изменения', () => {
|
||||
@@ -42,6 +43,33 @@ test('мастер показывает только реально получе
|
||||
]);
|
||||
});
|
||||
|
||||
test('поиск отсутствующего преподавателя не ограничивает учебный отдел его служебной кафедрой', () => {
|
||||
assert.equal(
|
||||
teacherAbsenceTeacherOptionsUrl({
|
||||
query: 'Иванов',
|
||||
role: 'EDUCATION_OFFICE',
|
||||
departmentId: 5
|
||||
}),
|
||||
'/api/users/teachers/options?query=%D0%98%D0%B2%D0%B0%D0%BD%D0%BE%D0%B2&limit=30'
|
||||
);
|
||||
assert.equal(
|
||||
teacherAbsenceTeacherOptionsUrl({
|
||||
query: 'Петров',
|
||||
selectedId: 17,
|
||||
role: 'DEPARTMENT',
|
||||
departmentId: 5
|
||||
}),
|
||||
'/api/users/teachers/options?query=%D0%9F%D0%B5%D1%82%D1%80%D0%BE%D0%B2&limit=30&departmentId=5&selectedId=17'
|
||||
);
|
||||
});
|
||||
|
||||
test('список преподавателей раскрывается поверх следующей карточки', async () => {
|
||||
const styles = await readFile(new URL('../admin/css/teacher-absences.css', import.meta.url), 'utf8');
|
||||
|
||||
assert.match(styles, /\.absence-create-card\s*\{[^}]*z-index:\s*20[^}]*overflow:\s*visible/s);
|
||||
assert.match(styles, /\.absence-create-card:focus-within\s*\{[^}]*z-index:\s*30/s);
|
||||
});
|
||||
|
||||
test('вкладка отсутствий доступна ответственным ролям и подключена к SPA', async () => {
|
||||
const [capabilities, main, index] = await Promise.all([
|
||||
readFile(new URL('../admin/js/role-capabilities.js', import.meta.url), 'utf8'),
|
||||
|
||||
Reference in New Issue
Block a user