feat(tracing): suport loading multiple files in trace viewer (#11880)

This commit is contained in:
Yury Semikhatsky 2022-02-07 17:05:42 -08:00 committed by GitHub
parent 4ef22d3387
commit 1e00218ead
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 182 additions and 78 deletions

View file

@ -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:

View file

@ -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;
} }

View file

@ -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: '',

View file

@ -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;

View file

@ -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;

View file

@ -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 }) => {

View file

@ -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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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>

View file

@ -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]+/);
});