feat(trace-viewer): add metainfo tab (#10205)

This commit is contained in:
Pavel Feldman 2021-11-09 22:13:51 -08:00 committed by GitHub
parent b2af576796
commit 03fee2f593
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 71 additions and 28 deletions

View File

@ -24,12 +24,15 @@ export type BrowserContextEventOptions = {
viewport?: Size, viewport?: Size,
deviceScaleFactor?: number, deviceScaleFactor?: number,
isMobile?: boolean, isMobile?: boolean,
userAgent?: string,
}; };
export type ContextCreatedTraceEvent = { export type ContextCreatedTraceEvent = {
version: number, version: number,
type: 'context-options', type: 'context-options',
browserName: string, browserName: string,
platform: string,
wallTime: number,
title?: string, title?: string,
options: BrowserContextEventOptions options: BrowserContextEventOptions
}; };

View File

@ -81,7 +81,9 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
version: VERSION, version: VERSION,
type: 'context-options', type: 'context-options',
browserName: this._context._browser.options.name, browserName: this._context._browser.options.name,
options: this._context._options options: this._context._options,
platform: process.platform,
wallTime: 0,
}; };
} }
@ -124,7 +126,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
this._appendTraceOperation(async () => { this._appendTraceOperation(async () => {
await mkdirIfNeeded(state.traceFile); await mkdirIfNeeded(state.traceFile);
await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title }) + '\n'); await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }) + '\n');
}); });
this._context.instrumentation.addListener(this); this._context.instrumentation.addListener(this);

View File

@ -21,6 +21,8 @@ export type ContextEntry = {
startTime: number; startTime: number;
endTime: number; endTime: number;
browserName: string; browserName: string;
platform?: string;
wallTime?: number;
title?: string; title?: string;
options: trace.BrowserContextEventOptions; options: trace.BrowserContextEventOptions;
pages: PageEntry[]; pages: PageEntry[];

View File

@ -104,6 +104,8 @@ export class TraceModel {
case 'context-options': { case 'context-options': {
this.contextEntry.browserName = event.browserName; this.contextEntry.browserName = event.browserName;
this.contextEntry.title = event.title; this.contextEntry.title = event.title;
this.contextEntry.platform = event.platform;
this.contextEntry.wallTime = event.wallTime;
this.contextEntry.options = event.options; this.contextEntry.options = event.options;
break; break;
} }

View File

@ -14,10 +14,6 @@
limitations under the License. limitations under the License.
*/ */
.action-list-title {
padding: 0 10px;
}
.action-list-content { .action-list-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -45,11 +45,6 @@ export const ActionList: React.FC<ActionListProps> = ({
}, [selectedAction, actionListRef]); }, [selectedAction, actionListRef]);
return <div className='action-list vbox'> return <div className='action-list vbox'>
<div className='action-list-title tab-strip'>
<div className='tab-element'>
<div className='tab-label'>Actions</div>
</div>
</div>
<div <div
className='action-list-content' className='action-list-content'
tabIndex={0} tabIndex={0}

View File

@ -47,11 +47,13 @@
} }
.call-line { .call-line {
padding: 0 0 2px 6px; padding: 4px 0 4px 6px;
flex: none; flex: none;
align-items: center; align-items: center;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
line-height: 18px;
white-space: nowrap;
} }
.call-line .datetime, .call-line .datetime,

View File

@ -34,6 +34,7 @@
font-size: 24px; font-size: 24px;
color: #666; color: #666;
font-weight: bold; font-weight: bold;
margin-bottom: 30px;
} }
.drop-target input { .drop-target input {
@ -45,7 +46,7 @@
background-color: rgb(0, 122, 204); background-color: rgb(0, 122, 204);
padding: 6px 4px; padding: 6px 4px;
border: none; border: none;
margin: 40px 0; margin: 30px 0;
cursor: pointer; cursor: pointer;
} }

View File

