mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 13:45:36 +03:00
feat(trace-viewer): add metainfo tab (#10205)
This commit is contained in:
parent
b2af576796
commit
03fee2f593
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
@ -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[];
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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); }}
|
||||||
|
@ -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]+/);
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user