Files
novalon-website/src/components/analytics/GlobalErrorTracker.test.tsx
T
zhangxiang 602ed6a671 test(hooks): finalize mutation test improvements and release acceptance
- Raise use-swipe-gesture mutation score to 66.13% (target 65%+)
- Maintain use-reduced-motion mutation score at 76.32% (target 50%+)
- Fix use-reduced-motion.ts ESLint set-state-in-effect warning
- Add UJ-10 deep searcher journey (category browse → article read → content discovery)
- Add 40 new test files (analytics, detail, sections, ui, lib components)
- Update test-strategy-plan.md to v2.0 (sync test count to 1591)
- Sync README.md with final release metrics

Quality gates: TS 0 errors, ESLint 0 errors, 121 suites / 1591 tests passed
2026-08-02 19:39:27 +08:00

295 lines
9.0 KiB
TypeScript

// @ts-nocheck
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
// ─── PromiseRejectionEvent Polyfill ──────────────────────────────────────
// jsdom does not define PromiseRejectionEvent, so we create it manually.
if (typeof PromiseRejectionEvent === 'undefined') {
(globalThis as any).PromiseRejectionEvent = class PromiseRejectionEvent
extends Event
{
reason: unknown;
promise: Promise<unknown>;
constructor(
type: string,
options: { reason: unknown; promise: Promise<unknown> }
) {
super(type, { cancelable: true });
this.reason = options.reason;
this.promise = options.promise;
}
};
}
// ─── Mocks ───────────────────────────────────────────────────────────────
const mockTrackError = jest.fn();
jest.mock('@/lib/analytics', () => ({
trackError: (...args: unknown[]) => mockTrackError(...args),
}));
// ─── Helpers ─────────────────────────────────────────────────────────────
function dispatchErrorEvent(
message: string,
filename?: string,
lineno?: number,
colno?: number
): ErrorEvent {
const event = new ErrorEvent('error', {
message,
filename: filename || 'https://example.com/app.js',
lineno: lineno || 10,
colno: colno || 1,
error: new Error(message),
bubbles: true,
cancelable: true,
});
window.dispatchEvent(event);
return event;
}
function dispatchUnhandledRejection(reason: unknown): PromiseRejectionEvent {
const event = new PromiseRejectionEvent('unhandledrejection', {
reason,
promise: Promise.reject(reason),
});
event.promise.catch(() => {}); // Suppress Node.js unhandled rejection warning
window.dispatchEvent(event);
return event;
}
function dispatchResourceError() {
const event = new Event('error', {
bubbles: true,
cancelable: true,
});
document.dispatchEvent(event);
return event;
}
// ─── Tests ───────────────────────────────────────────────────────────────
describe('GlobalErrorTracker', () => {
beforeEach(() => {
jest.clearAllMocks();
(process.env as Record<string, string>).NODE_ENV = 'test';
});
it('should return null (render nothing)', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
const { container } = render(<GlobalErrorTracker />);
expect(container.innerHTML).toBe('');
});
describe('JavaScript errors', () => {
it('should track JavaScript errors', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
dispatchErrorEvent('Something went wrong', 'https://example.com/app.js', 42, 5);
expect(mockTrackError).toHaveBeenCalledWith(
'javascript_error',
'Something went wrong',
false,
{
filename: 'https://example.com/app.js',
lineno: 42,
colno: 5,
stack: expect.any(String),
}
);
});
it('should include error stack trace truncated to 500 chars', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
const longStack = 'Error: test\n' + ' at fn (file.js:1:2)\n'.repeat(200);
const event = new ErrorEvent('error', {
message: 'Custom error',
filename: 'test.js',
lineno: 1,
colno: 1,
error: { stack: longStack },
});
window.dispatchEvent(event);
expect(mockTrackError).toHaveBeenCalledWith(
'javascript_error',
'Custom error',
false,
{
filename: 'test.js',
lineno: 1,
colno: 1,
stack: longStack.slice(0, 500),
}
);
});
});
describe('Unhandled promise rejections', () => {
it('should track unhandled promise rejections', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
dispatchUnhandledRejection(new Error('Promise failed'));
expect(mockTrackError).toHaveBeenCalledWith(
'unhandled_promise_rejection',
'Promise failed',
false,
{
reason_type: 'Error',
stack: expect.any(String),
}
);
});
it('should handle rejection with string reason', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
dispatchUnhandledRejection('string error reason');
expect(mockTrackError).toHaveBeenCalledWith(
'unhandled_promise_rejection',
'string error reason',
false,
{
reason_type: 'String',
stack: '',
}
);
});
it('should handle rejection with null reason', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
dispatchUnhandledRejection(null);
expect(mockTrackError).toHaveBeenCalledWith(
'unhandled_promise_rejection',
'null',
false,
{
reason_type: 'Unknown',
stack: '',
}
);
});
it('should handle rejection with undefined reason', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
dispatchUnhandledRejection(undefined);
// String(undefined) returns "undefined" which is truthy,
// so the message becomes "undefined" rather than the fallback
expect(mockTrackError).toHaveBeenCalledWith(
'unhandled_promise_rejection',
'undefined',
false,
{
reason_type: 'Unknown',
stack: '',
}
);
});
});
describe('Resource loading errors', () => {
it('should track resource loading errors from document', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
dispatchResourceError();
expect(mockTrackError).toHaveBeenCalledWith(
'resource_loading_error',
'Failed to load resource',
false
);
});
});
describe('Ignored errors', () => {
const ignoredMessages = [
'ResizeObserver loop limit exceeded',
'Script error',
'NetworkError: Failed to fetch',
'Loading CSS chunk main failed',
'Loading chunk 123 failed',
'Failed to fetch dynamically imported module',
'Non-Error promise rejection captured',
'webkit.messageHandlers is not defined',
'_AutofillCallbackHandler error',
];
ignoredMessages.forEach((msg) => {
it(`should ignore: "${msg}"`, () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
dispatchErrorEvent(msg);
expect(mockTrackError).not.toHaveBeenCalled();
});
});
it('should ignore promise rejections with ignored patterns', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
render(<GlobalErrorTracker />);
dispatchUnhandledRejection(new Error('ResizeObserver loop limit exceeded'));
expect(mockTrackError).not.toHaveBeenCalled();
});
});
describe('Cleanup', () => {
it('should remove event listeners on unmount', () => {
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
const documentRemoveEventListenerSpy = jest.spyOn(document, 'removeEventListener');
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
const { unmount } = render(<GlobalErrorTracker />);
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith('error', expect.any(Function), true);
expect(removeEventListenerSpy).toHaveBeenCalledWith('unhandledrejection', expect.any(Function));
expect(documentRemoveEventListenerSpy).toHaveBeenCalledWith('error', expect.any(Function), true);
removeEventListenerSpy.mockRestore();
documentRemoveEventListenerSpy.mockRestore();
});
it('should stop tracking errors after unmount', () => {
const { GlobalErrorTracker } = require('./GlobalErrorTracker');
const { unmount } = render(<GlobalErrorTracker />);
unmount();
// After unmount, the event listener is removed.
// Dispatching a simple error event should not trigger trackError.
// We avoid passing `error: new Error(...)` to prevent jsdom
// from reporting it as an unhandled exception.
window.dispatchEvent(
new ErrorEvent('error', {
message: 'Error after unmount',
})
);
expect(mockTrackError).not.toHaveBeenCalled();
});
});
});