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, / { 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('', bodyIndex); const tableEnd = calendarTab.indexOf('
', bodyIndex); const calendarTable = calendarTab.slice(tableStart, tableEnd); assert.doesNotMatch(calendarTable, /Название<\/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, /\s*/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('', 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/); });