chore(tracing): expose tracing api (#6523)

This commit is contained in:
Pavel Feldman 2021-05-12 12:21:54 -07:00 committed by GitHub
parent 460cc31941
commit 21cb726b7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 186 additions and 69 deletions

View File

@ -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 current working directory. If no path is provided, storage
state is still returned, but won't be saved to the disk. state is still returned, but won't be saved to the disk.
## property: BrowserContext.tracing
- type: <[Tracing]>
## async method: BrowserContext.unroute ## async method: BrowserContext.unroute
Removes a route created with [`method: BrowserContext.route`]. When [`param: handler`] is not specified, removes all Removes a route created with [`method: BrowserContext.route`]. When [`param: handler`] is not specified, removes all

View File

@ -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.

View File

@ -159,21 +159,25 @@ commandWithOpenOptions('pdf <url> <filename>', 'save page as pdf',
console.log(' $ pdf https://example.com example.pdf'); console.log(' $ pdf https://example.com example.pdf');
}); });
if (process.env.PWTRACE) { 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')
.option('--resources <dir>', 'load resources from shared folder') .description('Show trace viewer')
.description('Show trace viewer') .action(function(trace, command) {
.action(function(trace, command) { if (command.browser === 'cr')
showTraceViewer(trace, command.resources).catch(logErrorAndExit); command.browser = 'chromium';
}).on('--help', function() { if (command.browser === 'ff')
console.log(''); command.browser = 'firefox';
console.log('Examples:'); if (command.browser === 'wk')
console.log(''); command.browser = 'webkit';
console.log(' $ show-trace --resources=resources trace/file.trace'); showTraceViewer(trace, command.browser).catch(logErrorAndExit);
console.log(' $ show-trace trace/directory'); }).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') if (process.argv[2] === 'run-driver')
runDriver(); runDriver();

View File

@ -35,6 +35,7 @@ export { JSHandle } from './jsHandle';
export { Request, Response, Route, WebSocket } from './network'; export { Request, Response, Route, WebSocket } from './network';
export { Page } from './page'; export { Page } from './page';
export { Selectors } from './selectors'; export { Selectors } from './selectors';
export { Tracing } from './tracing';
export { Video } from './video'; export { Video } from './video';
export { Worker } from './worker'; export { Worker } from './worker';
export { CDPSession } from './cdpSession'; export { CDPSession } from './cdpSession';

View File

@ -50,7 +50,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
sdkLanguage: 'javascript' sdkLanguage: 'javascript'
}; };
readonly _tracing: Tracing; readonly tracing: Tracing;
readonly _backgroundPages = new Set<Page>(); readonly _backgroundPages = new Set<Page>();
readonly _serviceWorkers = new Set<Worker>(); readonly _serviceWorkers = new Set<Worker>();
@ -69,7 +69,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
if (parent instanceof Browser) if (parent instanceof Browser)
this._browser = parent; this._browser = parent;
this._isChromium = this._browser?._name === 'chromium'; this._isChromium = this._browser?._name === 'chromium';
this._tracing = new Tracing(this); this.tracing = new Tracing(this);
this._channel.on('bindingCall', ({binding}) => this._onBinding(BindingCall.from(binding))); this._channel.on('bindingCall', ({binding}) => this._onBinding(BindingCall.from(binding)));
this._channel.on('close', () => this._onClose()); this._channel.on('close', () => this._onClose());

View File

@ -14,18 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
import * as api from '../../types/types';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { Artifact } from './artifact'; import { Artifact } from './artifact';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
export class Tracing { export class Tracing implements api.Tracing {
private _context: BrowserContext; private _context: BrowserContext;
constructor(channel: BrowserContext) { constructor(channel: BrowserContext) {
this._context = channel; 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) => { await this._context._wrapApiCall('tracing.start', async (channel: channels.BrowserContextChannel) => {
return await channel.tracingStart(options); return await channel.tracingStart(options);
}); });

View File

@ -226,7 +226,7 @@ export type BrowserTypeLaunchParams = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
_traceDir?: string, traceDir?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
firefoxUserPrefs?: any, firefoxUserPrefs?: any,
slowMo?: number, slowMo?: number,
@ -251,7 +251,7 @@ export type BrowserTypeLaunchOptions = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
_traceDir?: string, traceDir?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
firefoxUserPrefs?: any, firefoxUserPrefs?: any,
slowMo?: number, slowMo?: number,
@ -279,7 +279,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
_traceDir?: string, traceDir?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
sdkLanguage: string, sdkLanguage: string,
noDefaultViewport?: boolean, noDefaultViewport?: boolean,
@ -349,7 +349,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
_traceDir?: string, traceDir?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
noDefaultViewport?: boolean, noDefaultViewport?: boolean,
viewport?: { viewport?: {

View File

@ -260,7 +260,7 @@ LaunchOptions:
username: string? username: string?
password: string? password: string?
downloadsPath: string? downloadsPath: string?
_traceDir: string? traceDir: string?
chromiumSandbox: boolean? chromiumSandbox: boolean?

View File

@ -172,7 +172,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
password: tOptional(tString), password: tOptional(tString),
})), })),
downloadsPath: tOptional(tString), downloadsPath: tOptional(tString),
_traceDir: tOptional(tString), traceDir: tOptional(tString),
chromiumSandbox: tOptional(tBoolean), chromiumSandbox: tOptional(tBoolean),
firefoxUserPrefs: tOptional(tAny), firefoxUserPrefs: tOptional(tAny),
slowMo: tOptional(tNumber), slowMo: tOptional(tNumber),
@ -197,7 +197,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
password: tOptional(tString), password: tOptional(tString),
})), })),
downloadsPath: tOptional(tString), downloadsPath: tOptional(tString),
_traceDir: tOptional(tString), traceDir: tOptional(tString),
chromiumSandbox: tOptional(tBoolean), chromiumSandbox: tOptional(tBoolean),
sdkLanguage: tString, sdkLanguage: tString,
noDefaultViewport: tOptional(tBoolean), noDefaultViewport: tOptional(tBoolean),

