361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
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 <anonymous>@path:line:col
|
|
const stack = `Error
|
|
at <anonymous>@http://example.com/js/app.js:10:15`;
|
|
|
|
const frames = parseStackTrace(stack);
|
|
|
|
expect(frames).toHaveLength(1);
|
|
expect(frames[0].function).toBe('<anonymous>');
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
});
|