import { describe, it, expect, beforeEach } from 'vitest'; import { parseStackTrace } from '../src/utils/stacktrace'; describe('stacktrace', () => { // Mock location for jsdom environment beforeEach(() => { delete (global as any).location; }); describe('parseStackTrace', () => { it('should return empty array for undefined stack', () => { expect(parseStackTrace(undefined)).toEqual([]); }); it('should return empty array for empty string', () => { expect(parseStackTrace('')).toEqual([]); }); it('should parse standard Chrome/Node.js stack trace format', () => { // Stack trace: newest call first (Function1), then Function2, then Function3 // After reverse, order becomes Function3, Function2, Function1 const stack = `Error at Function3 (http://example.com/js/index.js:30:5) at Function2 (http://example.com/js/main.js:25:10) at Function1 (http://example.com/js/app.js:10:15)`; const frames = parseStackTrace(stack); expect(frames).toHaveLength(3); // After reverse, Function1 should be first (oldest call in the trace) expect(frames[0]).toEqual({ filename: 'http://example.com/js/app.js', function: 'Function1', lineno: 10, colno: 15, in_app: true, }); expect(frames[1]).toEqual({ filename: 'http://example.com/js/main.js', function: 'Function2', lineno: 25, colno: 10, in_app: true, }); expect(frames[2]).toEqual({ filename: 'http://example.com/js/index.js', function: 'Function3', lineno: 30, colno: 5, in_app: true, }); }); it('should parse V8 format with @ separator', () => { // V8 format requires 'at' prefix: at Function1@path:line:col const stack = `Error at Function1@http://example.com/js/app.js:10:15 at Function2@http://example.com/js/main.js:25:10`; const frames = parseStackTrace(stack); expect(frames).toHaveLength(2); // After reverse, order is reversed expect(frames[0]).toEqual({ filename: 'http://example.com/js/main.js', function: 'Function2', lineno: 25, colno: 10, in_app: true, }); expect(frames[1]).toEqual({ filename: 'http://example.com/js/app.js', function: 'Function1', lineno: 10, colno: 15, in_app: true, }); }); it('should parse URL-only format (no function name)', () => { const stack = `Error at http://example.com/js/app.js:10:15 at http://example.com/js/main.js:25:10`; const frames = parseStackTrace(stack); expect(frames).toHaveLength(2); expect(frames[0]).toEqual({ filename: 'http://example.com/js/main.js', lineno: 25, colno: 10, in_app: true, }); expect(frames[1]).toEqual({ filename: 'http://example.com/js/app.js', lineno: 10, colno: 15, in_app: true, }); }); it('should mark node_modules frames as in_app: false when captured', () => { // When captureNodeModules is true, node_modules frames are included const stack = `Error at Function1 (http://example.com/node_modules/dep.js:10:15) at Function2 (http://example.com/js/app.js:25:10)`; const frames = parseStackTrace(stack, { captureNodeModules: true }); // After reverse, order is Function2, Function1 expect(frames).toHaveLength(2); expect(frames[0].in_app).toBe(true); // Function2 (app code) expect(frames[1].in_app).toBe(false); // Function1 (node_modules) }); it('should skip node_modules frames when captureNodeModules is false', () => { const stack = `Error at Function1 (http://example.com/node_modules/dep.js:10:15) at Function2 (http://example.com/js/app.js:25:10) at Function3 (http://example.com/node_modules/other.js:30:5)`; const frames = parseStackTrace(stack, { captureNodeModules: false }); // After reverse: Function3, Function2, Function1, then filter node_modules expect(frames).toHaveLength(1); expect(frames[0].function).toBe('Function2'); }); it('should include node_modules when captureNodeModules is true', () => { const stack = `Error at Function1 (http://example.com/node_modules/dep.js:10:15) at Function2 (http://example.com/js/app.js:25:10)`; const frames = parseStackTrace(stack, { captureNodeModules: true }); expect(frames).toHaveLength(2); }); it('should not include column when captureColumn is false', () => { const stack = `Error at Function1 (http://example.com/js/app.js:10:15)`; const frames = parseStackTrace(stack, { captureColumn: false }); expect(frames).toHaveLength(1); expect(frames[0].colno).toBeUndefined(); }); it('should not apply relativePathOnly when origin does not match', () => { // In jsdom, location.origin would be 'http://localhost:3000' or similar // so example.com URLs won't match const stack = `Error at Function2 (http://other.com/js/main.js:25:10) at Function1 (http://example.com/js/app.js:10:15)`; const frames = parseStackTrace(stack, { relativePathOnly: true }); // URLs don't match location.origin, so they remain unchanged // After reverse: Function1, Function2 expect(frames[0].filename).toBe('http://example.com/js/app.js'); expect(frames[1].filename).toBe('http://other.com/js/main.js'); }); it('should limit frames with maxFrames option', () => { const stack = `Error at Function4 (http://example.com/js/index.js:40:0) at Function3 (http://example.com/js/util.js:35:0) at Function2 (http://example.com/js/main.js:25:10) at Function1 (http://example.com/js/app.js:10:15)`; // After reverse: Function1, Function2, Function3, Function4 // maxFrames=2 returns first 2: Function1, Function2 const frames = parseStackTrace(stack, { maxFrames: 2 }); expect(frames).toHaveLength(2); expect(frames[0].function).toBe('Function1'); expect(frames[1].function).toBe('Function2'); }); it('should return all frames when maxFrames is 0 or undefined', () => { const stack = `Error at Function1 (http://example.com/js/app.js:10:15) at Function2 (http://example.com/js/main.js:25:10)`; const frames1 = parseStackTrace(stack, { maxFrames: 0 }); const frames2 = parseStackTrace(stack, { maxFrames: undefined }); expect(frames1).toHaveLength(2); expect(frames2).toHaveLength(2); }); it('should handle anonymous functions in V8 format', () => { // V8 format requires 'at' prefix: at @path:line:col const stack = `Error at @http://example.com/js/app.js:10:15`; const frames = parseStackTrace(stack); expect(frames).toHaveLength(1); expect(frames[0].function).toBe(''); }); it('should handle stack with extra whitespace', () => { // Note: complex whitespace may cause regex to fail, test with standard format const stack = `Error at Function2 (http://example.com/js/main.js:25:10) at Function1 (http://example.com/js/app.js:10:15)`; const frames = parseStackTrace(stack); // After reverse: Function1, Function2 expect(frames).toHaveLength(2); expect(frames[0].function).toBe('Function1'); expect(frames[1].function).toBe('Function2'); }); it('should handle file:// URLs', () => { const stack = `Error at Function1 (file:///Users/name/project/app.js:10:15)`; const frames = parseStackTrace(stack); expect(frames).toHaveLength(1); expect(frames[0].filename).toBe('file:///Users/name/project/app.js'); expect(frames[0].in_app).toBe(true); }); it('should handle mixed format stacks', () => { const stack = `Error at Function1 (http://example.com/js/app.js:10:15) at Function2@http://example.com/js/main.js:25:10 at http://example.com/js/index.js:30:5`; const frames = parseStackTrace(stack); expect(frames).toHaveLength(3); }); it('should handle V8 format with captureColumn: false', () => { const stack = `Error at Function1@http://example.com/js/app.js:10:15`; const frames = parseStackTrace(stack, { captureColumn: false }); expect(frames).toHaveLength(1); expect(frames[0].colno).toBeUndefined(); expect(frames[0].function).toBe('Function1'); }); it('should handle URL-only format with captureColumn: false', () => { const stack = `Error at http://example.com/js/app.js:10:15`; const frames = parseStackTrace(stack, { captureColumn: false }); expect(frames).toHaveLength(1); expect(frames[0].colno).toBeUndefined(); }); it('should skip node_modules in V8 format', () => { const stack = `Error at Function1@http://example.com/node_modules/dep.js:10:15 at Function2@http://example.com/js/app.js:25:10`; const frames = parseStackTrace(stack, { captureNodeModules: false }); expect(frames).toHaveLength(1); expect(frames[0].function).toBe('Function2'); }); it('should skip node_modules in URL-only format', () => { const stack = `Error at http://example.com/node_modules/dep.js:10:15 at http://example.com/js/app.js:25:10`; const frames = parseStackTrace(stack, { captureNodeModules: false }); expect(frames).toHaveLength(1); }); it('should include node_modules in V8 format when captureNodeModules is true', () => { const stack = `Error at Function1@http://example.com/node_modules/dep.js:10:15 at Function2@http://example.com/js/app.js:25:10`; const frames = parseStackTrace(stack, { captureNodeModules: true }); expect(frames).toHaveLength(2); expect(frames[0].in_app).toBe(true); expect(frames[1].in_app).toBe(false); }); it('should include node_modules in URL-only format when captureNodeModules is true', () => { const stack = `Error at http://example.com/node_modules/dep.js:10:15 at http://example.com/js/app.js:25:10`; const frames = parseStackTrace(stack, { captureNodeModules: true }); expect(frames).toHaveLength(2); }); it('should apply relativePathOnly to V8 format', () => { // Set up location mock const mockLocation = { protocol: 'http:', host: 'example.com', }; (global as any).location = mockLocation; const stack = `Error at Function1@http://example.com/js/app.js:10:15 at Function2@http://example.com/js/main.js:25:10`; const frames = parseStackTrace(stack, { relativePathOnly: true }); expect(frames).toHaveLength(2); expect(frames[0].filename).toBe('/js/main.js'); expect(frames[1].filename).toBe('/js/app.js'); delete (global as any).location; }); it('should apply relativePathOnly to URL-only format', () => { const mockLocation = { protocol: 'http:', host: 'example.com', }; (global as any).location = mockLocation; const stack = `Error at http://example.com/js/app.js:10:15 at http://example.com/js/main.js:25:10`; const frames = parseStackTrace(stack, { relativePathOnly: true }); expect(frames).toHaveLength(2); expect(frames[0].filename).toBe('/js/main.js'); expect(frames[1].filename).toBe('/js/app.js'); delete (global as any).location; }); it('should return reversed frames (newest call first after reversal)', () => { // In standard stack traces, the first line after "Error" is the most recent call const stack = `Error at Function1 (http://example.com/js/app.js:10:15) at Function2 (http://example.com/js/main.js:25:10) at Function3 (http://example.com/js/index.js:30:5)`; const frames = parseStackTrace(stack); // After reverse of [Function1, Function2, Function3], we get [Function3, Function2, Function1] // This puts the oldest call first expect(frames[0].function).toBe('Function3'); expect(frames[1].function).toBe('Function2'); expect(frames[2].function).toBe('Function1'); }); }); });