feat(tracing): suport loading multiple files in trace viewer (#11880)
This commit is contained in:
parent
4ef22d3387
commit
1e00218ead
|
|
@ -219,17 +219,17 @@ program
|
||||||
});
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('show-trace [trace]')
|
.command('show-trace [trace...]')
|
||||||
.option('-b, --browser <browserType>', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium')
|
.option('-b, --browser <browserType>', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium')
|
||||||
.description('Show trace viewer')
|
.description('Show trace viewer')
|
||||||
.action(function(trace, options) {
|
.action(function(traces, options) {
|
||||||
if (options.browser === 'cr')
|
if (options.browser === 'cr')
|
||||||
options.browser = 'chromium';
|
options.browser = 'chromium';
|
||||||
if (options.browser === 'ff')
|
if (options.browser === 'ff')
|
||||||
options.browser = 'firefox';
|
options.browser = 'firefox';
|
||||||
if (options.browser === 'wk')
|
if (options.browser === 'wk')
|
||||||
options.browser = 'webkit';
|
options.browser = 'webkit';
|
||||||
showTraceViewer(trace, options.browser, false, 9322).catch(logErrorAndExit);
|
showTraceViewer(traces, options.browser, false, 9322).catch(logErrorAndExit);
|
||||||
}).addHelpText('afterAll', `
|
}).addHelpText('afterAll', `
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,13 @@ import { internalCallMetadata } from '../../instrumentation';
|
||||||
import { createPlaywright } from '../../playwright';
|
import { createPlaywright } from '../../playwright';
|
||||||
import { ProgressController } from '../../progress';
|
import { ProgressController } from '../../progress';
|
||||||
|
|
||||||
export async function showTraceViewer(traceUrl: string, browserName: string, headless = false, port?: number): Promise<BrowserContext | undefined> {
|
export async function showTraceViewer(traceUrls: string[], browserName: string, headless = false, port?: number): Promise<BrowserContext | undefined> {
|
||||||
if (traceUrl && !traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) {
|
for (const traceUrl of traceUrls) {
|
||||||
// eslint-disable-next-line no-console
|
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) {
|
||||||
console.error(`Trace file ${traceUrl} does not exist!`);
|
// eslint-disable-next-line no-console
|
||||||
process.exit(1);
|
console.error(`Trace file ${traceUrl} does not exist!`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const server = new HttpServer();
|
const server = new HttpServer();
|
||||||
server.routePrefix('/trace', (request, response) => {
|
server.routePrefix('/trace', (request, response) => {
|
||||||
|
|
@ -84,6 +86,7 @@ export async function showTraceViewer(traceUrl: string, browserName: string, hea
|
||||||
else
|
else
|
||||||
page.on('close', () => process.exit());
|
page.on('close', () => process.exit());
|
||||||
|
|
||||||
await page.mainFrame().goto(internalCallMetadata(), urlPrefix + `/trace/index.html${traceUrl ? '?trace=' + traceUrl : ''}`);
|
const searchQuery = traceUrls.length ? '?' + traceUrls.map(t => `trace=${t}`).join('&') : '';
|
||||||
|
await page.mainFrame().goto(internalCallMetadata(), urlPrefix + `/trace/index.html${searchQuery}`);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import type { ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
|
||||||
import * as trace from '../../server/trace/common/traceEvents';
|
import * as trace from '../../server/trace/common/traceEvents';
|
||||||
|
|
||||||
export type ContextEntry = {
|
export type ContextEntry = {
|
||||||
|
traceUrl: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
browserName: string;
|
browserName: string;
|
||||||
|
|
@ -33,6 +34,8 @@ export type ContextEntry = {
|
||||||
hasSource: boolean;
|
hasSource: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MergedContexts = Pick<ContextEntry, 'startTime' | 'endTime' | 'browserName' | 'platform' | 'wallTime' | 'title' | 'options' | 'pages' | 'actions' | 'events' | 'hasSource'>;
|
||||||
|
|
||||||
export type PageEntry = {
|
export type PageEntry = {
|
||||||
screencastFrames: {
|
screencastFrames: {
|
||||||
sha1: string,
|
sha1: string,
|
||||||
|
|
@ -43,6 +46,7 @@ export type PageEntry = {
|
||||||
};
|
};
|
||||||
export function createEmptyContext(): ContextEntry {
|
export function createEmptyContext(): ContextEntry {
|
||||||
return {
|
return {
|
||||||
|
traceUrl: '',
|
||||||
startTime: Number.MAX_SAFE_INTEGER,
|
startTime: Number.MAX_SAFE_INTEGER,
|
||||||
endTime: 0,
|
endTime: 0,
|
||||||
browserName: '',
|
browserName: '',
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,7 @@ async function loadTrace(trace: string, clientId: string, progress: (done: numbe
|
||||||
if (entry)
|
if (entry)
|
||||||
return entry.traceModel;
|
return entry.traceModel;
|
||||||
const traceModel = new TraceModel();
|
const traceModel = new TraceModel();
|
||||||
let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : `file?path=${trace}`;
|
await traceModel.load(trace, progress);
|
||||||
// Dropbox does not support cors.
|
|
||||||
if (url.startsWith('https://www.dropbox.com/'))
|
|
||||||
url = 'https://dl.dropboxusercontent.com/' + url.substring('https://www.dropbox.com/'.length);
|
|
||||||
await traceModel.load(url, progress);
|
|
||||||
const snapshotServer = new SnapshotServer(traceModel.storage());
|
const snapshotServer = new SnapshotServer(traceModel.storage());
|
||||||
loadedTraces.set(trace, { traceModel, snapshotServer, clientId });
|
loadedTraces.set(trace, { traceModel, snapshotServer, clientId });
|
||||||
return traceModel;
|
return traceModel;
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,18 @@ export class TraceModel {
|
||||||
this.contextEntry = createEmptyContext();
|
this.contextEntry = createEmptyContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _formatUrl(trace: string) {
|
||||||
|
let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : `file?path=${trace}`;
|
||||||
|
// Dropbox does not support cors.
|
||||||
|
if (url.startsWith('https://www.dropbox.com/'))
|
||||||
|
url = 'https://dl.dropboxusercontent.com/' + url.substring('https://www.dropbox.com/'.length);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
async load(traceURL: string, progress: (done: number, total: number) => void) {
|
async load(traceURL: string, progress: (done: number, total: number) => void) {
|
||||||
|
this.contextEntry.traceUrl = traceURL;
|
||||||
const zipReader = new zipjs.ZipReader( // @ts-ignore
|
const zipReader = new zipjs.ZipReader( // @ts-ignore
|
||||||
new zipjs.HttpReader(traceURL, { mode: 'cors', preventHeadRequest: true }),
|
new zipjs.HttpReader(this._formatUrl(traceURL), { mode: 'cors', preventHeadRequest: true }),
|
||||||
{ useWebWorkers: false }) as zip.ZipReader;
|
{ useWebWorkers: false }) as zip.ZipReader;
|
||||||
let traceEntry: zip.Entry | undefined;
|
let traceEntry: zip.Entry | undefined;
|
||||||
let networkEntry: zip.Entry | undefined;
|
let networkEntry: zip.Entry | undefined;
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,12 @@ import { Boundaries, Size } from '../geometry';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
import { upperBound } from '../../uiUtils';
|
import { upperBound } from '../../uiUtils';
|
||||||
import { ContextEntry, PageEntry } from '../entries';
|
import { MergedContexts, PageEntry } from '../entries';
|
||||||
|
|
||||||
const tileSize = { width: 200, height: 45 };
|
const tileSize = { width: 200, height: 45 };
|
||||||
|
|
||||||
export const FilmStrip: React.FunctionComponent<{
|
export const FilmStrip: React.FunctionComponent<{
|
||||||
context: ContextEntry,
|
context: MergedContexts,
|
||||||
boundaries: Boundaries,
|
boundaries: Boundaries,
|
||||||
previewPoint?: { x: number, clientY: number },
|
previewPoint?: { x: number, clientY: number },
|
||||||
}> = ({ context, boundaries, previewPoint }) => {
|
}> = ({ context, boundaries, previewPoint }) => {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
|
import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
import { ContextEntry } from '../entries';
|
import { ContextEntry, MergedContexts, PageEntry } from '../entries';
|
||||||
|
|
||||||
const contextSymbol = Symbol('context');
|
const contextSymbol = Symbol('context');
|
||||||
const nextSymbol = Symbol('next');
|
const nextSymbol = Symbol('next');
|
||||||
|
|
@ -39,7 +39,7 @@ export function context(action: ActionTraceEvent): ContextEntry {
|
||||||
return (action as any)[contextSymbol];
|
return (action as any)[contextSymbol];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function next(action: ActionTraceEvent): ActionTraceEvent {
|
function next(action: ActionTraceEvent): ActionTraceEvent {
|
||||||
return (action as any)[nextSymbol];
|
return (action as any)[nextSymbol];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,3 +87,22 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
|
||||||
(action as any)[resourcesSymbol] = result;
|
(action as any)[resourcesSymbol] = result;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeContexts(contexts: ContextEntry[]): MergedContexts {
|
||||||
|
const newContext: MergedContexts = {
|
||||||
|
browserName: contexts[0].browserName,
|
||||||
|
platform: contexts[0].platform,
|
||||||
|
title: contexts[0].title,
|
||||||
|
options: contexts[0].options,
|
||||||
|
wallTime: contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur!), Number.MAX_VALUE),
|
||||||
|
startTime: contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE),
|
||||||
|
endTime: contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE),
|
||||||
|
pages: ([] as PageEntry[]).concat(...contexts.map(c => c.pages)),
|
||||||
|
actions: ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.actions)),
|
||||||
|
events: ([] as ActionTraceEvent[]).concat(...contexts.map(c => c.events)),
|
||||||
|
hasSource: contexts.some(c => c.hasSource)
|
||||||
|
};
|
||||||
|
newContext.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
|
||||||
|
newContext.events.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
|
||||||
|
return newContext;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import './tabbedPane.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
|
import { context } from './modelUtil';
|
||||||
|
|
||||||
export const SnapshotTab: React.FunctionComponent<{
|
export const SnapshotTab: React.FunctionComponent<{
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
|
|
@ -39,9 +40,11 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
if (action) {
|
if (action) {
|
||||||
const snapshot = snapshots[snapshotIndex];
|
const snapshot = snapshots[snapshotIndex];
|
||||||
if (snapshot && snapshot.snapshotName) {
|
if (snapshot && snapshot.snapshotName) {
|
||||||
const traceUrl = new URL(window.location.href).searchParams.get('trace');
|
const params = new URLSearchParams();
|
||||||
snapshotUrl = new URL(`snapshot/${action.metadata.pageId}?trace=${traceUrl}&name=${snapshot.snapshotName}`, window.location.href).toString();
|
params.set('trace', context(action).traceUrl);
|
||||||
snapshotInfoUrl = new URL(`snapshotInfo/${action.metadata.pageId}?trace=${traceUrl}&name=${snapshot.snapshotName}`, window.location.href).toString();
|
params.set('name', snapshot.snapshotName);
|
||||||
|
snapshotUrl = new URL(`snapshot/${action.metadata.pageId}?${params.toString()}`, window.location.href).toString();
|
||||||
|
snapshotInfoUrl = new URL(`snapshotInfo/${action.metadata.pageId}?${params.toString()}`, window.location.href).toString();
|
||||||
if (snapshot.snapshotName.includes('action')) {
|
if (snapshot.snapshotName.includes('action')) {
|
||||||
pointX = action.metadata.point?.x;
|
pointX = action.metadata.point?.x;
|
||||||
pointY = action.metadata.point?.y;
|
pointY = action.metadata.point?.y;
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
|
||||||
import { ContextEntry } from '../entries';
|
|
||||||
import './timeline.css';
|
|
||||||
import { Boundaries } from '../geometry';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
import { msToString } from '../../uiUtils';
|
import { msToString } from '../../uiUtils';
|
||||||
|
import { MergedContexts } from '../entries';
|
||||||
|
import { Boundaries } from '../geometry';
|
||||||
import { FilmStrip } from './filmStrip';
|
import { FilmStrip } from './filmStrip';
|
||||||
|
import { useMeasure } from './helpers';
|
||||||
|
import './timeline.css';
|
||||||
|
|
||||||
type TimelineBar = {
|
type TimelineBar = {
|
||||||
action?: ActionTraceEvent;
|
action?: ActionTraceEvent;
|
||||||
|
|
@ -37,7 +37,7 @@ type TimelineBar = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Timeline: React.FunctionComponent<{
|
export const Timeline: React.FunctionComponent<{
|
||||||
context: ContextEntry,
|
context: MergedContexts,
|
||||||
boundaries: Boundaries,
|
boundaries: Boundaries,
|
||||||
selectedAction: ActionTraceEvent | undefined,
|
selectedAction: ActionTraceEvent | undefined,
|
||||||
highlightedAction: ActionTraceEvent | undefined,
|
highlightedAction: ActionTraceEvent | undefined,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
import { ContextEntry, createEmptyContext } from '../entries';
|
import { ContextEntry, createEmptyContext, MergedContexts } from '../entries';
|
||||||
import { ActionList } from './actionList';
|
import { ActionList } from './actionList';
|
||||||
import { TabbedPane } from './tabbedPane';
|
import { TabbedPane } from './tabbedPane';
|
||||||
import { Timeline } from './timeline';
|
import { Timeline } from './timeline';
|
||||||
|
|
@ -32,9 +32,9 @@ import { msToString } from '../../uiUtils';
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
}> = () => {
|
}> = () => {
|
||||||
const [traceURL, setTraceURL] = React.useState<string>('');
|
const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
|
||||||
const [uploadedTraceName, setUploadedTraceName] = React.useState<string|null>(null);
|
const [uploadedTraceNames, setUploadedTraceNames] = React.useState<string[]>([]);
|
||||||
const [contextEntry, setContextEntry] = React.useState<ContextEntry>(emptyContext);
|
const [contextEntry, setContextEntry] = React.useState<MergedContexts>(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 [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||||
|
|
@ -44,17 +44,26 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(null);
|
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(null);
|
||||||
const [fileForLocalModeError, setFileForLocalModeError] = React.useState<string | null>(null);
|
const [fileForLocalModeError, setFileForLocalModeError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const processTraceFile = (file: File) => {
|
const processTraceFiles = (files: FileList) => {
|
||||||
const blobTraceURL = URL.createObjectURL(file);
|
const blobUrls = [];
|
||||||
|
const fileNames = [];
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.set('trace', blobTraceURL);
|
for (let i = 0; i < files.length; i++) {
|
||||||
url.searchParams.set('traceFileName', file.name);
|
const file = files.item(i);
|
||||||
|
if (!file)
|
||||||
|
continue;
|
||||||
|
const blobTraceURL = URL.createObjectURL(file);
|
||||||
|
blobUrls.push(blobTraceURL);
|
||||||
|
fileNames.push(file.name);
|
||||||
|
url.searchParams.append('trace', blobTraceURL);
|
||||||
|
url.searchParams.append('traceFileName', file.name);
|
||||||
|
}
|
||||||
const href = url.toString();
|
const href = url.toString();
|
||||||
// Snapshot loaders will inherit the trace url from the query parameters,
|
// Snapshot loaders will inherit the trace url from the query parameters,
|
||||||
// so set it here.
|
// so set it here.
|
||||||
window.history.pushState({}, '', href);
|
window.history.pushState({}, '', href);
|
||||||
setTraceURL(blobTraceURL);
|
setTraceURLs(blobUrls);
|
||||||
setUploadedTraceName(file.name);
|
setUploadedTraceNames(fileNames);
|
||||||
setSelectedAction(undefined);
|
setSelectedAction(undefined);
|
||||||
setDragOver(false);
|
setDragOver(false);
|
||||||
setProcessingErrorMessage(null);
|
setProcessingErrorMessage(null);
|
||||||
|
|
@ -62,58 +71,66 @@ export const Workbench: React.FunctionComponent<{
|
||||||
|
|
||||||
const handleDropEvent = (event: React.DragEvent<HTMLDivElement>) => {
|
const handleDropEvent = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
processTraceFile(event.dataTransfer.files[0]);
|
processTraceFiles(event.dataTransfer.files);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileInputChange = (event: any) => {
|
const handleFileInputChange = (event: any) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!event.target.files)
|
if (!event.target.files)
|
||||||
return;
|
return;
|
||||||
processTraceFile(event.target.files[0]);
|
processTraceFiles(event.target.files);
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const newTraceURL = new URL(window.location.href).searchParams.get('trace');
|
const newTraceURLs = new URL(window.location.href).searchParams.getAll('trace');
|
||||||
// Don't accept file:// URLs - this means we re opened locally.
|
// Don't accept file:// URLs - this means we re opened locally.
|
||||||
if (newTraceURL?.startsWith('file:')) {
|
for (const url of newTraceURLs) {
|
||||||
setFileForLocalModeError(newTraceURL);
|
if (url.startsWith('file:')) {
|
||||||
return;
|
setFileForLocalModeError(url || null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't re-use blob file URLs on page load (results in Fetch error)
|
// Don't re-use blob file URLs on page load (results in Fetch error)
|
||||||
if (newTraceURL && !newTraceURL.startsWith('blob:'))
|
if (!newTraceURLs.some(url => url.startsWith('blob:')))
|
||||||
setTraceURL(newTraceURL);
|
setTraceURLs(newTraceURLs);
|
||||||
}, [setTraceURL]);
|
}, [setTraceURLs]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (traceURL) {
|
if (traceURLs.length) {
|
||||||
const swListener = (event: any) => {
|
const swListener = (event: any) => {
|
||||||
if (event.data.method === 'progress')
|
if (event.data.method === 'progress')
|
||||||
setProgress(event.data.params);
|
setProgress(event.data.params);
|
||||||
};
|
};
|
||||||
navigator.serviceWorker.addEventListener('message', swListener);
|
navigator.serviceWorker.addEventListener('message', swListener);
|
||||||
setProgress({ done: 0, total: 1 });
|
setProgress({ done: 0, total: 1 });
|
||||||
const params = new URLSearchParams();
|
const contextEntries: ContextEntry[] = [];
|
||||||
params.set('trace', traceURL);
|
for (let i = 0; i < traceURLs.length; i++) {
|
||||||
if (uploadedTraceName)
|
const url = traceURLs[i];
|
||||||
params.set('traceFileName', uploadedTraceName);
|
const params = new URLSearchParams();
|
||||||
const response = await fetch(`context?${params.toString()}`);
|
params.set('trace', url);
|
||||||
if (!response.ok) {
|
if (uploadedTraceNames.length)
|
||||||
setTraceURL('');
|
params.set('traceFileName', uploadedTraceNames[i]);
|
||||||
setProcessingErrorMessage((await response.json()).error);
|
const response = await fetch(`context?${params.toString()}`);
|
||||||
return;
|
if (!response.ok) {
|
||||||
|
setTraceURLs([]);
|
||||||
|
setProcessingErrorMessage((await response.json()).error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const contextEntry = await response.json() as ContextEntry;
|
||||||
|
modelUtil.indexModel(contextEntry);
|
||||||
|
contextEntries.push(contextEntry);
|
||||||
}
|
}
|
||||||
const contextEntry = await response.json() as ContextEntry;
|
|
||||||
navigator.serviceWorker.removeEventListener('message', swListener);
|
navigator.serviceWorker.removeEventListener('message', swListener);
|
||||||
|
const contextEntry = modelUtil.mergeContexts(contextEntries);
|
||||||
setProgress({ done: 0, total: 0 });
|
setProgress({ done: 0, total: 0 });
|
||||||
modelUtil.indexModel(contextEntry);
|
setContextEntry(contextEntry!);
|
||||||
setContextEntry(contextEntry);
|
|
||||||
} else {
|
} else {
|
||||||
setContextEntry(emptyContext);
|
setContextEntry(emptyContext);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [traceURL, uploadedTraceName]);
|
}, [traceURLs, uploadedTraceNames]);
|
||||||
|
|
||||||
const boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime };
|
const boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime };
|
||||||
|
|
||||||
|
|
@ -199,7 +216,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
<div>3. Drop the trace from the download shelf into the page</div>
|
<div>3. Drop the trace from the download shelf into the page</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
{!dragOver && !fileForLocalModeError && (!traceURL || processingErrorMessage) && <div className='drop-target'>
|
{!dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage) && <div className='drop-target'>
|
||||||
<div className='processing-error'>{processingErrorMessage}</div>
|
<div className='processing-error'>{processingErrorMessage}</div>
|
||||||
<div className='title'>Drop Playwright Trace to load</div>
|
<div className='title'>Drop Playwright Trace to load</div>
|
||||||
<div>or</div>
|
<div>or</div>
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,12 @@ class TraceViewerPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise<TraceViewerPage>, runAndTrace: (body: () => Promise<void>) => Promise<TraceViewerPage> }>({
|
const test = playwrightTest.extend<{ showTraceViewer: (trace: string[]) => Promise<TraceViewerPage>, runAndTrace: (body: () => Promise<void>) => Promise<TraceViewerPage> }>({
|
||||||
showTraceViewer: async ({ playwright, 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 (traces: string[]) => {
|
||||||
contextImpl = await showTraceViewer(trace, browserName, headless);
|
contextImpl = await showTraceViewer(traces, browserName, headless);
|
||||||
browser = await playwright.chromium.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]);
|
||||||
});
|
});
|
||||||
|
|
@ -114,7 +114,7 @@ const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise
|
||||||
await context.tracing.start({ snapshots: true, screenshots: true, sources: true });
|
await context.tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||||
await body();
|
await body();
|
||||||
await context.tracing.stop({ path: traceFile });
|
await context.tracing.stop({ path: traceFile });
|
||||||
return showTraceViewer(traceFile);
|
return showTraceViewer([traceFile]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -180,12 +180,12 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => {
|
test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => {
|
||||||
const traceViewer = await showTraceViewer(testInfo.outputPath());
|
const traceViewer = await showTraceViewer([testInfo.outputPath()]);
|
||||||
await expect(traceViewer.page).toHaveTitle('Playwright Trace Viewer');
|
await expect(traceViewer.page).toHaveTitle('Playwright Trace Viewer');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await expect(traceViewer.actionTitles).toHaveText([
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
/browserContext.newPage/,
|
/browserContext.newPage/,
|
||||||
/page.gotodata:text\/html,<html>Hello world<\/html>/,
|
/page.gotodata:text\/html,<html>Hello world<\/html>/,
|
||||||
|
|
@ -206,7 +206,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should contain action info', async ({ showTraceViewer }) => {
|
test('should contain action info', 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.callLines.allTextContents();
|
const logLines = await traceViewer.callLines.allTextContents();
|
||||||
expect(logLines.length).toBeGreaterThan(10);
|
expect(logLines.length).toBeGreaterThan(10);
|
||||||
|
|
@ -215,14 +215,14 @@ test('should contain action info', async ({ showTraceViewer }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render events', async ({ showTraceViewer }) => {
|
test('should render events', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
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('should render console', async ({ showTraceViewer, browserName }) => {
|
||||||
test.fixme(browserName === 'firefox', 'Firefox generates stray console message for page error');
|
test.fixme(browserName === 'firefox', 'Firefox generates stray console message for page error');
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('page.evaluate');
|
await traceViewer.selectAction('page.evaluate');
|
||||||
await traceViewer.showConsoleTab();
|
await traceViewer.showConsoleTab();
|
||||||
|
|
||||||
|
|
@ -233,7 +233,7 @@ test('should render console', async ({ showTraceViewer, browserName }) => {
|
||||||
|
|
||||||
test('should open console errors on click', async ({ showTraceViewer, browserName }) => {
|
test('should open console errors on click', async ({ showTraceViewer, browserName }) => {
|
||||||
test.fixme(browserName === 'firefox', 'Firefox generates stray console message for page error');
|
test.fixme(browserName === 'firefox', 'Firefox generates stray console message for page error');
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
expect(await traceViewer.actionIconsText('page.evaluate')).toEqual(['2', '1']);
|
expect(await traceViewer.actionIconsText('page.evaluate')).toEqual(['2', '1']);
|
||||||
expect(await traceViewer.page.isHidden('.console-tab')).toBeTruthy();
|
expect(await traceViewer.page.isHidden('.console-tab')).toBeTruthy();
|
||||||
await (await traceViewer.actionIcons('page.evaluate')).click();
|
await (await traceViewer.actionIcons('page.evaluate')).click();
|
||||||
|
|
@ -241,7 +241,7 @@ test('should open console errors on click', async ({ showTraceViewer, browserNam
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show params and return value', async ({ showTraceViewer, browserName }) => {
|
test('should show params and return value', async ({ showTraceViewer, browserName }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('page.evaluate');
|
await traceViewer.selectAction('page.evaluate');
|
||||||
await expect(traceViewer.callLines).toHaveText([
|
await expect(traceViewer.callLines).toHaveText([
|
||||||
/page.evaluate/,
|
/page.evaluate/,
|
||||||
|
|
@ -255,7 +255,7 @@ test('should show params and return value', async ({ showTraceViewer, browserNam
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have correct snapshot size', async ({ showTraceViewer }, testInfo) => {
|
test('should have correct snapshot size', async ({ showTraceViewer }, testInfo) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('page.setViewport');
|
await traceViewer.selectAction('page.setViewport');
|
||||||
await traceViewer.selectSnapshot('Before');
|
await traceViewer.selectSnapshot('Before');
|
||||||
await expect(traceViewer.snapshotContainer).toHaveCSS('width', '1280px');
|
await expect(traceViewer.snapshotContainer).toHaveCSS('width', '1280px');
|
||||||
|
|
@ -266,7 +266,7 @@ test('should have correct snapshot size', async ({ showTraceViewer }, testInfo)
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have correct stack trace', async ({ showTraceViewer }) => {
|
test('should have correct stack trace', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
|
|
||||||
await traceViewer.selectAction('page.click');
|
await traceViewer.selectAction('page.click');
|
||||||
await traceViewer.showSourceTab();
|
await traceViewer.showSourceTab();
|
||||||
|
|
@ -277,7 +277,7 @@ test('should have correct stack trace', async ({ showTraceViewer }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have network requests', async ({ showTraceViewer }) => {
|
test('should have network requests', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('http://localhost');
|
await traceViewer.selectAction('http://localhost');
|
||||||
await traceViewer.showNetworkTab();
|
await traceViewer.showNetworkTab();
|
||||||
await expect(traceViewer.networkRequests).toHaveText([
|
await expect(traceViewer.networkRequests).toHaveText([
|
||||||
|
|
@ -573,7 +573,7 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show action source', async ({ showTraceViewer }) => {
|
test('should show action source', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('page.click');
|
await traceViewer.selectAction('page.click');
|
||||||
const page = traceViewer.page;
|
const page = traceViewer.page;
|
||||||
|
|
||||||
|
|
@ -613,7 +613,7 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should include metainfo', async ({ showTraceViewer, browserName }) => {
|
test('should include metainfo', async ({ showTraceViewer, browserName }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.page.locator('text=Metadata').click();
|
await traceViewer.page.locator('text=Metadata').click();
|
||||||
const callLine = traceViewer.page.locator('.call-line');
|
const callLine = traceViewer.page.locator('.call-line');
|
||||||
await expect(callLine.locator('text=start time')).toHaveText(/start time: [\d/,: ]+/);
|
await expect(callLine.locator('text=start time')).toHaveText(/start time: [\d/,: ]+/);
|
||||||
|
|
@ -626,3 +626,56 @@ test('should include metainfo', async ({ showTraceViewer, browserName }) => {
|
||||||
await expect(callLine.locator('text=actions')).toHaveText(/actions: [\d]+/);
|
await expect(callLine.locator('text=actions')).toHaveText(/actions: [\d]+/);
|
||||||
await expect(callLine.locator('text=events')).toHaveText(/events: [\d]+/);
|
await expect(callLine.locator('text=events')).toHaveText(/events: [\d]+/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should open two trace files', async ({ context, page, request, server, showTraceViewer }, testInfo) => {
|
||||||
|
await (request as any)._tracing.start({ snapshots: true });
|
||||||
|
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
||||||
|
{
|
||||||
|
const response = await request.get(server.PREFIX + '/simple.json');
|
||||||
|
await expect(response).toBeOK();
|
||||||
|
}
|
||||||
|
await page.goto(server.PREFIX + '/input/button.html');
|
||||||
|
{
|
||||||
|
const response = await request.head(server.PREFIX + '/simplezip.json');
|
||||||
|
await expect(response).toBeOK();
|
||||||
|
}
|
||||||
|
await page.click('button');
|
||||||
|
await page.click('button');
|
||||||
|
{
|
||||||
|
const response = await request.post(server.PREFIX + '/one-style.css');
|
||||||
|
expect(response).toBeOK();
|
||||||
|
}
|
||||||
|
const apiTrace = testInfo.outputPath('api.zip');
|
||||||
|
const contextTrace = testInfo.outputPath('context.zip');
|
||||||
|
await (request as any)._tracing.stop({ path: apiTrace });
|
||||||
|
await context.tracing.stop({ path: contextTrace });
|
||||||
|
|
||||||
|
|
||||||
|
const traceViewer = await showTraceViewer([contextTrace, apiTrace]);
|
||||||
|
await traceViewer.selectAction('apiRequestContext.head');
|
||||||
|
await traceViewer.selectAction('apiRequestContext.get');
|
||||||
|
await traceViewer.selectAction('apiRequestContext.post');
|
||||||
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
|
`apiRequestContext.get`,
|
||||||
|
`page.gotohttp://localhost:${server.PORT}/input/button.html`,
|
||||||
|
`apiRequestContext.head`,
|
||||||
|
`page.clickbutton`,
|
||||||
|
`page.clickbutton`,
|
||||||
|
`apiRequestContext.post`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await traceViewer.page.locator('text=Metadata').click();
|
||||||
|
const callLine = traceViewer.page.locator('.call-line');
|
||||||
|
// Should get metadata from the context trace
|
||||||
|
await expect(callLine.locator('text=start time')).toHaveText(/start time: [\d/,: ]+/);
|
||||||
|
// duration in the metatadata section
|
||||||
|
await expect(callLine.locator('text=duration').first()).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: 6/);
|
||||||
|
await expect(callLine.locator('text=events')).toHaveText(/events: [\d]+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue