// @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; constructor( type: string, options: { reason: unknown; promise: Promise } ) { 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).NODE_ENV = 'test'; }); it('should return null (render nothing)', () => { const { GlobalErrorTracker } = require('./GlobalErrorTracker'); const { container } = render(); expect(container.innerHTML).toBe(''); }); describe('JavaScript errors', () => { it('should track JavaScript errors', () => { const { GlobalErrorTracker } = require('./GlobalErrorTracker'); render(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); dispatchErrorEvent(msg); expect(mockTrackError).not.toHaveBeenCalled(); }); }); it('should ignore promise rejections with ignored patterns', () => { const { GlobalErrorTracker } = require('./GlobalErrorTracker'); render(); 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(); 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(); 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(); }); }); });