Skip to content

Commit

Permalink
feat(snapshots): various improvements (#5152)
Browse files Browse the repository at this point in the history
- Adopt "declarative shadow dom" format for shadow dom snapshots.
- Restore scroll positions.
- Render snapshot at arbitrary timestamp.
  • Loading branch information
dgozman committed Jan 26, 2021
1 parent a3af082 commit 0108d2d
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 84 deletions.
9 changes: 2 additions & 7 deletions src/cli/traceViewer/snapshotRouter.ts
Expand Up @@ -68,14 +68,9 @@ export class SnapshotRouter {
}
}

const mainFrameSnapshot = lastSnapshotEvent.get('');
if (!mainFrameSnapshot)
if (!lastSnapshotEvent.get(''))
return 'data:text/html,Snapshot is not available';

if (!mainFrameSnapshot.frameUrl.startsWith('http'))
this._pageUrl = 'http://playwright.snapshot/';
else
this._pageUrl = mainFrameSnapshot.frameUrl;
this._pageUrl = 'http://playwright.snapshot/?cachebusting=' + Date.now();
return this._pageUrl;
}

Expand Down
5 changes: 2 additions & 3 deletions src/cli/traceViewer/traceViewer.ts
Expand Up @@ -98,15 +98,14 @@ class TraceViewer {

return fs.readFileSync(path.join(this._document.resourcesDir, sha1)).toString('base64');
});
await uiPage.exposeBinding('renderSnapshot', async (_, arg: { action: ActionTraceEvent, snapshot: { name: string, snapshotId?: string } }) => {
await uiPage.exposeBinding('renderSnapshot', async (_, arg: { action: ActionTraceEvent, snapshot: { snapshotId?: string, snapshotTime?: number } }) => {
const { action, snapshot } = arg;
if (!this._document)
return;
try {
const contextEntry = this._document.model.contexts.find(entry => entry.created.contextId === action.contextId)!;
const pageEntry = contextEntry.pages.find(entry => entry.created.pageId === action.pageId)!;
const snapshotTime = snapshot.name === 'before' ? action.startTime : (snapshot.name === 'after' ? action.endTime : undefined);
const pageUrl = await this._document.snapshotRouter.selectSnapshot(contextEntry, pageEntry, snapshot.snapshotId, snapshotTime);
const pageUrl = await this._document.snapshotRouter.selectSnapshot(contextEntry, pageEntry, snapshot.snapshotId, snapshot.snapshotTime);

// TODO: fix Playwright bug where frame.name is lost (empty).
const snapshotFrame = uiPage.frames()[1];
Expand Down
2 changes: 1 addition & 1 deletion src/cli/traceViewer/web/index.tsx
Expand Up @@ -26,7 +26,7 @@ declare global {
getTraceModel(): Promise<TraceModel>;
readFile(filePath: string): Promise<string>;
readResource(sha1: string): Promise<string>;
renderSnapshot(arg: { action: trace.ActionTraceEvent, snapshot: { name: string, snapshotId?: string } }): void;
renderSnapshot(arg: { action: trace.ActionTraceEvent, snapshot: { snapshotId?: string, snapshotTime?: number } }): void;
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/cli/traceViewer/web/ui/helpers.tsx
Expand Up @@ -72,3 +72,29 @@ export const Expandable: React.FunctionComponent<{
{ expanded && <div style={{ display: 'flex', flex: 'auto', margin: '5px 0 5px 20px' }}>{body}</div> }
</div>;
};

export function msToString(ms: number): string {
if (!isFinite(ms))
return '-';

if (ms === 0)
return '0';

if (ms < 1000)
return ms.toFixed(0) + 'ms';

const seconds = ms / 1000;
if (seconds < 60)
return seconds.toFixed(1) + 's';

const minutes = seconds / 60;
if (minutes < 60)
return minutes.toFixed(1) + 'm';

const hours = minutes / 60;
if (hours < 24)
return hours.toFixed(1) + 'h';

const days = hours / 24;
return days.toFixed(1) + 'd';
}
30 changes: 19 additions & 11 deletions src/cli/traceViewer/web/ui/propertiesTabbedPane.tsx
Expand Up @@ -15,18 +15,20 @@
*/

import { ActionEntry } from '../../traceModel';
import { Size } from '../geometry';
import { Boundaries, Size } from '../geometry';
import { NetworkTab } from './networkTab';
import { SourceTab } from './sourceTab';
import './propertiesTabbedPane.css';
import * as React from 'react';
import { useMeasure } from './helpers';
import { msToString, useMeasure } from './helpers';
import { LogsTab } from './logsTab';

export const PropertiesTabbedPane: React.FunctionComponent<{
actionEntry: ActionEntry | undefined,
snapshotSize: Size,
}> = ({ actionEntry, snapshotSize }) => {
selectedTime: number | undefined,
boundaries: Boundaries,
}> = ({ actionEntry, snapshotSize, selectedTime, boundaries }) => {
const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network' | 'logs'>('snapshot');
return <div className='properties-tabbed-pane'>
<div className='vbox'>
Expand All @@ -51,7 +53,7 @@ export const PropertiesTabbedPane: React.FunctionComponent<{
</div>
</div>
<div className='properties-tab-content' style={{ display: selected === 'snapshot' ? 'flex' : 'none' }}>
<SnapshotTab actionEntry={actionEntry} snapshotSize={snapshotSize} />
<SnapshotTab actionEntry={actionEntry} snapshotSize={snapshotSize} selectedTime={selectedTime} boundaries={boundaries} />
</div>
<div className='properties-tab-content' style={{ display: selected === 'source' ? 'flex' : 'none' }}>
<SourceTab actionEntry={actionEntry} />
Expand All @@ -69,18 +71,24 @@ export const PropertiesTabbedPane: React.FunctionComponent<{
const SnapshotTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined,
snapshotSize: Size,
}> = ({ actionEntry, snapshotSize }) => {
selectedTime: number | undefined,
boundaries: Boundaries,
}> = ({ actionEntry, snapshotSize, selectedTime, boundaries }) => {
const [measure, ref] = useMeasure<HTMLDivElement>();

let snapshots: { name: string, snapshotId?: string }[] = [];

let snapshots: { name: string, snapshotId?: string, snapshotTime?: number }[] = [];
snapshots = (actionEntry ? (actionEntry.action.snapshots || []) : []).slice();
if (!snapshots.length || snapshots[0].name !== 'before')
snapshots.unshift({ name: 'before', snapshotId: undefined });
snapshots.unshift({ name: 'before', snapshotTime: actionEntry ? actionEntry.action.startTime : 0 });
if (snapshots[snapshots.length - 1].name !== 'after')
snapshots.push({ name: 'after', snapshotId: undefined });
snapshots.push({ name: 'after', snapshotTime: actionEntry ? actionEntry.action.endTime : 0 });
if (selectedTime)
snapshots = [{ name: msToString(selectedTime - boundaries.minimum), snapshotTime: selectedTime }];

const [snapshotIndex, setSnapshotIndex] = React.useState(0);
React.useEffect(() => {
setSnapshotIndex(0);
}, [selectedTime]);

const iframeRef = React.createRef<HTMLIFrameElement>();
React.useEffect(() => {
Expand All @@ -89,9 +97,9 @@ const SnapshotTab: React.FunctionComponent<{
}, [actionEntry, iframeRef]);

React.useEffect(() => {
if (actionEntry)
if (actionEntry && snapshots[snapshotIndex])
(window as any).renderSnapshot({ action: actionEntry.action, snapshot: snapshots[snapshotIndex] });
}, [actionEntry, snapshotIndex]);
}, [actionEntry, snapshotIndex, selectedTime]);

const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
return <div className='snapshot-tab'>
Expand Down
1 change: 1 addition & 0 deletions src/cli/traceViewer/web/ui/timeline.css
Expand Up @@ -63,6 +63,7 @@
}

.timeline-lane.timeline-bars {
pointer-events: auto;
margin-bottom: 10px;
overflow: visible;
}
Expand Down
45 changes: 15 additions & 30 deletions src/cli/traceViewer/web/ui/timeline.tsx
Expand Up @@ -19,7 +19,7 @@ import { ContextEntry, InterestingPageEvent, ActionEntry, trace } from '../../tr
import './timeline.css';
import { Boundaries } from '../geometry';
import * as React from 'react';
import { useMeasure } from './helpers';
import { msToString, useMeasure } from './helpers';

type TimelineBar = {
entry?: ActionEntry;
Expand All @@ -40,7 +40,8 @@ export const Timeline: React.FunctionComponent<{
highlightedAction: ActionEntry | undefined,
onSelected: (action: ActionEntry) => void,
onHighlighted: (action: ActionEntry | undefined) => void,
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
onTimeSelected: (time: number) => void,
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted, onTimeSelected }) => {
const [measure, ref] = useMeasure<HTMLDivElement>();
const [previewX, setPreviewX] = React.useState<number | undefined>();
const [hoveredBar, setHoveredBar] = React.useState<TimelineBar | undefined>();
Expand Down Expand Up @@ -147,16 +148,25 @@ export const Timeline: React.FunctionComponent<{
const onMouseLeave = () => {
setPreviewX(undefined);
};
const onClick = (event: React.MouseEvent) => {
const onActionClick = (event: React.MouseEvent) => {
if (ref.current) {
const x = event.clientX - ref.current.getBoundingClientRect().left;
const bar = findHoveredBar(x);
if (bar && bar.entry)
onSelected(bar.entry);
event.stopPropagation();
}
};
const onTimeClick = (event: React.MouseEvent) => {
if (ref.current) {
const x = event.clientX - ref.current.getBoundingClientRect().left;
const time = positionToTime(measure.width, boundaries, x);
onTimeSelected(time);
event.stopPropagation();
}
};

return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}>
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onTimeClick}>
<div className='timeline-grid'>{
offsets.map((offset, index) => {
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
Expand All @@ -177,7 +187,7 @@ export const Timeline: React.FunctionComponent<{
</div>;
})
}</div>
<div className='timeline-lane timeline-bars'>{
<div className='timeline-lane timeline-bars' onClick={onActionClick}>{
bars.map((bar, index) => {
return <div key={index}
className={'timeline-bar ' + bar.type + (targetBar === bar ? ' selected' : '')}
Expand Down Expand Up @@ -233,28 +243,3 @@ function positionToTime(clientWidth: number, boundaries: Boundaries, x: number):
return x / clientWidth * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
}

function msToString(ms: number): string {
if (!isFinite(ms))
return '-';

if (ms === 0)
return '0';

if (ms < 1000)
return ms.toFixed(0) + 'ms';

const seconds = ms / 1000;
if (seconds < 60)
return seconds.toFixed(1) + 's';

const minutes = seconds / 60;
if (minutes < 60)
return minutes.toFixed(1) + 'm';

const hours = minutes / 60;
if (hours < 24)
return hours.toFixed(1) + 'h';

const days = hours / 24;
return days.toFixed(1) + 'd';
}
23 changes: 19 additions & 4 deletions src/cli/traceViewer/web/ui/workbench.tsx
Expand Up @@ -29,6 +29,7 @@ export const Workbench: React.FunctionComponent<{
const [context, setContext] = React.useState(traceModel.contexts[0]);
const [selectedAction, setSelectedAction] = React.useState<ActionEntry | undefined>();
const [highlightedAction, setHighlightedAction] = React.useState<ActionEntry | undefined>();
const [selectedTime, setSelectedTime] = React.useState<number | undefined>();

const actions = React.useMemo(() => {
const actions: ActionEntry[] = [];
Expand All @@ -38,6 +39,7 @@ export const Workbench: React.FunctionComponent<{
}, [context]);

const snapshotSize = context.created.viewportSize || { width: 1280, height: 720 };
const boundaries = { minimum: context.startTime, maximum: context.endTime };

return <div className='vbox workbench'>
<GlobalStyles />
Expand All @@ -51,17 +53,22 @@ export const Workbench: React.FunctionComponent<{
onChange={context => {
setContext(context);
setSelectedAction(undefined);
setSelectedTime(undefined);
}}
/>
</div>
<div style={{ background: 'white', paddingLeft: '20px', flex: 'none' }}>
<Timeline
context={context}
boundaries={{ minimum: context.startTime, maximum: context.endTime }}
boundaries={boundaries}
selectedAction={selectedAction}
highlightedAction={highlightedAction}
onSelected={action => setSelectedAction(action)}
onSelected={action => {
setSelectedAction(action);
setSelectedTime(undefined);
}}
onHighlighted={action => setHighlightedAction(action)}
onTimeSelected={time => setSelectedTime(time)}
/>
</div>
<div className='hbox'>
Expand All @@ -70,11 +77,19 @@ export const Workbench: React.FunctionComponent<{
actions={actions}
selectedAction={selectedAction}
highlightedAction={highlightedAction}
onSelected={action => setSelectedAction(action)}
onSelected={action => {
setSelectedAction(action);
setSelectedTime(undefined);
}}
onHighlighted={action => setHighlightedAction(action)}
/>
</div>
<PropertiesTabbedPane actionEntry={selectedAction} snapshotSize={snapshotSize} />
<PropertiesTabbedPane
actionEntry={selectedAction}
snapshotSize={snapshotSize}
selectedTime={selectedTime}
boundaries={boundaries}
/>
</div>
</div>;
};

0 comments on commit 0108d2d

Please sign in to comment.