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) {
|
_onUncaughtError(params: Protocol.Page.uncaughtErrorPayload) {
|
||||||
const {name, message} = splitErrorMessage(params.message);
|
const { name, message } = splitErrorMessage(params.message);
|
||||||
|
|
||||||
const error = new Error(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;
|
error.name = name;
|
||||||
this._page.emit(Page.Events.PageError, error);
|
this._page.emit(Page.Events.PageError, error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,13 +120,14 @@ export class TraceViewer {
|
||||||
const urlPrefix = await this._server.start();
|
const urlPrefix = await this._server.start();
|
||||||
|
|
||||||
const traceViewerPlaywright = createPlaywright(true);
|
const traceViewerPlaywright = createPlaywright(true);
|
||||||
const args = [
|
const traceViewerBrowser = isUnderTest() ? 'chromium' : this._browserName;
|
||||||
|
const args = traceViewerBrowser === 'chromium' ? [
|
||||||
'--app=data:text/html,',
|
'--app=data:text/html,',
|
||||||
'--window-size=1280,800'
|
'--window-size=1280,800'
|
||||||
];
|
] : [];
|
||||||
if (isUnderTest())
|
if (isUnderTest())
|
||||||
args.push(`--remote-debugging-port=0`);
|
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.
|
// TODO: store language in the trace.
|
||||||
sdkLanguage: 'javascript',
|
sdkLanguage: 'javascript',
|
||||||
args,
|
args,
|
||||||
|
|
|
||||||
|
|
@ -502,16 +502,21 @@ export class WKPage implements PageDelegate {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (level === 'error' && source === 'javascript') {
|
if (level === 'error' && source === 'javascript') {
|
||||||
const {name, message} = splitErrorMessage(text);
|
const { name, message } = splitErrorMessage(text);
|
||||||
const error = new Error(message);
|
|
||||||
|
let stack: string;
|
||||||
if (event.message.stackTrace) {
|
if (event.message.stackTrace) {
|
||||||
error.stack = event.message.stackTrace.map(callFrame => {
|
stack = text + '\n' + event.message.stackTrace.map(callFrame => {
|
||||||
return `${callFrame.functionName}@${callFrame.url}:${callFrame.lineNumber}:${callFrame.columnNumber}`;
|
return ` at ${callFrame.functionName || 'unknown'} (${callFrame.url}:${callFrame.lineNumber}:${callFrame.columnNumber})`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
} else {
|
} else {
|
||||||
error.stack = '';
|
stack = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const error = new Error(message);
|
||||||
|
error.stack = stack;
|
||||||
error.name = name;
|
error.name = name;
|
||||||
|
|
||||||
this._page.emit(Page.Events.PageError, error);
|
this._page.emit(Page.Events.PageError, error);
|
||||||
return;
|
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 { LogsTab } from './logsTab';
|
||||||
import { SplitView } from '../../components/splitView';
|
import { SplitView } from '../../components/splitView';
|
||||||
import { useAsyncMemo } from './helpers';
|
import { useAsyncMemo } from './helpers';
|
||||||
|
import { ConsoleTab } from './consoleTab';
|
||||||
|
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
|
|
@ -86,6 +87,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
<SnapshotTab action={selectedAction} snapshotSize={snapshotSize} />
|
<SnapshotTab action={selectedAction} snapshotSize={snapshotSize} />
|
||||||
<TabbedPane tabs={[
|
<TabbedPane tabs={[
|
||||||
{ id: 'logs', title: 'Log', render: () => <LogsTab action={selectedAction} /> },
|
{ 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: 'source', title: 'Source', render: () => <SourceTab action={selectedAction} /> },
|
||||||
{ id: 'network', title: 'Network', render: () => <NetworkTab context={context} action={selectedAction} nextAction={nextAction}/> },
|
{ id: 'network', title: 'Network', render: () => <NetworkTab context={context} action={selectedAction} nextAction={nextAction}/> },
|
||||||
]}/>
|
]}/>
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ function b() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function c() {
|
function c() {
|
||||||
window.e = new Error('Fancy error!');
|
throw new Error('Fancy error!');
|
||||||
throw window.e;
|
|
||||||
}
|
}
|
||||||
//# sourceURL=myscript.js
|
//# sourceURL=myscript.js
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,32 @@
|
||||||
import { test as it, expect } from './pageTest';
|
import { test as it, expect } from './pageTest';
|
||||||
|
|
||||||
it('should fire', async ({page, server, browserName}) => {
|
it('should fire', async ({page, server, browserName}) => {
|
||||||
|
const url = server.PREFIX + '/error.html';
|
||||||
const [error] = await Promise.all([
|
const [error] = await Promise.all([
|
||||||
page.waitForEvent('pageerror'),
|
page.waitForEvent('pageerror'),
|
||||||
page.goto(server.PREFIX + '/error.html'),
|
page.goto(url),
|
||||||
]);
|
]);
|
||||||
expect(error.name).toBe('Error');
|
expect(error.name).toBe('Error');
|
||||||
expect(error.message).toBe('Fancy error!');
|
expect(error.message).toBe('Fancy error!');
|
||||||
let stack = await page.evaluate(() => window['e'].stack);
|
if (browserName === 'chromium') {
|
||||||
// Note that WebKit reports the stack of the 'throw' statement instead of the Error constructor call.
|
expect(error.stack).toBe(`Error: Fancy error!
|
||||||
if (browserName === 'webkit')
|
at c (myscript.js:14:11)
|
||||||
stack = stack.replace('14:25', '15:19');
|
at b (myscript.js:10:5)
|
||||||
expect(error.stack).toBe(stack);
|
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 }) => {
|
it('should not receive console message for pageError', async ({ page, server, browserName }) => {
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { Browser, Page } from '../../../index';
|
import type { Browser, Page } from '../../index';
|
||||||
import { showTraceViewer } from '../../../lib/server/trace/viewer/traceViewer';
|
import { showTraceViewer } from '../../lib/server/trace/viewer/traceViewer';
|
||||||
import { playwrightTest } from '../../config/browserTest';
|
import { playwrightTest } from '../config/browserTest';
|
||||||
import { expect } from '../../config/test-runner';
|
import { expect } from '../config/test-runner';
|
||||||
|
|
||||||
class TraceViewerPage {
|
class TraceViewerPage {
|
||||||
constructor(public page: Page) {}
|
constructor(public page: Page) {}
|
||||||
|
|
@ -48,15 +48,30 @@ class TraceViewerPage {
|
||||||
const result = [...set];
|
const result = [...set];
|
||||||
return result.sort();
|
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> }>({
|
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 browser: Browser;
|
||||||
let contextImpl: any;
|
let contextImpl: any;
|
||||||
await use(async (trace: string) => {
|
await use(async (trace: string) => {
|
||||||
contextImpl = await showTraceViewer(trace, browserName, headless);
|
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]);
|
return new TraceViewerPage(browser.contexts()[0].pages()[0]);
|
||||||
});
|
});
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|
@ -66,12 +81,18 @@ const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise
|
||||||
|
|
||||||
let traceFile: string;
|
let traceFile: string;
|
||||||
|
|
||||||
test.beforeAll(async ({ browser }, workerInfo) => {
|
test.beforeAll(async ({ browser, browserName }, workerInfo) => {
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
await context.tracing.start({ name: 'test', screenshots: true, snapshots: true });
|
await context.tracing.start({ name: 'test', screenshots: true, snapshots: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.goto('data:text/html,<html>Hello world</html>');
|
await page.goto('data:text/html,<html>Hello world</html>');
|
||||||
await page.setContent('<button>Click</button>');
|
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 page.click('"Click"');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
|
|
@ -83,7 +104,7 @@ test.beforeAll(async ({ browser }, workerInfo) => {
|
||||||
console.error('Error');
|
console.error('Error');
|
||||||
});
|
});
|
||||||
await page.close();
|
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 });
|
await context.tracing.stop({ path: traceFile });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -97,6 +118,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
expect(await traceViewer.actionTitles()).toEqual([
|
expect(await traceViewer.actionTitles()).toEqual([
|
||||||
'page.goto',
|
'page.goto',
|
||||||
'page.setContent',
|
'page.setContent',
|
||||||
|
'page.evaluate',
|
||||||
'page.click',
|
'page.click',
|
||||||
'page.waitForNavigation',
|
'page.waitForNavigation',
|
||||||
'page.goto',
|
'page.goto',
|
||||||
|
|
@ -107,7 +129,6 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
test('should contain action log', async ({ showTraceViewer }) => {
|
test('should contain action log', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer(traceFile);
|
||||||
await traceViewer.selectAction('page.click');
|
await traceViewer.selectAction('page.click');
|
||||||
|
|
||||||
const logLines = await traceViewer.logLines();
|
const logLines = await traceViewer.logLines();
|
||||||
expect(logLines.length).toBeGreaterThan(10);
|
expect(logLines.length).toBeGreaterThan(10);
|
||||||
expect(logLines).toContain('attempting click action');
|
expect(logLines).toContain('attempting click action');
|
||||||
|
|
@ -119,3 +140,18 @@ test('should render events', async ({ showTraceViewer }) => {
|
||||||
const events = await traceViewer.eventBars();
|
const events = await traceViewer.eventBars();
|
||||||
expect(events).toContain('page_console');
|
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