mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 05:37:20 +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,
|
||||
deviceScaleFactor?: number,
|
||||
isMobile?: boolean,
|
||||
userAgent?: string,
|
||||
};
|
||||
|
||||
export type ContextCreatedTraceEvent = {
|
||||
version: number,
|
||||
type: 'context-options',
|
||||
browserName: string,
|
||||
platform: string,
|
||||
wallTime: number,
|
||||
title?: string,
|
||||
options: BrowserContextEventOptions
|
||||
};
|
||||
|
@ -81,7 +81,9 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
|
||||
version: VERSION,
|
||||
type: 'context-options',
|
||||
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 () => {
|
||||
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);
|
||||
|
@ -21,6 +21,8 @@ export type ContextEntry = {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
browserName: string;
|
||||
platform?: string;
|
||||
wallTime?: number;
|
||||
title?: string;
|
||||
options: trace.BrowserContextEventOptions;
|
||||
pages: PageEntry[];
|
||||
|
@ -104,6 +104,8 @@ export class TraceModel {
|
||||
case 'context-options': {
|
||||
this.contextEntry.browserName = event.browserName;
|
||||
this.contextEntry.title = event.title;
|
||||
this.contextEntry.platform = event.platform;
|
||||
this.contextEntry.wallTime = event.wallTime;
|
||||
this.contextEntry.options = event.options;
|
||||
break;
|
||||
}
|
||||
|
@ -14,10 +14,6 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.action-list-title {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.action-list-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -45,11 +45,6 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||
}, [selectedAction, actionListRef]);
|
||||
|
||||
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
|
||||
className='action-list-content'
|
||||
tabIndex={0}
|
||||
|
@ -47,11 +47,13 @@
|
||||
}
|
||||
|
||||
.call-line {
|
||||
padding: 0 0 2px 6px;
|
||||
padding: 4px 0 4px 6px;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.call-line .datetime,
|
||||
|
@ -34,6 +34,7 @@
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.drop-target input {
|
||||
@ -45,7 +46,7 @@
|
||||
background-color: rgb(0, 122, 204);
|
||||
padding: 6px 4px;
|
||||
border: none;
|
||||
margin: 40px 0;
|
||||
margin: 30px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import { CallTab } from './callTab';
|
||||
import { SplitView } from '../../components/splitView';
|
||||
import { ConsoleTab } from './consoleTab';
|
||||
import * as modelUtil from './modelUtil';
|
||||
import { msToString } from '../../uiUtils';
|
||||
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
}> = () => {
|
||||
@ -35,7 +36,8 @@ export const Workbench: React.FunctionComponent<{
|
||||
const [contextEntry, setContextEntry] = React.useState<ContextEntry>(emptyContext);
|
||||
const [selectedAction, setSelectedAction] = 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 [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'>
|
||||
<SnapshotTab action={selectedAction} defaultSnapshotInfo={defaultSnapshotInfo} />
|
||||
<TabbedPane tabs={tabs} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>
|
||||
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
||||
</SplitView>
|
||||
<ActionList
|
||||
actions={contextEntry.actions}
|
||||
selectedAction={selectedAction}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={action => {
|
||||
setSelectedAction(action);
|
||||
}}
|
||||
onHighlighted={action => setHighlightedAction(action)}
|
||||
setSelectedTab={setSelectedTab}
|
||||
/>
|
||||
<TabbedPane tabs={
|
||||
[
|
||||
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList
|
||||
actions={contextEntry.actions}
|
||||
selectedAction={selectedAction}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={action => {
|
||||
setSelectedAction(action);
|
||||
}}
|
||||
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>
|
||||
{!!progress.total && <div className='progress'>
|
||||
<div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div>
|
||||
</div>}
|
||||
{!dragOver && !traceURL && <div className='drop-target'>
|
||||
<div className='title'>Drop Playwright Trace to load</div>
|
||||
<div>or</div>
|
||||
<button onClick={() => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.click();
|
||||
input.addEventListener('change', e => handleFileInputChange(e));
|
||||
}}>...or select file</button>
|
||||
<div>Playwright Trace Viewer is a progressive web app, it does not send your trace anywhere,
|
||||
it opens it locally instead.</div>
|
||||
}}>Select file</button>
|
||||
<div style={{ maxWidth: 400 }}>Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere,
|
||||
it opens it locally.</div>
|
||||
</div>}
|
||||
{dragOver && <div className='drop-target'
|
||||
onDragLeave={() => { setDragOver(false); }}
|
||||
|
@ -579,3 +579,18 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) =>
|
||||
const snapshotFrame = await traceViewer.snapshotFrame('page.evaluate');
|
||||
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