@ -28,6 +28,7 @@ import { CallTab } from './callTab';
import { SplitView } from '../../components/splitView'; import { SplitView } from '../../components/splitView';
import { ConsoleTab } from './consoleTab'; import { ConsoleTab } from './consoleTab';
import * as modelUtil from './modelUtil'; import * as modelUtil from './modelUtil';
import { msToString } from '../../uiUtils';
export const Workbench: React.FunctionComponent<{ export const Workbench: React.FunctionComponent<{
}> = () => { }> = () => {
@ -35,7 +36,8 @@ export const Workbench: React.FunctionComponent<{
const [contextEntry, setContextEntry] = React.useState<ContextEntry>(emptyContext); const [contextEntry, setContextEntry] = React.useState<ContextEntry>(emptyContext);
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(); const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>(); const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
const [selectedTab, setSelectedTab] = React.useState<string>('logs'); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs');
const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
const [dragOver, setDragOver] = React.useState<boolean>(false); const [dragOver, setDragOver] = React.useState<boolean>(false);
@ -122,32 +124,55 @@ export const Workbench: React.FunctionComponent<{
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}> <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
<SplitView sidebarSize={300} orientation='horizontal'> <SplitView sidebarSize={300} orientation='horizontal'>
<SnapshotTab action={selectedAction} defaultSnapshotInfo={defaultSnapshotInfo} /> <SnapshotTab action={selectedAction} defaultSnapshotInfo={defaultSnapshotInfo} />
<TabbedPane tabs={tabs} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/> <TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
</SplitView> </SplitView>
<ActionList <TabbedPane tabs={
actions={contextEntry.actions} [
selectedAction={selectedAction} { id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
highlightedAction={highlightedAction} actions={contextEntry.actions}
onSelected={action => { selectedAction={selectedAction}
setSelectedAction(action); highlightedAction={highlightedAction}
}} onSelected={action => {
onHighlighted={action => setHighlightedAction(action)} setSelectedAction(action);
setSelectedTab={setSelectedTab} }}
/> onHighlighted={action => setHighlightedAction(action)}
setSelectedTab={setSelectedPropertiesTab}
/> },
{ id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'>
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
{contextEntry.wallTime && <div className='call-line'>start time: <span className='datetime' title={new Date(contextEntry.wallTime).toLocaleString()}>{new Date(contextEntry.wallTime).toLocaleString()}</span></div>}
<div className='call-line'>duration: <span className='number' title={msToString(contextEntry.endTime - contextEntry.startTime)}>{msToString(contextEntry.endTime - contextEntry.startTime)}</span></div>
<div className='call-section'>Browser</div>
<div className='call-line'>engine: <span className='string' title={contextEntry.browserName}>{contextEntry.browserName}</span></div>
{contextEntry.platform && <div className='call-line'>platform: <span className='string' title={contextEntry.platform}>{contextEntry.platform}</span></div>}
{contextEntry.options.userAgent && <div className='call-line'>user agent: <span className='datetime' title={contextEntry.options.userAgent}>{contextEntry.options.userAgent}</span></div>}
<div className='call-section'>Viewport</div>
{contextEntry.options.viewport && <div className='call-line'>width: <span className='number' title={String(!!contextEntry.options.viewport?.width)}>{contextEntry.options.viewport.width}</span></div>}
{contextEntry.options.viewport && <div className='call-line'>height: <span className='number' title={String(!!contextEntry.options.viewport?.height)}>{contextEntry.options.viewport.height}</span></div>}
<div className='call-line'>is mobile: <span className='boolean' title={String(!!contextEntry.options.isMobile)}>{String(!!contextEntry.options.isMobile)}</span></div>
{contextEntry.options.deviceScaleFactor && <div className='call-line'>device scale: <span className='number' title={String(contextEntry.options.deviceScaleFactor)}>{String(contextEntry.options.deviceScaleFactor)}</span></div>}
<div className='call-section'>Counts</div>
<div className='call-line'>pages: <span className='number'>{contextEntry.pages.length}</span></div>
<div className='call-line'>actions: <span className='number'>{contextEntry.actions.length}</span></div>
<div className='call-line'>events: <span className='number'>{contextEntry.events.length}</span></div>
</div> },
]
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
</SplitView> </SplitView>
{!!progress.total && <div className='progress'> {!!progress.total && <div className='progress'>
<div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div> <div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div>
</div>} </div>}
{!dragOver && !traceURL && <div className='drop-target'> {!dragOver && !traceURL && <div className='drop-target'>
<div className='title'>Drop Playwright Trace to load</div> <div className='title'>Drop Playwright Trace to load</div>
<div>or</div>
<button onClick={() => { <button onClick={() => {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.click(); input.click();
input.addEventListener('change', e => handleFileInputChange(e)); input.addEventListener('change', e => handleFileInputChange(e));
}}>...or select file</button> }}>Select file</button>
<div>Playwright Trace Viewer is a progressive web app, it does not send your trace anywhere, <div style={{ maxWidth: 400 }}>Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere,
it opens it locally instead.</div> it opens it locally.</div>
</div>} </div>}
{dragOver && <div className='drop-target' {dragOver && <div className='drop-target'
onDragLeave={() => { setDragOver(false); }} onDragLeave={() => { setDragOver(false); }}

View File

@ -579,3 +579,18 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) =>
const snapshotFrame = await traceViewer.snapshotFrame('page.evaluate'); const snapshotFrame = await traceViewer.snapshotFrame('page.evaluate');
await expect(snapshotFrame.locator('img')).toHaveJSProperty('naturalWidth', 10); await expect(snapshotFrame.locator('img')).toHaveJSProperty('naturalWidth', 10);
}); });
test('should include metainfo', async ({ showTraceViewer, browserName }) => {
const traceViewer = await showTraceViewer(traceFile);
await traceViewer.page.locator('text=Metadata').click();
const callLine = traceViewer.page.locator('.call-line');
await expect(callLine.locator('text=start time')).toHaveText(/start time: [\d/,: ]+/);
await expect(callLine.locator('text=duration')).toHaveText(/duration: [\dms]+/);
await expect(callLine.locator('text=engine')).toHaveText(/engine: [\w]+/);
await expect(callLine.locator('text=platform')).toHaveText(/platform: [\w]+/);
await expect(callLine.locator('text=width')).toHaveText(/width: [\d]+/);
await expect(callLine.locator('text=height')).toHaveText(/height: [\d]+/);
await expect(callLine.locator('text=pages')).toHaveText(/pages: 1/);
await expect(callLine.locator('text=actions')).toHaveText(/actions: [\d]+/);
await expect(callLine.locator('text=events')).toHaveText(/events: [\d]+/);
});