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,12 +26,14 @@ 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)) { | ||||
| 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) => { | ||||
|     const url = new URL('http://localhost' + request.url!); | ||||
|  | @ -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); | ||||
|     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 contextEntries: ContextEntry[] = []; | ||||
|         for (let i = 0; i < traceURLs.length; i++) { | ||||
|           const url = traceURLs[i]; | ||||
|           const params = new URLSearchParams(); | ||||
|         params.set('trace', traceURL); | ||||
|         if (uploadedTraceName) | ||||
|           params.set('traceFileName', uploadedTraceName); | ||||
|           params.set('trace', url); | ||||
|           if (uploadedTraceNames.length) | ||||
|             params.set('traceFileName', uploadedTraceNames[i]); | ||||
|           const response = await fetch(`context?${params.toString()}`); | ||||
|           if (!response.ok) { | ||||
|           setTraceURL(''); | ||||
|             setTraceURLs([]); | ||||
|             setProcessingErrorMessage((await response.json()).error); | ||||
|             return; | ||||
|           } | ||||
|           const contextEntry = await response.json() as ContextEntry; | ||||
|         navigator.serviceWorker.removeEventListener('message', swListener); | ||||
|         setProgress({ done: 0, total: 0 }); | ||||
|           modelUtil.indexModel(contextEntry); | ||||
|         setContextEntry(contextEntry); | ||||
|           contextEntries.push(contextEntry); | ||||
|         } | ||||
|         navigator.serviceWorker.removeEventListener('message', swListener); | ||||
|         const contextEntry = modelUtil.mergeContexts(contextEntries); | ||||
|         setProgress({ done: 0, total: 0 }); | ||||
|         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