diff --git a/src/utils/stackTrace.ts b/src/utils/stackTrace.ts
index 5b9c66f3ee..917da9461d 100644
--- a/src/utils/stackTrace.ts
+++ b/src/utils/stackTrace.ts
@@ -31,21 +31,9 @@ export function rewriteErrorMessage(e: Error, newMessage: string): Error {
return e;
}
-const PW_LIB_DIRS = [
- 'playwright',
- 'playwright-chromium',
- 'playwright-firefox',
- 'playwright-webkit',
- path.join('@playwright', 'test'),
-].map(packageName => path.sep + packageName);
-
-const runnerNpmPkgLib = path.join('@playwright', 'test', 'lib', 'test');
-const runnerLib = path.join('lib', 'test');
-const runnerSrc = path.join('src', 'test');
-
-function includesFileInPlaywrightSubDir(subDir: string, fileName: string) {
- return PW_LIB_DIRS.map(p => path.join(p, subDir)).some(libDir => fileName.includes(libDir));
-}
+const ROOT_DIR = path.resolve(__dirname, '..', '..');
+const CLIENT_LIB = path.join(ROOT_DIR, 'lib', 'client');
+const CLIENT_SRC = path.join(ROOT_DIR, 'src', 'client');
export type ParsedStackTrace = {
frames: StackFrame[];
@@ -59,44 +47,52 @@ export function captureStackTrace(): ParsedStackTrace {
const error = new Error();
const stack = error.stack!;
Error.stackTraceLimit = stackTraceLimit;
- const frames: StackFrame[] = [];
- const frameTexts: string[] = [];
- const lines = stack.split('\n').reverse();
- let apiName = '';
- const isTesting = !!process.env.PWTEST_CLI_ALLOW_TEST_COMMAND || isUnderTest();
-
- for (const line of lines) {
+ const isTesting = isUnderTest();
+ type ParsedFrame = {
+ frame: StackFrame;
+ frameText: string;
+ inClient: boolean;
+ };
+ let parsedFrames = stack.split('\n').map(line => {
const frame = stackUtils.parseLine(line);
if (!frame || !frame.file)
- continue;
+ return null;
if (frame.file.startsWith('internal'))
- continue;
+ return null;
const fileName = path.resolve(process.cwd(), frame.file);
if (isTesting && fileName.includes(path.join('playwright', 'tests', 'config', 'coverage.js')))
- continue;
- if (isFilePartOfPlaywright(isTesting, fileName)) {
+ return null;
+ const parsed: ParsedFrame = {
+ frame: {
+ file: fileName,
+ line: frame.line,
+ column: frame.column,
+ function: frame.function,
+ },
+ frameText: line,
+ inClient: fileName.startsWith(CLIENT_LIB) || fileName.startsWith(CLIENT_SRC),
+ };
+ return parsed;
+ }).filter(frame => !!frame) as ParsedFrame[];
+
+ let apiName = '';
+ // Deepest transition between non-client code calling into client code
+ // is the api entry.
+ for (let i = 0; i < parsedFrames.length - 1; i++) {
+ if (parsedFrames[i].inClient && !parsedFrames[i + 1].inClient) {
+ const frame = parsedFrames[i].frame;
apiName = frame.function ? frame.function[0].toLowerCase() + frame.function.slice(1) : '';
+ parsedFrames = parsedFrames.slice(i + 1);
break;
}
- frameTexts.push(line);
- frames.push({
- file: fileName,
- line: frame.line,
- column: frame.column,
- function: frame.function,
- });
}
- frames.reverse();
- frameTexts.reverse();
- return { frames, frameTexts, apiName };
-}
-function isFilePartOfPlaywright(isTesting: boolean, fileName: string): boolean {
- const isPlaywrightTest = fileName.includes(runnerNpmPkgLib);
- const isLocalPlaywright = isTesting && (fileName.includes(runnerSrc) || fileName.includes(runnerLib));
- const isInPlaywright = (includesFileInPlaywrightSubDir('src', fileName) || includesFileInPlaywrightSubDir('lib', fileName));
- return !isPlaywrightTest && !isLocalPlaywright && isInPlaywright;
+ return {
+ frames: parsedFrames.map(p => p.frame),
+ frameTexts: parsedFrames.map(p => p.frameText),
+ apiName
+ };
}
export function splitErrorMessage(message: string): { name: string, message: string } {
diff --git a/src/web/traceViewer/ui/stackTrace.tsx b/src/web/traceViewer/ui/stackTrace.tsx
index bda6b0294f..0e1d3f34f2 100644
--- a/src/web/traceViewer/ui/stackTrace.tsx
+++ b/src/web/traceViewer/ui/stackTrace.tsx
@@ -26,6 +26,8 @@ export const StackTraceView: React.FunctionComponent<{
const frames = action?.metadata.stack || [];
return
{
frames.map((frame, index) => {
+ // Windows frames are E:\path\to\file
+ const pathSep = frame.file[1] === ':' ? '\\' : '/';
return
- {frame.file.split('/').pop()}
+ {frame.file.split(pathSep).pop()}
{':' + frame.line}
diff --git a/tests/trace-viewer/trace-viewer.spec.ts b/tests/trace-viewer/trace-viewer.spec.ts
index 2615edfaaa..7892bfd357 100644
--- a/tests/trace-viewer/trace-viewer.spec.ts
+++ b/tests/trace-viewer/trace-viewer.spec.ts
@@ -46,6 +46,14 @@ class TraceViewerPage {
await this.page.click(`.snapshot-tab .tab-label:has-text("${name}")`);
}
+ async showConsoleTab() {
+ await this.page.click('text="Console"');
+ }
+
+ async showSourceTab() {
+ await this.page.click('text="Source"');
+ }
+
async callLines() {
await this.page.waitForSelector('.call-line:visible');
return await this.page.$$eval('.call-line:visible', ee => ee.map(e => e.textContent));
@@ -78,6 +86,11 @@ class TraceViewerPage {
return await this.page.$$eval('.console-stack:visible', ee => ee.map(e => e.textContent));
}
+ async sourceStack() {
+ await this.page.waitForSelector('.stack-trace-frame:visible');
+ return await this.page.$$eval('.stack-trace-frame:visible', ee => ee.map(e => (e as HTMLElement).innerText.replace(/\s+/g, ' ')));
+ }
+
async snapshotSize() {
return this.page.$eval('.snapshot-container', e => {
const style = window.getComputedStyle(e);
@@ -102,7 +115,7 @@ const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise
let traceFile: string;
-test.beforeAll(async ({ browser, browserName }, workerInfo) => {
+test.beforeAll(async function recordTrace({ browser, browserName, browserType }, workerInfo) {
const context = await browser.newContext();
await context.tracing.start({ name: 'test', screenshots: true, snapshots: true });
const page = await context.newPage();
@@ -115,15 +128,27 @@ test.beforeAll(async ({ browser, browserName }, workerInfo) => {
setTimeout(() => { throw new Error('Unhandled exception'); }, 0);
return 'return ' + a;
}, { a: 'paramA', b: 4 });
- await page.click('"Click"');
+
+ async function doClick() {
+ await page.click('"Click"');
+ }
+ await doClick();
+
await Promise.all([
page.waitForNavigation(),
page.waitForTimeout(200).then(() => page.goto('data:text/html,Hello world 2'))
]);
await page.setViewportSize({ width: 500, height: 600 });
- await page.close();
- traceFile = path.join(workerInfo.project.outputDir, browserName, 'trace.zip');
- await context.tracing.stop({ path: traceFile });
+
+ // Go through instrumentation to exercise reentrant stack traces.
+ (browserType as any)._onWillCloseContext = async () => {
+ await page.hover('body');
+ await page.close();
+ traceFile = path.join(workerInfo.project.outputDir, browserName, 'trace.zip');
+ await context.tracing.stop({ path: traceFile });
+ };
+ await context.close();
+ (browserType as any)._onWillCloseContext = undefined;
});
test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => {
@@ -141,6 +166,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
'page.waitForNavigation',
'page.gotodata:text/html,Hello world 2',
'page.setViewportSize',
+ 'page.hoverbody',
]);
});
@@ -163,7 +189,7 @@ test('should render console', async ({ showTraceViewer, browserName }) => {
test.fixme(browserName === 'firefox', 'Firefox generates stray console message for page error');
const traceViewer = await showTraceViewer(traceFile);
await traceViewer.selectAction('page.evaluate');
- await traceViewer.page.click('"Console"');
+ await traceViewer.showConsoleTab();
const events = await traceViewer.consoleLines();
expect(events).toEqual(['Info', 'Warning', 'Error', 'Unhandled exception']);
@@ -203,3 +229,22 @@ test('should have correct snapshot size', async ({ showTraceViewer }) => {
await traceViewer.selectSnapshot('After');
expect(await traceViewer.snapshotSize()).toEqual({ width: '500px', height: '600px' });
});
+
+test('should have correct stack trace', async ({ showTraceViewer }) => {
+ const traceViewer = await showTraceViewer(traceFile);
+
+ await traceViewer.selectAction('page.click');
+ await traceViewer.showSourceTab();
+ const stack1 = await traceViewer.sourceStack();
+ expect(stack1.slice(0, 2)).toEqual([
+ 'doClick trace-viewer.spec.ts :133',
+ 'recordTrace trace-viewer.spec.ts :135',
+ ]);
+
+ await traceViewer.selectAction('page.hover');
+ await traceViewer.showSourceTab();
+ const stack2 = await traceViewer.sourceStack();
+ expect(stack2.slice(0, 1)).toEqual([
+ 'BrowserType.browserType._onWillCloseContext trace-viewer.spec.ts :145',
+ ]);
+});