feat(trace-viewer): render console messages (#7418)
This commit is contained in:
parent
1771caee8b
commit
b9b0faf120
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
74
src/web/traceViewer/ui/consoleTab.css
Normal file
74
src/web/traceViewer/ui/consoleTab.css
Normal 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;
|
||||
}
|
||||
87
src/web/traceViewer/ui/consoleTab.tsx
Normal file
87
src/web/traceViewer/ui/consoleTab.tsx
Normal 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';
|
||||
}
|
||||
|
|
@ -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}/> },
|
||||
]}/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
Loading…
Reference in a new issue