api(trace): introduce artifacts options (#3914)

api(trace): introduce artifacts options

This introduces launch({ artifactsPath }) and newContext({ relativeArtifactsPath, recordTrace }) options.
- artifactsPath option controls the directory where all artifacts go. If not passed, artifacts are not collected.
- relativeArtifactsPath can be used to put context-specific artifacts into a subfolder. If not passed, shared artifactsPath is used.
- recordTrace controls trace recording.

We also expose trace types under playwright/types/trace.d.ts.

In the follow up:
- videos will be put into artifactsPath;
- downloads will be put into artifactsPath, or keep using existing downloadsPath when artifactsPath is not specified.
This commit is contained in:
Dmitry Gozman 2020-09-18 11:54:00 -07:00 committed by GitHub
parent bff9fb21ec
commit 0ade6af689
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 203 additions and 120 deletions

View File

@ -220,10 +220,12 @@ Indicates that the browser is connected.
- `password` <[string]> - `password` <[string]>
- `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'. - `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
- `logger` <[Logger]> Logger sink for Playwright logging. - `logger` <[Logger]> Logger sink for Playwright logging.
- `relativeArtifactsPath` <[string]> Specifies a folder for artifacts like downloads, videos and traces, relative to `artifactsPath` from [`browserType.launch`](#browsertypelaunchoptions). Defaults to `.`.
- `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for new pages. - `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for new pages.
- `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size. - `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `width` <[number]> Video frame width. - `width` <[number]> Video frame width.
- `height` <[number]> Video frame height. - `height` <[number]> Video frame height.
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
- returns: <[Promise]<[BrowserContext]>> - returns: <[Promise]<[BrowserContext]>>
Creates a new browser context. It won't share cookies/cache with other browser contexts. Creates a new browser context. It won't share cookies/cache with other browser contexts.
@ -266,10 +268,12 @@ Creates a new browser context. It won't share cookies/cache with other browser c
- `password` <[string]> - `password` <[string]>
- `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'. - `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
- `logger` <[Logger]> Logger sink for Playwright logging. - `logger` <[Logger]> Logger sink for Playwright logging.
- `relativeArtifactsPath` <[string]> Specifies a folder for artifacts like downloads, videos and traces, relative to `artifactsPath` from [`browserType.launch`](#browsertypelaunchoptions). Defaults to `.`.
- `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for the new page. - `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for the new page.
- `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size. - `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `width` <[number]> Video frame width. - `width` <[number]> Video frame width.
- `height` <[number]> Video frame height. - `height` <[number]> Video frame height.
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
- returns: <[Promise]<[Page]>> - returns: <[Promise]<[Page]>>
Creates a new page in a new browser context. Closing this page will close the context as well. Creates a new page in a new browser context. Closing this page will close the context as well.
@ -4200,6 +4204,7 @@ This methods attaches Playwright to an existing browser instance.
- `username` <[string]> Optional username to use if HTTP proxy requires authentication. - `username` <[string]> Optional username to use if HTTP proxy requires authentication.
- `password` <[string]> Optional password to use if HTTP proxy requires authentication. - `password` <[string]> Optional password to use if HTTP proxy requires authentication.
- `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. - `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `artifactsPath` <[string]> Specifies a folder for various artifacts like downloads, videos and traces. If not specified, artifacts are not collected.
- `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. - `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `chromiumSandbox` <[boolean]> Enable Chromium sandboxing. Defaults to `true`. - `chromiumSandbox` <[boolean]> Enable Chromium sandboxing. Defaults to `true`.
- `firefoxUserPrefs` <[Object]<[string], [string]|[number]|[boolean]>> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). - `firefoxUserPrefs` <[Object]<[string], [string]|[number]|[boolean]>> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
@ -4243,6 +4248,7 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
- `password` <[string]> Optional password to use if HTTP proxy requires authentication. - `password` <[string]> Optional password to use if HTTP proxy requires authentication.
- `acceptDownloads` <[boolean]> Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled. - `acceptDownloads` <[boolean]> Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled.
- `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. - `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `artifactsPath` <[string]> Specifies a folder for various artifacts like downloads, videos and traces. If not specified, artifacts are not collected.
- `chromiumSandbox` <[boolean]> Enable Chromium sandboxing. Defaults to `true`. - `chromiumSandbox` <[boolean]> Enable Chromium sandboxing. Defaults to `true`.
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
@ -4275,11 +4281,13 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
- `username` <[string]> - `username` <[string]>
- `password` <[string]> - `password` <[string]>
- `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'. - `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
- `relativeArtifactsPath` <[string]> Specifies a folder for artifacts like downloads, videos and traces, relative to `artifactsPath`. Defaults to `.`.
- `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. - `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for new pages. - `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for new pages.
- `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size. - `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `width` <[number]> Video frame width. - `width` <[number]> Video frame width.
- `height` <[number]> Video frame height. - `height` <[number]> Video frame height.
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
- returns: <[Promise]<[BrowserContext]>> Promise that resolves to the persistent browser context instance. - returns: <[Promise]<[BrowserContext]>> Promise that resolves to the persistent browser context instance.
Launches browser that uses persistent storage located at `userDataDir` and returns the only context. Closing this context will automatically close the browser. Launches browser that uses persistent storage located at `userDataDir` and returns the only context. Closing this context will automatically close the browser.
@ -4297,6 +4305,7 @@ Launches browser that uses persistent storage located at `userDataDir` and retur
- `username` <[string]> Optional username to use if HTTP proxy requires authentication. - `username` <[string]> Optional username to use if HTTP proxy requires authentication.
- `password` <[string]> Optional password to use if HTTP proxy requires authentication. - `password` <[string]> Optional password to use if HTTP proxy requires authentication.
- `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. - `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `artifactsPath` <[string]> Specifies a folder for various artifacts like downloads, videos and traces. If not specified, artifacts are not collected.
- `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed. - `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `chromiumSandbox` <[boolean]> Enable Chromium sandboxing. Defaults to `true`. - `chromiumSandbox` <[boolean]> Enable Chromium sandboxing. Defaults to `true`.
- `firefoxUserPrefs` <[Object]<[string], [string]|[number]|[boolean]>> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). - `firefoxUserPrefs` <[Object]<[string], [string]|[number]|[boolean]>> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).

View File

@ -82,6 +82,7 @@ export type LaunchServerOptions = {
password?: string password?: string
}, },
downloadsPath?: string, downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string, _videosPath?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
port?: number, port?: number,

View File

@ -21,9 +21,11 @@ import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher';
import { Connection } from './client/connection'; import { Connection } from './client/connection';
import { BrowserServerLauncherImpl } from './browserServerImpl'; import { BrowserServerLauncherImpl } from './browserServerImpl';
import { installDebugController } from './debug/debugController'; import { installDebugController } from './debug/debugController';
import { installTracer } from './trace/tracer';
export function setupInProcess(playwright: PlaywrightImpl): PlaywrightAPI { export function setupInProcess(playwright: PlaywrightImpl): PlaywrightAPI {
installDebugController(); installDebugController();
installTracer();
const clientConnection = new Connection(); const clientConnection = new Connection();
const dispatcherConnection = new DispatcherConnection(); const dispatcherConnection = new DispatcherConnection();

View File

@ -168,6 +168,7 @@ export type BrowserTypeLaunchParams = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string, _videosPath?: string,
firefoxUserPrefs?: any, firefoxUserPrefs?: any,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
@ -195,6 +196,7 @@ export type BrowserTypeLaunchOptions = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string, _videosPath?: string,
firefoxUserPrefs?: any, firefoxUserPrefs?: any,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
@ -226,6 +228,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string, _videosPath?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
@ -260,6 +263,8 @@ export type BrowserTypeLaunchPersistentContextParams = {
hasTouch?: boolean, hasTouch?: boolean,
colorScheme?: 'light' | 'dark' | 'no-preference', colorScheme?: 'light' | 'dark' | 'no-preference',
acceptDownloads?: boolean, acceptDownloads?: boolean,
relativeArtifactsPath?: string,
recordTrace?: boolean,
}; };
export type BrowserTypeLaunchPersistentContextOptions = { export type BrowserTypeLaunchPersistentContextOptions = {
executablePath?: string, executablePath?: string,
@ -283,6 +288,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string, _videosPath?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
@ -317,6 +323,8 @@ export type BrowserTypeLaunchPersistentContextOptions = {
hasTouch?: boolean, hasTouch?: boolean,
colorScheme?: 'light' | 'dark' | 'no-preference', colorScheme?: 'light' | 'dark' | 'no-preference',
acceptDownloads?: boolean, acceptDownloads?: boolean,
relativeArtifactsPath?: string,
recordTrace?: boolean,
}; };
export type BrowserTypeLaunchPersistentContextResult = { export type BrowserTypeLaunchPersistentContextResult = {
context: BrowserContextChannel, context: BrowserContextChannel,
@ -371,6 +379,8 @@ export type BrowserNewContextParams = {
hasTouch?: boolean, hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference', colorScheme?: 'dark' | 'light' | 'no-preference',
acceptDownloads?: boolean, acceptDownloads?: boolean,
relativeArtifactsPath?: string,
recordTrace?: boolean,
_recordVideos?: boolean, _recordVideos?: boolean,
_videoSize?: { _videoSize?: {
width: number, width: number,
@ -409,6 +419,8 @@ export type BrowserNewContextOptions = {
hasTouch?: boolean, hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference', colorScheme?: 'dark' | 'light' | 'no-preference',
acceptDownloads?: boolean, acceptDownloads?: boolean,
relativeArtifactsPath?: string,
recordTrace?: boolean,
_recordVideos?: boolean, _recordVideos?: boolean,
_videoSize?: { _videoSize?: {
width: number, width: number,

View File

@ -220,6 +220,7 @@ BrowserType:
username: string? username: string?
password: string? password: string?
downloadsPath: string? downloadsPath: string?
artifactsPath: string?
_videosPath: string? _videosPath: string?
firefoxUserPrefs: json? firefoxUserPrefs: json?
chromiumSandbox: boolean? chromiumSandbox: boolean?
@ -259,6 +260,7 @@ BrowserType:
username: string? username: string?
password: string? password: string?
downloadsPath: string? downloadsPath: string?
artifactsPath: string?
_videosPath: string? _videosPath: string?
chromiumSandbox: boolean? chromiumSandbox: boolean?
slowMo: number? slowMo: number?
@ -306,6 +308,8 @@ BrowserType:
- dark - dark
- no-preference - no-preference
acceptDownloads: boolean? acceptDownloads: boolean?
relativeArtifactsPath: string?
recordTrace: boolean?
returns: returns:
context: BrowserContext context: BrowserContext
@ -367,6 +371,8 @@ Browser:
- light - light
- no-preference - no-preference
acceptDownloads: boolean? acceptDownloads: boolean?
relativeArtifactsPath: string?
recordTrace: boolean?
_recordVideos: boolean? _recordVideos: boolean?
_videoSize: _videoSize:
type: object? type: object?

View File

@ -121,6 +121,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
password: tOptional(tString), password: tOptional(tString),
})), })),
downloadsPath: tOptional(tString), downloadsPath: tOptional(tString),
artifactsPath: tOptional(tString),
_videosPath: tOptional(tString), _videosPath: tOptional(tString),
firefoxUserPrefs: tOptional(tAny), firefoxUserPrefs: tOptional(tAny),
chromiumSandbox: tOptional(tBoolean), chromiumSandbox: tOptional(tBoolean),
@ -149,6 +150,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
password: tOptional(tString), password: tOptional(tString),
})), })),
downloadsPath: tOptional(tString), downloadsPath: tOptional(tString),
artifactsPath: tOptional(tString),
_videosPath: tOptional(tString), _videosPath: tOptional(tString),
chromiumSandbox: tOptional(tBoolean), chromiumSandbox: tOptional(tBoolean),
slowMo: tOptional(tNumber), slowMo: tOptional(tNumber),
@ -183,6 +185,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
hasTouch: tOptional(tBoolean), hasTouch: tOptional(tBoolean),
colorScheme: tOptional(tEnum(['light', 'dark', 'no-preference'])), colorScheme: tOptional(tEnum(['light', 'dark', 'no-preference'])),
acceptDownloads: tOptional(tBoolean), acceptDownloads: tOptional(tBoolean),
relativeArtifactsPath: tOptional(tString),
recordTrace: tOptional(tBoolean),
}); });
scheme.BrowserCloseParams = tOptional(tObject({})); scheme.BrowserCloseParams = tOptional(tObject({}));
scheme.BrowserNewContextParams = tObject({ scheme.BrowserNewContextParams = tObject({
@ -217,6 +221,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
hasTouch: tOptional(tBoolean), hasTouch: tOptional(tBoolean),
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])), colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])),
acceptDownloads: tOptional(tBoolean), acceptDownloads: tOptional(tBoolean),
relativeArtifactsPath: tOptional(tString),
recordTrace: tOptional(tBoolean),
_recordVideos: tOptional(tBoolean), _recordVideos: tOptional(tBoolean),
_videoSize: tOptional(tObject({ _videoSize: tOptional(tObject({
width: tNumber, width: tNumber,

View File

@ -21,8 +21,10 @@ import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher';
import { Electron } from './server/electron/electron'; import { Electron } from './server/electron/electron';
import { gracefullyCloseAll } from './server/processLauncher'; import { gracefullyCloseAll } from './server/processLauncher';
import { installDebugController } from './debug/debugController'; import { installDebugController } from './debug/debugController';
import { installTracer } from './trace/tracer';
installDebugController(); installDebugController();
installTracer();
const dispatcherConnection = new DispatcherConnection(); const dispatcherConnection = new DispatcherConnection();
const transport = new Transport(process.stdout, process.stdin); const transport = new Transport(process.stdout, process.stdin);

View File

@ -32,6 +32,7 @@ export interface BrowserProcess {
export type BrowserOptions = types.UIOptions & { export type BrowserOptions = types.UIOptions & {
name: string, name: string,
artifactsPath?: string,
downloadsPath?: string, downloadsPath?: string,
_videosPath?: string, _videosPath?: string,
headful?: boolean, headful?: boolean,

View File

@ -27,6 +27,7 @@ import { Page, PageBinding } from './page';
import { Progress, ProgressController, ProgressResult } from './progress'; import { Progress, ProgressController, ProgressResult } from './progress';
import { Selectors, serverSelectors } from './selectors'; import { Selectors, serverSelectors } from './selectors';
import * as types from './types'; import * as types from './types';
import * as path from 'path';
export class Video { export class Video {
private readonly _path: string; private readonly _path: string;
@ -92,6 +93,7 @@ export abstract class BrowserContext extends EventEmitter {
readonly _browserContextId: string | undefined; readonly _browserContextId: string | undefined;
private _selectors?: Selectors; private _selectors?: Selectors;
readonly _actionListeners = new Set<ActionListener>(); readonly _actionListeners = new Set<ActionListener>();
readonly _artifactsPath?: string;
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
super(); super();
@ -99,6 +101,11 @@ export abstract class BrowserContext extends EventEmitter {
this._options = options; this._options = options;
this._browserContextId = browserContextId; this._browserContextId = browserContextId;
this._isPersistentContext = !browserContextId; this._isPersistentContext = !browserContextId;
if (browser._options.artifactsPath) {
this._artifactsPath = browser._options.artifactsPath;
if (options.relativeArtifactsPath)
this._artifactsPath = path.join(this._artifactsPath, options.relativeArtifactsPath);
}
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill); this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
} }

View File

@ -96,6 +96,7 @@ export abstract class BrowserType {
slowMo: options.slowMo, slowMo: options.slowMo,
persistent, persistent,
headful: !options.headless, headful: !options.headless,
artifactsPath: options.artifactsPath,
downloadsPath, downloadsPath,
_videosPath, _videosPath,
browserProcess, browserProcess,
@ -134,6 +135,7 @@ export abstract class BrowserType {
} }
return dir; return dir;
}; };
// TODO: use artifactsPath for downloads and videos.
const downloadsPath = await ensurePath(DOWNLOADS_FOLDER, options.downloadsPath); const downloadsPath = await ensurePath(DOWNLOADS_FOLDER, options.downloadsPath);
const _videosPath = await ensurePath(VIDEOS_FOLDER, options._videosPath); const _videosPath = await ensurePath(VIDEOS_FOLDER, options._videosPath);

View File

@ -240,6 +240,8 @@ export type BrowserContextOptions = {
acceptDownloads?: boolean, acceptDownloads?: boolean,
_recordVideos?: boolean, _recordVideos?: boolean,
_videoSize?: Size, _videoSize?: Size,
recordTrace?: boolean,
relativeArtifactsPath?: string,
}; };
export type EnvArray = { name: string, value: string }[]; export type EnvArray = { name: string, value: string }[];
@ -257,6 +259,7 @@ type LaunchOptionsBase = {
headless?: boolean, headless?: boolean,
devtools?: boolean, devtools?: boolean,
proxy?: ProxySettings, proxy?: ProxySettings,
artifactsPath?: string,
downloadsPath?: string, downloadsPath?: string,
_videosPath?: string, _videosPath?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,

View File

@ -26,6 +26,7 @@ import * as types from '../server/types';
import { SnapshotData, takeSnapshotInFrame } from './snapshotterInjected'; import { SnapshotData, takeSnapshotInFrame } from './snapshotterInjected';
import { assert, calculateSha1, createGuid } from '../utils/utils'; import { assert, calculateSha1, createGuid } from '../utils/utils';
import { ElementHandle } from '../server/dom'; import { ElementHandle } from '../server/dom';
import { FrameSnapshot, PageSnapshot } from './traceTypes';
export type SnapshotterResource = { export type SnapshotterResource = {
pageId: string, pageId: string,
@ -41,18 +42,6 @@ export type SnapshotterBlob = {
sha1: string, sha1: string,
}; };
export type FrameSnapshot = {
frameId: string,
url: string,
html: string,
resourceOverrides: { url: string, sha1: string }[],
};
export type PageSnapshot = {
viewportSize?: { width: number, height: number },
// First frame is the main frame.
frames: FrameSnapshot[],
};
export interface SnapshotterDelegate { export interface SnapshotterDelegate {
onBlob(blob: SnapshotterBlob): void; onBlob(blob: SnapshotterBlob): void;
onResource(resource: SnapshotterResource): void; onResource(resource: SnapshotterResource): void;

View File

@ -69,3 +69,25 @@ export type ActionTraceEvent = {
stack?: string, stack?: string,
error?: string, error?: string,
}; };
export type TraceEvent =
ContextCreatedTraceEvent |
ContextDestroyedTraceEvent |
PageCreatedTraceEvent |
PageDestroyedTraceEvent |
NetworkResourceTraceEvent |
ActionTraceEvent;
export type FrameSnapshot = {
frameId: string,
url: string,
html: string,
resourceOverrides: { url: string, sha1: string }[],
};
export type PageSnapshot = {
viewportSize?: { width: number, height: number },
// First frame is the main frame.
frames: FrameSnapshot[],
};

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { ActionListener, ActionMetadata, BrowserContext } from '../server/browserContext'; import { ActionListener, ActionMetadata, BrowserContext, ContextListener, contextListeners } from '../server/browserContext';
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter'; import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes'; import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes';
import * as path from 'path'; import * as path from 'path';
@ -23,7 +23,6 @@ import * as fs from 'fs';
import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../utils/utils'; import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../utils/utils';
import { Page } from '../server/page'; import { Page } from '../server/page';
import { Snapshotter } from './snapshotter'; import { Snapshotter } from './snapshotter';
import * as types from '../server/types';
import { ElementHandle } from '../server/dom'; import { ElementHandle } from '../server/dom';
import { helper, RegisteredListener } from '../server/helper'; import { helper, RegisteredListener } from '../server/helper';
import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings'; import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings';
@ -33,36 +32,35 @@ const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs)); const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
const fsAccessAsync = util.promisify(fs.access.bind(fs)); const fsAccessAsync = util.promisify(fs.access.bind(fs));
// TODO: merge Trace and ContextTracer. export function installTracer() {
export class Tracer implements ActionListener { contextListeners.add(new Tracer());
private _context: BrowserContext; }
private _contextTracer: ContextTracer;
constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) { class Tracer implements ContextListener {
this._context = context; private _contextTracers = new Map<BrowserContext, ContextTracer>();
this._contextTracer = new ContextTracer(context, traceStorageDir, traceFile);
this._context._actionListeners.add(this); async onContextCreated(context: BrowserContext): Promise<void> {
if (!context._options.recordTrace)
return;
if (!context._artifactsPath)
throw new Error(`"recordTrace" option requires "artifactsPath" to be specified`);
const traceStorageDir = path.join(context._browser._options.artifactsPath!, '.playwright-shared');
const traceFile = path.join(context._artifactsPath, 'playwright.trace');
const contextTracer = new ContextTracer(context, traceStorageDir, traceFile);
this._contextTracers.set(context, contextTracer);
} }
async captureSnapshot(page: Page, options: types.TimeoutOptions & { label?: string } = {}): Promise<void> { async onContextDestroyed(context: BrowserContext): Promise<void> {
await this._contextTracer.captureSnapshot(page, options); const contextTracer = this._contextTracers.get(context);
} if (contextTracer) {
await contextTracer.dispose().catch(e => {});
async dispose(): Promise<void> { this._contextTracers.delete(context);
this._context._actionListeners.delete(this);
await this._contextTracer.dispose();
}
async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise<void> {
try {
await this._contextTracer.recordAction(result, metadata);
} catch (e) {
// Do not throw from instrumentation.
} }
} }
} }
class ContextTracer implements SnapshotterDelegate { class ContextTracer implements SnapshotterDelegate, ActionListener {
private _context: BrowserContext;
private _contextId: string; private _contextId: string;
private _traceStoragePromise: Promise<string>; private _traceStoragePromise: Promise<string>;
private _appendEventChain: Promise<string>; private _appendEventChain: Promise<string>;
@ -73,6 +71,7 @@ class ContextTracer implements SnapshotterDelegate {
private _pageToId = new Map<Page, string>(); private _pageToId = new Map<Page, string>();
constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) { constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) {
this._context = context;
this._contextId = 'context@' + createGuid(); this._contextId = 'context@' + createGuid();
this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir); this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir);
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile); this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
@ -90,6 +89,7 @@ class ContextTracer implements SnapshotterDelegate {
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)), helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)),
]; ];
this._context._actionListeners.add(this);
} }
onBlob(blob: SnapshotterBlob): void { onBlob(blob: SnapshotterBlob): void {
@ -114,22 +114,8 @@ class ContextTracer implements SnapshotterDelegate {
return this._pageToId.get(page)!; return this._pageToId.get(page)!;
} }
async captureSnapshot(page: Page, options: types.TimeoutOptions & { label?: string } = {}): Promise<void> { async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise<void> {
const snapshot = await this._takeSnapshot(page, undefined, options.timeout); try {
if (!snapshot)
return;
const event: ActionTraceEvent = {
type: 'action',
contextId: this._contextId,
action: 'snapshot',
pageId: this._pageToId.get(page),
label: options.label || 'snapshot',
snapshot,
};
this._appendTraceEvent(event);
}
async recordAction(result: ProgressResult, metadata: ActionMetadata) {
const snapshot = await this._takeSnapshot(metadata.page, typeof metadata.target === 'string' ? undefined : metadata.target); const snapshot = await this._takeSnapshot(metadata.page, typeof metadata.target === 'string' ? undefined : metadata.target);
const event: ActionTraceEvent = { const event: ActionTraceEvent = {
type: 'action', type: 'action',
@ -146,6 +132,8 @@ class ContextTracer implements SnapshotterDelegate {
error: result.error ? result.error.stack : undefined, error: result.error ? result.error.stack : undefined,
}; };
this._appendTraceEvent(event); this._appendTraceEvent(event);
} catch (e) {
}
} }
private _onPage(page: Page) { private _onPage(page: Page) {
@ -190,6 +178,7 @@ class ContextTracer implements SnapshotterDelegate {
async dispose() { async dispose() {
this._disposed = true; this._disposed = true;
this._context._actionListeners.delete(this);
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
this._pageToId.clear(); this._pageToId.clear();
this._snapshotter.dispose(); this._snapshotter.dispose();

View File

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
import childProcess from 'child_process'; import childProcess from 'child_process';
import type { LaunchOptions, BrowserType, Browser, BrowserContext, Page, BrowserServer } from '../index'; import type { LaunchOptions, BrowserType, Browser, BrowserContext, Page, BrowserServer, BrowserContextOptions } from '../index';
import { TestServer } from '../utils/testserver'; import { TestServer } from '../utils/testserver';
import { Connection } from '../lib/client/connection'; import { Connection } from '../lib/client/connection';
import { Transport } from '../lib/protocol/transport'; import { Transport } from '../lib/protocol/transport';
@ -88,8 +88,7 @@ export const options = {
HEADLESS: !!valueFromEnv('HEADLESS', true), HEADLESS: !!valueFromEnv('HEADLESS', true),
WIRE: !!process.env.PWWIRE, WIRE: !!process.env.PWWIRE,
SLOW_MO: valueFromEnv('SLOW_MO', 0), SLOW_MO: valueFromEnv('SLOW_MO', 0),
// Tracing is currently not implemented under wire. TRACING: valueFromEnv('TRACING', false),
TRACING: valueFromEnv('TRACING', false) && !process.env.PWWIRE,
}; };
defineWorkerFixture('httpService', async ({parallelIndex}, test) => { defineWorkerFixture('httpService', async ({parallelIndex}, test) => {
@ -121,16 +120,16 @@ const getExecutablePath = browserName => {
return process.env.WKPATH; return process.env.WKPATH;
}; };
defineWorkerFixture('defaultBrowserOptions', async ({browserName}, test) => { defineWorkerFixture('defaultBrowserOptions', async ({browserName}, runTest, config) => {
const executablePath = getExecutablePath(browserName); const executablePath = getExecutablePath(browserName);
if (executablePath) if (executablePath)
console.error(`Using executable at ${executablePath}`); console.error(`Using executable at ${executablePath}`);
await test({ await runTest({
handleSIGINT: false, handleSIGINT: false,
slowMo: options.SLOW_MO, slowMo: options.SLOW_MO,
headless: options.HEADLESS, headless: options.HEADLESS,
executablePath executablePath,
artifactsPath: config.outputDir,
}); });
}); });
@ -237,27 +236,20 @@ defineWorkerFixture('golden', async ({browserName}, test) => {
await test(p => path.join(browserName, p)); await test(p => path.join(browserName, p));
}); });
defineTestFixture('context', async ({browser, toImpl}, runTest, info) => { defineTestFixture('context', async ({browser}, runTest, info) => {
const context = await browser.newContext();
if (options.TRACING) {
const { test, config } = info; const { test, config } = info;
const traceStorageDir = path.join(config.outputDir, 'trace-storage');
const relativePath = path.relative(config.testDir, test.file).replace(/\.spec\.[jt]s/, ''); const relativePath = path.relative(config.testDir, test.file).replace(/\.spec\.[jt]s/, '');
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_'); const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
const traceFile = path.join(config.outputDir, relativePath, sanitizedTitle + '.trace'); const contextOptions: BrowserContextOptions = {
const tracerFactory = require('../lib/trace/tracer').Tracer; relativeArtifactsPath: path.join(relativePath, sanitizedTitle),
(context as any).__tracer = new tracerFactory(toImpl(context), traceStorageDir, traceFile); recordTrace: !!options.TRACING,
} };
const context = await browser.newContext(contextOptions);
await runTest(context); await runTest(context);
await context.close(); await context.close();
if ((context as any).__tracer)
await (context as any).__tracer.dispose();
}); });
defineTestFixture('page', async ({context, playwright, toImpl}, runTest, info) => { defineTestFixture('page', async ({context}, runTest, info) => {
const page = await context.newPage(); const page = await context.newPage();
await runTest(page); await runTest(page);
const { test, config, result } = info; const { test, config, result } = info;
@ -266,8 +258,6 @@ defineTestFixture('page', async ({context, playwright, toImpl}, runTest, info) =
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_'); const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
const assetPath = path.join(config.outputDir, relativePath, sanitizedTitle) + '-failed.png'; const assetPath = path.join(config.outputDir, relativePath, sanitizedTitle) + '-failed.png';
await page.screenshot({ timeout: 5000, path: assetPath }); await page.screenshot({ timeout: 5000, path: assetPath });
if ((playwright as any).__tracer)
await (playwright as any).__tracer.captureSnapshot(toImpl(page), { timeout: 5000, label: 'Test Failed' });
} }
}); });

View File

@ -1,24 +0,0 @@
/**
* 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 { it, options } from './playwright.fixtures';
it('should not throw', (test, parameters) => {
test.skip(!options.TRACING);
}, async ({page, server, context, toImpl}) => {
await page.goto(server.PREFIX + '/snapshot/snapshot-with-css.html');
await (context as any).__tracer.captureSnapshot(toImpl(page), { label: 'snapshot' });
});

65
test/trace.spec.ts Normal file
View File

@ -0,0 +1,65 @@
/**
* 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 { it, expect } from './playwright.fixtures';
import type * as trace from '../types/trace';
import * as path from 'path';
import * as fs from 'fs';
it('should record trace', async ({browserType, defaultBrowserOptions, server, tmpDir}) => {
const browser = await browserType.launch({
...defaultBrowserOptions,
artifactsPath: tmpDir,
});
const context = await browser.newContext({ recordTrace: true });
const page = await context.newPage();
const url = server.PREFIX + '/snapshot/snapshot-with-css.html';
await page.goto(url);
await context.close();
await browser.close();
const traceFile = path.join(tmpDir, 'playwright.trace');
const traceFileContent = await fs.promises.readFile(traceFile, 'utf8');
const traceEvents = traceFileContent.split('\n').filter(line => !!line).map(line => JSON.parse(line)) as trace.TraceEvent[];
const contextEvent = traceEvents.find(event => event.type === 'context-created') as trace.ContextCreatedTraceEvent;
expect(contextEvent).toBeTruthy();
const contextId = contextEvent.contextId;
const pageEvent = traceEvents.find(event => event.type === 'page-created') as trace.PageCreatedTraceEvent;
expect(pageEvent).toBeTruthy();
expect(pageEvent.contextId).toBe(contextId);
const pageId = pageEvent.pageId;
const gotoEvent = traceEvents.find(event => event.type === 'action' && event.action === 'goto') as trace.ActionTraceEvent;
expect(gotoEvent).toBeTruthy();
expect(gotoEvent.contextId).toBe(contextId);
expect(gotoEvent.pageId).toBe(pageId);
expect(gotoEvent.value).toBe(url);
expect(gotoEvent.snapshot).toBeTruthy();
expect(fs.existsSync(path.join(tmpDir, '.playwright-shared', gotoEvent.snapshot!.sha1))).toBe(true);
});
it('should require artifactsPath', async ({browserType, defaultBrowserOptions}) => {
const browser = await browserType.launch({
...defaultBrowserOptions,
artifactsPath: undefined,
});
const error = await browser.newContext({ recordTrace: true }).catch(e => e);
expect(error.message).toContain('"recordTrace" option requires "artifactsPath" to be specified');
await browser.close();
});

View File

@ -32,6 +32,7 @@ let documentation;
if (!fs.existsSync(typesDir)) if (!fs.existsSync(typesDir))
fs.mkdirSync(typesDir) fs.mkdirSync(typesDir)
fs.writeFileSync(path.join(typesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'server', 'chromium', 'protocol.ts')), 'utf8'); fs.writeFileSync(path.join(typesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'server', 'chromium', 'protocol.ts')), 'utf8');
fs.writeFileSync(path.join(typesDir, 'trace.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'trace', 'traceTypes.ts')), 'utf8');
const browser = await chromium.launch(); const browser = await chromium.launch();
const page = await browser.newPage(); const page = await browser.newPage();
const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md')); const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md'));