mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
292 lines
9.9 KiB
JavaScript
292 lines
9.9 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Sentry 错误反馈弹窗专项测试
|
||
*
|
||
* 覆盖 shouldShowReportDialog 冷却机制、弹窗触发条件、
|
||
* showReportDialog 中文参数、SentryMock fallback 等场景
|
||
*
|
||
* 注意:本测试中的 createShouldShowReportDialog 与 sentry-loader.js 冷却逻辑保持一致。
|
||
* 修改 sentry-loader.js / sentry-init.js 中的冷却逻辑时需同步更新此处。
|
||
*
|
||
* 环境说明:jsdom 下 window/document 不可 delete,本测试不改写全局 window。
|
||
*/
|
||
|
||
// ===================================
|
||
// 模拟 Sentry API(不触碰 jsdom 全局 window)
|
||
// ===================================
|
||
|
||
const mockShowReportDialog = jest.fn();
|
||
const mockLastEventId = jest.fn(() => 'test-event-id');
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks();
|
||
jest.useFakeTimers();
|
||
});
|
||
|
||
afterEach(() => {
|
||
jest.useRealTimers();
|
||
});
|
||
|
||
// ===================================
|
||
// 辅助函数:与 sentry-loader.js 冷却逻辑对齐
|
||
// ===================================
|
||
|
||
/**
|
||
* 独立实现 shouldShowReportDialog,与 sentry-loader.js 逻辑一致
|
||
* 用于单元测试冷却机制的正确性
|
||
*/
|
||
function createShouldShowReportDialog(options = {}) {
|
||
const { isDev = false } = options;
|
||
let lastReportDialogTime = 0;
|
||
let lastReportDialogFingerprint = '';
|
||
const COOLDOWN_MS = 60000;
|
||
|
||
function shouldShowReportDialog(event) {
|
||
const empty = { shouldShow: false, fingerprint: '' };
|
||
|
||
// 空事件防御
|
||
if (!event || typeof event !== 'object') return empty;
|
||
|
||
// 仅生产环境弹窗
|
||
if (isDev) return empty;
|
||
|
||
// 仅对异常事件弹窗(captureMessage 等不弹)
|
||
if (!event.exception || !event.exception.values || !event.exception.values.length) {
|
||
return empty;
|
||
}
|
||
|
||
// 生成错误指纹:取第一个异常的 type + value 组合
|
||
const firstException = event.exception.values[0];
|
||
const fingerprint = (firstException.type || '') + ':' + (firstException.value || '');
|
||
|
||
// 冷却检查:避免短时间内反复弹窗
|
||
const now = Date.now();
|
||
if (now - lastReportDialogTime < COOLDOWN_MS) {
|
||
return { shouldShow: false, fingerprint };
|
||
}
|
||
|
||
// 同一错误不重复弹窗
|
||
if (fingerprint === lastReportDialogFingerprint) {
|
||
return { shouldShow: false, fingerprint };
|
||
}
|
||
|
||
return { shouldShow: true, fingerprint };
|
||
}
|
||
|
||
function reset() {
|
||
lastReportDialogTime = 0;
|
||
lastReportDialogFingerprint = '';
|
||
}
|
||
|
||
function simulateDialogShown(fingerprint) {
|
||
lastReportDialogTime = Date.now();
|
||
lastReportDialogFingerprint = fingerprint;
|
||
}
|
||
|
||
return { shouldShowReportDialog, reset, simulateDialogShown };
|
||
}
|
||
|
||
// ===================================
|
||
// 测试用例
|
||
// ===================================
|
||
|
||
describe('Sentry 错误反馈弹窗', () => {
|
||
describe('shouldShowReportDialog 冷却机制', () => {
|
||
it('非生产环境不弹窗', () => {
|
||
const { shouldShowReportDialog } = createShouldShowReportDialog({ isDev: true });
|
||
const event = {
|
||
exception: { values: [{ type: 'Error', value: 'test error' }] },
|
||
};
|
||
const result = shouldShowReportDialog(event);
|
||
expect(result.shouldShow).toBe(false);
|
||
});
|
||
|
||
it('非异常事件不弹窗', () => {
|
||
const { shouldShowReportDialog } = createShouldShowReportDialog();
|
||
const event = { message: 'test message' };
|
||
const result = shouldShowReportDialog(event);
|
||
expect(result.shouldShow).toBe(false);
|
||
});
|
||
|
||
it('空事件对象不弹窗', () => {
|
||
const { shouldShowReportDialog } = createShouldShowReportDialog();
|
||
expect(shouldShowReportDialog(null).shouldShow).toBe(false);
|
||
expect(shouldShowReportDialog(undefined).shouldShow).toBe(false);
|
||
expect(shouldShowReportDialog({}).shouldShow).toBe(false);
|
||
expect(shouldShowReportDialog('string').shouldShow).toBe(false);
|
||
expect(shouldShowReportDialog(123).shouldShow).toBe(false);
|
||
});
|
||
|
||
it('无 exception.values 的事件不弹窗', () => {
|
||
const { shouldShowReportDialog } = createShouldShowReportDialog();
|
||
const event = { exception: {} };
|
||
expect(shouldShowReportDialog(event).shouldShow).toBe(false);
|
||
});
|
||
|
||
it('空 exception.values 数组不弹窗', () => {
|
||
const { shouldShowReportDialog } = createShouldShowReportDialog();
|
||
const event = { exception: { values: [] } };
|
||
expect(shouldShowReportDialog(event).shouldShow).toBe(false);
|
||
});
|
||
|
||
it('首个异常事件调用弹窗', () => {
|
||
const { shouldShowReportDialog } = createShouldShowReportDialog();
|
||
const event = {
|
||
exception: { values: [{ type: 'Error', value: 'test error' }] },
|
||
};
|
||
const result = shouldShowReportDialog(event);
|
||
expect(result.shouldShow).toBe(true);
|
||
expect(result.fingerprint).toBe('Error:test error');
|
||
});
|
||
|
||
it('60 秒内不同错误不重复弹窗', () => {
|
||
const { shouldShowReportDialog, simulateDialogShown } = createShouldShowReportDialog();
|
||
|
||
// 模拟第一个错误已触发弹窗
|
||
simulateDialogShown('Error:first error');
|
||
|
||
// 60 秒内不同错误不应弹窗
|
||
const event = {
|
||
exception: { values: [{ type: 'TypeError', value: 'second error' }] },
|
||
};
|
||
const result = shouldShowReportDialog(event);
|
||
expect(result.shouldShow).toBe(false);
|
||
});
|
||
|
||
it('60 秒后不同错误允许弹窗', () => {
|
||
const { shouldShowReportDialog, simulateDialogShown } = createShouldShowReportDialog();
|
||
|
||
// 模拟第一个错误已触发弹窗
|
||
simulateDialogShown('Error:first error');
|
||
|
||
// 快进 60 秒
|
||
jest.advanceTimersByTime(60000);
|
||
|
||
// 不同错误应弹窗
|
||
const event = {
|
||
exception: { values: [{ type: 'TypeError', value: 'second error' }] },
|
||
};
|
||
const result = shouldShowReportDialog(event);
|
||
expect(result.shouldShow).toBe(true);
|
||
expect(result.fingerprint).toBe('TypeError:second error');
|
||
});
|
||
|
||
it('同一 type:value 指纹不重复弹窗', () => {
|
||
const { shouldShowReportDialog, simulateDialogShown } = createShouldShowReportDialog();
|
||
|
||
// 模拟同一错误已触发弹窗
|
||
simulateDialogShown('Error:same error');
|
||
|
||
// 即使冷却时间已过,同一指纹也不弹窗
|
||
jest.advanceTimersByTime(60000);
|
||
|
||
const event = {
|
||
exception: { values: [{ type: 'Error', value: 'same error' }] },
|
||
};
|
||
const result = shouldShowReportDialog(event);
|
||
expect(result.shouldShow).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('showReportDialog 中文参数', () => {
|
||
it('调用 showReportDialog 传递中文参数', () => {
|
||
// 模拟 showReportDialog 的中文参数传递逻辑
|
||
// (与 sentry-loader.js / sentry-init.js 中的调用一致)
|
||
const params = {
|
||
eventId: 'test-123',
|
||
title: '问题反馈',
|
||
subtitle: '抱歉,发生了错误',
|
||
subtitle2: '您的反馈将帮助我们改进服务',
|
||
labelName: '名称',
|
||
labelEmail: '邮箱',
|
||
labelComments: '问题描述(选填)',
|
||
labelClose: '关闭',
|
||
labelSubmit: '提交反馈',
|
||
successMessage: '感谢您的反馈!',
|
||
};
|
||
mockShowReportDialog(params);
|
||
|
||
expect(mockShowReportDialog).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
eventId: 'test-123',
|
||
title: '问题反馈',
|
||
subtitle: '抱歉,发生了错误',
|
||
subtitle2: '您的反馈将帮助我们改进服务',
|
||
labelName: '名称',
|
||
labelEmail: '邮箱',
|
||
labelComments: '问题描述(选填)',
|
||
labelClose: '关闭',
|
||
labelSubmit: '提交反馈',
|
||
successMessage: '感谢您的反馈!',
|
||
})
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('SentryMock fallback', () => {
|
||
it('SentryMock 不抛错', () => {
|
||
const SentryMock = {
|
||
init: () => {},
|
||
captureException: () => {},
|
||
captureMessage: () => {},
|
||
addBreadcrumb: () => {},
|
||
setUser: () => {},
|
||
setTag: () => {},
|
||
setContext: () => {},
|
||
withScope: (cb) => cb({ setContext: () => {}, setTag: () => {} }),
|
||
browserTracingIntegration: () => ({}),
|
||
feedbackIntegration: () => ({}),
|
||
replayIntegration: () => ({}),
|
||
showReportDialog: () => {},
|
||
lastEventId: () => null,
|
||
};
|
||
|
||
expect(() => SentryMock.showReportDialog({ eventId: 'test' })).not.toThrow();
|
||
expect(() => SentryMock.lastEventId()).not.toThrow();
|
||
expect(() => SentryMock.captureException(new Error('test'))).not.toThrow();
|
||
expect(() => SentryMock.captureMessage('test')).not.toThrow();
|
||
expect(() => SentryMock.feedbackIntegration()).not.toThrow();
|
||
expect(() => SentryMock.browserTracingIntegration()).not.toThrow();
|
||
expect(() => SentryMock.replayIntegration()).not.toThrow();
|
||
});
|
||
|
||
it('SentryMock lastEventId 返回 null', () => {
|
||
const SentryMock = {
|
||
lastEventId: () => null,
|
||
};
|
||
expect(SentryMock.lastEventId()).toBeNull();
|
||
// 保留 mockLastEventId 引用,避免未使用告警(后续可对接真实 loader 导出)
|
||
expect(mockLastEventId()).toBe('test-event-id');
|
||
});
|
||
});
|
||
|
||
describe('beforeSend 弹窗错误容错', () => {
|
||
it('showReportDialog 抛错不影响 beforeSend 返回原事件', () => {
|
||
const errorEvent = {
|
||
event_id: 'test-event-123',
|
||
exception: { values: [{ type: 'Error', value: 'test error' }] },
|
||
};
|
||
|
||
// 创建会抛错的 mock
|
||
const throwingMock = jest.fn(() => {
|
||
throw new Error('弹窗加载失败');
|
||
});
|
||
|
||
// 模拟 beforeSend 中的弹窗调用逻辑(与 sentry-loader.js / sentry-init.js 一致)
|
||
let beforeSendResult;
|
||
try {
|
||
throwingMock({ eventId: errorEvent.event_id });
|
||
} catch (e) {
|
||
// 忽略弹窗错误(与源码一致)
|
||
}
|
||
beforeSendResult = errorEvent;
|
||
|
||
// 验证原事件未被修改且正常返回
|
||
expect(beforeSendResult).toBe(errorEvent);
|
||
expect(beforeSendResult.event_id).toBe('test-event-123');
|
||
expect(throwingMock).toHaveBeenCalledWith({ eventId: 'test-event-123' });
|
||
});
|
||
});
|
||
});
|