fix(electron): better support for custom schemas (#13329)
This commit is contained in:
		
							parent
							
								
									67989e01d1
								
							
						
					
					
						commit
						6ca58e18cb
					
				|  | @ -333,7 +333,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string) { | |||
|           expectValue(cssText); | ||||
|           // Compensate for the extra 'cssText' text node.
 | ||||
|           extraNodes++; | ||||
|           return checkAndReturn(['style', {}, cssText]); | ||||
|           return checkAndReturn([nodeName, {}, cssText]); | ||||
|         } | ||||
| 
 | ||||
|         const attrs: { [attr: string]: string } = {}; | ||||
|  |  | |||
|  | @ -40,10 +40,16 @@ export class SnapshotRenderer { | |||
|   } | ||||
| 
 | ||||
|   render(): RenderedFrameSnapshot { | ||||
|     const visit = (n: NodeSnapshot, snapshotIndex: number): string => { | ||||
|     const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined): string => { | ||||
|       // Text node.
 | ||||
|       if (typeof n === 'string') | ||||
|         return escapeText(n); | ||||
|       if (typeof n === 'string') { | ||||
|         const text = escapeText(n); | ||||
|         // Best-effort Electron support: rewrite custom protocol in url() links in stylesheets.
 | ||||
|         // Old snapshotter was sending lower-case.
 | ||||
|         if (parentTag === 'STYLE' || parentTag === 'style') | ||||
|           return rewriteURLsInStyleSheetForCustomProtocol(text); | ||||
|         return text; | ||||
|       } | ||||
| 
 | ||||
|       if (!(n as any)._string) { | ||||
|         if (Array.isArray(n[0])) { | ||||
|  | @ -53,7 +59,7 @@ export class SnapshotRenderer { | |||
|             const nodes = snapshotNodes(this._snapshots[referenceIndex]); | ||||
|             const nodeIndex = n[0][1]; | ||||
|             if (nodeIndex >= 0 && nodeIndex < nodes.length) | ||||
|               (n as any)._string = visit(nodes[nodeIndex], referenceIndex); | ||||
|               (n as any)._string = visit(nodes[nodeIndex], referenceIndex, parentTag); | ||||
|           } | ||||
|         } else if (typeof n[0] === 'string') { | ||||
|           // Element node.
 | ||||
|  | @ -68,7 +74,7 @@ export class SnapshotRenderer { | |||
|           } | ||||
|           builder.push('>'); | ||||
|           for (let i = 2; i < n.length; i++) | ||||
|             builder.push(visit(n[i], snapshotIndex)); | ||||
|             builder.push(visit(n[i], snapshotIndex, n[0])); | ||||
|           if (!autoClosing.has(n[0])) | ||||
|             builder.push('</', n[0], '>'); | ||||
|           (n as any)._string = builder.join(''); | ||||
|  | @ -81,7 +87,7 @@ export class SnapshotRenderer { | |||
|     }; | ||||
| 
 | ||||
|     const snapshot = this._snapshot; | ||||
|     let html = visit(snapshot.html, this._index); | ||||
|     let html = visit(snapshot.html, this._index, undefined); | ||||
|     if (!html) | ||||
|       return { html: '', pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index }; | ||||
| 
 | ||||
|  | @ -275,8 +281,12 @@ function snapshotScript() { | |||
|   return `\n(${applyPlaywrightAttributes.toString()})()`; | ||||
| } | ||||
| 
 | ||||
| const schemas = ['about:', 'blob:', 'data:', 'file:', 'ftp:', 'http:', 'https:', 'mailto:', 'sftp:', 'ws:', 'wss:' ]; | ||||
| 
 | ||||
| /** | ||||
|  * Best-effort Electron support: rewrite custom protocol in DOM. | ||||
|  * vscode-file://vscode-app/ -> https://pw-vscode-file--vscode-app/
 | ||||
|  */ | ||||
| const schemas = ['about:', 'blob:', 'data:', 'file:', 'ftp:', 'http:', 'https:', 'mailto:', 'sftp:', 'ws:', 'wss:' ]; | ||||
| const kLegacyBlobPrefix = 'http://playwright.bloburl/#'; | ||||
| 
 | ||||
| export function rewriteURLForCustomProtocol(href: string): string { | ||||
|  | @ -298,9 +308,24 @@ export function rewriteURLForCustomProtocol(href: string): string { | |||
|     // Rewrite blob and custom schemas.
 | ||||
|     const prefix = 'pw-' + url.protocol.slice(0, url.protocol.length - 1); | ||||
|     url.protocol = 'https:'; | ||||
|     url.hostname = url.hostname ? `${prefix}.${url.hostname}` : prefix; | ||||
|     url.hostname = url.hostname ? `${prefix}--${url.hostname}` : prefix; | ||||
|     return url.toString(); | ||||
|   } catch { | ||||
|     return href; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Best-effort Electron support: rewrite custom protocol in inline stylesheets. | ||||
|  * vscode-file://vscode-app/ -> https://pw-vscode-file--vscode-app/
 | ||||
|  */ | ||||
| const urlInCSSRegex = /url\(['"]?([\w-]+:)\/\//ig; | ||||
| 
 | ||||
| function rewriteURLsInStyleSheetForCustomProtocol(text: string): string { | ||||
|   return text.replace(urlInCSSRegex, (match: string, protocol: string) => { | ||||
|     const isBlob = protocol === 'blob:'; | ||||
|     if (!isBlob && schemas.includes(protocol)) | ||||
|       return match; | ||||
|     return match.replace(protocol + '//', `https://pw-${protocol.slice(0, -1)}--`); | ||||
|   }); | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,133 @@ | |||
| /** | ||||
|  * Copyright (c) Microsoft Corporation. | ||||
|  * | ||||
|  * 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. | ||||
|  */ | ||||
| 
 | ||||
| import type { Fixtures, Frame, Locator, Page, Browser, BrowserContext } from '@playwright/test'; | ||||
| import { showTraceViewer } from '../../packages/playwright-core/lib/server/trace/viewer/traceViewer'; | ||||
| 
 | ||||
| type BaseTestFixtures = { | ||||
|   context: BrowserContext; | ||||
| }; | ||||
| 
 | ||||
| type BaseWorkerFixtures = { | ||||
|   headless: boolean; | ||||
|   browser: Browser; | ||||
|   browserName: 'chromium' | 'firefox' | 'webkit'; | ||||
|   playwright: typeof import('@playwright/test'); | ||||
| }; | ||||
| 
 | ||||
| export type TraceViewerFixtures = { | ||||
|   showTraceViewer: (trace: string[]) => Promise<TraceViewerPage>; | ||||
|   runAndTrace: (body: () => Promise<void>) => Promise<TraceViewerPage>; | ||||
| }; | ||||
| 
 | ||||
| class TraceViewerPage { | ||||
|   actionTitles: Locator; | ||||
|   callLines: Locator; | ||||
|   consoleLines: Locator; | ||||
|   consoleLineMessages: Locator; | ||||
|   consoleStacks: Locator; | ||||
|   stackFrames: Locator; | ||||
|   networkRequests: Locator; | ||||
|   snapshotContainer: Locator; | ||||
| 
 | ||||
|   constructor(public page: Page) { | ||||
|     this.actionTitles = page.locator('.action-title'); | ||||
|     this.callLines = page.locator('.call-line'); | ||||
|     this.consoleLines = page.locator('.console-line'); | ||||
|     this.consoleLineMessages = page.locator('.console-line-message'); | ||||
|     this.consoleStacks = page.locator('.console-stack'); | ||||
|     this.stackFrames = page.locator('.stack-trace-frame'); | ||||
|     this.networkRequests = page.locator('.network-request-title'); | ||||
|     this.snapshotContainer = page.locator('.snapshot-container'); | ||||
|   } | ||||
| 
 | ||||
|   async actionIconsText(action: string) { | ||||
|     const entry = await this.page.waitForSelector(`.action-entry:has-text("${action}")`); | ||||
|     await entry.waitForSelector('.action-icon-value:visible'); | ||||
|     return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent)); | ||||
|   } | ||||
| 
 | ||||
|   async actionIcons(action: string) { | ||||
|     return await this.page.waitForSelector(`.action-entry:has-text("${action}") .action-icons`); | ||||
|   } | ||||
| 
 | ||||
|   async selectAction(title: string, ordinal: number = 0) { | ||||
|     await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click(); | ||||
|   } | ||||
| 
 | ||||
|   async selectSnapshot(name: string) { | ||||
|     await this.page.click(`.snapshot-tab .tab-label:has-text("${name}")`); | ||||
|   } | ||||
| 
 | ||||
|   async showConsoleTab() { | ||||
|     await this.page.click('text="Console"'); | ||||
|   } | ||||
| 
 | ||||
|   async showSourceTab() { | ||||
|     await this.page.click('text="Source"'); | ||||
|   } | ||||
| 
 | ||||
|   async showNetworkTab() { | ||||
|     await this.page.click('text="Network"'); | ||||
|   } | ||||
| 
 | ||||
|   async eventBars() { | ||||
|     await this.page.waitForSelector('.timeline-bar.event:visible'); | ||||
|     const list = await this.page.$$eval('.timeline-bar.event:visible', ee => ee.map(e => e.className)); | ||||
|     const set = new Set<string>(); | ||||
|     for (const item of list) { | ||||
|       for (const className of item.split(' ')) | ||||
|         set.add(className); | ||||
|     } | ||||
|     const result = [...set]; | ||||
|     return result.sort(); | ||||
|   } | ||||
| 
 | ||||
|   async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<Frame> { | ||||
|     const existing = this.page.mainFrame().childFrames()[0]; | ||||
|     await Promise.all([ | ||||
|       existing ? existing.waitForNavigation() as any : Promise.resolve(), | ||||
|       this.selectAction(actionName, ordinal), | ||||
|     ]); | ||||
|     while (this.page.frames().length < (hasSubframe ? 3 : 2)) | ||||
|       await this.page.waitForEvent('frameattached'); | ||||
|     return this.page.mainFrame().childFrames()[0]; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export const traceViewerFixtures: Fixtures<TraceViewerFixtures, {}, BaseTestFixtures, BaseWorkerFixtures> = { | ||||
|   showTraceViewer: async ({ playwright, browserName, headless }, use) => { | ||||
|     let browser: Browser; | ||||
|     let contextImpl: any; | ||||
|     await use(async (traces: string[]) => { | ||||
|       contextImpl = await showTraceViewer(traces, browserName, headless); | ||||
|       browser = await playwright.chromium.connectOverCDP(contextImpl._browser.options.wsEndpoint); | ||||
|       return new TraceViewerPage(browser.contexts()[0].pages()[0]); | ||||
|     }); | ||||
|     await browser?.close(); | ||||
|     await contextImpl?._browser.close(); | ||||
|   }, | ||||
| 
 | ||||
|   runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => { | ||||
|     await use(async (body: () => Promise<void>) => { | ||||
|       const traceFile = testInfo.outputPath('trace.zip'); | ||||
|       await context.tracing.start({ snapshots: true, screenshots: true, sources: true }); | ||||
|       await body(); | ||||
|       await context.tracing.stop({ path: traceFile }); | ||||
|       return showTraceViewer([traceFile]); | ||||
|     }); | ||||
|   }, | ||||
| }; | ||||
|  | @ -0,0 +1,3 @@ | |||
| button { | ||||
|   font-weight: bold; | ||||
| } | ||||
|  | @ -0,0 +1,15 @@ | |||
| <!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|     <link rel="stylesheet" href="vscode-file://style.css"> | ||||
|     <style> | ||||
|       @import url("vscode-file://imported.css"); | ||||
|       button { | ||||
|         background: url(background.png); | ||||
|       } | ||||
|     </style> | ||||
|   </head> | ||||
|   <body> | ||||
|     <button>Click me</button> | ||||
|   </body> | ||||
| </html> | ||||
|  | @ -0,0 +1,3 @@ | |||
| button { | ||||
|   color: red; | ||||
| } | ||||
|  | @ -1,3 +1,11 @@ | |||
| const { app } = require('electron'); | ||||
| const { app, protocol } = require('electron'); | ||||
| const path = require('path'); | ||||
| 
 | ||||
| app.on('window-all-closed', e => e.preventDefault()); | ||||
| 
 | ||||
| app.whenReady().then(() => { | ||||
|   protocol.registerFileProtocol('vscode-file', (request, callback) => { | ||||
|     const url = request.url.substring('vscode-file'.length + 3); | ||||
|     callback({ path: path.join(__dirname, 'assets', url) }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -0,0 +1,47 @@ | |||
| /** | ||||
|  * Copyright (c) Microsoft Corporation. | ||||
|  * | ||||
|  * 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. | ||||
|  */ | ||||
| 
 | ||||
| import { electronTest as test, expect } from './electronTest'; | ||||
| 
 | ||||
| test.skip(({ trace }) => trace === 'on'); | ||||
| // test.slow();
 | ||||
| 
 | ||||
| test('should record trace', async ({ newWindow, server, runAndTrace }) => { | ||||
|   const traceViewer = await runAndTrace(async () => { | ||||
|     const window = await newWindow(); | ||||
|     await window.goto(server.PREFIX + '/input/button.html'); | ||||
|     await window.click('button'); | ||||
|     expect(await window.evaluate('result')).toBe('Clicked'); | ||||
|   }); | ||||
|   await expect(traceViewer.actionTitles).toHaveText([ | ||||
|     /page.goto/, | ||||
|     /page.click/, | ||||
|     /page.evaluate/, | ||||
|   ]); | ||||
| }); | ||||
| 
 | ||||
| test('should support custom protocol', async ({ electronApp, newWindow, server, runAndTrace }) => { | ||||
|   const window = await newWindow(); | ||||
|   await electronApp.evaluate(async ({ BrowserWindow }) => { | ||||
|     BrowserWindow.getAllWindows()[0].loadURL('vscode-file://index.html'); | ||||
|   }); | ||||
|   const traceViewer = await runAndTrace(async () => { | ||||
|     await window.click('button'); | ||||
|   }); | ||||
|   const frame = await traceViewer.snapshotFrame('page.click'); | ||||
|   await expect(frame.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); | ||||
|   await expect(frame.locator('button')).toHaveCSS('font-weight', '700'); | ||||
| }); | ||||
|  | @ -16,8 +16,9 @@ | |||
| 
 | ||||
| import { baseTest } from '../config/baseTest'; | ||||
| import * as path from 'path'; | ||||
| import { ElectronApplication, Page } from 'playwright-core'; | ||||
| import { ElectronApplication, Page } from '@playwright/test'; | ||||
| import { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi'; | ||||
| import { traceViewerFixtures, TraceViewerFixtures } from '../config/traceViewerFixtures'; | ||||
| export { expect } from '@playwright/test'; | ||||
| 
 | ||||
| type ElectronTestFixtures = PageTestFixtures & { | ||||
|  | @ -27,7 +28,7 @@ type ElectronTestFixtures = PageTestFixtures & { | |||
| 
 | ||||
| const electronVersion = require('electron/package.json').version; | ||||
| 
 | ||||
| export const electronTest = baseTest.extend<ElectronTestFixtures, PageWorkerFixtures>({ | ||||
| export const electronTest = baseTest.extend<TraceViewerFixtures>(traceViewerFixtures).extend<ElectronTestFixtures, PageWorkerFixtures>({ | ||||
|   browserVersion: [electronVersion, { scope: 'worker' }], | ||||
|   browserMajorVersion: [Number(electronVersion.split('.')[0]), { scope: 'worker' }], | ||||
|   isAndroid: [false, { scope: 'worker' }], | ||||
|  | @ -71,4 +72,8 @@ export const electronTest = baseTest.extend<ElectronTestFixtures, PageWorkerFixt | |||
|   page: async ({ newWindow }, run) => { | ||||
|     await run(await newWindow()); | ||||
|   }, | ||||
| 
 | ||||
|   context: async ({ electronApp }, run) => { | ||||
|     await run(electronApp.context()); | ||||
|   }, | ||||
| }); | ||||
|  |  | |||
|  | @ -68,11 +68,11 @@ it.describe('snapshots', () => { | |||
|   it('should respect inline CSSOM change', async ({ page, toImpl, snapshotter }) => { | ||||
|     await page.setContent('<style>button { color: red; }</style><button>Hello</button>'); | ||||
|     const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1'); | ||||
|     expect(distillSnapshot(snapshot1)).toBe('<style>button { color: red; }</style><BUTTON>Hello</BUTTON>'); | ||||
|     expect(distillSnapshot(snapshot1)).toBe('<STYLE>button { color: red; }</STYLE><BUTTON>Hello</BUTTON>'); | ||||
| 
 | ||||
|     await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; }); | ||||
|     const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2'); | ||||
|     expect(distillSnapshot(snapshot2)).toBe('<style>button { color: blue; }</style><BUTTON>Hello</BUTTON>'); | ||||
|     expect(distillSnapshot(snapshot2)).toBe('<STYLE>button { color: blue; }</STYLE><BUTTON>Hello</BUTTON>'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should respect node removal', async ({ page, toImpl, snapshotter }) => { | ||||
|  |  | |||
|  | @ -14,110 +14,12 @@ | |||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import { TraceViewerFixtures, traceViewerFixtures } from '../config/traceViewerFixtures'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import type { Browser, Frame, Locator, Page } from 'playwright-core'; | ||||
| import { showTraceViewer } from '../../packages/playwright-core/lib/server/trace/viewer/traceViewer'; | ||||
| import { playwrightTest, expect } from '../config/browserTest'; | ||||
| import { expect, playwrightTest } from '../config/browserTest'; | ||||
| 
 | ||||
| class TraceViewerPage { | ||||
|   actionTitles: Locator; | ||||
|   callLines: Locator; | ||||
|   consoleLines: Locator; | ||||
|   consoleLineMessages: Locator; | ||||
|   consoleStacks: Locator; | ||||
|   stackFrames: Locator; | ||||
|   networkRequests: Locator; | ||||
|   snapshotContainer: Locator; | ||||
| 
 | ||||
|   constructor(public page: Page) { | ||||
|     this.actionTitles = page.locator('.action-title'); | ||||
|     this.callLines = page.locator('.call-line'); | ||||
|     this.consoleLines = page.locator('.console-line'); | ||||
|     this.consoleLineMessages = page.locator('.console-line-message'); | ||||
|     this.consoleStacks = page.locator('.console-stack'); | ||||
|     this.stackFrames = page.locator('.stack-trace-frame'); | ||||
|     this.networkRequests = page.locator('.network-request-title'); | ||||
|     this.snapshotContainer = page.locator('.snapshot-container'); | ||||
|   } | ||||
| 
 | ||||
|   async actionIconsText(action: string) { | ||||
|     const entry = await this.page.waitForSelector(`.action-entry:has-text("${action}")`); | ||||
|     await entry.waitForSelector('.action-icon-value:visible'); | ||||
|     return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent)); | ||||
|   } | ||||
| 
 | ||||
|   async actionIcons(action: string) { | ||||
|     return await this.page.waitForSelector(`.action-entry:has-text("${action}") .action-icons`); | ||||
|   } | ||||
| 
 | ||||
|   async selectAction(title: string, ordinal: number = 0) { | ||||
|     await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click(); | ||||
|   } | ||||
| 
 | ||||
|   async selectSnapshot(name: string) { | ||||
|     await this.page.click(`.snapshot-tab .tab-label:has-text("${name}")`); | ||||
|   } | ||||
| 
 | ||||
|   async showConsoleTab() { | ||||
|     await this.page.click('text="Console"'); | ||||
|   } | ||||
| 
 | ||||
|   async showSourceTab() { | ||||
|     await this.page.click('text="Source"'); | ||||
|   } | ||||
| 
 | ||||
|   async showNetworkTab() { | ||||
|     await this.page.click('text="Network"'); | ||||
|   } | ||||
| 
 | ||||
|   async eventBars() { | ||||
|     await this.page.waitForSelector('.timeline-bar.event:visible'); | ||||
|     const list = await this.page.$$eval('.timeline-bar.event:visible', ee => ee.map(e => e.className)); | ||||
|     const set = new Set<string>(); | ||||
|     for (const item of list) { | ||||
|       for (const className of item.split(' ')) | ||||
|         set.add(className); | ||||
|     } | ||||
|     const result = [...set]; | ||||
|     return result.sort(); | ||||
|   } | ||||
| 
 | ||||
|   async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<Frame> { | ||||
|     const existing = this.page.mainFrame().childFrames()[0]; | ||||
|     await Promise.all([ | ||||
|       existing ? existing.waitForNavigation() as any : Promise.resolve(), | ||||
|       this.selectAction(actionName, ordinal), | ||||
|     ]); | ||||
|     while (this.page.frames().length < (hasSubframe ? 3 : 2)) | ||||
|       await this.page.waitForEvent('frameattached'); | ||||
|     return this.page.mainFrame().childFrames()[0]; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 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 (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]); | ||||
|     }); | ||||
|     await browser?.close(); | ||||
|     await contextImpl?._browser.close(); | ||||
|   }, | ||||
| 
 | ||||
|   runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => { | ||||
|     await use(async (body: () => Promise<void>) => { | ||||
|       const traceFile = testInfo.outputPath('trace.zip'); | ||||
|       await context.tracing.start({ snapshots: true, screenshots: true, sources: true }); | ||||
|       await body(); | ||||
|       await context.tracing.stop({ path: traceFile }); | ||||
|       return showTraceViewer([traceFile]); | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
| const test = playwrightTest.extend<TraceViewerFixtures>(traceViewerFixtures); | ||||
| 
 | ||||
| test.skip(({ trace }) => trace === 'on'); | ||||
| test.slow(); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue