Files
magistr/frontend/tests/react-shared.test.mjs
dipatrik10 97bf3cc50a 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
2026-09-28 23:15:04 +03:00

97 lines
4.4 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 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(), /Рабочее содержимое/);
});