Skip to content

Commit 609bc4c

Browse files
authored
chore: add stack trace utilities and tests (#2371)
1 parent 1e2b464 commit 609bc4c

File tree

4 files changed

+104
-29
lines changed

4 files changed

+104
-29
lines changed

src/debug/sourceMap.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616

1717
import * as fs from 'fs';
1818
import * as util from 'util';
19-
import * as path from 'path';
20-
21-
// NOTE: update this to point to playwright/lib when moving this file.
22-
const PLAYWRIGHT_LIB_PATH = path.normalize(path.join(__dirname, '..'));
19+
import { getCallerFilePath } from './stackTrace';
2320

2421
type Position = {
2522
line: number;
@@ -118,28 +115,3 @@ function advancePosition(position: Position, delta: Position) {
118115
column: delta.column + (delta.line ? 0 : position.column),
119116
};
120117
}
121-
122-
function getCallerFilePath(): string | null {
123-
const error = new Error();
124-
const stackFrames = (error.stack || '').split('\n').slice(1);
125-
// Find first stackframe that doesn't point to PLAYWRIGHT_LIB_PATH.
126-
for (let frame of stackFrames) {
127-
frame = frame.trim();
128-
if (!frame.startsWith('at '))
129-
return null;
130-
if (frame.endsWith(')')) {
131-
const from = frame.indexOf('(');
132-
frame = frame.substring(from + 1, frame.length - 1);
133-
} else {
134-
frame = frame.substring('at '.length);
135-
}
136-
const match = frame.match(/^(?:async )?(.*):(\d+):(\d+)$/);
137-
if (!match)
138-
return null;
139-
const filePath = match[1];
140-
if (filePath.startsWith(PLAYWRIGHT_LIB_PATH))
141-
continue;
142-
return filePath;
143-
}
144-
return null;
145-
}

src/debug/stackTrace.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as path from 'path';
18+
19+
// NOTE: update this to point to playwright/lib when moving this file.
20+
const PLAYWRIGHT_LIB_PATH = path.normalize(path.join(__dirname, '..'));
21+
22+
type ParsedStackFrame = { filePath: string, functionName: string };
23+
24+
function parseStackFrame(frame: string): ParsedStackFrame | null {
25+
frame = frame.trim();
26+
if (!frame.startsWith('at '))
27+
return null;
28+
frame = frame.substring('at '.length);
29+
if (frame.startsWith('async '))
30+
frame = frame.substring('async '.length);
31+
let location: string;
32+
let functionName: string;
33+
if (frame.endsWith(')')) {
34+
const from = frame.indexOf('(');
35+
location = frame.substring(from + 1, frame.length - 1);
36+
functionName = frame.substring(0, from).trim();
37+
} else {
38+
location = frame;
39+
functionName = '';
40+
}
41+
const match = location.match(/^(?:async )?([^(]*):(\d+):(\d+)$/);
42+
if (!match)
43+
return null;
44+
const filePath = match[1];
45+
return { filePath, functionName };
46+
}
47+
48+
export function getCallerFilePath(ignorePrefix = PLAYWRIGHT_LIB_PATH): string | null {
49+
const error = new Error();
50+
const stackFrames = (error.stack || '').split('\n').slice(1);
51+
// Find first stackframe that doesn't point to ignorePrefix.
52+
for (const frame of stackFrames) {
53+
const parsed = parseStackFrame(frame);
54+
if (!parsed)
55+
return null;
56+
if (parsed.filePath.startsWith(ignorePrefix) || parsed.filePath === __filename)
57+
continue;
58+
return parsed.filePath;
59+
}
60+
return null;
61+
}
62+
63+
export function getCurrentApiCall(prefix = PLAYWRIGHT_LIB_PATH): string {
64+
const error = new Error();
65+
const stackFrames = (error.stack || '').split('\n').slice(1);
66+
// Find last stackframe that points to prefix - that should be the api call.
67+
let apiName: string = '';
68+
for (const frame of stackFrames) {
69+
const parsed = parseStackFrame(frame);
70+
if (!parsed || (!parsed.filePath.startsWith(prefix) && parsed.filePath !== __filename))
71+
break;
72+
apiName = parsed.functionName;
73+
}
74+
const parts = apiName.split('.');
75+
if (parts.length && parts[0].length) {
76+
parts[0] = parts[0][0].toLowerCase() + parts[0].substring(1);
77+
if (parts[0] === 'webKit')
78+
parts[0] = 'webkit';
79+
}
80+
return parts.join('.');
81+
}

test/fixtures.spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,20 @@ describe('Fixtures', function() {
177177
});
178178
});
179179
});
180+
181+
describe('StackTrace', () => {
182+
it('caller file path', async state => {
183+
const stackTrace = require(path.join(state.playwrightPath, 'lib', 'debug', 'stackTrace'));
184+
const callme = require('./fixtures/callback');
185+
const filePath = callme(() => {
186+
return stackTrace.getCallerFilePath(path.join(__dirname, 'fixtures') + path.sep);
187+
});
188+
expect(filePath).toBe(__filename);
189+
});
190+
it('api call', async state => {
191+
const stackTrace = require(path.join(state.playwrightPath, 'lib', 'debug', 'stackTrace'));
192+
const callme = require('./fixtures/callback');
193+
const apiCall = callme(stackTrace.getCurrentApiCall.bind(stackTrace, path.join(__dirname, 'fixtures') + path.sep));
194+
expect(apiCall).toBe('callme');
195+
});
196+
});

test/fixtures/callback.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function callme(cb) {
2+
return cb();
3+
}
4+
5+
module.exports = callme;

0 commit comments

Comments
 (0)