feat(trace-viewer): render console messages (#7418)

This commit is contained in:
Pavel Feldman 2021-07-01 14:31:20 -07:00 committed by GitHub
parent 1771caee8b
commit b9b0faf120
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 246 additions and 28 deletions

View file

@ -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);
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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 <div className='console-tab'>{
entries.map((entry, index) => {
const { message, error } = entry;
if (message) {
const url = message.location.url;
const filename = url ? url.substring(url.lastIndexOf('/') + 1) : '<anonymous>';
return <div className={'console-line ' + message.type} key={index}>
<span className='console-location'>{filename}:{message.location.lineNumber}</span>
<span className={'codicon codicon-' + iconClass(message)}></span>
<span className='console-line-message'>{message.text}</span>
</div>;
}
if (error) {
const { error: errorObject, value } = error;
if (errorObject) {
return <div className='console-line error' key={index}>
<span className={'codicon codicon-error'}></span>
<span className='console-line-message'>{errorObject.message}</span>
<div className='console-stack'>{errorObject.stack}</div>
</div>;
} else {
return <div className='console-line error' key={index}>
<span className={'codicon codicon-error'}></span>
<span className='console-line-message'>{value}</span>
</div>;
}
}
return null;
})
}</div>;
};
function iconClass(message: channels.ConsoleMessageInitializer): string {
switch (message.type) {
case 'error': return 'error';
case 'warning': return 'warning';
}
return 'blank';
}

View file

@ -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<{
<SnapshotTab action={selectedAction} snapshotSize={snapshotSize} />
<TabbedPane tabs={[
{ id: 'logs', title: 'Log', render: () => <LogsTab action={selectedAction} /> },
{ id: 'console', title: 'Console', render: () => <ConsoleTab context={context} action={selectedAction} nextAction={nextAction}/> },
{ id: 'source', title: 'Source', render: () => <SourceTab action={selectedAction} /> },
{ id: 'network', title: 'Network', render: () => <NetworkTab context={context} action={selectedAction} nextAction={nextAction}/> },
]}/>

View file

@ -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
</script>

View file

@ -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 }) => {

View file

@ -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<TraceViewerPage> }>({
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,<html>Hello world</html>');
await page.setContent('<button>Click</button>');
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');
});