Files
request-portal/src/webparts/requestPortal/shared/diagnostics.test.ts
T
gorbenko.pavel 5cbc9eb89b
Tests / frontend-test (push) Successful in 1m9s
отображение ошибки
2026-08-11 12:51:26 +05:00

109 lines
4.8 KiB
TypeScript

import { describeReason, installErrorLogging } from './diagnostics';
// jsdom не порождает unhandledrejection сам — событие диспатчим руками, как это
// сделал бы браузер. Конструктора PromiseRejectionEvent в jsdom нет, поэтому
// собираем обычное Event и дописываем reason (слушатель читает только его).
function emitRejection(reason: unknown): void {
const event = new Event('unhandledrejection') as Event & { reason?: unknown };
event.reason = reason;
window.dispatchEvent(event);
}
describe('installErrorLogging', () => {
let errors: unknown[][];
let spy: jest.SpyInstance;
beforeEach(() => {
errors = [];
spy = jest.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
errors.push(args);
});
installErrorLogging();
});
afterEach(() => spy.mockRestore());
it('печатает сообщение и стек обычной ошибки', () => {
emitRejection(new Error('Список недоступен'));
const text = errors.map((a) => a.join(' ')).join('\n');
expect(text).toContain('Список недоступен');
expect(text).toContain('Стек');
});
it('не теряет причину, если это не Error, а объект (типичный отказ PnPjs)', () => {
emitRejection({ status: 403, message: 'Access denied' });
const text = errors.map((a) => a.map((x) => JSON.stringify(x)).join(' ')).join('\n');
expect(text).toContain('403');
expect(text).toContain('Access denied');
});
it('не падает на причине с циклической ссылкой', () => {
const cyclic: Record<string, unknown> = { name: 'зацикленное' };
cyclic.self = cyclic;
expect(() => emitRejection(cyclic)).not.toThrow();
expect(errors.length).toBeGreaterThan(0);
});
it('строка-причина попадает в лог как есть', () => {
emitRejection('просто текст');
expect(errors.map((a) => a.join(' ')).join('\n')).toContain('просто текст');
});
it('повторный вызов не вешает второй слушатель — ошибка не двоится', () => {
installErrorLogging();
installErrorLogging();
emitRejection(new Error('однократно'));
const count = errors.filter((a) => String(a[0]).indexOf('Необработанная ошибка') !== -1).length;
expect(count).toBe(1);
});
});
// describeReason — то, что пользователь реально видит в отчёте об ошибке (и в
// угловом уведомлении ErrorOverlay), поэтому пустых строк он давать не должен
// ни на одном виде причины.
describe('describeReason', () => {
it('промис без причины описывается словами, а не пустой строкой', () => {
// Здесь и ломалось: JSON.stringify(undefined) возвращает undefined, и
// сообщение схлопывалось в пустоту — по отчёту нельзя было понять даже
// того, что причины не было вовсе.
expect(describeReason(undefined)).toContain('без причины');
});
it('null тоже описывается', () => {
expect(describeReason(null)).toContain('null');
});
it('ошибка — имя и сообщение', () => {
expect(describeReason(new TypeError('нет доступа'))).toBe('TypeError: нет доступа');
});
it('ошибка с пустым сообщением не даёт пустую строку', () => {
expect(describeReason(new Error(''))).toContain('пустое сообщение');
});
it('строка возвращается как есть', () => {
expect(describeReason('сервер недоступен')).toBe('сервер недоступен');
});
it('обычный объект сериализуется', () => {
expect(describeReason({ status: 404 })).toContain('404');
});
it('циклическая ссылка не роняет разбор — остаются ключи', () => {
// Внутри ошибок PnPjs лежит Response со ссылками на себя: JSON.stringify на
// таком объекте бросает, и раньше это происходило прямо внутри обработчика
// события.
const circular: Record<string, unknown> = { status: 500 };
circular.self = circular;
const text = describeReason(circular);
expect(text).toContain('status');
expect(text).toContain('self');
});
});