diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts
index 3a79dfe55f..66feb65926 100644
--- a/src/server/firefox/ffPage.ts
+++ b/src/server/firefox/ffPage.ts
@@ -228,10 +228,9 @@ export class FFPage implements PageDelegate {
}
_onUncaughtError(params: Protocol.Page.uncaughtErrorPayload) {
- const {name, message} = splitErrorMessage(params.message);
-
+ const { name, message } = splitErrorMessage(params.message);
const error = new Error(message);
- error.stack = params.stack;
+ error.stack = params.message + '\n' + params.stack.split('\n').filter(Boolean).map(a => a.replace(/([^@]*)@(.*)/, ' at $1 ($2)')).join('\n');
error.name = name;
this._page.emit(Page.Events.PageError, error);
}
diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts
index 7d26ff21d3..d70c0f7e12 100644
--- a/src/server/trace/viewer/traceViewer.ts
+++ b/src/server/trace/viewer/traceViewer.ts
@@ -120,13 +120,14 @@ export class TraceViewer {
const urlPrefix = await this._server.start();
const traceViewerPlaywright = createPlaywright(true);
- const args = [
+ const traceViewerBrowser = isUnderTest() ? 'chromium' : this._browserName;
+ const args = traceViewerBrowser === 'chromium' ? [
'--app=data:text/html,',
'--window-size=1280,800'
- ];
+ ] : [];
if (isUnderTest())
args.push(`--remote-debugging-port=0`);
- const context = await traceViewerPlaywright[this._browserName as 'chromium'].launchPersistentContext(internalCallMetadata(), '', {
+ const context = await traceViewerPlaywright[traceViewerBrowser as 'chromium'].launchPersistentContext(internalCallMetadata(), '', {
// TODO: store language in the trace.
sdkLanguage: 'javascript',
args,
diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts
index 9218cc2758..c3191461d8 100644
--- a/src/server/webkit/wkPage.ts
+++ b/src/server/webkit/wkPage.ts
@@ -502,16 +502,21 @@ export class WKPage implements PageDelegate {
return;
}
if (level === 'error' && source === 'javascript') {
- const {name, message} = splitErrorMessage(text);
- const error = new Error(message);
+ const { name, message } = splitErrorMessage(text);
+
+ let stack: string;
if (event.message.stackTrace) {
- error.stack = event.message.stackTrace.map(callFrame => {
- return `${callFrame.functionName}@${callFrame.url}:${callFrame.lineNumber}:${callFrame.columnNumber}`;
+ stack = text + '\n' + event.message.stackTrace.map(callFrame => {
+ return ` at ${callFrame.functionName || 'unknown'} (${callFrame.url}:${callFrame.lineNumber}:${callFrame.columnNumber})`;
}).join('\n');
} else {
- error.stack = '';
+ stack = '';
}
+
+ const error = new Error(message);
+ error.stack = stack;
error.name = name;
+
this._page.emit(Page.Events.PageError, error);
return;
}
diff --git a/src/web/traceViewer/ui/consoleTab.css b/src/web/traceViewer/ui/consoleTab.css
new file mode 100644
index 0000000000..5e84022105
--- /dev/null
+++ b/src/web/traceViewer/ui/consoleTab.css
@@ -0,0 +1,74 @@
+/*
+ Copyright (c) Microsoft Corporation.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+
+.console-tab {
+ flex: auto;
+ line-height: 16px;
+ white-space: pre;
+ overflow: auto;
+ padding-top: 3px;
+ user-select: text;
+}
+
+.console-line {
+ flex: none;
+ padding: 3px 0 3px 3px;
+ align-items: center;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+}
+
+.console-line.error {
+ background: #fff0f0;
+ border-top-color: #ffd6d6;
+ border-bottom-color: #ffd6d6;
+ color: red;
+}
+
+.console-line.warning {
+ background: #fffbe5;
+ border-top-color: #fff5c2;
+ border-bottom-color: #fff5c2;
+}
+
+.console-line .codicon {
+ padding: 0 2px 0 3px;
+ position: relative;
+ flex: none;
+ top: 1px;
+}
+
+.console-line.warning .codicon {
+ color: darkorange;
+}
+
+.console-line-message {
+ white-space: initial;
+ word-break: break-word;
+ position: relative;
+ top: -2px;
+}
+
+.console-location {
+ padding-right: 3px;
+ float: right;
+}
+
+.console-stack {
+ white-space: pre-wrap;
+ margin: 3px;
+}
diff --git a/src/web/traceViewer/ui/consoleTab.tsx b/src/web/traceViewer/ui/consoleTab.tsx
new file mode 100644
index 0000000000..a16c619efa
--- /dev/null
+++ b/src/web/traceViewer/ui/consoleTab.tsx
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as React from 'react';
+import './consoleTab.css';
+import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
+import { ContextEntry } from '../../../server/trace/viewer/traceModel';
+import * as channels from '../../../protocol/channels';
+
+export const ConsoleTab: React.FunctionComponent<{
+ context: ContextEntry,
+ action: ActionTraceEvent | undefined,
+ nextAction: ActionTraceEvent | undefined,
+}> = ({ context, action, nextAction }) => {
+ const entries = React.useMemo(() => {
+ if (!action)
+ return [];
+ const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
+ for (const page of context.pages) {
+ for (const event of page.events) {
+ if (event.metadata.method !== 'console' && event.metadata.method !== 'pageError')
+ continue;
+ if (event.metadata.startTime < action.metadata.startTime || (nextAction && event.metadata.startTime >= nextAction.metadata.startTime))
+ continue;
+ if (event.metadata.method === 'console') {
+ const { guid } = event.metadata.params.message;
+ entries.push({ message: page.objects[guid] });
+ }
+ if (event.metadata.method === 'pageError')
+ entries.push({ error: event.metadata.params.error });
+ }
+ }
+ return entries;
+ }, [context, action]);
+
+ return
{
+ entries.map((entry, index) => {
+ const { message, error } = entry;
+ if (message) {
+ const url = message.location.url;
+ const filename = url ? url.substring(url.lastIndexOf('/') + 1) : '
';
+ return
+ {filename}:{message.location.lineNumber}
+
+ {message.text}
+
;
+ }
+ if (error) {
+ const { error: errorObject, value } = error;
+ if (errorObject) {
+ return
+
+
{errorObject.message}
+
{errorObject.stack}
+
;
+ } else {
+ return
+
+ {value}
+
;
+ }
+ }
+ return null;
+ })
+ } ;
+};
+
+function iconClass(message: channels.ConsoleMessageInitializer): string {
+ switch (message.type) {
+ case 'error': return 'error';
+ case 'warning': return 'warning';
+ }
+ return 'blank';
+}
diff --git a/src/web/traceViewer/ui/workbench.tsx b/src/web/traceViewer/ui/workbench.tsx
index b44cdc716d..c84336c395 100644
--- a/src/web/traceViewer/ui/workbench.tsx
+++ b/src/web/traceViewer/ui/workbench.tsx
@@ -28,6 +28,7 @@ import { SnapshotTab } from './snapshotTab';
import { LogsTab } from './logsTab';
import { SplitView } from '../../components/splitView';
import { useAsyncMemo } from './helpers';
+import { ConsoleTab } from './consoleTab';
export const Workbench: React.FunctionComponent<{
@@ -86,6 +87,7 @@ export const Workbench: React.FunctionComponent<{
},
+ { id: 'console', title: 'Console', render: () => },
{ id: 'source', title: 'Source', render: () => },
{ id: 'network', title: 'Network', render: () => },
]}/>
diff --git a/tests/assets/error.html b/tests/assets/error.html
index 26978c466b..9532d14cf7 100644
--- a/tests/assets/error.html
+++ b/tests/assets/error.html
@@ -11,8 +11,7 @@ function b() {
}
function c() {
- window.e = new Error('Fancy error!');
- throw window.e;
+ throw new Error('Fancy error!');
}
//# sourceURL=myscript.js
diff --git a/tests/page/page-event-pageerror.spec.ts b/tests/page/page-event-pageerror.spec.ts
index e31bad6091..c56ba845ed 100644
--- a/tests/page/page-event-pageerror.spec.ts
+++ b/tests/page/page-event-pageerror.spec.ts
@@ -18,17 +18,32 @@
import { test as it, expect } from './pageTest';
it('should fire', async ({page, server, browserName}) => {
+ const url = server.PREFIX + '/error.html';
const [error] = await Promise.all([
page.waitForEvent('pageerror'),
- page.goto(server.PREFIX + '/error.html'),
+ page.goto(url),
]);
expect(error.name).toBe('Error');
expect(error.message).toBe('Fancy error!');
- let stack = await page.evaluate(() => window['e'].stack);
- // Note that WebKit reports the stack of the 'throw' statement instead of the Error constructor call.
- if (browserName === 'webkit')
- stack = stack.replace('14:25', '15:19');
- expect(error.stack).toBe(stack);
+ if (browserName === 'chromium') {
+ expect(error.stack).toBe(`Error: Fancy error!
+ at c (myscript.js:14:11)
+ at b (myscript.js:10:5)
+ at a (myscript.js:6:5)
+ at myscript.js:3:1`);
+ } else if (browserName === 'webkit') {
+ expect(error.stack).toBe(`Error: Fancy error!
+ at c (${url}:14:36)
+ at b (${url}:10:6)
+ at a (${url}:6:6)
+ at global code (${url}:3:2)`);
+ } else if (browserName === 'firefox') {
+ expect(error.stack).toBe(`Error: Fancy error!
+ at c (myscript.js:14:11)
+ at b (myscript.js:10:5)
+ at a (myscript.js:6:5)
+ at (myscript.js:3:1)`);
+ }
});
it('should not receive console message for pageError', async ({ page, server, browserName }) => {
diff --git a/tests/chromium/trace-viewer/trace-viewer.spec.ts b/tests/trace-viewer/trace-viewer.spec.ts
similarity index 64%
rename from tests/chromium/trace-viewer/trace-viewer.spec.ts
rename to tests/trace-viewer/trace-viewer.spec.ts
index 5414a4e919..597508b418 100644
--- a/tests/chromium/trace-viewer/trace-viewer.spec.ts
+++ b/tests/trace-viewer/trace-viewer.spec.ts
@@ -15,10 +15,10 @@
*/
import path from 'path';
-import type { Browser, Page } from '../../../index';
-import { showTraceViewer } from '../../../lib/server/trace/viewer/traceViewer';
-import { playwrightTest } from '../../config/browserTest';
-import { expect } from '../../config/test-runner';
+import type { Browser, Page } from '../../index';
+import { showTraceViewer } from '../../lib/server/trace/viewer/traceViewer';
+import { playwrightTest } from '../config/browserTest';
+import { expect } from '../config/test-runner';
class TraceViewerPage {
constructor(public page: Page) {}
@@ -48,15 +48,30 @@ class TraceViewerPage {
const result = [...set];
return result.sort();
}
+
+ async consoleLines() {
+ await this.page.waitForSelector('.console-line-message:visible');
+ return await this.page.$$eval('.console-line-message:visible', ee => ee.map(e => e.textContent));
+ }
+
+ async consoleLineTypes() {
+ await this.page.waitForSelector('.console-line-message:visible');
+ return await this.page.$$eval('.console-line:visible', ee => ee.map(e => e.className));
+ }
+
+ async consoleStacks() {
+ await this.page.waitForSelector('.console-stack:visible');
+ return await this.page.$$eval('.console-stack:visible', ee => ee.map(e => e.textContent));
+ }
}
const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise }>({
- showTraceViewer: async ({ browserType, browserName, headless }, use) => {
+ showTraceViewer: async ({ playwright, browserName, headless }, use) => {
let browser: Browser;
let contextImpl: any;
await use(async (trace: string) => {
contextImpl = await showTraceViewer(trace, browserName, headless);
- browser = await browserType.connectOverCDP({ endpointURL: contextImpl._browser.options.wsEndpoint });
+ browser = await playwright.chromium.connectOverCDP({ endpointURL: contextImpl._browser.options.wsEndpoint });
return new TraceViewerPage(browser.contexts()[0].pages()[0]);
});
await browser.close();
@@ -66,12 +81,18 @@ const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise
let traceFile: string;
-test.beforeAll(async ({ browser }, workerInfo) => {
+test.beforeAll(async ({ browser, browserName }, workerInfo) => {
const context = await browser.newContext();
await context.tracing.start({ name: 'test', screenshots: true, snapshots: true });
const page = await context.newPage();
await page.goto('data:text/html,Hello world');
await page.setContent('');
+ await page.evaluate(() => {
+ console.log('Info');
+ console.warn('Warning');
+ console.error('Error');
+ setTimeout(() => { throw new Error('Unhandled exception'); }, 0);
+ });
await page.click('"Click"');
await Promise.all([
page.waitForNavigation(),
@@ -83,7 +104,7 @@ test.beforeAll(async ({ browser }, workerInfo) => {
console.error('Error');
});
await page.close();
- traceFile = path.join(workerInfo.project.outputDir, 'trace.zip');
+ traceFile = path.join(workerInfo.project.outputDir, browserName, 'trace.zip');
await context.tracing.stop({ path: traceFile });
});
@@ -97,6 +118,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
expect(await traceViewer.actionTitles()).toEqual([
'page.goto',
'page.setContent',
+ 'page.evaluate',
'page.click',
'page.waitForNavigation',
'page.goto',
@@ -107,7 +129,6 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
test('should contain action log', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer(traceFile);
await traceViewer.selectAction('page.click');
-
const logLines = await traceViewer.logLines();
expect(logLines.length).toBeGreaterThan(10);
expect(logLines).toContain('attempting click action');
@@ -119,3 +140,18 @@ test('should render events', async ({ showTraceViewer }) => {
const events = await traceViewer.eventBars();
expect(events).toContain('page_console');
});
+
+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"');
+
+ const events = await traceViewer.consoleLines();
+ expect(events).toEqual(['Info', 'Warning', 'Error', 'Unhandled exception']);
+ const types = await traceViewer.consoleLineTypes();
+ expect(types).toEqual(['console-line log', 'console-line warning', 'console-line error', 'console-line error']);
+ const stacks = await traceViewer.consoleStacks();
+ expect(stacks.length).toBe(1);
+ expect(stacks[0]).toContain('Error: Unhandled exception');
+});