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
|
||||
.command('show-trace [trace]')
|
||||
.command('show-trace [trace...]')
|
||||
.option('-b, --browser <browserType>', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium')
|
||||
.description('Show trace viewer')
|
||||
.action(function(trace, options) {
|
||||
.action(function(traces, options) {
|
||||
if (options.browser === 'cr')
|
||||
options.browser = 'chromium';
|
||||
if (options.browser === 'ff')
|
||||
options.browser = 'firefox';
|
||||
if (options.browser === 'wk')
|
||||
options.browser = 'webkit';
|
||||
showTraceViewer(trace, options.browser, false, 9322).catch(logErrorAndExit);
|
||||
showTraceViewer(traces, options.browser, false, 9322).catch(logErrorAndExit);
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
|
|
|
|||
|
|
@ -26,11 +26,13 @@ import { internalCallMetadata } from '../../instrumentation';
|
|||
import { createPlaywright } from '../../playwright';
|
||||
import { ProgressController } from '../../progress';
|
||||
|
||||
export async function showTraceViewer(traceUrl: string, browserName: string, headless = false, port?: number): Promise<BrowserContext | undefined> {
|
||||
if (traceUrl && !traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Trace file ${traceUrl} does not exist!`);
|
||||
process.exit(1);
|
||||
export async function showTraceViewer(traceUrls: string[], browserName: string, headless = false, port?: number): Promise<BrowserContext | undefined> {
|
||||
for (const traceUrl of traceUrls) {
|
||||
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Trace file ${traceUrl} does not exist!`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const server = new HttpServer();
|
||||
server.routePrefix('/trace', (request, response) => {
|
||||
|
|
@ -84,6 +86,7 @@ export async function showTraceViewer(traceUrl: string, browserName: string, hea
|
|||
else
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type { ResourceSnapshot } from '../../server/trace/common/snapshotTypes';
|
|||
import * as trace from '../../server/trace/common/traceEvents';
|
||||
|
||||
export type ContextEntry = {
|
||||
traceUrl: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
browserName: string;
|
||||
|
|
@ -33,6 +34,8 @@ export type ContextEntry = {
|
|||
hasSource: boolean;
|
||||
};
|
||||
|
||||
export type MergedContexts = Pick<ContextEntry, 'startTime' | 'endTime' | 'browserName' | 'platform' | 'wallTime' | 'title' | 'options' | 'pages' | 'actions' | 'events' | 'hasSource'>;
|
||||
|
||||
export type PageEntry = {
|
||||
screencastFrames: {
|
||||
sha1: string,
|
||||
|
|
@ -43,6 +46,7 @@ export type PageEntry = {
|
|||
};
|
||||
export function createEmptyContext(): ContextEntry {
|
||||
return {
|
||||
traceUrl: '',
|
||||
startTime: Number.MAX_SAFE_INTEGER,
|
||||
endTime: 0,
|
||||
browserName: '',
|
||||
|
|
|
|||
|
|
@ -37,11 +37,7 @@ async function loadTrace(trace: string, clientId: string, progress: (done: numbe
|
|||
if (entry)
|
||||
return entry.traceModel;
|
||||
const traceModel = new TraceModel();
|
||||
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);
|
||||
await traceModel.load(url, progress);
|
||||
await traceModel.load(trace, progress);
|
||||
const snapshotServer = new SnapshotServer(traceModel.storage());
|
||||
loadedTraces.set(trace, { traceModel, snapshotServer, clientId });
|
||||
return traceModel;
|
||||
|
|
|
|||
|
|
@ -37,9 +37,18 @@ export class TraceModel {
|
|||
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) {
|
||||
this.contextEntry.traceUrl = traceURL;
|
||||
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;
|
||||
let traceEntry: zip.Entry | undefined;
|
||||
let networkEntry: zip.Entry | undefined;
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ import { Boundaries, Size } from '../geometry';
|
|||
import * as React from 'react';
|
||||
import { useMeasure } from './helpers';
|
||||
import { upperBound } from '../../uiUtils';
|
||||
import { ContextEntry, PageEntry } from '../entries';
|
||||
import { MergedContexts, PageEntry } from '../entries';
|
||||
|
||||
const tileSize = { width: 200, height: 45 };
|
||||
|
||||
export const FilmStrip: React.FunctionComponent<{
|
||||
context: ContextEntry,
|
||||
context: MergedContexts,
|
||||
boundaries: Boundaries,
|
||||
previewPoint?: { x: number, clientY: number },
|
||||
}> = ({ context, boundaries, previewPoint }) => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { ResourceSnapshot } from '../../../server/trace/common/snapshotTypes';
|
||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||
import { ContextEntry } from '../entries';
|
||||
import { ContextEntry, MergedContexts, PageEntry } from '../entries';
|
||||
|
||||
const contextSymbol = Symbol('context');
|
||||
const nextSymbol = Symbol('next');
|
||||
|
|
@ -39,7 +39,7 @@ export function context(action: ActionTraceEvent): ContextEntry {
|
|||
return (action as any)[contextSymbol];
|
||||
}
|
||||
|
||||
export function next(action: ActionTraceEvent): ActionTraceEvent {
|
||||
function next(action: ActionTraceEvent): ActionTraceEvent {
|
||||
return (action as any)[nextSymbol];
|
||||
}
|
||||
|
||||
|
|
@ -87,3 +87,22 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
|
|||
(action as any)[resourcesSymbol] = 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 { useMeasure } from './helpers';
|
||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||
import { context } from './modelUtil';
|
||||
|
||||
export const SnapshotTab: React.FunctionComponent<{
|
||||
action: ActionTraceEvent | undefined,
|
||||
|
|
@ -39,9 +40,11 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
if (action) {
|
||||
const snapshot = snapshots[snapshotIndex];
|
||||
if (snapshot && snapshot.snapshotName) {
|
||||
const traceUrl = new URL(window.location.href).searchParams.get('trace');
|
||||
snapshotUrl = new URL(`snapshot/${action.metadata.pageId}?trace=${traceUrl}&name=${snapshot.snapshotName}`, window.location.href).toString();
|
||||
snapshotInfoUrl = new URL(`snapshotInfo/${action.metadata.pageId}?trace=${traceUrl}&name=${snapshot.snapshotName}`, window.location.href).toString();
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', context(action).traceUrl);
|
||||
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')) {
|
||||
pointX = action.metadata.point?.x;
|
||||
pointY = action.metadata.point?.y;
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@
|
|||
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 { useMeasure } from './helpers';
|
||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||
import { msToString } from '../../uiUtils';
|
||||
import { MergedContexts } from '../entries';
|
||||
import { Boundaries } from '../geometry';
|
||||
import { FilmStrip } from './filmStrip';
|
||||
import { useMeasure } from './helpers';
|
||||
import './timeline.css';
|
||||
|
||||
type TimelineBar = {
|
||||
action?: ActionTraceEvent;
|
||||
|
|
@ -37,7 +37,7 @@ type TimelineBar = {
|
|||
};
|
||||
|
||||
export const Timeline: React.FunctionComponent<{
|
||||
context: ContextEntry,
|
||||
context: MergedContexts,
|
||||
boundaries: Boundaries,
|
||||
selectedAction: ActionTraceEvent | undefined,
|
||||
highlightedAction: ActionTraceEvent | undefined,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||
import { ContextEntry, createEmptyContext } from '../entries';
|
||||
import { ContextEntry, createEmptyContext, MergedContexts } from '../entries';
|
||||
import { ActionList } from './actionList';
|
||||
import { TabbedPane } from './tabbedPane';
|
||||
import { Timeline } from './timeline';
|
||||
|
|
@ -32,9 +32,9 @@ import { msToString } from '../../uiUtils';
|
|||
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
}> = () => {
|
||||
const [traceURL, setTraceURL] = React.useState<string>('');
|
||||
const [uploadedTraceName, setUploadedTraceName] = React.useState<string|null>(null);
|
||||
const [contextEntry, setContextEntry] = React.useState<ContextEntry>(emptyContext);
|
||||
const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
|
||||
const [uploadedTraceNames, setUploadedTraceNames] = React.useState<string[]>([]);
|
||||
const [contextEntry, setContextEntry] = React.useState<MergedContexts>(emptyContext);
|
||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
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 [fileForLocalModeError, setFileForLocalModeError] = React.useState<string | null>(null);
|
||||
|
||||
const processTraceFile = (file: File) => {
|
||||
const blobTraceURL = URL.createObjectURL(file);
|
||||
const processTraceFiles = (files: FileList) => {
|
||||
const blobUrls = [];
|
||||
const fileNames = [];
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('trace', blobTraceURL);
|
||||
url.searchParams.set('traceFileName', file.name);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
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();
|
||||
// Snapshot loaders will inherit the trace url from the query parameters,
|
||||
// so set it here.
|
||||
window.history.pushState({}, '', href);
|
||||
setTraceURL(blobTraceURL);
|
||||
setUploadedTraceName(file.name);
|
||||
setTraceURLs(blobUrls);
|
||||
setUploadedTraceNames(fileNames);
|
||||
setSelectedAction(undefined);
|
||||
setDragOver(false);
|
||||
setProcessingErrorMessage(null);
|
||||
|
|
@ -62,58 +71,66 @@ export const Workbench: React.FunctionComponent<{
|
|||
|
||||
const handleDropEvent = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
processTraceFile(event.dataTransfer.files[0]);
|
||||
processTraceFiles(event.dataTransfer.files);
|
||||
};
|
||||
|
||||
const handleFileInputChange = (event: any) => {
|
||||
event.preventDefault();
|
||||
if (!event.target.files)
|
||||
return;
|
||||
processTraceFile(event.target.files[0]);
|
||||
processTraceFiles(event.target.files);
|
||||
};
|
||||
|
||||
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.
|
||||
if (newTraceURL?.startsWith('file:')) {
|
||||
setFileForLocalModeError(newTraceURL);
|
||||
return;
|
||||
for (const url of newTraceURLs) {
|
||||
if (url.startsWith('file:')) {
|
||||
setFileForLocalModeError(url || null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't re-use blob file URLs on page load (results in Fetch error)
|
||||
if (newTraceURL && !newTraceURL.startsWith('blob:'))
|
||||
setTraceURL(newTraceURL);
|
||||
}, [setTraceURL]);
|
||||
if (!newTraceURLs.some(url => url.startsWith('blob:')))
|
||||
setTraceURLs(newTraceURLs);
|
||||
}, [setTraceURLs]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (traceURL) {
|
||||
if (traceURLs.length) {
|
||||
const swListener = (event: any) => {
|
||||
if (event.data.method === 'progress')
|
||||
setProgress(event.data.params);
|
||||
};
|
||||
navigator.serviceWorker.addEventListener('message', swListener);
|
||||
setProgress({ done: 0, total: 1 });
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', traceURL);
|
||||
if (uploadedTraceName)
|
||||
params.set('traceFileName', uploadedTraceName);
|
||||
const response = await fetch(`context?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
setTraceURL('');
|
||||
setProcessingErrorMessage((await response.json()).error);
|
||||
return;
|
||||
const contextEntries: ContextEntry[] = [];
|
||||
for (let i = 0; i < traceURLs.length; i++) {
|
||||
const url = traceURLs[i];
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', url);
|
||||
if (uploadedTraceNames.length)
|
||||
params.set('traceFileName', uploadedTraceNames[i]);
|
||||
const response = await fetch(`context?${params.toString()}`);
|
||||
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);
|
||||
const contextEntry = modelUtil.mergeContexts(contextEntries);
|
||||
setProgress({ done: 0, total: 0 });
|
||||
modelUtil.indexModel(contextEntry);
|
||||
setContextEntry(contextEntry);
|
||||
setContextEntry(contextEntry!);
|
||||
} else {
|
||||
setContextEntry(emptyContext);
|
||||
}
|
||||
})();
|
||||
}, [traceURL, uploadedTraceName]);
|
||||
}, [traceURLs, uploadedTraceNames]);
|
||||
|
||||
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>
|
||||
</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='title'>Drop Playwright Trace to load</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) => {
|
||||
let browser: Browser;
|
||||
let contextImpl: any;
|
||||
await use(async (trace: string) => {
|
||||
contextImpl = await showTraceViewer(trace, browserName, headless);
|
||||
await use(async (traces: string[]) => {
|
||||
contextImpl = await showTraceViewer(traces, browserName, headless);
|
||||
browser = await playwright.chromium.connectOverCDP({ endpointURL: contextImpl._browser.options.wsEndpoint });
|
||||
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 body();
|
||||
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) => {
|
||||
const traceViewer = await showTraceViewer(testInfo.outputPath());
|
||||
const traceViewer = await showTraceViewer([testInfo.outputPath()]);
|
||||
await expect(traceViewer.page).toHaveTitle('Playwright Trace Viewer');
|
||||
});
|
||||
|
||||
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await expect(traceViewer.actionTitles).toHaveText([
|
||||
/browserContext.newPage/,
|
||||
/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 }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.selectAction('page.click');
|
||||
const logLines = await traceViewer.callLines.allTextContents();
|
||||
expect(logLines.length).toBeGreaterThan(10);
|
||||
|
|
@ -215,14 +215,14 @@ test('should contain action info', async ({ showTraceViewer }) => {
|
|||
});
|
||||
|
||||
test('should render events', async ({ showTraceViewer }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
const events = await traceViewer.eventBars();
|
||||
expect(events).toContain('page_console');
|
||||
});
|
||||
|
||||
test('should render console', async ({ showTraceViewer, browserName }) => {
|
||||
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.showConsoleTab();
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ test('should render console', 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');
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
expect(await traceViewer.actionIconsText('page.evaluate')).toEqual(['2', '1']);
|
||||
expect(await traceViewer.page.isHidden('.console-tab')).toBeTruthy();
|
||||
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 }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.selectAction('page.evaluate');
|
||||
await expect(traceViewer.callLines).toHaveText([
|
||||
/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) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.selectAction('page.setViewport');
|
||||
await traceViewer.selectSnapshot('Before');
|
||||
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 }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
|
||||
await traceViewer.selectAction('page.click');
|
||||
await traceViewer.showSourceTab();
|
||||
|
|
@ -277,7 +277,7 @@ test('should have correct stack trace', 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.showNetworkTab();
|
||||
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 }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await traceViewer.selectAction('page.click');
|
||||
const page = traceViewer.page;
|
||||
|
||||
|
|
@ -613,7 +613,7 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) =>
|
|||
});
|
||||
|
||||
test('should include metainfo', async ({ showTraceViewer, browserName }) => {
|
||||
const traceViewer = await showTraceViewer(traceFile);
|
||||
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/,: ]+/);
|
||||
|
|
@ -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=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 New Issue