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]>
- `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.
- `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.
- `_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.
- `height` <[number]> Video frame height.
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
- returns: <[Promise]<[BrowserContext]>>
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]>
- `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.
- `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.
- `_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.
- `height` <[number]> Video frame height.
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
- returns: <[Promise]<[Page]>>
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.
- `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.
- `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.
- `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).
@ -4243,6 +4248,7 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
- `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.
- `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`.
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. 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]>
- `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`'.
- `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.
- `_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.
- `width` <[number]> Video frame width.
- `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.
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.
- `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.
- `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.
- `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).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import * as types from '../server/types';
import { SnapshotData, takeSnapshotInFrame } from './snapshotterInjected';
import { assert, calculateSha1, createGuid } from '../utils/utils';
import { ElementHandle } from '../server/dom';
import { FrameSnapshot, PageSnapshot } from './traceTypes';
export type SnapshotterResource = {
pageId: string,
@ -41,18 +42,6 @@ export type SnapshotterBlob = {
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 {
onBlob(blob: SnapshotterBlob): void;
onResource(resource: SnapshotterResource): void;

View File

@ -69,3 +69,25 @@ export type ActionTraceEvent = {
stack?: 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.
*/
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 { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes';
import * as path from 'path';
@ -23,7 +23,6 @@ import * as fs from 'fs';
import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../utils/utils';
import { Page } from '../server/page';
import { Snapshotter } from './snapshotter';
import * as types from '../server/types';
import { ElementHandle } from '../server/dom';
import { helper, RegisteredListener } from '../server/helper';
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 fsAccessAsync = util.promisify(fs.access.bind(fs));
// TODO: merge Trace and ContextTracer.
export class Tracer implements ActionListener {
private _context: BrowserContext;
private _contextTracer: ContextTracer;
export function installTracer() {
contextListeners.add(new Tracer());
}
constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) {
this._context = context;
this._contextTracer = new ContextTracer(context, traceStorageDir, traceFile);
this._context._actionListeners.add(this);
class Tracer implements ContextListener {
private _contextTracers = new Map<BrowserContext, ContextTracer>();
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> {
await this._contextTracer.captureSnapshot(page, options);
}
async dispose(): Promise<void> {
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.
async onContextDestroyed(context: BrowserContext): Promise<void> {
const contextTracer = this._contextTracers.get(context);
if (contextTracer) {
await contextTracer.dispose().catch(e => {});
this._contextTracers.delete(context);
}
}
}
class ContextTracer implements SnapshotterDelegate {
class ContextTracer implements SnapshotterDelegate, ActionListener {
private _context: BrowserContext;
private _contextId: string;
private _traceStoragePromise: Promise<string>;
private _appendEventChain: Promise<string>;
@ -73,6 +71,7 @@ class ContextTracer implements SnapshotterDelegate {
private _pageToId = new Map<Page, string>();
constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) {
this._context = context;
this._contextId = 'context@' + createGuid();
this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir);
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
@ -90,6 +89,7 @@ class ContextTracer implements SnapshotterDelegate {
this._eventListeners = [
helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)),
];
this._context._actionListeners.add(this);
}
onBlob(blob: SnapshotterBlob): void {
@ -114,38 +114,26 @@ class ContextTracer implements SnapshotterDelegate {
return this._pageToId.get(page)!;
}
async captureSnapshot(page: Page, options: types.TimeoutOptions & { label?: string } = {}): Promise<void> {
const snapshot = await this._takeSnapshot(page, undefined, options.timeout);
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 event: ActionTraceEvent = {
type: 'action',
contextId: this._contextId,
pageId: this._pageToId.get(metadata.page),
action: metadata.type,
selector: typeof metadata.target === 'string' ? metadata.target : undefined,
value: metadata.value,
snapshot,
startTime: result.startTime,
endTime: result.endTime,
stack: metadata.stack,
logs: result.logs.slice(),
error: result.error ? result.error.stack : undefined,
};
this._appendTraceEvent(event);
async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise<void> {
try {
const snapshot = await this._takeSnapshot(metadata.page, typeof metadata.target === 'string' ? undefined : metadata.target);
const event: ActionTraceEvent = {
type: 'action',
contextId: this._contextId,
pageId: this._pageToId.get(metadata.page),
action: metadata.type,
selector: typeof metadata.target === 'string' ? metadata.target : undefined,
value: metadata.value,
snapshot,
startTime: result.startTime,
endTime: result.endTime,
stack: metadata.stack,
logs: result.logs.slice(),
error: result.error ? result.error.stack : undefined,
};
this._appendTraceEvent(event);
} catch (e) {
}
}
private _onPage(page: Page) {
@ -190,6 +178,7 @@ class ContextTracer implements SnapshotterDelegate {
async dispose() {
this._disposed = true;
this._context._actionListeners.delete(this);
helper.removeEventListeners(this._eventListeners);
this._pageToId.clear();
this._snapshotter.dispose();

View File

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path';
import os from 'os';
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 { Connection } from '../lib/client/connection';
import { Transport } from '../lib/protocol/transport';
@ -88,8 +88,7 @@ export const options = {
HEADLESS: !!valueFromEnv('HEADLESS', true),
WIRE: !!process.env.PWWIRE,
SLOW_MO: valueFromEnv('SLOW_MO', 0),
// Tracing is currently not implemented under wire.
TRACING: valueFromEnv('TRACING', false) && !process.env.PWWIRE,
TRACING: valueFromEnv('TRACING', false),
};
defineWorkerFixture('httpService', async ({parallelIndex}, test) => {
@ -121,16 +120,16 @@ const getExecutablePath = browserName => {
return process.env.WKPATH;
};
defineWorkerFixture('defaultBrowserOptions', async ({browserName}, test) => {
defineWorkerFixture('defaultBrowserOptions', async ({browserName}, runTest, config) => {
const executablePath = getExecutablePath(browserName);
if (executablePath)
console.error(`Using executable at ${executablePath}`);
await test({
await runTest({
handleSIGINT: false,
slowMo: options.SLOW_MO,
headless: options.HEADLESS,
executablePath
executablePath,
artifactsPath: config.outputDir,
});
});
@ -237,27 +236,20 @@ defineWorkerFixture('golden', async ({browserName}, test) => {
await test(p => path.join(browserName, p));
});
defineTestFixture('context', async ({browser, toImpl}, runTest, info) => {
const context = await browser.newContext();
if (options.TRACING) {
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 sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
const traceFile = path.join(config.outputDir, relativePath, sanitizedTitle + '.trace');
const tracerFactory = require('../lib/trace/tracer').Tracer;
(context as any).__tracer = new tracerFactory(toImpl(context), traceStorageDir, traceFile);
}
defineTestFixture('context', async ({browser}, runTest, info) => {
const { test, config } = info;
const relativePath = path.relative(config.testDir, test.file).replace(/\.spec\.[jt]s/, '');
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
const contextOptions: BrowserContextOptions = {
relativeArtifactsPath: path.join(relativePath, sanitizedTitle),
recordTrace: !!options.TRACING,
};
const context = await browser.newContext(contextOptions);
await runTest(context);
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();
await runTest(page);
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 assetPath = path.join(config.outputDir, relativePath, sanitizedTitle) + '-failed.png';
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))
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, 'trace.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'trace', 'traceTypes.ts')), 'utf8');
const browser = await chromium.launch();
const page = await browser.newPage();
const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md'));
@ -81,7 +82,7 @@ ${generateDevicesTypes()}
});
/**
* @param {string} overriddes
* @param {string} overriddes
*/
function objectDefinitionsToString(overriddes) {
let definition;