chrome-devtools-frontend
Version:
Chrome DevTools UI
344 lines (297 loc) • 11.6 kB
text/typescript
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Console from './console.js';
const {urlString} = Platform.DevToolsPath;
const {parseSourcePositionsFromErrorStack} = Console.ErrorStackParser;
describe('ErrorStackParser', () => {
let runtimeModel;
let parseErrorStack: (stack: string) => Console.ErrorStackParser.ParsedErrorFrame[] | null;
const fileTestingUrl = urlString`file:///testing.js`;
beforeEach(() => {
// TODO(crbug/1280141): Remove complicated stubbing code once `parseSourcePositionsFromErrorStack`
// no longer needs a RuntimeModel.
runtimeModel = sinon.createStubInstance(SDK.RuntimeModel.RuntimeModel, {
target: sinon.createStubInstance(SDK.Target.Target, {
inspectedURL: urlString`http://www.example.org`,
}),
debuggerModel: sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel, {
scriptsForSourceURL: [],
}),
});
parseErrorStack = parseSourcePositionsFromErrorStack.bind(null, runtimeModel);
});
it('returns null for invalid strings', () => {
assert.isNull(parseErrorStack(''));
assert.isNull(parseErrorStack('foobar'));
});
it('returns null if there are no stack frames', () => {
assert.isNull(parseErrorStack('Error: bar'));
});
it('accepts stacks if the first word of any line after the first line is "at"', () => {
assert.isNotNull(parseErrorStack('!\nat foo'));
assert.isNotNull(parseErrorStack('ReferenceError:\n at foo'));
});
it('omits position information for frames it cannot parse', () => {
const frames = parseErrorStack(`Error: standard error
not a valid line
at file:///testing.js:42:5`);
assert.exists(frames);
assert.strictEqual(frames[1].line, ' not a valid line');
assert.isUndefined(frames[1].link);
});
it('returns null when encountering an invalid frame after a valid one', () => {
const frames = parseErrorStack(`Error: standard error
at foo (file:///testing.js:20:3)
not a valid line
at file:///testing.js:42:5`);
assert.isNull(frames);
});
it('returns null for invalid frame URLs', () => {
const frames = parseErrorStack(`Error: standard error
at foo (schemeWithColon::20:3)`);
assert.isNull(frames);
});
it('omits position information for anonymous scripts', () => {
const frames = parseErrorStack(`Error: standard error
at foo (<anonymous>:10:3)`);
assert.exists(frames);
assert.strictEqual(frames[1].line, ' at foo (<anonymous>:10:3)');
assert.isUndefined(frames[1].link);
});
it('detects URLs with line and column information in braces', () => {
const frames = parseErrorStack(`Error: standard error
at foo (file:///testing.js:10:3)`);
assert.exists(frames);
assert.deepEqual(frames[1].link, {
url: fileTestingUrl,
prefix: ' at foo (',
suffix: ')',
lineNumber: 9, // 0-based.
columnNumber: 2, // 0-based.
enclosedInBraces: true,
});
});
it('detects URLs without line or column information in braces', () => {
const frames = parseErrorStack(`Error: standard error
at foo (file:///testing.js)`);
assert.exists(frames);
assert.deepEqual(frames[1].link, {
url: fileTestingUrl,
prefix: ' at foo (',
suffix: ')',
lineNumber: undefined,
columnNumber: undefined,
enclosedInBraces: true,
});
});
it('detects URLs with line and column information without braces', () => {
const frames = parseErrorStack(`Error: standard error
at file:///testing.js:42:3`);
assert.exists(frames);
assert.deepEqual(frames[1].link, {
url: fileTestingUrl,
prefix: ' at ',
suffix: '',
lineNumber: 41, // 0-based.
columnNumber: 2, // 0-based.
enclosedInBraces: false,
});
});
it('detects URLs without braces with the "async" keyword present', () => {
const frames = parseErrorStack(`Error: standard error
at async file:///testing.js:42:3`);
assert.exists(frames);
assert.deepEqual(frames[1].link, {
url: fileTestingUrl,
prefix: ' at async ',
suffix: '',
lineNumber: 41, // 0-based.
columnNumber: 2, // 0-based.
enclosedInBraces: false,
});
});
it('detects URLs with parens', () => {
const url = urlString`http://localhost:5173/src/routes/(v2-routes)/project/+layout.ts?ts=12345`;
const frames = parseErrorStack(`ZodError:
at load (${url}:33:5)
at ${url}:1:1`);
assert.exists(frames);
assert.lengthOf(frames, 3);
assert.deepEqual(frames[1].link, {
url,
prefix: ' at load (',
suffix: ')',
lineNumber: 32, // 0-based.
columnNumber: 4, // 0-based.
enclosedInBraces: true,
});
assert.deepEqual(frames[2].link, {
url,
prefix: ' at ',
suffix: '',
lineNumber: 0, // 0-based.
columnNumber: 0, // 0-based.
enclosedInBraces: false,
});
});
it('correctly handles eval frames', () => {
const url = urlString`http://www.chromium.org/foo.js`;
const frames = parseErrorStack(`Error: MyError
at eval (eval at <anonymous> (${url}:42:1), <anonymous>:1:1)`);
assert.exists(frames);
assert.lengthOf(frames, 2);
assert.deepEqual(frames[1].link, {
url,
prefix: ' at eval (eval at <anonymous> (',
suffix: '), <anonymous>:1:1)',
lineNumber: 41, // 0-based.
columnNumber: 0, // 0-based.
enclosedInBraces: true,
});
});
it('uses the inspected target URL to complete relative URLs', () => {
const frames = parseErrorStack(`Error: standard error
at foo (testing.js:10:3)`);
assert.exists(frames);
assert.strictEqual(frames[1].link?.url, urlString`http://www.example.org/testing.js`);
});
it('uses the inspected target URL to complete relative URLs in eval frames', () => {
const frames = parseErrorStack(`Error: localObj.func
at Object.func (test.js:26:25)
at eval (eval at testFunction (inspected-page.html:29:11), <anonymous>:1:10)`);
assert.exists(frames);
assert.lengthOf(frames, 3);
assert.deepEqual(frames[2].link, {
url: urlString`http://www.example.org/inspected-page.html`,
prefix: ' at eval (eval at testFunction (',
suffix: '), <anonymous>:1:10)',
lineNumber: 28, // 0-based.
columnNumber: 10, // 0-based.
enclosedInBraces: true,
});
});
it('uses the inspected target URL to complete relative URLs with parens', () => {
const frames = parseErrorStack(`Error: wat
at foo (/(abc)/foo.js:2:3)
at async bar (/(abc)/foo.js:1:2)
at /(abc)/foo.js:10:20`);
assert.exists(frames);
assert.lengthOf(frames, 4);
assert.deepEqual(frames[1].link, {
url: urlString`http://www.example.org/(abc)/foo.js`,
prefix: ' at foo (',
suffix: ')',
lineNumber: 1, // 0-based.
columnNumber: 2, // 0-based.
enclosedInBraces: true,
});
assert.deepEqual(frames[2].link, {
url: urlString`http://www.example.org/(abc)/foo.js`,
prefix: ' at async bar (',
suffix: ')',
lineNumber: 0, // 0-based.
columnNumber: 1, // 0-based.
enclosedInBraces: true,
});
assert.deepEqual(frames[3].link, {
url: urlString`http://www.example.org/(abc)/foo.js`,
prefix: ' at ',
suffix: '',
lineNumber: 9, // 0-based.
columnNumber: 19, // 0-based.
enclosedInBraces: false,
});
});
describe('augmentErrorStackWithScriptIds', () => {
const sid = (id: string) => id as Protocol.Runtime.ScriptId;
it('sets the scriptId for matching frames', () => {
const parsedFrames = parseErrorStack(`Error: some error
at foo (http://example.com/a.js:6:3)
at bar (http://example.com/b.js:43:14)`);
assert.exists(parsedFrames);
const protocolFrames: Protocol.Runtime.CallFrame[] = [
{
url: 'http://example.com/a.js',
scriptId: sid('25'),
lineNumber: 5,
columnNumber: 2,
functionName: 'foo',
},
{
url: 'http://example.com/b.js',
scriptId: sid('30'),
lineNumber: 42,
columnNumber: 13,
functionName: 'bar',
},
];
Console.ErrorStackParser.augmentErrorStackWithScriptIds(parsedFrames, {callFrames: protocolFrames});
assert.strictEqual(parsedFrames[1].link?.scriptId, sid('25'));
assert.strictEqual(parsedFrames[2].link?.scriptId, sid('30'));
});
it('omits the scriptId for non-matching frames', () => {
const parsedFrames = parseErrorStack(`Error: some error
at http://example.com/a.js:6:3`);
assert.exists(parsedFrames);
const protocolFrames: Protocol.Runtime.CallFrame[] = [{
url: 'http://example.com/a.js',
scriptId: sid('25'),
lineNumber: 10,
columnNumber: 4,
functionName: 'foo',
}];
Console.ErrorStackParser.augmentErrorStackWithScriptIds(parsedFrames, {callFrames: protocolFrames});
assert.exists(parsedFrames[1].link);
assert.isUndefined(parsedFrames[1].link.scriptId);
});
it('handles different number or frames', () => {
const parsedFrames = parseErrorStack(`Error: some error
at foo (http://example.com/a.js:6:3)
at Array.forEach (<anonymous>)
at bar (http://example.com/b.js:43:14)`);
assert.exists(parsedFrames);
const protocolFrames: Protocol.Runtime.CallFrame[] = [
{
url: 'http://example.com/a.js',
scriptId: sid('25'),
lineNumber: 5,
columnNumber: 2,
functionName: 'foo',
},
{
url: 'http://example.com/b.js',
scriptId: sid('30'),
lineNumber: 42,
columnNumber: 13,
functionName: 'bar',
},
];
Console.ErrorStackParser.augmentErrorStackWithScriptIds(parsedFrames, {callFrames: protocolFrames});
assert.strictEqual(parsedFrames[1].link?.scriptId, sid('25'));
assert.isUndefined(parsedFrames[2].link);
assert.strictEqual(parsedFrames[3].link?.scriptId, sid('30'));
});
it('combines builtin frames', () => {
const parsedFrames = parseErrorStack(`Error: some error
at foo (http://example.com/a.js:6:3)
at Array.forEach (<anonymous>)
at JSON.parse (<anonymous>)
at bar (http://example.com/b.js:43:14)`);
assert.exists(parsedFrames);
assert.isUndefined(parsedFrames[0].link);
assert.isUndefined(parsedFrames[0].isCallFrame);
assert.strictEqual(parsedFrames[1].link?.url, urlString`http://example.com/a.js`);
assert.isTrue(parsedFrames[1].isCallFrame);
assert.isUndefined(parsedFrames[2].link);
assert.isTrue(parsedFrames[2].isCallFrame);
assert.strictEqual(
parsedFrames[2].line, ' at Array.forEach (<anonymous>)\n at JSON.parse (<anonymous>)');
assert.strictEqual(parsedFrames[3].link?.url, urlString`http://example.com/b.js`);
assert.isTrue(parsedFrames[3].isCallFrame);
});
});
});