Files
magistr/frontend/tests/admin-ui-regressions.test.mjs
2026-08-20 00:55:27 +03:00

164 lines
9.0 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 { readFile } from 'node:fs/promises';
import test from 'node:test';
import {
scheduleSlotActionLabel,
shouldMarkRuleFormDirty
} from '../admin/js/views/schedule.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 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'),
readFrontendFile('admin/js/main.js'),
readFrontendFile('admin/settings/index.html'),
readFrontendFile('admin/settings/js/main.js')
]);
files.forEach(content => assert.doesNotMatch(content, /menu-toggle/));
});
test('заголовок вкладки соответствует конструктору правил', async () => {
const mainScript = await readFrontendFile('admin/js/main.js');
assert.match(mainScript, /schedule:\s*\{\s*title:\s*'Конструктор правил'/);
});
test('основная кнопка показа находится в заголовке блока параметров', async () => {
const scheduleView = await readFrontendFile('admin/views/schedule-view.html');
const controlHeaderIndex = scheduleView.indexOf('schedule-view-control-header');
const controlGridIndex = scheduleView.indexOf('schedule-view-control-grid');
const loadButtonIndex = scheduleView.indexOf('id="schedule-view-load"');
assert.ok(controlHeaderIndex >= 0);
assert.ok(loadButtonIndex > controlHeaderIndex);
assert.ok(loadButtonIndex < controlGridIndex);
assert.equal(scheduleView.match(/id="schedule-view-load"/g)?.length, 1);
});
test('пустое состояние расписания содержит собственную кнопку обновления', async () => {
const scheduleScript = await readFrontendFile('admin/js/views/schedule-view.js');
assert.match(scheduleScript, /SCHEDULE_REFRESH_PROMPT\s*=\s*'Нажмите «Показать», чтобы обновить расписание'/);
assert.match(scheduleScript, /data-schedule-view-reload/);
assert.match(scheduleScript, /closest\('\[data-schedule-view-reload\]'\)[\s\S]*?loadData\(\)/);
assert.match(scheduleScript, /setTableState\(message,\s*message === SCHEDULE_REFRESH_PROMPT\)/);
});
test('действия слотов имеют размер соседнего поля', async () => {
const [components, scheduleScript] = await Promise.all([
readFrontendFile('admin/css/components.css'),
readFrontendFile('admin/js/views/schedule.js')
]);
assert.match(components, /\.schedule-slot-remove\s*\{[^}]*min-width:\s*125px[^}]*min-height:\s*44px/s);
assert.match(scheduleScript, /class="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 calendarScript = await readFrontendFile('admin/js/views/academic-calendar.js');
assert.match(calendarScript, /translate3d\(\$\{left\}px, \$\{top\}px, 0\) scale\(1\)/);
assert.match(calendarScript, /\{ duration: 1, fill: 'forwards' \}/);
assert.doesNotMatch(calendarScript, /\.style\.(?:left|top)\s*=/);
});
test('светлая тема задаёт контрастные пункты и заголовки левого меню', async () => {
const [layout, index] = await Promise.all([
readFrontendFile('admin/css/layout.css'),
readFrontendFile('admin/index.html')
]);
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(index, />Расписание<\/div>/);
});
test('таблица календарных графиков не содержит отдельный столбец названия', async () => {
const [calendarView, calendarScript] = await Promise.all([
readFrontendFile('admin/views/academic-calendar.html'),
readFrontendFile('admin/js/views/academic-calendar.js')
]);
const bodyIndex = calendarView.indexOf('id="academic-calendars-tbody"');
const tableStart = calendarView.lastIndexOf('<table>', bodyIndex);
const tableEnd = calendarView.indexOf('</table>', bodyIndex);
const calendarTable = calendarView.slice(tableStart, tableEnd);
assert.doesNotMatch(calendarTable, /<th>Название<\/th>/);
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/);
});