Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
97 lines
4.4 KiB
JavaScript
97 lines
4.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
|
||
import { requestJson, ApiRequestError } from '../react/shared/api-client.js';
|
||
import { loadAuthorizedSession } from '../react/shared/auth-client.js';
|
||
import { resolveSessionState, SESSION_PHASES } from '../react/shared/session-policy.js';
|
||
import {
|
||
renderAsyncState,
|
||
renderErrorBoundaryContent,
|
||
renderFormAlert
|
||
} from '../dist/tests/react-shared-renderer.mjs';
|
||
|
||
test('общая политика сессии различает разрешённую и запрещённую роль', () => {
|
||
const ready = resolveSessionState({ role: 'STUDENT', userId: '7' }, ['STUDENT']);
|
||
const denied = resolveSessionState({ role: 'TEACHER', userId: '8' }, ['STUDENT']);
|
||
|
||
assert.equal(ready.phase, SESSION_PHASES.ready);
|
||
assert.equal(ready.session.userId, '7');
|
||
assert.equal(denied.phase, SESSION_PHASES.unauthorized);
|
||
assert.equal(denied.message, 'У вас нет доступа к этому разделу');
|
||
});
|
||
|
||
test('auth-адаптер возвращает безопасное состояние при ошибке восстановления', async () => {
|
||
const state = await loadAuthorizedSession(['STUDENT'], {
|
||
restoreSessionImpl: async () => {
|
||
throw new Error('Техническая причина не должна попасть в UI');
|
||
}
|
||
});
|
||
|
||
assert.equal(state.phase, SESSION_PHASES.error);
|
||
assert.equal(state.message, 'Не удалось восстановить сессию');
|
||
assert.doesNotMatch(state.message, /Техническая причина/);
|
||
});
|
||
|
||
test('общий API-клиент сериализует JSON и сохраняет same-origin auth-транспорт', async () => {
|
||
const calls = [];
|
||
const result = await requestJson('/api/example', {
|
||
json: { value: 'данные' },
|
||
method: 'POST'
|
||
}, {
|
||
fetchImpl: async (endpoint, options) => {
|
||
calls.push({ endpoint, options });
|
||
return new Response(JSON.stringify({ saved: true }), {
|
||
headers: { 'Content-Type': 'application/json' },
|
||
status: 200
|
||
});
|
||
}
|
||
});
|
||
|
||
assert.deepEqual(result, { saved: true });
|
||
assert.equal(calls[0].endpoint, '/api/example');
|
||
assert.equal(calls[0].options.method, 'POST');
|
||
assert.equal(new Headers(calls[0].options.headers).get('Accept'), 'application/json');
|
||
assert.equal(new Headers(calls[0].options.headers).get('Content-Type'), 'application/json');
|
||
assert.deepEqual(JSON.parse(calls[0].options.body), { value: 'данные' });
|
||
});
|
||
|
||
test('общий API-клиент не выводит технический ответ сервера как пользовательскую ошибку', async () => {
|
||
await assert.rejects(
|
||
requestJson('/api/example', { errorMessage: 'Не удалось загрузить пример' }, {
|
||
fetchImpl: async () => new Response(JSON.stringify({ message: 'Internal failure' }), {
|
||
headers: { 'Content-Type': 'application/json' },
|
||
status: 500
|
||
})
|
||
}),
|
||
error => {
|
||
assert.ok(error instanceof ApiRequestError);
|
||
assert.equal(error.message, 'Не удалось загрузить пример');
|
||
assert.equal(error.status, 500);
|
||
assert.equal(error.data.message, 'Internal failure');
|
||
return true;
|
||
}
|
||
);
|
||
});
|
||
|
||
test('общие React-состояния имеют русские тексты и доступную семантику', () => {
|
||
const loading = renderAsyncState({ kind: 'loading' });
|
||
const failure = renderAsyncState({
|
||
kind: 'error',
|
||
message: 'Не удалось получить расписание',
|
||
onRetry: () => {}
|
||
});
|
||
const alert = renderFormAlert({
|
||
alert: { message: 'Проверьте поля', type: 'error' },
|
||
id: 'form-alert'
|
||
});
|
||
|
||
assert.match(loading, /role="status"/);
|
||
assert.match(loading, /Загружаем данные/);
|
||
assert.match(failure, /role="alert"/);
|
||
assert.match(failure, /Не удалось получить расписание/);
|
||
assert.match(failure, /Повторить/);
|
||
assert.match(alert, /id="form-alert"/);
|
||
assert.match(alert, /Проверьте поля/);
|
||
assert.match(renderErrorBoundaryContent(), /Рабочее содержимое/);
|
||
});
|