View File

@ -115,7 +115,7 @@ export abstract class BrowserType extends SdkObject {
protocolLogger, protocolLogger,
browserLogsCollector, browserLogsCollector,
wsEndpoint: options.useWebSocket ? (transport as WebSocketTransport).wsEndpoint : undefined, wsEndpoint: options.useWebSocket ? (transport as WebSocketTransport).wsEndpoint : undefined,
traceDir: options._traceDir, traceDir: options.traceDir,
}; };
if (persistent) if (persistent)
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);

View File

@ -243,24 +243,11 @@ function rootScript() {
pointElement.style.margin = '-10px 0 0 -10px'; pointElement.style.margin = '-10px 0 0 -10px';
pointElement.style.zIndex = '2147483647'; pointElement.style.zIndex = '2147483647';
let current = document.createElement('iframe'); const iframe = document.createElement('iframe');
document.body.appendChild(current); document.body.appendChild(iframe);
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;
(window as any).showSnapshot = async (url: string, options: { point?: Point } = {}) => { (window as any).showSnapshot = async (url: string, options: { point?: Point } = {}) => {
await showPromise; await showPromise;
next.src = url; iframe.src = url;
if (options.point) { if (options.point) {
pointElement.style.left = options.point.x + 'px'; pointElement.style.left = options.point.x + 'px';
pointElement.style.top = options.point.y + 'px'; pointElement.style.top = options.point.y + 'px';

View File

@ -31,10 +31,11 @@ const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
class TraceViewer { class TraceViewer {
private _server: HttpServer; private _server: HttpServer;
private _browserName: string;
constructor(traceDir: string, resourcesDir?: string) { constructor(traceDir: string, browserName: string) {
if (!resourcesDir) this._browserName = browserName;
resourcesDir = path.join(traceDir, 'resources'); const resourcesDir = path.join(traceDir, 'resources');
// Served by TraceServer // Served by TraceServer
// - "/tracemodel" - json with trace model. // - "/tracemodel" - json with trace model.
@ -124,7 +125,7 @@ class TraceViewer {
]; ];
if (isUnderTest()) if (isUnderTest())
args.push(`--remote-debugging-port=0`); 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. // TODO: store language in the trace.
sdkLanguage: 'javascript', sdkLanguage: 'javascript',
args, args,
@ -144,7 +145,7 @@ class TraceViewer {
} }
} }
export async function showTraceViewer(traceDir: string, resourcesDir?: string) { export async function showTraceViewer(traceDir: string, browserName: string) {
const traceViewer = new TraceViewer(traceDir, resourcesDir); const traceViewer = new TraceViewer(traceDir, browserName);
await traceViewer.show(); await traceViewer.show();
} }

View File

@ -272,7 +272,7 @@ type LaunchOptionsBase = {
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
useWebSocket?: boolean, useWebSocket?: boolean,
_traceDir?: string, traceDir?: string,
}; };
export type LaunchOptions = LaunchOptionsBase & { export type LaunchOptions = LaunchOptionsBase & {
firefoxUserPrefs?: { [key: string]: string | number | boolean }, firefoxUserPrefs?: { [key: string]: string | number | boolean },

View File

@ -56,7 +56,7 @@ class PlaywrightEnv {
async beforeAll(args: CommonArgs & PlaywrightEnvOptions, workerInfo: folio.WorkerInfo): Promise<PlaywrightWorkerArgs> { async beforeAll(args: CommonArgs & PlaywrightEnvOptions, workerInfo: folio.WorkerInfo): Promise<PlaywrightWorkerArgs> {
this._browserType = args.playwright[args.browserName]; this._browserType = args.playwright[args.browserName];
this._browserOptions = { this._browserOptions = {
_traceDir: args.traceDir, traceDir: args.traceDir,
channel: args.browserChannel, channel: args.browserChannel,
headless: !args.headful, headless: !args.headful,
handleSIGINT: false, handleSIGINT: false,

View File

@ -41,7 +41,7 @@ class PageEnv {
async beforeAll(args: AllOptions & CommonArgs, workerInfo: folio.WorkerInfo) { async beforeAll(args: AllOptions & CommonArgs, workerInfo: folio.WorkerInfo) {
this._browser = await args.playwright[args.browserName].launch({ this._browser = await args.playwright[args.browserName].launch({
...args.launchOptions, ...args.launchOptions,
_traceDir: args.traceDir, traceDir: args.traceDir,
channel: args.browserChannel, channel: args.browserChannel,
headless: !args.headful, headless: !args.headful,
handleSIGINT: false, handleSIGINT: false,

View File

@ -132,9 +132,9 @@ it.describe('snapshots', () => {
await previewPage.evaluate(snapshotId => { await previewPage.evaluate(snapshotId => {
(window as any).showSnapshot(snapshotId); (window as any).showSnapshot(snapshotId);
}, `${snapshot.snapshot().pageId}?name=snapshot${counter}`); }, `${snapshot.snapshot().pageId}?name=snapshot${counter}`);
while (previewPage.frames().length < 4) while (previewPage.frames().length < 3)
await new Promise(f => previewPage.once('frameattached', f)); 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'); expect(await button.textContent()).toBe('Hello iframe');
}); });

View File

@ -29,13 +29,13 @@ test.beforeEach(async ({ browserName, headful }) => {
}); });
test('should collect trace', async ({ context, page, server, browserName }, testInfo) => { 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.goto(server.EMPTY_PAGE);
await page.setContent('<button>Click</button>'); await page.setContent('<button>Click</button>');
await page.click('"Click"'); await page.click('"Click"');
await page.close(); await page.close();
await (context as any)._tracing.stop(); await (context as any).tracing.stop();
await (context as any)._tracing.export(testInfo.outputPath('trace.zip')); await (context as any).tracing.export(testInfo.outputPath('trace.zip'));
const { events } = await parseTrace(testInfo.outputPath('trace.zip')); const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
expect(events[0].type).toBe('context-metadata'); 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) => { 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.goto(server.EMPTY_PAGE);
await page.setContent('<button>Click</button>'); await page.setContent('<button>Click</button>');
await page.click('"Click"'); await page.click('"Click"');
await page.close(); await page.close();
await (context as any)._tracing.stop(); await (context as any).tracing.stop();
await (context as any)._tracing.export(testInfo.outputPath('trace.zip')); await (context as any).tracing.export(testInfo.outputPath('trace.zip'));
const { events } = await parseTrace(testInfo.outputPath('trace.zip')); const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
expect(events.some(e => e.type === 'frame-snapshot')).toBeFalsy(); 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) => { 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.goto(server.EMPTY_PAGE);
await page.setContent('<button>Click</button>'); await page.setContent('<button>Click</button>');
await page.click('"Click"'); await page.click('"Click"');
await (context as any)._tracing.stop(); await (context as any).tracing.stop();
await (context as any)._tracing.export(testInfo.outputPath('trace1.zip')); 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.dblclick('"Click"');
await page.close(); await page.close();
await (context as any)._tracing.stop(); await (context as any).tracing.stop();
await (context as any)._tracing.export(testInfo.outputPath('trace2.zip')); await (context as any).tracing.export(testInfo.outputPath('trace2.zip'));
{ {
const { events } = await parseTrace(testInfo.outputPath('trace1.zip')); const { events } = await parseTrace(testInfo.outputPath('trace1.zip'));
@ -127,15 +127,15 @@ for (const params of [
const previewHeight = params.height * scale; const previewHeight = params.height * scale;
const context = await contextFactory({ viewport: { width: params.width, height: params.height }}); 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(); const page = await context.newPage();
// Make sure we have a chance to paint. // Make sure we have a chance to paint.
for (let i = 0; i < 10; ++i) { for (let i = 0; i < 10; ++i) {
await page.setContent('<body style="box-sizing: border-box; width: 100%; height: 100%; margin:0; background: red; border: 50px solid blue"></body>'); await page.setContent('<body style="box-sizing: border-box; width: 100%; height: 100%; margin:0; background: red; border: 50px solid blue"></body>');
await page.evaluate(() => new Promise(requestAnimationFrame)); await page.evaluate(() => new Promise(requestAnimationFrame));
} }
await (context as any)._tracing.stop(); await (context as any).tracing.stop();
await (context as any)._tracing.export(testInfo.outputPath('trace.zip')); await (context as any).tracing.export(testInfo.outputPath('trace.zip'));
const { events, resources } = await parseTrace(testInfo.outputPath('trace.zip')); const { events, resources } = await parseTrace(testInfo.outputPath('trace.zip'));
const frames = events.filter(e => e.type === 'screencast-frame'); const frames = events.filter(e => e.type === 'screencast-frame');

48
types/types.d.ts vendored
View File

@ -5455,6 +5455,8 @@ export interface BrowserContext {
}>; }>;
}>; }>;
tracing: Tracing;
/** /**
* Removes a route created with * Removes a route created with
* [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browsercontextrouteurl-handler). * [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<void>; tap(x: number, y: number): Promise<void>;
} }
/**
* 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<void>;
/**
* 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<void>;
/**
* Stop tracing.
*/
stop(): Promise<void>;
}
/** /**
* When browser context is created with the `recordVideo` option, each page has a video object associated with it. * When browser context is created with the `recordVideo` option, each page has a video object associated with it.
* *