feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md). - 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой - Удалены legacy-views и переходный слой LegacyTabView (45 файлов) - Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки - CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf) - Фиксы: lazy-импорт именованных экспортов (краш после авторизации), растяжение #admin-root/#settings-root на всю ширину, возврат subjects/lessonTypes из справочников (краш просмотра расписаний), отказ от заведомо широкого поиска расписания без выбранной цели, центрирование карточки логина - Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину - Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
This commit is contained in:
@@ -6,7 +6,7 @@ import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const frontendRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const sourceExtensions = new Set(['.html', '.js', '.mjs', '.css']);
|
||||
const sourceExtensions = new Set(['.html', '.js', '.jsx', '.mjs', '.css']);
|
||||
|
||||
async function sourceFiles(directory = frontendRoot) {
|
||||
const result = [];
|
||||
@@ -77,8 +77,9 @@ test('Docker-образ включает локальный telemetry bundle и
|
||||
const dockerfile = await readFile(join(frontendRoot, 'Dockerfile'), 'utf8');
|
||||
const telemetry = await readFile(join(frontendRoot, 'telemetry.js'), 'utf8');
|
||||
assert.match(dockerfile, /npm ci/);
|
||||
assert.match(dockerfile, /npm run build:vendor/);
|
||||
assert.match(dockerfile, /scripts\/build-vendor\.mjs/);
|
||||
assert.match(dockerfile, /npm run build/);
|
||||
assert.match(dockerfile, /scripts\/build-react\.mjs scripts\/build-vendor\.mjs/);
|
||||
assert.match(dockerfile, /COPY --from=frontend-assets \/build\/dist\/react\//);
|
||||
assert.match(dockerfile, /COPY --from=frontend-assets \/build\/dist\/vendor\//);
|
||||
assert.match(dockerfile, /magistr-security\.conf/);
|
||||
for (const asset of ['ui-foundation.css', 'schedule-overview.css', 'schedule-overview.js']) {
|
||||
@@ -93,10 +94,13 @@ test('Docker-образ включает локальный telemetry bundle и
|
||||
|
||||
test('сообщения об ошибках не вставляют текст исключения через innerHTML', async () => {
|
||||
const files = [
|
||||
'admin/js/main.js',
|
||||
'admin/settings/js/main.js',
|
||||
'admin/settings/js/views/database.js',
|
||||
'admin/js/views/teacher-requests.js'
|
||||
'react/admin/AdminApp.jsx',
|
||||
'react/admin/tabs/TeacherRequestsTab.jsx',
|
||||
'react/admin/tabs/DepartmentWorkspaceTab.jsx',
|
||||
'react/admin/tabs/AuditoriumWorkloadTab.jsx',
|
||||
'react/admin/tabs/AcademicCalendarTab.jsx',
|
||||
'react/settings/SettingsApp.jsx',
|
||||
'react/settings/DatabaseTab.jsx'
|
||||
];
|
||||
|
||||
for (const relativePath of files) {
|
||||
@@ -112,22 +116,22 @@ test('сообщения об ошибках не вставляют текст
|
||||
});
|
||||
|
||||
test('пароли скрыты и помечены для менеджера паролей', async () => {
|
||||
const users = await readFile(join(frontendRoot, 'admin/views/users.html'), 'utf8');
|
||||
const database = await readFile(join(frontendRoot, 'admin/settings/views/database.html'), 'utf8');
|
||||
const teacherRequests = await readFile(join(frontendRoot, 'admin/js/views/teacher-requests.js'), 'utf8');
|
||||
const users = await readFile(join(frontendRoot, 'react/admin/tabs/UsersTab.jsx'), 'utf8');
|
||||
const databaseTab = await readFile(join(frontendRoot, 'react/settings/DatabaseTab.jsx'), 'utf8');
|
||||
const teacherRequests = await readFile(join(frontendRoot, 'react/admin/tabs/TeacherRequestsTab.jsx'), 'utf8');
|
||||
|
||||
assert.match(users, /id="new-password"[^>]*type="password"|type="password"[^>]*id="new-password"/);
|
||||
assert.match(users, /id="new-password"[^>]*autocomplete="new-password"/);
|
||||
assert.match(database, /id="tenant-password"[^>]*type="password"|type="password"[^>]*id="tenant-password"/);
|
||||
assert.match(database, /id="tenant-password"[^>]*autocomplete="new-password"/);
|
||||
assert.match(teacherRequests, /type="password"[^>]*class="teacher-request-password"[^>]*autocomplete="new-password"/);
|
||||
assert.match(users, /id="new-password"[^>]*autoComplete="new-password"/);
|
||||
assert.match(databaseTab, /id="tenant-password"[\s\S]*?type="password"|type="password"[\s\S]*?id="tenant-password"/);
|
||||
assert.match(databaseTab, /id="tenant-password"[\s\S]*?autoComplete="new-password"|autoComplete="new-password"[\s\S]*?id="tenant-password"/);
|
||||
assert.match(teacherRequests, /type="password"[^>]*className="teacher-request-password"[^>]*autoComplete="new-password"/);
|
||||
});
|
||||
|
||||
test('tenant UI и production-логи соблюдают русский языковой регламент', async () => {
|
||||
const database = await readFile(join(frontendRoot, 'admin/settings/js/views/database.js'), 'utf8');
|
||||
assert.doesNotMatch(database, /\bOnline\b|\bOffline\b/);
|
||||
assert.match(database, /Доступно/);
|
||||
assert.match(database, /Недоступно/);
|
||||
const databaseTab = await readFile(join(frontendRoot, 'react/settings/DatabaseTab.jsx'), 'utf8');
|
||||
assert.doesNotMatch(databaseTab, /\bOnline\b|\bOffline\b/);
|
||||
assert.match(databaseTab, /Доступно/);
|
||||
assert.match(databaseTab, /Недоступно/);
|
||||
|
||||
const backendRoot = join(frontendRoot, '..', 'backend', 'src', 'main', 'java', 'com', 'magistr', 'app', 'config', 'tenant');
|
||||
const logSources = await Promise.all([
|
||||
@@ -144,7 +148,7 @@ test('tenant UI и production-логи соблюдают русский язы
|
||||
);
|
||||
|
||||
const frontendSources = (await sourceCorpus())
|
||||
.filter(({ path }) => extname(path) === '.js')
|
||||
.filter(({ path }) => ['.js', '.jsx'].includes(extname(path)))
|
||||
.map(({ text }) => text)
|
||||
.join('\n');
|
||||
assert.doesNotMatch(frontendSources, /console\.(?:log|info|warn|error)\([^\n]*(?:Failed|Error)/);
|
||||
@@ -166,10 +170,10 @@ test('единая матрица ролей согласует admin и setting
|
||||
assert.equal(educationOffice.defaultSettingsTab, 'time-slots');
|
||||
assert.equal(capabilitiesForRole('DEPARTMENT').settingsTabs.length, 0);
|
||||
|
||||
const adminMain = await readFile(join(frontendRoot, 'admin/js/main.js'), 'utf8');
|
||||
const settingsMain = await readFile(join(frontendRoot, 'admin/settings/js/main.js'), 'utf8');
|
||||
assert.match(adminMain, /from '\.\/role-capabilities\.js'/);
|
||||
assert.match(settingsMain, /from '\.\.\/\.\.\/js\/role-capabilities\.js'/);
|
||||
assert.doesNotMatch(adminMain, /const ROLE_NAVIGATION/);
|
||||
assert.doesNotMatch(settingsMain, /const ROLE_TABS/);
|
||||
const adminService = await readFile(join(frontendRoot, 'react/admin/admin-service.js'), 'utf8');
|
||||
const settingsApp = await readFile(join(frontendRoot, 'react/settings/SettingsApp.jsx'), 'utf8');
|
||||
assert.match(adminService, /from '\.\.\/\.\.\/admin\/js\/role-capabilities\.js'/);
|
||||
assert.match(settingsApp, /from '\.\.\/\.\.\/admin\/js\/role-capabilities\.js'/);
|
||||
assert.doesNotMatch(adminService, /const ROLE_NAVIGATION/);
|
||||
assert.doesNotMatch(settingsApp, /const ROLE_TABS/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user