| 
									
										
										
										
											2020-09-25 23:30:46 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Copyright Microsoft Corporation. All rights reserved. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  |  * you may not use this file except in compliance with the License. | 
					
						
							|  |  |  |  * You may obtain a copy of the License at | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  *     http://www.apache.org/licenses/LICENSE-2.0
 | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  |  * distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  |  * See the License for the specific language governing permissions and | 
					
						
							|  |  |  |  * limitations under the License. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-11 10:52:17 -04:00
										 |  |  | import type { Frame, Page } from 'playwright-core'; | 
					
						
							| 
									
										
										
										
											2022-06-17 16:11:22 -07:00
										 |  |  | import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile'; | 
					
						
							| 
									
										
										
										
											2023-05-05 15:12:18 -07:00
										 |  |  | import type { TraceModelBackend } from '../../packages/trace-viewer/src/traceModel'; | 
					
						
							| 
									
										
										
										
											2023-02-27 22:31:47 -08:00
										 |  |  | import type { StackFrame } from '../../packages/protocol/src/channels'; | 
					
						
							| 
									
										
										
										
											2023-03-14 15:58:55 -07:00
										 |  |  | import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/utils/isomorphic/traceUtils'; | 
					
						
							| 
									
										
										
										
											2023-05-05 15:12:18 -07:00
										 |  |  | import { TraceModel } from '../../packages/trace-viewer/src/traceModel'; | 
					
						
							| 
									
										
										
										
											2023-07-05 11:20:28 -07:00
										 |  |  | import type { ActionTreeItem } from '../../packages/trace-viewer/src/ui/modelUtil'; | 
					
						
							|  |  |  | import { buildActionTree, MultiTraceModel } from '../../packages/trace-viewer/src/ui/modelUtil'; | 
					
						
							| 
									
										
										
										
											2023-09-19 16:21:09 -07:00
										 |  |  | import type { ActionTraceEvent, ConsoleMessageTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace'; | 
					
						
							| 
									
										
										
										
											2020-09-25 23:30:46 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> { | 
					
						
							|  |  |  |   const handle = await page.evaluateHandle(async ({ frameId, url }) => { | 
					
						
							|  |  |  |     const frame = document.createElement('iframe'); | 
					
						
							|  |  |  |     frame.src = url; | 
					
						
							|  |  |  |     frame.id = frameId; | 
					
						
							|  |  |  |     document.body.appendChild(frame); | 
					
						
							|  |  |  |     await new Promise(x => frame.onload = x); | 
					
						
							|  |  |  |     return frame; | 
					
						
							|  |  |  |   }, { frameId, url }); | 
					
						
							| 
									
										
										
										
											2023-02-27 15:29:20 -08:00
										 |  |  |   return handle.asElement().contentFrame() as Promise<Frame>; | 
					
						
							| 
									
										
										
										
											2020-09-25 23:30:46 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export async function detachFrame(page: Page, frameId: string) { | 
					
						
							|  |  |  |   await page.evaluate(frameId => { | 
					
						
							| 
									
										
										
										
											2023-02-27 15:29:20 -08:00
										 |  |  |     document.getElementById(frameId)!.remove(); | 
					
						
							| 
									
										
										
										
											2020-09-25 23:30:46 -07:00
										 |  |  |   }, frameId); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export async function verifyViewport(page: Page, width: number, height: number) { | 
					
						
							| 
									
										
										
										
											2023-01-03 17:30:36 -08:00
										 |  |  |   // `expect` may clash in test runner tests if imported eagerly.
 | 
					
						
							|  |  |  |   const { expect } = require('@playwright/test'); | 
					
						
							| 
									
										
										
										
											2023-02-27 15:29:20 -08:00
										 |  |  |   expect(page.viewportSize()!.width).toBe(width); | 
					
						
							|  |  |  |   expect(page.viewportSize()!.height).toBe(height); | 
					
						
							| 
									
										
										
										
											2020-09-25 23:30:46 -07:00
										 |  |  |   expect(await page.evaluate('window.innerWidth')).toBe(width); | 
					
						
							|  |  |  |   expect(await page.evaluate('window.innerHeight')).toBe(height); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-23 17:59:07 +02:00
										 |  |  | export function expectedSSLError(browserName: string, platform: string): RegExp { | 
					
						
							|  |  |  |   if (browserName === 'chromium') | 
					
						
							|  |  |  |     return /net::(ERR_CERT_AUTHORITY_INVALID|ERR_CERT_INVALID)/; | 
					
						
							|  |  |  |   if (browserName === 'webkit') { | 
					
						
							| 
									
										
										
										
											2023-07-31 11:24:04 -07:00
										 |  |  |     if (platform === 'darwin') | 
					
						
							| 
									
										
										
										
											2023-08-23 17:59:07 +02:00
										 |  |  |       return /The certificate for this server is invalid/; | 
					
						
							| 
									
										
										
										
											2023-07-31 11:24:04 -07:00
										 |  |  |     else if (platform === 'win32') | 
					
						
							| 
									
										
										
										
											2023-08-23 17:59:07 +02:00
										 |  |  |       return /SSL peer certificate or SSH remote key was not OK/; | 
					
						
							| 
									
										
										
										
											2020-09-25 23:30:46 -07:00
										 |  |  |     else | 
					
						
							| 
									
										
										
										
											2023-08-23 17:59:07 +02:00
										 |  |  |       return /Unacceptable TLS certificate/; | 
					
						
							| 
									
										
										
										
											2020-09-25 23:30:46 -07:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-08-23 17:59:07 +02:00
										 |  |  |   return /SSL_ERROR_UNKNOWN/; | 
					
						
							| 
									
										
										
										
											2020-09-25 23:30:46 -07:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2021-05-04 18:59:39 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | export function chromiumVersionLessThan(a: string, b: string) { | 
					
						
							|  |  |  |   const left: number[] = a.split('.').map(e => Number(e)); | 
					
						
							|  |  |  |   const right: number[] = b.split('.').map(e => Number(e)); | 
					
						
							|  |  |  |   for (let i = 0; i < 4; i++) { | 
					
						
							|  |  |  |     if (left[i] > right[i]) | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     if (left[i] < right[i]) | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return false; | 
					
						
							| 
									
										
										
										
											2021-09-08 20:32:52 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | let didSuppressUnverifiedCertificateWarning = false; | 
					
						
							|  |  |  | let originalEmitWarning: (warning: string | Error, ...args: any[]) => void; | 
					
						
							|  |  |  | export function suppressCertificateWarning() { | 
					
						
							|  |  |  |   if (didSuppressUnverifiedCertificateWarning) | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   didSuppressUnverifiedCertificateWarning = true; | 
					
						
							|  |  |  |   // Supress one-time warning:
 | 
					
						
							|  |  |  |   // https://github.com/nodejs/node/blob/1bbe66f432591aea83555d27dd76c55fea040a0d/lib/internal/options.js#L37-L49
 | 
					
						
							|  |  |  |   originalEmitWarning = process.emitWarning; | 
					
						
							|  |  |  |   process.emitWarning = (warning, ...args) => { | 
					
						
							|  |  |  |     if (typeof warning === 'string' && warning.includes('NODE_TLS_REJECT_UNAUTHORIZED')) { | 
					
						
							|  |  |  |       process.emitWarning = originalEmitWarning; | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return originalEmitWarning.call(process, warning, ...args); | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2021-12-09 17:21:17 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-07 17:16:26 -07:00
										 |  |  | export async function parseTraceRaw(file: string): Promise<{ events: any[], resources: Map<string, Buffer>, actions: string[], actionObjects: ActionTraceEvent[], stacks: Map<string, StackFrame[]> }> { | 
					
						
							| 
									
										
										
										
											2022-06-17 16:11:22 -07:00
										 |  |  |   const zipFS = new ZipFile(file); | 
					
						
							| 
									
										
										
										
											2021-12-09 17:21:17 -08:00
										 |  |  |   const resources = new Map<string, Buffer>(); | 
					
						
							|  |  |  |   for (const entry of await zipFS.entries()) | 
					
						
							|  |  |  |     resources.set(entry, await zipFS.read(entry)); | 
					
						
							|  |  |  |   zipFS.close(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-15 22:33:40 -07:00
										 |  |  |   const actionMap = new Map<string, ActionTraceEvent>(); | 
					
						
							| 
									
										
										
										
											2023-02-27 15:29:20 -08:00
										 |  |  |   const events: any[] = []; | 
					
						
							| 
									
										
										
										
											2023-02-27 22:31:47 -08:00
										 |  |  |   for (const traceFile of [...resources.keys()].filter(name => name.endsWith('.trace'))) { | 
					
						
							|  |  |  |     for (const line of resources.get(traceFile)!.toString().split('\n')) { | 
					
						
							| 
									
										
										
										
											2023-03-15 22:33:40 -07:00
										 |  |  |       if (line) { | 
					
						
							|  |  |  |         const event = JSON.parse(line) as TraceEvent; | 
					
						
							| 
									
										
										
										
											2023-07-07 17:16:26 -07:00
										 |  |  |         events.push(event); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-15 22:33:40 -07:00
										 |  |  |         if (event.type === 'before') { | 
					
						
							|  |  |  |           const action: ActionTraceEvent = { | 
					
						
							|  |  |  |             ...event, | 
					
						
							|  |  |  |             type: 'action', | 
					
						
							|  |  |  |             endTime: 0, | 
					
						
							|  |  |  |             log: [] | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  |           actionMap.set(event.callId, action); | 
					
						
							|  |  |  |         } else if (event.type === 'input') { | 
					
						
							|  |  |  |           const existing = actionMap.get(event.callId); | 
					
						
							|  |  |  |           existing.inputSnapshot = event.inputSnapshot; | 
					
						
							|  |  |  |           existing.point = event.point; | 
					
						
							|  |  |  |         } else if (event.type === 'after') { | 
					
						
							|  |  |  |           const existing = actionMap.get(event.callId); | 
					
						
							|  |  |  |           existing.afterSnapshot = event.afterSnapshot; | 
					
						
							|  |  |  |           existing.endTime = event.endTime; | 
					
						
							|  |  |  |           existing.log = event.log; | 
					
						
							|  |  |  |           existing.error = event.error; | 
					
						
							|  |  |  |           existing.result = event.result; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2023-02-27 22:31:47 -08:00
										 |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   for (const networkFile of [...resources.keys()].filter(name => name.endsWith('.network'))) { | 
					
						
							|  |  |  |     for (const line of resources.get(networkFile)!.toString().split('\n')) { | 
					
						
							|  |  |  |       if (line) | 
					
						
							|  |  |  |         events.push(JSON.parse(line)); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-12-09 17:21:17 -08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-02-22 21:08:47 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-27 22:31:47 -08:00
										 |  |  |   const stacks: Map<string, StackFrame[]> = new Map(); | 
					
						
							|  |  |  |   for (const stacksFile of [...resources.keys()].filter(name => name.endsWith('.stacks'))) { | 
					
						
							|  |  |  |     for (const [key, value] of parseClientSideCallMetadata(JSON.parse(resources.get(stacksFile)!.toString()))) | 
					
						
							|  |  |  |       stacks.set(key, value); | 
					
						
							| 
									
										
										
										
											2021-12-09 17:21:17 -08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-02-22 21:08:47 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-07 17:16:26 -07:00
										 |  |  |   const actionObjects = [...actionMap.values()]; | 
					
						
							|  |  |  |   actionObjects.sort((a, b) => a.startTime - b.startTime); | 
					
						
							| 
									
										
										
										
											2021-12-09 17:21:17 -08:00
										 |  |  |   return { | 
					
						
							|  |  |  |     events, | 
					
						
							|  |  |  |     resources, | 
					
						
							| 
									
										
										
										
											2023-07-07 17:16:26 -07:00
										 |  |  |     actions: actionObjects.map(a => a.apiName), | 
					
						
							|  |  |  |     actionObjects, | 
					
						
							| 
									
										
										
										
											2023-02-22 21:08:47 -08:00
										 |  |  |     stacks, | 
					
						
							| 
									
										
										
										
											2021-12-09 17:21:17 -08:00
										 |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-06-16 15:33:32 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-19 16:21:09 -07:00
										 |  |  | export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[] }> { | 
					
						
							| 
									
										
										
										
											2023-05-05 15:12:18 -07:00
										 |  |  |   const backend = new TraceBackend(file); | 
					
						
							|  |  |  |   const traceModel = new TraceModel(); | 
					
						
							|  |  |  |   await traceModel.load(backend, () => {}); | 
					
						
							|  |  |  |   const model = new MultiTraceModel(traceModel.contextEntries); | 
					
						
							| 
									
										
										
										
											2023-07-05 11:20:28 -07:00
										 |  |  |   const { rootItem } = buildActionTree(model.actions); | 
					
						
							|  |  |  |   const actionTree: string[] = []; | 
					
						
							|  |  |  |   const visit = (actionItem: ActionTreeItem, indent: string) => { | 
					
						
							|  |  |  |     actionTree.push(`${indent}${actionItem.action?.apiName || actionItem.id}`); | 
					
						
							|  |  |  |     for (const child of actionItem.children) | 
					
						
							|  |  |  |       visit(child, indent + '  '); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  |   rootItem.children.forEach(a => visit(a, '')); | 
					
						
							| 
									
										
										
										
											2023-05-05 15:12:18 -07:00
										 |  |  |   return { | 
					
						
							|  |  |  |     apiNames: model.actions.map(a => a.apiName), | 
					
						
							|  |  |  |     resources: backend.entries, | 
					
						
							|  |  |  |     actions: model.actions, | 
					
						
							|  |  |  |     events: model.events, | 
					
						
							|  |  |  |     model, | 
					
						
							|  |  |  |     traceModel, | 
					
						
							| 
									
										
										
										
											2023-07-05 11:20:28 -07:00
										 |  |  |     actionTree, | 
					
						
							| 
									
										
										
										
											2023-05-05 15:12:18 -07:00
										 |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-16 15:33:32 -08:00
										 |  |  | export async function parseHar(file: string): Promise<Map<string, Buffer>> { | 
					
						
							| 
									
										
										
										
											2022-06-17 16:11:22 -07:00
										 |  |  |   const zipFS = new ZipFile(file); | 
					
						
							| 
									
										
										
										
											2022-06-16 15:33:32 -08:00
										 |  |  |   const resources = new Map<string, Buffer>(); | 
					
						
							|  |  |  |   for (const entry of await zipFS.entries()) | 
					
						
							|  |  |  |     resources.set(entry, await zipFS.read(entry)); | 
					
						
							|  |  |  |   zipFS.close(); | 
					
						
							|  |  |  |   return resources; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-07-01 13:57:33 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | export function waitForTestLog<T>(page: Page, prefix: string): Promise<T> { | 
					
						
							|  |  |  |   return new Promise<T>(resolve => { | 
					
						
							|  |  |  |     page.on('console', message => { | 
					
						
							|  |  |  |       const text = message.text(); | 
					
						
							|  |  |  |       if (text.startsWith(prefix)) { | 
					
						
							|  |  |  |         const json = text.substring(prefix.length); | 
					
						
							|  |  |  |         resolve(JSON.parse(json)); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-08-10 15:10:25 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | const ansiRegex = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g'); | 
					
						
							|  |  |  | export function stripAnsi(str: string): string { | 
					
						
							|  |  |  |   return str.replace(ansiRegex, ''); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-05-05 15:12:18 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TraceBackend implements TraceModelBackend { | 
					
						
							|  |  |  |   private _fileName: string; | 
					
						
							|  |  |  |   private _entriesPromise: Promise<Map<string, Buffer>>; | 
					
						
							|  |  |  |   readonly entries = new Map<string, Buffer>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(fileName: string) { | 
					
						
							|  |  |  |     this._fileName = fileName; | 
					
						
							|  |  |  |     this._entriesPromise = this._readEntries(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private async _readEntries(): Promise<Map<string, Buffer>> { | 
					
						
							|  |  |  |     const zipFS = new ZipFile(this._fileName); | 
					
						
							|  |  |  |     for (const entry of await zipFS.entries()) | 
					
						
							|  |  |  |       this.entries.set(entry, await zipFS.read(entry)); | 
					
						
							|  |  |  |     zipFS.close(); | 
					
						
							|  |  |  |     return this.entries; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   isLive() { | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   traceURL() { | 
					
						
							|  |  |  |     return 'file://' + this._fileName; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async entryNames(): Promise<string[]> { | 
					
						
							|  |  |  |     const entries = await this._entriesPromise; | 
					
						
							|  |  |  |     return [...entries.keys()]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async hasEntry(entryName: string): Promise<boolean> { | 
					
						
							|  |  |  |     const entries = await this._entriesPromise; | 
					
						
							|  |  |  |     return entries.has(entryName); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async readText(entryName: string): Promise<string | undefined> { | 
					
						
							|  |  |  |     const entries = await this._entriesPromise; | 
					
						
							|  |  |  |     const entry = entries.get(entryName); | 
					
						
							|  |  |  |     if (!entry) | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     return entry.toString(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async readBlob(entryName: string) { | 
					
						
							|  |  |  |     const entries = await this._entriesPromise; | 
					
						
							|  |  |  |     const entry = entries.get(entryName); | 
					
						
							|  |  |  |     return entry as any; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |