diff --git a/docs/api.md b/docs/api.md index b9b4e085609fb..7e0cf63e67288 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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). diff --git a/src/client/types.ts b/src/client/types.ts index 1d8af018a4b54..342d285bf346a 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -82,6 +82,7 @@ export type LaunchServerOptions = { password?: string }, downloadsPath?: string, + artifactsPath?: string, _videosPath?: string, chromiumSandbox?: boolean, port?: number, diff --git a/src/inprocess.ts b/src/inprocess.ts index ffff5271ca2a3..069557abaca8f 100644 --- a/src/inprocess.ts +++ b/src/inprocess.ts @@ -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(); diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 3b772ecda9baf..a37c1470137f3 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -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, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 6f272514aede6..285e3c0bee0c3 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -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? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 87c63f8a53550..f6717e1befc68 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -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, diff --git a/src/server.ts b/src/server.ts index 163e01ce9445b..3a57caa046c59 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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); diff --git a/src/server/browser.ts b/src/server/browser.ts index 1ff02fd5c7fd6..da7c11a0e897f 100644 --- a/src/server/browser.ts +++ b/src/server/browser.ts @@ -32,6 +32,7 @@ export interface BrowserProcess { export type BrowserOptions = types.UIOptions & { name: string, + artifactsPath?: string, downloadsPath?: string, _videosPath?: string, headful?: boolean, diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 8478285702478..ba9fb7b139df1 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -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(); + 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); } diff --git a/src/server/browserType.ts b/src/server/browserType.ts index eaf04d8bff771..48b9c1f0bd4e8 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -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); diff --git a/src/server/types.ts b/src/server/types.ts index ecbb88fa466ef..1b9d0150e2bf2 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -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, diff --git a/src/trace/snapshotter.ts b/src/trace/snapshotter.ts index d826f156e4933..cebfec5efe4fd 100644 --- a/src/trace/snapshotter.ts +++ b/src/trace/snapshotter.ts @@ -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; diff --git a/src/trace/traceTypes.ts b/src/trace/traceTypes.ts index 32b384a771070..0eeceef77a7e3 100644 --- a/src/trace/traceTypes.ts +++ b/src/trace/traceTypes.ts @@ -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[], +}; diff --git a/src/trace/tracer.ts b/src/trace/tracer.ts index 6bb0b9d1c1136..8ad4ca917b4cd 100644 --- a/src/trace/tracer.ts +++ b/src/trace/tracer.ts @@ -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; - - constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) { - this._context = context; - this._contextTracer = new ContextTracer(context, traceStorageDir, traceFile); - this._context._actionListeners.add(this); - } - - async captureSnapshot(page: Page, options: types.TimeoutOptions & { label?: string } = {}): Promise { - await this._contextTracer.captureSnapshot(page, options); - } +export function installTracer() { + contextListeners.add(new Tracer()); +} - async dispose(): Promise { - this._context._actionListeners.delete(this); - await this._contextTracer.dispose(); - } +class Tracer implements ContextListener { + private _contextTracers = new Map(); - async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise { - try { - await this._contextTracer.recordAction(result, metadata); - } catch (e) { - // Do not throw from instrumentation. + async onContextCreated(context: BrowserContext): Promise { + 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 onContextDestroyed(context: BrowserContext): Promise { + 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; private _appendEventChain: Promise; @@ -73,6 +71,7 @@ class ContextTracer implements SnapshotterDelegate { private _pageToId = new Map(); 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 { - 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 { + 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(); diff --git a/test/playwright.fixtures.ts b/test/playwright.fixtures.ts index 1cd9ae575a98f..952b2fbf812c6 100644 --- a/test/playwright.fixtures.ts +++ b/test/playwright.fixtures.ts @@ -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' }); } }); diff --git a/test/snapshot.spec.ts b/test/snapshot.spec.ts deleted file mode 100644 index bc5306746d2c8..0000000000000 --- a/test/snapshot.spec.ts +++ /dev/null @@ -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' }); -}); diff --git a/test/trace.spec.ts b/test/trace.spec.ts new file mode 100644 index 0000000000000..87ef38e057b6f --- /dev/null +++ b/test/trace.spec.ts @@ -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(); +}); diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 56c81b2032626..665ffc5a8a4c1 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -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;