109 lines
4.8 KiB
TypeScript
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');
|
|
});
|
|
});
|