mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	feat(trace-viewer): add metainfo tab (#10205)
This commit is contained in:
		
							parent
							
								
									b2af576796
								
							
						
					
					
						commit
						03fee2f593
					
				| @ -24,12 +24,15 @@ export type BrowserContextEventOptions = { | |||||||
|   viewport?: Size, |   viewport?: Size, | ||||||
|   deviceScaleFactor?: number, |   deviceScaleFactor?: number, | ||||||
|   isMobile?: boolean, |   isMobile?: boolean, | ||||||
|  |   userAgent?: string, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type ContextCreatedTraceEvent = { | export type ContextCreatedTraceEvent = { | ||||||
|   version: number, |   version: number, | ||||||
|   type: 'context-options', |   type: 'context-options', | ||||||
|   browserName: string, |   browserName: string, | ||||||
|  |   platform: string, | ||||||
|  |   wallTime: number, | ||||||
|   title?: string, |   title?: string, | ||||||
|   options: BrowserContextEventOptions |   options: BrowserContextEventOptions | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -81,7 +81,9 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha | |||||||
|       version: VERSION, |       version: VERSION, | ||||||
|       type: 'context-options', |       type: 'context-options', | ||||||
|       browserName: this._context._browser.options.name, |       browserName: this._context._browser.options.name, | ||||||
|       options: this._context._options |       options: this._context._options, | ||||||
|  |       platform: process.platform, | ||||||
|  |       wallTime: 0, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -124,7 +126,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha | |||||||
| 
 | 
 | ||||||
|     this._appendTraceOperation(async () => { |     this._appendTraceOperation(async () => { | ||||||
|       await mkdirIfNeeded(state.traceFile); |       await mkdirIfNeeded(state.traceFile); | ||||||
|       await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title }) + '\n'); |       await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }) + '\n'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     this._context.instrumentation.addListener(this); |     this._context.instrumentation.addListener(this); | ||||||
|  | |||||||
| @ -21,6 +21,8 @@ export type ContextEntry = { | |||||||
|   startTime: number; |   startTime: number; | ||||||
|   endTime: number; |   endTime: number; | ||||||
|   browserName: string; |   browserName: string; | ||||||
|  |   platform?: string; | ||||||
|  |   wallTime?: number; | ||||||
|   title?: string; |   title?: string; | ||||||
|   options: trace.BrowserContextEventOptions; |   options: trace.BrowserContextEventOptions; | ||||||
|   pages: PageEntry[]; |   pages: PageEntry[]; | ||||||
|  | |||||||
| @ -104,6 +104,8 @@ export class TraceModel { | |||||||
|       case 'context-options': { |       case 'context-options': { | ||||||
|         this.contextEntry.browserName = event.browserName; |         this.contextEntry.browserName = event.browserName; | ||||||
|         this.contextEntry.title = event.title; |         this.contextEntry.title = event.title; | ||||||
|  |         this.contextEntry.platform = event.platform; | ||||||
|  |         this.contextEntry.wallTime = event.wallTime; | ||||||
|         this.contextEntry.options = event.options; |         this.contextEntry.options = event.options; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -14,10 +14,6 @@ | |||||||
|   limitations under the License. |   limitations under the License. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| .action-list-title { |  | ||||||
|   padding: 0 10px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-list-content { | .action-list-content { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|  | |||||||
| @ -45,11 +45,6 @@ export const ActionList: React.FC<ActionListProps> = ({ | |||||||
|   }, [selectedAction, actionListRef]); |   }, [selectedAction, actionListRef]); | ||||||
| 
 | 
 | ||||||
|   return <div className='action-list vbox'> |   return <div className='action-list vbox'> | ||||||
|     <div className='action-list-title tab-strip'> |  | ||||||
|       <div className='tab-element'> |  | ||||||
|         <div className='tab-label'>Actions</div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|     <div |     <div | ||||||
|       className='action-list-content' |       className='action-list-content' | ||||||
|       tabIndex={0} |       tabIndex={0} | ||||||
|  | |||||||
| @ -47,11 +47,13 @@ | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .call-line { | .call-line { | ||||||
|   padding: 0 0 2px 6px; |   padding: 4px 0 4px 6px; | ||||||
|   flex: none; |   flex: none; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   text-overflow: ellipsis; |   text-overflow: ellipsis; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|  |   line-height: 18px; | ||||||
|  |   white-space: nowrap; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .call-line .datetime, | .call-line .datetime, | ||||||
|  | |||||||
| @ -34,6 +34,7 @@ | |||||||
|   font-size: 24px; |   font-size: 24px; | ||||||
|   color: #666; |   color: #666; | ||||||
|   font-weight: bold; |   font-weight: bold; | ||||||
|  |   margin-bottom: 30px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .drop-target input { | .drop-target input { | ||||||
| @ -45,7 +46,7 @@ | |||||||
|   background-color: rgb(0, 122, 204); |   background-color: rgb(0, 122, 204); | ||||||
|   padding: 6px 4px; |   padding: 6px 4px; | ||||||
|   border: none; |   border: none; | ||||||
|   margin: 40px 0; |   margin: 30px 0; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -28,6 +28,7 @@ import { CallTab } from './callTab'; | |||||||
| import { SplitView } from '../../components/splitView'; | import { SplitView } from '../../components/splitView'; | ||||||
| import { ConsoleTab } from './consoleTab'; | import { ConsoleTab } from './consoleTab'; | ||||||
| import * as modelUtil from './modelUtil'; | import * as modelUtil from './modelUtil'; | ||||||
|  | import { msToString } from '../../uiUtils'; | ||||||
| 
 | 
 | ||||||
| export const Workbench: React.FunctionComponent<{ | export const Workbench: React.FunctionComponent<{ | ||||||
| }> = () => { | }> = () => { | ||||||
| @ -35,7 +36,8 @@ export const Workbench: React.FunctionComponent<{ | |||||||
|   const [contextEntry, setContextEntry] = React.useState<ContextEntry>(emptyContext); |   const [contextEntry, setContextEntry] = React.useState<ContextEntry>(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 [selectedTab, setSelectedTab] = React.useState<string>('logs'); |   const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions'); | ||||||
|  |   const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs'); | ||||||
|   const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); |   const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); | ||||||
|   const [dragOver, setDragOver] = React.useState<boolean>(false); |   const [dragOver, setDragOver] = React.useState<boolean>(false); | ||||||
| 
 | 
 | ||||||
| @ -122,32 +124,55 @@ export const Workbench: React.FunctionComponent<{ | |||||||
|     <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}> |     <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}> | ||||||
|       <SplitView sidebarSize={300} orientation='horizontal'> |       <SplitView sidebarSize={300} orientation='horizontal'> | ||||||
|         <SnapshotTab action={selectedAction} defaultSnapshotInfo={defaultSnapshotInfo} /> |         <SnapshotTab action={selectedAction} defaultSnapshotInfo={defaultSnapshotInfo} /> | ||||||
|         <TabbedPane tabs={tabs} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/> |         <TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/> | ||||||
|       </SplitView> |       </SplitView> | ||||||
|       <ActionList |       <TabbedPane tabs={ | ||||||
|         actions={contextEntry.actions} |         [ | ||||||
|         selectedAction={selectedAction} |           { id: 'actions', title: 'Actions', count: 0, render: () => <ActionList | ||||||
|         highlightedAction={highlightedAction} |             actions={contextEntry.actions} | ||||||
|         onSelected={action => { |             selectedAction={selectedAction} | ||||||
|           setSelectedAction(action); |             highlightedAction={highlightedAction} | ||||||
|         }} |             onSelected={action => { | ||||||
|         onHighlighted={action => setHighlightedAction(action)} |               setSelectedAction(action); | ||||||
|         setSelectedTab={setSelectedTab} |             }} | ||||||
|       /> |             onHighlighted={action => setHighlightedAction(action)} | ||||||
|  |             setSelectedTab={setSelectedPropertiesTab} | ||||||
|  |           /> }, | ||||||
|  |           { id: 'metadata', title: 'Metadata', count: 0, render: () => <div className='vbox'> | ||||||
|  |             <div className='call-section' style={{ paddingTop: 2 }}>Time</div> | ||||||
|  |             {contextEntry.wallTime && <div className='call-line'>start time: <span className='datetime' title={new Date(contextEntry.wallTime).toLocaleString()}>{new Date(contextEntry.wallTime).toLocaleString()}</span></div>} | ||||||
|  |             <div className='call-line'>duration: <span className='number' title={msToString(contextEntry.endTime - contextEntry.startTime)}>{msToString(contextEntry.endTime - contextEntry.startTime)}</span></div> | ||||||
|  |             <div className='call-section'>Browser</div> | ||||||
|  |             <div className='call-line'>engine: <span className='string' title={contextEntry.browserName}>{contextEntry.browserName}</span></div> | ||||||
|  |             {contextEntry.platform && <div className='call-line'>platform: <span className='string' title={contextEntry.platform}>{contextEntry.platform}</span></div>} | ||||||
|  |             {contextEntry.options.userAgent && <div className='call-line'>user agent: <span className='datetime' title={contextEntry.options.userAgent}>{contextEntry.options.userAgent}</span></div>} | ||||||
|  |             <div className='call-section'>Viewport</div> | ||||||
|  |             {contextEntry.options.viewport && <div className='call-line'>width: <span className='number' title={String(!!contextEntry.options.viewport?.width)}>{contextEntry.options.viewport.width}</span></div>} | ||||||
|  |             {contextEntry.options.viewport && <div className='call-line'>height: <span className='number' title={String(!!contextEntry.options.viewport?.height)}>{contextEntry.options.viewport.height}</span></div>} | ||||||
|  |             <div className='call-line'>is mobile: <span className='boolean' title={String(!!contextEntry.options.isMobile)}>{String(!!contextEntry.options.isMobile)}</span></div> | ||||||
|  |             {contextEntry.options.deviceScaleFactor && <div className='call-line'>device scale: <span className='number' title={String(contextEntry.options.deviceScaleFactor)}>{String(contextEntry.options.deviceScaleFactor)}</span></div>} | ||||||
|  |             <div className='call-section'>Counts</div> | ||||||
|  |             <div className='call-line'>pages: <span className='number'>{contextEntry.pages.length}</span></div> | ||||||
|  |             <div className='call-line'>actions: <span className='number'>{contextEntry.actions.length}</span></div> | ||||||
|  |             <div className='call-line'>events: <span className='number'>{contextEntry.events.length}</span></div> | ||||||
|  |           </div> }, | ||||||
|  |         ] | ||||||
|  |       } selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/> | ||||||
|     </SplitView> |     </SplitView> | ||||||
|     {!!progress.total && <div className='progress'> |     {!!progress.total && <div className='progress'> | ||||||
|       <div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div> |       <div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div> | ||||||
|     </div>} |     </div>} | ||||||
|     {!dragOver && !traceURL && <div className='drop-target'> |     {!dragOver && !traceURL && <div className='drop-target'> | ||||||
|       <div className='title'>Drop Playwright Trace to load</div> |       <div className='title'>Drop Playwright Trace to load</div> | ||||||
|  |       <div>or</div> | ||||||
|       <button onClick={() => { |       <button onClick={() => { | ||||||
|         const input = document.createElement('input'); |         const input = document.createElement('input'); | ||||||
|         input.type = 'file'; |         input.type = 'file'; | ||||||
|         input.click(); |         input.click(); | ||||||
|         input.addEventListener('change', e => handleFileInputChange(e)); |         input.addEventListener('change', e => handleFileInputChange(e)); | ||||||
|       }}>...or select file</button> |       }}>Select file</button> | ||||||
|       <div>Playwright Trace Viewer is a progressive web app, it does not send your trace anywhere, |       <div style={{ maxWidth: 400 }}>Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, | ||||||
|         it opens it locally instead.</div> |         it opens it locally.</div> | ||||||
|     </div>} |     </div>} | ||||||
|     {dragOver && <div className='drop-target' |     {dragOver && <div className='drop-target' | ||||||
|       onDragLeave={() => { setDragOver(false); }} |       onDragLeave={() => { setDragOver(false); }} | ||||||
|  | |||||||
| @ -579,3 +579,18 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) => | |||||||
|   const snapshotFrame = await traceViewer.snapshotFrame('page.evaluate'); |   const snapshotFrame = await traceViewer.snapshotFrame('page.evaluate'); | ||||||
|   await expect(snapshotFrame.locator('img')).toHaveJSProperty('naturalWidth', 10); |   await expect(snapshotFrame.locator('img')).toHaveJSProperty('naturalWidth', 10); | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | test('should include metainfo', async ({ showTraceViewer, browserName }) => { | ||||||
|  |   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/,: ]+/); | ||||||
|  |   await expect(callLine.locator('text=duration')).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: [\d]+/); | ||||||
|  |   await expect(callLine.locator('text=events')).toHaveText(/events: [\d]+/); | ||||||
|  | }); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Pavel Feldman
						Pavel Feldman