import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; const readFrontendFile = relativePath => readFile( new URL(`../${relativePath}`, import.meta.url), 'utf8' ); test('все пользовательские оболочки завершают каскад единой визуальной основой', async () => { const entrypoints = [ ['index.html', 'ui-foundation.css'], ['admin/index.html', '../ui-foundation.css'], ['admin/settings/index.html', '../../ui-foundation.css'], ['teacher/index.html', '../ui-foundation.css'], ['student/index.html', '../ui-foundation.css'] ]; for (const [file, expectedHref] of entrypoints) { const html = await readFrontendFile(file); const stylesheets = Array.from( html.matchAll(/]*rel="stylesheet"[^>]*href="([^"]+)"[^>]*>/g), match => match[1] ); assert.equal(stylesheets.at(-1), expectedHref, file); } }); test('единая визуальная основа фиксирует типографику, размеры и доступный фокус', async () => { const foundation = await readFrontendFile('ui-foundation.css'); assert.match(foundation, /--ui-font-sans:/); assert.match(foundation, /--ui-control-sm:\s*32px/); assert.match(foundation, /--ui-control-md:\s*40px/); assert.match(foundation, /--ui-control-lg:\s*48px/); assert.match(foundation, /\.btn-sm\s*\{[^}]*min-height:\s*var\(--ui-control-sm\)/s); assert.match(foundation, /\.btn-md\s*\{[^}]*min-height:\s*var\(--ui-control-md\)/s); assert.match(foundation, /\.btn-lg\s*\{[^}]*min-height:\s*var\(--ui-control-lg\)/s); assert.match(foundation, /:focus-visible\s*\{[^}]*outline:/s); assert.match(foundation, /prefers-reduced-motion:\s*reduce/); }); test('локальные стили используют общие шрифтовые токены', async () => { const files = [ 'style.css', 'student/style.css', 'teacher/style.css', 'admin/css/main.css', 'admin/settings/css/main.css', 'admin/css/schedule-quality.css', 'admin/css/schedule-versions.css' ]; for (const file of files) { const css = await readFrontendFile(file); assert.doesNotMatch(css, /font-family:\s*(?:['"]?Inter|['"]?Segoe UI|Georgia|['"]Times New Roman)/, file); } }); test('корневые контейнеры админ-оболочек растягиваются на всю ширину окна', async () => { // body в admin/css/main.css и admin/settings/css/main.css — flex-контейнер: // без растяжения корневой div сжимается до max-content контента, и узкие // страницы (карточки, формы) не занимают всю ширину монитора, а дашборд // «растягивается» только после загрузки данных. flex-basis 0 обязателен: // intrinsic-замер max-content раздувает вложенные flex-фильтры // (панель загруженности аудиторий) до ~1 млн px горизонтального скролла. const cases = [ ['admin/css/layout.css', '#admin-root'], ['admin/settings/css/layout.css', '#settings-root'] ]; for (const [file, selector] of cases) { const css = await readFrontendFile(file); const escaped = selector.replace('#', '\\#'); assert.match( css, new RegExp(`${escaped}\\s*\\{[^}]*flex:\\s*1 1 0%\\s*;[^}]*min-width:\\s*0`, 's'), `${file}: ${selector} должен растягиваться ровно на ширину окна (flex: 1 1 0%; min-width: 0)` ); } }); test('архивные действия и кнопки закрытия используют системные размеры', async () => { const actionSources = await Promise.all([ readFrontendFile('react/admin/tabs/ClassroomsTab.jsx'), readFrontendFile('react/admin/tabs/GroupsTab.jsx'), readFrontendFile('react/admin/tabs/UsersTab.jsx') ]); actionSources.forEach(source => { assert.match(source, /class(?:Name)?="btn btn-sm btn-secondary btn-restore-/); }); const modalViews = await Promise.all([ 'react/admin/tabs/AcademicCalendarTab.jsx', 'react/admin/tabs/ClassroomsTab.jsx', 'react/admin/tabs/GroupsTab.jsx', 'react/admin/tabs/ScheduleTab.jsx', 'react/admin/tabs/ScheduleVersionsTab.jsx', 'react/admin/tabs/SubjectsTab.jsx', 'react/admin/tabs/UniversityStructureTab.jsx' ].map(readFrontendFile)); const closeButtons = modalViews.flatMap(html => Array.from( html.matchAll(/]*class(?:Name)?="[^"]*(?:modal-close|version-modal-close|settings-modal-close)[^"]*"[^>]*>/g), match => match[0] )); assert.ok(closeButtons.length > 0); closeButtons.forEach(button => { assert.match(button, /type="button"/); assert.match(button, /class(?:Name)?="[^"]*\bbtn\b[^"]*\bbtn-icon-md\b/); assert.match(button, /aria-label="Закрыть окно"/); }); });