разные правки
This commit is contained in:
@@ -12,6 +12,7 @@ import * as strings from 'RequestPortalWebPartStrings';
|
|||||||
import RequestPortal from './components/RequestPortal';
|
import RequestPortal from './components/RequestPortal';
|
||||||
import { IRequestPortalProps } from './components/IRequestPortalProps';
|
import { IRequestPortalProps } from './components/IRequestPortalProps';
|
||||||
import { initSP } from './shared/pnp/pnpjs';
|
import { initSP } from './shared/pnp/pnpjs';
|
||||||
|
import { installErrorLogging } from './shared/diagnostics';
|
||||||
import { ErrorOverlay } from './shared/ui';
|
import { ErrorOverlay } from './shared/ui';
|
||||||
|
|
||||||
export interface IRequestPortalWebPartProps {
|
export interface IRequestPortalWebPartProps {
|
||||||
@@ -41,6 +42,7 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onInit(): Promise<void> {
|
protected onInit(): Promise<void> {
|
||||||
|
installErrorLogging(); // до всего остального — чтобы поймать и ранние отклонения
|
||||||
initSP(this.context); // единый клиент PnPjs для записи в списки
|
initSP(this.context); // единый клиент PnPjs для записи в списки
|
||||||
return this._getEnvironmentMessage().then(message => {
|
return this._getEnvironmentMessage().then(message => {
|
||||||
this._environmentMessage = message;
|
this._environmentMessage = message;
|
||||||
|
|||||||
@@ -37,11 +37,20 @@
|
|||||||
// Фильтр по статусу — только на вкладке «Инициированные». Стоит слева от поиска
|
// Фильтр по статусу — только на вкладке «Инициированные». Стоит слева от поиска
|
||||||
// и прижат к нему: вместе это один блок «чем сузить список».
|
// и прижат к нему: вместе это один блок «чем сузить список».
|
||||||
.statusFilter {
|
.statusFilter {
|
||||||
|
// appearance: none + своя SVG-галочка — как в shared/ui/Select. Нативная
|
||||||
|
// стрелка ОС рисуется своим шрифтом и рядом с остальными полями портала
|
||||||
|
// выглядит чужеродно (и по-разному в разных браузерах).
|
||||||
|
appearance: none;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
padding: 9px 13px;
|
// Справа место под галочку; по вертикали — как у .search, чтобы оба контрола
|
||||||
|
// в шапке были одной высоты.
|
||||||
|
padding: 9px 34px 9px 13px;
|
||||||
border: 1px solid $gray-300;
|
border: 1px solid $gray-300;
|
||||||
border-radius: $r-sm;
|
border-radius: $r-sm;
|
||||||
background: $white;
|
background-color: $white;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%236B7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 12px center;
|
||||||
font-family: $font-sans;
|
font-family: $font-sans;
|
||||||
font-size: $fs-sm;
|
font-size: $fs-sm;
|
||||||
color: $gray-900;
|
color: $gray-900;
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Читаемый лог необработанных отклонений промисов.
|
||||||
|
//
|
||||||
|
// Зачем: SPFx вешает на window свой обработчик unhandledrejection, и в консоль
|
||||||
|
// попадает только его собственный стек внутри минифицированного бандла —
|
||||||
|
// примерно такой:
|
||||||
|
//
|
||||||
|
// Error
|
||||||
|
// at vp (...request-portal-web-part_<hash>.js:9:397946)
|
||||||
|
// at t.onUnhandledRejection (...)
|
||||||
|
//
|
||||||
|
// Ни причины, ни места, где промис отклонился, там нет: SPFx логирует САМ факт,
|
||||||
|
// а не `reason`. Поэтому вешаем свой слушатель — он срабатывает наравне с
|
||||||
|
// SPFx-овским и печатает то, что действительно нужно: сообщение, стек и сам
|
||||||
|
// объект причины (у ошибок PnPjs/SharePoint внутри лежит ответ сервера).
|
||||||
|
//
|
||||||
|
// Ничего не подавляем (preventDefault не зовём) — поведение портала не
|
||||||
|
// меняется, добавляется только диагностика.
|
||||||
|
|
||||||
|
const PREFIX = '[Портал заявок]';
|
||||||
|
|
||||||
|
// Слушатель вешается один раз на страницу: на странице может быть несколько
|
||||||
|
// экземпляров веб-части, и каждый вызвал бы install() в onInit — без флага
|
||||||
|
// одна и та же ошибка печаталась бы столько раз, сколько их размещено.
|
||||||
|
let installed = false;
|
||||||
|
|
||||||
|
// Причиной отклонения может быть что угодно, не только Error — у PnPjs это
|
||||||
|
// бывает объект с полями ответа. Разбираем по факту, а не по ожиданию.
|
||||||
|
function describeReason(reason: unknown): string {
|
||||||
|
if (reason instanceof Error) return `${reason.name}: ${reason.message}`;
|
||||||
|
if (typeof reason === 'string') return reason;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(reason);
|
||||||
|
} catch {
|
||||||
|
// Циклические ссылки — тогда хотя бы тип.
|
||||||
|
return Object.prototype.toString.call(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installErrorLogging(): void {
|
||||||
|
if (installed) return;
|
||||||
|
installed = true;
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
|
||||||
|
const reason: unknown = e.reason;
|
||||||
|
console.error(`${PREFIX} Необработанная ошибка: ${describeReason(reason)}`);
|
||||||
|
// Стек и объект — отдельными аргументами: так консоль браузера даёт
|
||||||
|
// раскрыть объект и перейти по стеку, а не показывает «[object Object]».
|
||||||
|
if (reason instanceof Error && reason.stack) console.error(`${PREFIX} Стек:`, reason.stack);
|
||||||
|
console.error(`${PREFIX} Причина целиком:`, reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user