diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index f6c2131210..6bbdbb8631 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -975,6 +975,9 @@ The file path to save the storage state to. If [`option: path`] is a relative pa current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. +## property: BrowserContext.tracing +- type: <[Tracing]> + ## async method: BrowserContext.unroute Removes a route created with [`method: BrowserContext.route`]. When [`param: handler`] is not specified, removes all diff --git a/docs/src/api/class-tracing.md b/docs/src/api/class-tracing.md new file mode 100644 index 0000000000..49a20ae8a6 --- /dev/null +++ b/docs/src/api/class-tracing.md @@ -0,0 +1,72 @@ +# class: Tracing + +Tracing object for collecting test traces that can be opened using +Playwright CLI. + +## async method: Tracing.export + +Export trace into the file with the given name. Should be called after the +tracing has stopped. + +### param: Tracing.export.path +- `path` <[path]> + +File to save the trace into. + +## async method: Tracing.start + +Start tracing. + +```js +await context.tracing.start({ name: 'trace', screenshots: true, snapshots: true }); +const page = await context.newPage(); +await page.goto('https://playwright.dev'); +await context.tracing.stop(); +await context.tracing.export('trace.zip'); +``` + +```java +context.tracing.start(page, new Tracing.StartOptions() + .setName("trace") + .setScreenshots(true) + .setSnapshots(true); +Page page = context.newPage(); +page.goto('https://playwright.dev'); +context.tracing.stop(); +context.tracing.export(Paths.get("trace.zip"))) +``` + +```python async +await context.tracing.start(name="trace" screenshots=True snapshots=True) +await page.goto("https://playwright.dev") +await context.tracing.stop() +await context.tracing.export("trace.zip") +``` + +```python sync +context.tracing.start(name="trace" screenshots=True snapshots=True) +page.goto("https://playwright.dev") +context.tracing.stop() +context.tracing.export("trace.zip") +``` + +### option: Tracing.start.name +- `name` <[string]> + +If specified, the trace is going to be saved into the file with the +given name. + +### option: Tracing.start.screenshots +- `screenshots` <[boolean]> + +Whether to capture screenshots during tracing. Screenshots are used to build +a timeline preview. + +### option: Tracing.start.snapshots +- `snapshots` <[boolean]> + +Whether to capture DOM snapshot on every action. + +## async method: Tracing.stop + +Stop tracing. diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 4b3663de55..4e78ee6ae1 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -159,21 +159,25 @@ commandWithOpenOptions('pdf ', 'save page as pdf', console.log(' $ pdf https://example.com example.pdf'); }); -if (process.env.PWTRACE) { - program - .command('show-trace [trace]') - .option('--resources ', 'load resources from shared folder') - .description('Show trace viewer') - .action(function(trace, command) { - showTraceViewer(trace, command.resources).catch(logErrorAndExit); - }).on('--help', function() { - console.log(''); - console.log('Examples:'); - console.log(''); - console.log(' $ show-trace --resources=resources trace/file.trace'); - console.log(' $ show-trace trace/directory'); - }); -} +program + .command('show-trace [trace]') + .option('-b, --browser ', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium') + .description('Show trace viewer') + .action(function(trace, command) { + if (command.browser === 'cr') + command.browser = 'chromium'; + if (command.browser === 'ff') + command.browser = 'firefox'; + if (command.browser === 'wk') + command.browser = 'webkit'; + showTraceViewer(trace, command.browser).catch(logErrorAndExit); + }).on('--help', function() { + console.log(''); + console.log('Examples:'); + console.log(''); + console.log(' $ show-trace --resources=resources trace/file.trace'); + console.log(' $ show-trace trace/directory'); + }); if (process.argv[2] === 'run-driver') runDriver(); diff --git a/src/client/api.ts b/src/client/api.ts index fa0a7a165a..c5a0b1ad92 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -35,6 +35,7 @@ export { JSHandle } from './jsHandle'; export { Request, Response, Route, WebSocket } from './network'; export { Page } from './page'; export { Selectors } from './selectors'; +export { Tracing } from './tracing'; export { Video } from './video'; export { Worker } from './worker'; export { CDPSession } from './cdpSession'; diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index 9ab8c7c9d9..0df343192b 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -50,7 +50,7 @@ export class BrowserContext extends ChannelOwner(); readonly _serviceWorkers = new Set(); @@ -69,7 +69,7 @@ export class BrowserContext extends ChannelOwner this._onBinding(BindingCall.from(binding))); this._channel.on('close', () => this._onClose()); diff --git a/src/client/tracing.ts b/src/client/tracing.ts index 198ef53c50..644ebbac6a 100644 --- a/src/client/tracing.ts +++ b/src/client/tracing.ts @@ -14,18 +14,19 @@ * limitations under the License. */ +import * as api from '../../types/types'; import * as channels from '../protocol/channels'; import { Artifact } from './artifact'; import { BrowserContext } from './browserContext'; -export class Tracing { +export class Tracing implements api.Tracing { private _context: BrowserContext; constructor(channel: BrowserContext) { this._context = channel; } - async start(options: { snapshots?: boolean, screenshots?: boolean } = {}) { + async start(options: { name?: string, snapshots?: boolean, screenshots?: boolean } = {}) { await this._context._wrapApiCall('tracing.start', async (channel: channels.BrowserContextChannel) => { return await channel.tracingStart(options); }); diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 0fcf0ef0cd..fa1c2edf28 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -226,7 +226,7 @@ export type BrowserTypeLaunchParams = { password?: string, }, downloadsPath?: string, - _traceDir?: string, + traceDir?: string, chromiumSandbox?: boolean, firefoxUserPrefs?: any, slowMo?: number, @@ -251,7 +251,7 @@ export type BrowserTypeLaunchOptions = { password?: string, }, downloadsPath?: string, - _traceDir?: string, + traceDir?: string, chromiumSandbox?: boolean, firefoxUserPrefs?: any, slowMo?: number, @@ -279,7 +279,7 @@ export type BrowserTypeLaunchPersistentContextParams = { password?: string, }, downloadsPath?: string, - _traceDir?: string, + traceDir?: string, chromiumSandbox?: boolean, sdkLanguage: string, noDefaultViewport?: boolean, @@ -349,7 +349,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { password?: string, }, downloadsPath?: string, - _traceDir?: string, + traceDir?: string, chromiumSandbox?: boolean, noDefaultViewport?: boolean, viewport?: { diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 80cf723750..347d23d694 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -260,7 +260,7 @@ LaunchOptions: username: string? password: string? downloadsPath: string? - _traceDir: string? + traceDir: string? chromiumSandbox: boolean? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 99819277a1..c2f69b8de3 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -172,7 +172,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { password: tOptional(tString), })), downloadsPath: tOptional(tString), - _traceDir: tOptional(tString), + traceDir: tOptional(tString), chromiumSandbox: tOptional(tBoolean), firefoxUserPrefs: tOptional(tAny), slowMo: tOptional(tNumber), @@ -197,7 +197,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { password: tOptional(tString), })), downloadsPath: tOptional(tString), - _traceDir: tOptional(tString), + traceDir: tOptional(tString), chromiumSandbox: tOptional(tBoolean), sdkLanguage: tString, noDefaultViewport: tOptional(tBoolean), diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 43dbe021ad..1e657b7e0e 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -115,7 +115,7 @@ export abstract class BrowserType extends SdkObject { protocolLogger, browserLogsCollector, wsEndpoint: options.useWebSocket ? (transport as WebSocketTransport).wsEndpoint : undefined, - traceDir: options._traceDir, + traceDir: options.traceDir, }; if (persistent) validateBrowserContextOptions(persistent, browserOptions); diff --git a/src/server/snapshot/snapshotServer.ts b/src/server/snapshot/snapshotServer.ts index 4c9bb31f64..9475919590 100644 --- a/src/server/snapshot/snapshotServer.ts +++ b/src/server/snapshot/snapshotServer.ts @@ -243,24 +243,11 @@ function rootScript() { pointElement.style.margin = '-10px 0 0 -10px'; pointElement.style.zIndex = '2147483647'; - let current = document.createElement('iframe'); - document.body.appendChild(current); - let next = document.createElement('iframe'); - document.body.appendChild(next); - next.style.visibility = 'hidden'; - const onload = () => { - const temp = current; - current = next; - next = temp; - current.style.visibility = 'visible'; - next.style.visibility = 'hidden'; - }; - current.onload = onload; - next.onload = onload; - + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); (window as any).showSnapshot = async (url: string, options: { point?: Point } = {}) => { await showPromise; - next.src = url; + iframe.src = url; if (options.point) { pointElement.style.left = options.point.x + 'px'; pointElement.style.top = options.point.y + 'px'; diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts index 4efe31d3c4..aa09284c54 100644 --- a/src/server/trace/viewer/traceViewer.ts +++ b/src/server/trace/viewer/traceViewer.ts @@ -31,10 +31,11 @@ const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); class TraceViewer { private _server: HttpServer; + private _browserName: string; - constructor(traceDir: string, resourcesDir?: string) { - if (!resourcesDir) - resourcesDir = path.join(traceDir, 'resources'); + constructor(traceDir: string, browserName: string) { + this._browserName = browserName; + const resourcesDir = path.join(traceDir, 'resources'); // Served by TraceServer // - "/tracemodel" - json with trace model. @@ -124,7 +125,7 @@ class TraceViewer { ]; if (isUnderTest()) args.push(`--remote-debugging-port=0`); - const context = await traceViewerPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', { + const context = await traceViewerPlaywright[this._browserName as 'chromium'].launchPersistentContext(internalCallMetadata(), '', { // TODO: store language in the trace. sdkLanguage: 'javascript', args, @@ -144,7 +145,7 @@ class TraceViewer { } } -export async function showTraceViewer(traceDir: string, resourcesDir?: string) { - const traceViewer = new TraceViewer(traceDir, resourcesDir); +export async function showTraceViewer(traceDir: string, browserName: string) { + const traceViewer = new TraceViewer(traceDir, browserName); await traceViewer.show(); } diff --git a/src/server/types.ts b/src/server/types.ts index 1d808845b2..e7de7320d8 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -272,7 +272,7 @@ type LaunchOptionsBase = { chromiumSandbox?: boolean, slowMo?: number, useWebSocket?: boolean, - _traceDir?: string, + traceDir?: string, }; export type LaunchOptions = LaunchOptionsBase & { firefoxUserPrefs?: { [key: string]: string | number | boolean }, diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 1af05a7c79..d7f0f2d934 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -56,7 +56,7 @@ class PlaywrightEnv { async beforeAll(args: CommonArgs & PlaywrightEnvOptions, workerInfo: folio.WorkerInfo): Promise { this._browserType = args.playwright[args.browserName]; this._browserOptions = { - _traceDir: args.traceDir, + traceDir: args.traceDir, channel: args.browserChannel, headless: !args.headful, handleSIGINT: false, diff --git a/tests/config/default.config.ts b/tests/config/default.config.ts index aaae09f334..bc7afb05da 100644 --- a/tests/config/default.config.ts +++ b/tests/config/default.config.ts @@ -41,7 +41,7 @@ class PageEnv { async beforeAll(args: AllOptions & CommonArgs, workerInfo: folio.WorkerInfo) { this._browser = await args.playwright[args.browserName].launch({ ...args.launchOptions, - _traceDir: args.traceDir, + traceDir: args.traceDir, channel: args.browserChannel, headless: !args.headful, handleSIGINT: false, diff --git a/tests/snapshotter.spec.ts b/tests/snapshotter.spec.ts index 4be3c54b04..e96c77677f 100644 --- a/tests/snapshotter.spec.ts +++ b/tests/snapshotter.spec.ts @@ -132,9 +132,9 @@ it.describe('snapshots', () => { await previewPage.evaluate(snapshotId => { (window as any).showSnapshot(snapshotId); }, `${snapshot.snapshot().pageId}?name=snapshot${counter}`); - while (previewPage.frames().length < 4) + while (previewPage.frames().length < 3) await new Promise(f => previewPage.once('frameattached', f)); - const button = await previewPage.frames()[3].waitForSelector('button'); + const button = await previewPage.frames()[2].waitForSelector('button'); expect(await button.textContent()).toBe('Hello iframe'); }); diff --git a/tests/tracing.spec.ts b/tests/tracing.spec.ts index 47e489a288..ec33d97667 100644 --- a/tests/tracing.spec.ts +++ b/tests/tracing.spec.ts @@ -29,13 +29,13 @@ test.beforeEach(async ({ browserName, headful }) => { }); test('should collect trace', async ({ context, page, server, browserName }, testInfo) => { - await (context as any)._tracing.start({ name: 'test', screenshots: true, snapshots: true }); + await (context as any).tracing.start({ name: 'test', screenshots: true, snapshots: true }); await page.goto(server.EMPTY_PAGE); await page.setContent(''); await page.click('"Click"'); await page.close(); - await (context as any)._tracing.stop(); - await (context as any)._tracing.export(testInfo.outputPath('trace.zip')); + await (context as any).tracing.stop(); + await (context as any).tracing.export(testInfo.outputPath('trace.zip')); const { events } = await parseTrace(testInfo.outputPath('trace.zip')); expect(events[0].type).toBe('context-metadata'); @@ -51,13 +51,13 @@ test('should collect trace', async ({ context, page, server, browserName }, test }); test('should collect trace', async ({ context, page, server }, testInfo) => { - await (context as any)._tracing.start({ name: 'test' }); + await (context as any).tracing.start({ name: 'test' }); await page.goto(server.EMPTY_PAGE); await page.setContent(''); await page.click('"Click"'); await page.close(); - await (context as any)._tracing.stop(); - await (context as any)._tracing.export(testInfo.outputPath('trace.zip')); + await (context as any).tracing.stop(); + await (context as any).tracing.export(testInfo.outputPath('trace.zip')); const { events } = await parseTrace(testInfo.outputPath('trace.zip')); expect(events.some(e => e.type === 'frame-snapshot')).toBeFalsy(); @@ -65,18 +65,18 @@ test('should collect trace', async ({ context, page, server }, testInfo) => { }); test('should collect two traces', async ({ context, page, server }, testInfo) => { - await (context as any)._tracing.start({ name: 'test1', screenshots: true, snapshots: true }); + await (context as any).tracing.start({ name: 'test1', screenshots: true, snapshots: true }); await page.goto(server.EMPTY_PAGE); await page.setContent(''); await page.click('"Click"'); - await (context as any)._tracing.stop(); - await (context as any)._tracing.export(testInfo.outputPath('trace1.zip')); + await (context as any).tracing.stop(); + await (context as any).tracing.export(testInfo.outputPath('trace1.zip')); - await (context as any)._tracing.start({ name: 'test2', screenshots: true, snapshots: true }); + await (context as any).tracing.start({ name: 'test2', screenshots: true, snapshots: true }); await page.dblclick('"Click"'); await page.close(); - await (context as any)._tracing.stop(); - await (context as any)._tracing.export(testInfo.outputPath('trace2.zip')); + await (context as any).tracing.stop(); + await (context as any).tracing.export(testInfo.outputPath('trace2.zip')); { const { events } = await parseTrace(testInfo.outputPath('trace1.zip')); @@ -127,15 +127,15 @@ for (const params of [ const previewHeight = params.height * scale; const context = await contextFactory({ viewport: { width: params.width, height: params.height }}); - await (context as any)._tracing.start({ name: 'test', screenshots: true, snapshots: true }); + await (context as any).tracing.start({ name: 'test', screenshots: true, snapshots: true }); const page = await context.newPage(); // Make sure we have a chance to paint. for (let i = 0; i < 10; ++i) { await page.setContent(''); await page.evaluate(() => new Promise(requestAnimationFrame)); } - await (context as any)._tracing.stop(); - await (context as any)._tracing.export(testInfo.outputPath('trace.zip')); + await (context as any).tracing.stop(); + await (context as any).tracing.export(testInfo.outputPath('trace.zip')); const { events, resources } = await parseTrace(testInfo.outputPath('trace.zip')); const frames = events.filter(e => e.type === 'screencast-frame'); diff --git a/types/types.d.ts b/types/types.d.ts index e69fc50964..a6348bae66 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -5455,6 +5455,8 @@ export interface BrowserContext { }>; }>; + tracing: Tracing; + /** * Removes a route created with * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browsercontextrouteurl-handler). @@ -10264,6 +10266,52 @@ export interface Touchscreen { tap(x: number, y: number): Promise; } +/** + * Tracing object for collecting test traces that can be opened using Playwright CLI. + */ +export interface Tracing { + /** + * Export trace into the file with the given name. Should be called after the tracing has stopped. + * @param path File to save the trace into. + */ + export(path: string): Promise; + + /** + * Start tracing. + * + * ```js + * await context.tracing.start({ name: 'trace', screenshots: true, snapshots: true }); + * const page = await context.newPage(); + * await page.goto('https://playwright.dev'); + * await context.tracing.stop(); + * await context.tracing.export('trace.zip'); + * ``` + * + * @param options + */ + start(options?: { + /** + * If specified, the trace is going to be saved into the file with the given name. + */ + name?: string; + + /** + * Whether to capture screenshots during tracing. Screenshots are used to build a timeline preview. + */ + screenshots?: boolean; + + /** + * Whether to capture DOM snapshot on every action. + */ + snapshots?: boolean; + }): Promise; + + /** + * Stop tracing. + */ + stop(): Promise; +} + /** * When browser context is created with the `recordVideo` option, each page has a video object associated with it. *