- 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
295 lines
9.0 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
}); |