feat(screencast): add expreimental public API on context (#3766)

This commit is contained in:
Yury Semikhatsky 2020-09-04 22:37:38 -07:00 committed by GitHub
parent f6aab9e5bd
commit 66985fc5f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 289 additions and 132 deletions

View File

@ -220,6 +220,9 @@ 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.
- `_recordVideos` <[Object]> **experimental** Enables automatic video recording for new pages. The video will have frames with the provided dimensions. 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.
- 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.
@ -262,6 +265,9 @@ 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.
- `_recordVideos` <[Object]> **experimental** Enables automatic video recording for the new page. The video will have frames with the provided dimensions. 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.
- 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.
@ -684,6 +690,7 @@ page.removeListener('request', logRequest);
``` ```
<!-- GEN:toc --> <!-- GEN:toc -->
- [event: '_videostarted'](#event-videostarted)
- [event: 'close'](#event-close-1) - [event: 'close'](#event-close-1)
- [event: 'console'](#event-console) - [event: 'console'](#event-console)
- [event: 'crash'](#event-crash) - [event: 'crash'](#event-crash)
@ -770,6 +777,12 @@ page.removeListener('request', logRequest);
- [page.workers()](#pageworkers) - [page.workers()](#pageworkers)
<!-- GEN:stop --> <!-- GEN:stop -->
#### event: '_videostarted'
- <[Object]> Video object.
**experimental**
Emitted when video recording has started for this page. The event will fire only if [`_recordVideos`](#browsernewcontextoptions) option is configured on the parent context.
#### event: 'close' #### event: 'close'
Emitted when the page closes. Emitted when the page closes.
@ -4157,6 +4170,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.
- `_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).
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
@ -4231,6 +4245,10 @@ 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`'.
- `_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` <[Object]> **experimental** Enables automatic video recording for the new page. The video will have frames with the provided dimensions. 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.
- 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.
@ -4248,6 +4266,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.
- `_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).
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.

View File

@ -48,7 +48,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
const base = new EventEmitter(); const base = new EventEmitter();
this._channel = new Proxy(base, { this._channel = new Proxy(base, {
get: (obj: any, prop) => { get: (obj: any, prop) => {
if (String(prop).startsWith('_')) if (String(prop).startsWith('_') && String(prop) !== '_enableScreencast' && String(prop) !== '_disableScreencast')
return obj[prop]; return obj[prop];
if (prop === 'then') if (prop === 'then')
return obj.then; return obj.then;

View File

@ -40,6 +40,7 @@ import { WebKitBrowser } from './webkitBrowser';
import { FirefoxBrowser } from './firefoxBrowser'; import { FirefoxBrowser } from './firefoxBrowser';
import { debugLogger } from '../utils/debugLogger'; import { debugLogger } from '../utils/debugLogger';
import { SelectorsOwner } from './selectors'; import { SelectorsOwner } from './selectors';
import { Video } from './video';
class Root extends ChannelOwner<channels.Channel, {}> { class Root extends ChannelOwner<channels.Channel, {}> {
constructor(connection: Connection) { constructor(connection: Connection) {
@ -207,6 +208,9 @@ export class Connection {
case 'Route': case 'Route':
result = new Route(parent, type, guid, initializer); result = new Route(parent, type, guid, initializer);
break; break;
case 'Video':
result = new Video(parent, type, guid, initializer);
break;
case 'Stream': case 'Stream':
result = new Stream(parent, type, guid, initializer); result = new Stream(parent, type, guid, initializer);
break; break;

View File

@ -50,6 +50,7 @@ export const Events = {
Load: 'load', Load: 'load',
Popup: 'popup', Popup: 'popup',
Worker: 'worker', Worker: 'worker',
_VideoStarted: '_videostarted',
}, },
Worker: { Worker: {

View File

@ -42,6 +42,7 @@ import * as util from 'util';
import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types'; import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types';
import { evaluationScript, urlMatches } from './clientHelper'; import { evaluationScript, urlMatches } from './clientHelper';
import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils'; import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils';
import { Video } from './video';
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & { type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
width?: string | number, width?: string | number,
@ -122,6 +123,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this._channel.on('response', ({ response }) => this.emit(Events.Page.Response, Response.from(response))); this._channel.on('response', ({ response }) => this.emit(Events.Page.Response, Response.from(response)));
this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request))); this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request)));
this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker))); this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));
this._channel.on('videoStarted', params => this._onVideoStarted(params));
if (this._browserContext._browserName === 'chromium') { if (this._browserContext._browserName === 'chromium') {
this.coverage = new ChromiumCoverage(this._channel); this.coverage = new ChromiumCoverage(this._channel);
@ -175,6 +177,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this.emit(Events.Page.Worker, worker); this.emit(Events.Page.Worker, worker);
} }
private _onVideoStarted(params: channels.PageVideoStartedEvent): void {
this.emit(Events.Page._VideoStarted, Video.from(params.video));
}
_onClose() { _onClose() {
this._closed = true; this._closed = true;
this._browserContext._pages.delete(this); this._browserContext._pages.delete(this);

View File

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

39
src/client/video.ts Normal file
View File

@ -0,0 +1,39 @@
/**
* 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 * as channels from '../protocol/channels';
import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
export class Video extends ChannelOwner<channels.VideoChannel, channels.VideoInitializer> {
private _browser: Browser | undefined;
static from(channel: channels.VideoChannel): Video {
return (channel as any)._object;
}
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.VideoInitializer) {
super(parent, type, guid, initializer);
this._browser = (parent as BrowserContext)._browser;
}
async path(): Promise<string> {
if (this._browser && this._browser._isRemote)
throw new Error(`Path is not available when using browserType.connect().`);
return (await this._channel.path()).value;
}
}

View File

@ -117,7 +117,7 @@ export class DispatcherConnection {
onmessage = (message: object) => {}; onmessage = (message: object) => {};
private _validateParams: (type: string, method: string, params: any) => any; private _validateParams: (type: string, method: string, params: any) => any;
async sendMessageToClient(guid: string, method: string, params: any, disallowDispatchers?: boolean): Promise<any> { sendMessageToClient(guid: string, method: string, params: any, disallowDispatchers?: boolean) {
const allowDispatchers = !disallowDispatchers; const allowDispatchers = !disallowDispatchers;
this.onmessage({ guid, method, params: this._replaceDispatchersWithGuids(params, allowDispatchers) }); this.onmessage({ guid, method, params: this._replaceDispatchersWithGuids(params, allowDispatchers) });
} }

View File

@ -30,6 +30,7 @@ import { serializeResult, parseArgument } from './jsHandleDispatcher';
import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
import { FileChooser } from '../server/fileChooser'; import { FileChooser } from '../server/fileChooser';
import { CRCoverage } from '../server/chromium/crCoverage'; import { CRCoverage } from '../server/chromium/crCoverage';
import { VideoDispatcher } from './videoDispatcher';
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel { export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
private _page: Page; private _page: Page;
@ -48,7 +49,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
page.on(Page.Events.Crash, () => this._dispatchEvent('crash')); page.on(Page.Events.Crash, () => this._dispatchEvent('crash'));
page.on(Page.Events.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded')); page.on(Page.Events.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded'));
page.on(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) })); page.on(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) }));
page.on(Page.Events.Download, dialog => this._dispatchEvent('download', { download: new DownloadDispatcher(this._scope, dialog) })); page.on(Page.Events.Download, download => this._dispatchEvent('download', { download: new DownloadDispatcher(this._scope, download) }));
this._page.on(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', { this._page.on(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
element: new ElementHandleDispatcher(this._scope, fileChooser.element()), element: new ElementHandleDispatcher(this._scope, fileChooser.element()),
isMultiple: fileChooser.isMultiple() isMultiple: fileChooser.isMultiple()
@ -65,6 +66,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
})); }));
page.on(Page.Events.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) })); page.on(Page.Events.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) }));
page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) })); page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) }));
page.on(Page.Events.VideoStarted, screencast => this._dispatchEvent('videoStarted', { video: new VideoDispatcher(this._scope, screencast) }));
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) })); page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
} }

View File

@ -0,0 +1,29 @@
/**
* 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 * as channels from '../protocol/channels';
import { Video } from '../server/browserContext';
import { Dispatcher, DispatcherScope } from './dispatcher';
export class VideoDispatcher extends Dispatcher<Video, channels.VideoInitializer> implements channels.VideoChannel {
constructor(scope: DispatcherScope, screencast: Video) {
super(scope, screencast, 'Video', {});
}
async path(): Promise<channels.VideoPathResult> {
return { value: await this._object.path() };
}
}

View File

@ -164,6 +164,7 @@ export type BrowserTypeLaunchParams = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
_videosPath?: string,
firefoxUserPrefs?: any, firefoxUserPrefs?: any,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
@ -190,6 +191,7 @@ export type BrowserTypeLaunchOptions = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
_videosPath?: string,
firefoxUserPrefs?: any, firefoxUserPrefs?: any,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
@ -220,6 +222,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
noDefaultViewport?: boolean, noDefaultViewport?: boolean,
@ -276,6 +279,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
password?: string, password?: string,
}, },
downloadsPath?: string, downloadsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
noDefaultViewport?: boolean, noDefaultViewport?: boolean,
@ -363,6 +367,10 @@ export type BrowserNewContextParams = {
hasTouch?: boolean, hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference', colorScheme?: 'dark' | 'light' | 'no-preference',
acceptDownloads?: boolean, acceptDownloads?: boolean,
_recordVideos?: {
width: number,
height: number,
},
}; };
export type BrowserNewContextOptions = { export type BrowserNewContextOptions = {
noDefaultViewport?: boolean, noDefaultViewport?: boolean,
@ -396,6 +404,10 @@ export type BrowserNewContextOptions = {
hasTouch?: boolean, hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference', colorScheme?: 'dark' | 'light' | 'no-preference',
acceptDownloads?: boolean, acceptDownloads?: boolean,
_recordVideos?: {
width: number,
height: number,
},
}; };
export type BrowserNewContextResult = { export type BrowserNewContextResult = {
context: BrowserContextChannel, context: BrowserContextChannel,
@ -645,6 +657,7 @@ export interface PageChannel extends Channel {
on(event: 'requestFinished', callback: (params: PageRequestFinishedEvent) => void): this; on(event: 'requestFinished', callback: (params: PageRequestFinishedEvent) => void): this;
on(event: 'response', callback: (params: PageResponseEvent) => void): this; on(event: 'response', callback: (params: PageResponseEvent) => void): this;
on(event: 'route', callback: (params: PageRouteEvent) => void): this; on(event: 'route', callback: (params: PageRouteEvent) => void): this;
on(event: 'videoStarted', callback: (params: PageVideoStartedEvent) => void): this;
on(event: 'worker', callback: (params: PageWorkerEvent) => void): this; on(event: 'worker', callback: (params: PageWorkerEvent) => void): this;
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>; setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams): Promise<PageSetDefaultTimeoutNoReplyResult>; setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams): Promise<PageSetDefaultTimeoutNoReplyResult>;
@ -727,6 +740,9 @@ export type PageRouteEvent = {
route: RouteChannel, route: RouteChannel,
request: RequestChannel, request: RequestChannel,
}; };
export type PageVideoStartedEvent = {
video: VideoChannel,
};
export type PageWorkerEvent = { export type PageWorkerEvent = {
worker: WorkerChannel, worker: WorkerChannel,
}; };
@ -2107,6 +2123,17 @@ export type DialogDismissParams = {};
export type DialogDismissOptions = {}; export type DialogDismissOptions = {};
export type DialogDismissResult = void; export type DialogDismissResult = void;
// ----------- Video -----------
export type VideoInitializer = {};
export interface VideoChannel extends Channel {
path(params?: VideoPathParams): Promise<VideoPathResult>;
}
export type VideoPathParams = {};
export type VideoPathOptions = {};
export type VideoPathResult = {
value: string,
};
// ----------- Download ----------- // ----------- Download -----------
export type DownloadInitializer = { export type DownloadInitializer = {
url: string, url: string,

View File

@ -212,6 +212,7 @@ BrowserType:
username: string? username: string?
password: string? password: string?
downloadsPath: string? downloadsPath: string?
_videosPath: string?
firefoxUserPrefs: json? firefoxUserPrefs: json?
chromiumSandbox: boolean? chromiumSandbox: boolean?
slowMo: number? slowMo: number?
@ -250,6 +251,7 @@ BrowserType:
username: string? username: string?
password: string? password: string?
downloadsPath: string? downloadsPath: string?
_videosPath: string?
chromiumSandbox: boolean? chromiumSandbox: boolean?
slowMo: number? slowMo: number?
noDefaultViewport: boolean? noDefaultViewport: boolean?
@ -357,6 +359,11 @@ Browser:
- light - light
- no-preference - no-preference
acceptDownloads: boolean? acceptDownloads: boolean?
_recordVideos:
type: object?
properties:
width: number
height: number
returns: returns:
context: BrowserContext context: BrowserContext
@ -892,6 +899,10 @@ Page:
route: Route route: Route
request: Request request: Request
videoStarted:
parameters:
video: Video
worker: worker:
parameters: parameters:
worker: Worker worker: Worker
@ -1771,6 +1782,17 @@ Dialog:
Video:
type: interface
commands:
path:
returns:
value: string
Download: Download:
type: interface type: interface

View File

@ -118,6 +118,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
password: tOptional(tString), password: tOptional(tString),
})), })),
downloadsPath: tOptional(tString), downloadsPath: tOptional(tString),
_videosPath: tOptional(tString),
firefoxUserPrefs: tOptional(tAny), firefoxUserPrefs: tOptional(tAny),
chromiumSandbox: tOptional(tBoolean), chromiumSandbox: tOptional(tBoolean),
slowMo: tOptional(tNumber), slowMo: tOptional(tNumber),
@ -145,6 +146,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
password: tOptional(tString), password: tOptional(tString),
})), })),
downloadsPath: tOptional(tString), downloadsPath: tOptional(tString),
_videosPath: tOptional(tString),
chromiumSandbox: tOptional(tBoolean), chromiumSandbox: tOptional(tBoolean),
slowMo: tOptional(tNumber), slowMo: tOptional(tNumber),
noDefaultViewport: tOptional(tBoolean), noDefaultViewport: tOptional(tBoolean),
@ -212,6 +214,10 @@ 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),
_recordVideos: tOptional(tObject({
width: tNumber,
height: tNumber,
})),
}); });
scheme.BrowserCrNewBrowserCDPSessionParams = tOptional(tObject({})); scheme.BrowserCrNewBrowserCDPSessionParams = tOptional(tObject({}));
scheme.BrowserCrStartTracingParams = tObject({ scheme.BrowserCrStartTracingParams = tObject({
@ -799,6 +805,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
promptText: tOptional(tString), promptText: tOptional(tString),
}); });
scheme.DialogDismissParams = tOptional(tObject({})); scheme.DialogDismissParams = tOptional(tObject({}));
scheme.VideoPathParams = tOptional(tObject({}));
scheme.DownloadPathParams = tOptional(tObject({})); scheme.DownloadPathParams = tOptional(tObject({}));
scheme.DownloadSaveAsParams = tObject({ scheme.DownloadSaveAsParams = tObject({
path: tString, path: tString,

View File

@ -15,7 +15,7 @@
*/ */
import * as types from './types'; import * as types from './types';
import { BrowserContext, Screencast } from './browserContext'; import { BrowserContext, Video } from './browserContext';
import { Page } from './page'; import { Page } from './page';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Download } from './download'; import { Download } from './download';
@ -32,6 +32,7 @@ export interface BrowserProcess {
export type BrowserOptions = types.UIOptions & { export type BrowserOptions = types.UIOptions & {
name: string, name: string,
downloadsPath?: string, downloadsPath?: string,
_videosPath?: string,
headful?: boolean, headful?: boolean,
persistent?: types.BrowserContextOptions, // Undefined means no persistent context. persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
browserProcess: BrowserProcess, browserProcess: BrowserProcess,
@ -47,7 +48,7 @@ export abstract class Browser extends EventEmitter {
private _downloads = new Map<string, Download>(); private _downloads = new Map<string, Download>();
_defaultContext: BrowserContext | null = null; _defaultContext: BrowserContext | null = null;
private _startedClosing = false; private _startedClosing = false;
private readonly _idToScreencast = new Map<string, Screencast>(); private readonly _idToVideo = new Map<string, Video>();
constructor(options: BrowserOptions) { constructor(options: BrowserOptions) {
super(); super();
@ -86,16 +87,16 @@ export abstract class Browser extends EventEmitter {
this._downloads.delete(uuid); this._downloads.delete(uuid);
} }
_screencastStarted(screencastId: string, file: string, page: Page) { _videoStarted(videoId: string, file: string): Video {
const screencast = new Screencast(file, page); const video = new Video(file);
this._idToScreencast.set(screencastId, screencast); this._idToVideo.set(videoId, video);
page._browserContext.emit(BrowserContext.Events.ScreencastStarted, screencast); return video;
} }
_screencastFinished(screencastId: string) { _videoFinished(videoId: string) {
const screencast = this._idToScreencast.get(screencastId); const video = this._idToVideo.get(videoId);
this._idToScreencast.delete(screencastId); this._idToVideo.delete(videoId);
screencast!._finishCallback(); video!._finishCallback();
} }
_didClose() { _didClose() {

View File

@ -15,33 +15,29 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs';
import { helper } from './helper';
import * as network from './network';
import * as path from 'path';
import { Page, PageBinding } from './page';
import { TimeoutSettings } from '../utils/timeoutSettings';
import * as frames from './frames';
import * as types from './types';
import { Download } from './download';
import { Browser } from './browser';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { TimeoutSettings } from '../utils/timeoutSettings';
import { Browser } from './browser';
import { Download } from './download';
import * as frames from './frames';
import { helper } from './helper';
import { instrumentingAgents } from './instrumentation';
import * as network from './network';
import { Page, PageBinding } from './page';
import { Progress } from './progress'; import { Progress } from './progress';
import { Selectors, serverSelectors } from './selectors'; import { Selectors, serverSelectors } from './selectors';
import { instrumentingAgents } from './instrumentation'; import * as types from './types';
export class Screencast { export class Video {
readonly page: Page;
private readonly _path: string; private readonly _path: string;
_finishCallback: () => void = () => {}; _finishCallback: () => void = () => {};
private readonly _finishedPromise: Promise<void>; private readonly _finishedPromise: Promise<void>;
constructor(path: string, page: Page) { constructor(path: string) {
this._path = path; this._path = path;
this.page = page;
this._finishedPromise = new Promise(fulfill => this._finishCallback = fulfill); this._finishedPromise = new Promise(fulfill => this._finishCallback = fulfill);
} }
async path(): Promise<string | null> { async path(): Promise<string> {
await this._finishedPromise; await this._finishedPromise;
return this._path; return this._path;
} }
@ -51,13 +47,11 @@ export abstract class BrowserContext extends EventEmitter {
static Events = { static Events = {
Close: 'close', Close: 'close',
Page: 'page', Page: 'page',
ScreencastStarted: 'screencaststarted',
}; };
readonly _timeoutSettings = new TimeoutSettings(); readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>(); readonly _pageBindings = new Map<string, PageBinding>();
readonly _options: types.BrowserContextOptions; readonly _options: types.BrowserContextOptions;
_screencastOptions: types.ContextScreencastOptions | null = null;
_requestInterceptor?: network.RouteHandler; _requestInterceptor?: network.RouteHandler;
private _isPersistentContext: boolean; private _isPersistentContext: boolean;
private _closedStatus: 'open' | 'closing' | 'closed' = 'open'; private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
@ -174,15 +168,6 @@ export abstract class BrowserContext extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout); this._timeoutSettings.setDefaultTimeout(timeout);
} }
async _enableScreencast(options: types.ContextScreencastOptions) {
this._screencastOptions = options;
fs.mkdirSync(path.dirname(options.dir), {recursive: true});
}
_disableScreencast() {
this._screencastOptions = null;
}
async _loadDefaultContext(progress: Progress) { async _loadDefaultContext(progress: Progress) {
if (!this.pages().length) { if (!this.pages().length) {
const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page); const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page);

View File

@ -41,6 +41,7 @@ const mkdirAsync = util.promisify(fs.mkdir);
const mkdtempAsync = util.promisify(fs.mkdtemp); const mkdtempAsync = util.promisify(fs.mkdtemp);
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err))); const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-'); const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
const VIDEOS_FOLDER = path.join(os.tmpdir(), 'playwright_videos-');
type WebSocketNotPipe = { webSocketRegex: RegExp, stream: 'stdout' | 'stderr' }; type WebSocketNotPipe = { webSocketRegex: RegExp, stream: 'stdout' | 'stderr' };
@ -89,7 +90,7 @@ export abstract class BrowserTypeBase implements BrowserType {
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<Browser> { async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<Browser> {
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined; options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
const { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, userDataDir); const { browserProcess, downloadsPath, _videosPath, transport } = await this._launchProcess(progress, options, !!persistent, userDataDir);
if ((options as any).__testHookBeforeCreateBrowser) if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser(); await (options as any).__testHookBeforeCreateBrowser();
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
@ -98,6 +99,7 @@ export abstract class BrowserTypeBase implements BrowserType {
persistent, persistent,
headful: !options.headless, headful: !options.headless,
downloadsPath, downloadsPath,
_videosPath,
browserProcess, browserProcess,
proxy: options.proxy, proxy: options.proxy,
}; };
@ -109,7 +111,7 @@ export abstract class BrowserTypeBase implements BrowserType {
return browser; return browser;
} }
private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, transport: ConnectionTransport }> { private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, _videosPath: string, transport: ConnectionTransport }> {
const { const {
ignoreDefaultArgs, ignoreDefaultArgs,
ignoreAllDefaultArgs, ignoreAllDefaultArgs,
@ -123,14 +125,19 @@ export abstract class BrowserTypeBase implements BrowserType {
const env = options.env ? envArrayToObject(options.env) : process.env; const env = options.env ? envArrayToObject(options.env) : process.env;
const tempDirectories = []; const tempDirectories = [];
let downloadsPath: string; const ensurePath = async (tmpPrefix: string, pathFromOptions?: string) => {
if (options.downloadsPath) { let dir;
downloadsPath = options.downloadsPath; if (pathFromOptions) {
await mkdirAsync(options.downloadsPath, { recursive: true }); dir = pathFromOptions;
} else { await mkdirAsync(pathFromOptions, { recursive: true });
downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER); } else {
tempDirectories.push(downloadsPath); dir = await mkdtempAsync(tmpPrefix);
} tempDirectories.push(dir);
}
return dir;
};
const downloadsPath = await ensurePath(DOWNLOADS_FOLDER, options.downloadsPath);
const _videosPath = await ensurePath(VIDEOS_FOLDER, options._videosPath);
if (!userDataDir) { if (!userDataDir) {
userDataDir = await mkdtempAsync(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`)); userDataDir = await mkdtempAsync(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
@ -204,7 +211,7 @@ export abstract class BrowserTypeBase implements BrowserType {
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream]; const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
transport = new PipeTransport(stdio[3], stdio[4]); transport = new PipeTransport(stdio[3], stdio[4]);
} }
return { browserProcess, downloadsPath, transport }; return { browserProcess, downloadsPath, _videosPath, transport };
} }
abstract _defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[]; abstract _defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[];

View File

@ -460,10 +460,10 @@ class FrameSession {
promises.push(this._evaluateOnNewDocument(source)); promises.push(this._evaluateOnNewDocument(source));
for (const source of this._crPage._page._evaluateOnNewDocumentSources) for (const source of this._crPage._page._evaluateOnNewDocumentSources)
promises.push(this._evaluateOnNewDocument(source)); promises.push(this._evaluateOnNewDocument(source));
if (this._crPage._browserContext._screencastOptions) { if (this._crPage._browserContext._options._recordVideos) {
const contextOptions = this._crPage._browserContext._screencastOptions; const contextOptions = this._crPage._browserContext._options._recordVideos;
const screencastId = createGuid(); const screencastId = createGuid();
const outputFile = path.join(contextOptions.dir, screencastId + '.webm'); const outputFile = path.join(this._crPage._browserContext._browser._options._videosPath!, screencastId + '.webm');
const options = Object.assign({}, contextOptions, {outputFile}); const options = Object.assign({}, contextOptions, {outputFile});
promises.push(this._startScreencast(screencastId, options)); promises.push(this._startScreencast(screencastId, options));
} }
@ -764,7 +764,11 @@ class FrameSession {
this._screencastState = 'started'; this._screencastState = 'started';
this._videoRecorder = videoRecorder; this._videoRecorder = videoRecorder;
this._screencastId = screencastId; this._screencastId = screencastId;
this._crPage._browserContext._browser._screencastStarted(screencastId, options.outputFile, this._page); const video = this._crPage._browserContext._browser._videoStarted(screencastId, options.outputFile);
this._crPage.pageOrError().then(pageOrError => {
if (pageOrError instanceof Page)
pageOrError.emit(Page.Events.VideoStarted, video);
}).catch(() => {});
} catch (e) { } catch (e) {
videoRecorder.stop().catch(() => {}); videoRecorder.stop().catch(() => {});
throw e; throw e;
@ -783,7 +787,7 @@ class FrameSession {
this._screencastId = null; this._screencastId = null;
this._screencastState = 'stopped'; this._screencastState = 'stopped';
await recorder.stop().catch(() => {}); await recorder.stop().catch(() => {});
this._crPage._browserContext._browser._screencastFinished(screencastId); this._crPage._browserContext._browser._videoFinished(screencastId);
} }
} }

View File

@ -15,10 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { assert } from '../../utils/utils';
import { Browser, BrowserOptions } from '../browser'; import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { helper, RegisteredListener } from '../helper'; import { helper, RegisteredListener } from '../helper';
import { assert } from '../../utils/utils';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding } from '../page'; import { Page, PageBinding } from '../page';
import { ConnectionTransport } from '../transport'; import { ConnectionTransport } from '../transport';
@ -164,7 +164,7 @@ export class FFBrowser extends Browser {
} }
_onScreencastFinished(payload: Protocol.Browser.screencastFinishedPayload) { _onScreencastFinished(payload: Protocol.Browser.screencastFinishedPayload) {
this._screencastFinished(payload.screencastId); this._videoFinished(payload.screencastId);
} }
} }
@ -222,6 +222,14 @@ export class FFBrowserContext extends BrowserContext {
promises.push(this.setOffline(this._options.offline)); promises.push(this.setOffline(this._options.offline));
if (this._options.colorScheme) if (this._options.colorScheme)
promises.push(this._browser._connection.send('Browser.setColorScheme', { browserContextId, colorScheme: this._options.colorScheme })); promises.push(this._browser._connection.send('Browser.setColorScheme', { browserContextId, colorScheme: this._options.colorScheme }));
if (this._options._recordVideos) {
await this._browser._connection.send('Browser.setScreencastOptions', {
...this._options._recordVideos,
dir: this._browser._options._videosPath!,
browserContextId: this._browserContextId
});
}
await Promise.all(promises); await Promise.all(promises);
} }
@ -326,11 +334,6 @@ export class FFBrowserContext extends BrowserContext {
await this._browser._connection.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: !!this._requestInterceptor }); await this._browser._connection.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: !!this._requestInterceptor });
} }
async _enableScreencast(options: types.ContextScreencastOptions): Promise<void> {
await super._enableScreencast(options);
await this._browser._connection.send('Browser.setScreencastOptions', Object.assign({}, options, { browserContextId: this._browserContextId}));
}
async _doClose() { async _doClose() {
assert(this._browserContextId); assert(this._browserContextId);
await this._browser._connection.send('Browser.removeBrowserContext', { browserContextId: this._browserContextId }); await this._browser._connection.send('Browser.removeBrowserContext', { browserContextId: this._browserContextId });

View File

@ -31,7 +31,7 @@ import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
import { FFNetworkManager } from './ffNetworkManager'; import { FFNetworkManager } from './ffNetworkManager';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
import { Screencast } from '../browserContext'; import { Video } from '../browserContext';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -50,7 +50,7 @@ export class FFPage implements PageDelegate {
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>; private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
private _eventListeners: RegisteredListener[]; private _eventListeners: RegisteredListener[];
private _workers = new Map<string, { frameId: string, session: FFSession }>(); private _workers = new Map<string, { frameId: string, session: FFSession }>();
private readonly _idToScreencast = new Map<string, Screencast>(); private readonly _idToScreencast = new Map<string, Video>();
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) { constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
this._session = session; this._session = session;
@ -258,7 +258,11 @@ export class FFPage implements PageDelegate {
} }
_onScreencastStarted(event: Protocol.Page.screencastStartedPayload) { _onScreencastStarted(event: Protocol.Page.screencastStartedPayload) {
this._browserContext._browser._screencastStarted(event.screencastId, event.file, this._page); const video = this._browserContext._browser._videoStarted(event.screencastId, event.file);
this.pageOrError().then(pageOrError => {
if (pageOrError instanceof Page)
pageOrError.emit(Page.Events.VideoStarted, video);
}).catch(() => {});
} }
async exposeBinding(binding: PageBinding) { async exposeBinding(binding: PageBinding) {

View File

@ -114,6 +114,7 @@ export class Page extends EventEmitter {
Load: 'load', Load: 'load',
Popup: 'popup', Popup: 'popup',
Worker: 'worker', Worker: 'worker',
VideoStarted: 'videostarted',
}; };
private _closedState: 'open' | 'closing' | 'closed' = 'open'; private _closedState: 'open' | 'closing' | 'closed' = 'open';

View File

@ -52,19 +52,12 @@ export type ScreenshotOptions = ElementScreenshotOptions & {
clip?: Rect, clip?: Rect,
}; };
export type ScreencastOptions = { export type PageScreencastOptions = {
width: number, width: number,
height: number, height: number,
};
export type PageScreencastOptions = ScreencastOptions & {
outputFile: string, outputFile: string,
}; };
export type ContextScreencastOptions = ScreencastOptions & {
dir: string,
};
export type URLMatch = string | RegExp | ((url: URL) => boolean); export type URLMatch = string | RegExp | ((url: URL) => boolean);
export type Credentials = { export type Credentials = {
@ -245,6 +238,10 @@ export type BrowserContextOptions = {
hasTouch?: boolean, hasTouch?: boolean,
colorScheme?: ColorScheme, colorScheme?: ColorScheme,
acceptDownloads?: boolean, acceptDownloads?: boolean,
_recordVideos?: {
width: number,
height: number
}
}; };
export type EnvArray = { name: string, value: string }[]; export type EnvArray = { name: string, value: string }[];
@ -263,6 +260,7 @@ type LaunchOptionsBase = {
devtools?: boolean, devtools?: boolean,
proxy?: ProxySettings, proxy?: ProxySettings,
downloadsPath?: string, downloadsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number, slowMo?: number,
}; };

View File

@ -126,7 +126,7 @@ export class WKBrowser extends Browser {
} }
_onScreencastFinished(payload: Protocol.Playwright.screencastFinishedPayload) { _onScreencastFinished(payload: Protocol.Playwright.screencastFinishedPayload) {
this._screencastFinished(payload.screencastId); this._videoFinished(payload.screencastId);
} }
_onPageProxyCreated(event: Protocol.Playwright.pageProxyCreatedPayload) { _onPageProxyCreated(event: Protocol.Playwright.pageProxyCreatedPayload) {

View File

@ -15,28 +15,28 @@
* limitations under the License. * limitations under the License.
*/ */
import * as jpeg from 'jpeg-js';
import * as path from 'path';
import * as png from 'pngjs';
import { assert, createGuid, debugAssert, headersArrayToObject } from '../../utils/utils';
import * as accessibility from '../accessibility';
import * as dialog from '../dialog';
import * as dom from '../dom';
import * as frames from '../frames'; import * as frames from '../frames';
import { helper, RegisteredListener } from '../helper'; import { helper, RegisteredListener } from '../helper';
import * as dom from '../dom'; import { JSHandle } from '../javascript';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding, PageDelegate } from '../page';
import * as types from '../types';
import { Protocol } from './protocol';
import { getAccessibilityTree } from './wkAccessibility';
import { WKBrowserContext } from './wkBrowser';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
import { WKExecutionContext } from './wkExecutionContext'; import { WKExecutionContext } from './wkExecutionContext';
import { RawKeyboardImpl, RawMouseImpl } from './wkInput';
import { WKInterceptableRequest } from './wkInterceptableRequest'; import { WKInterceptableRequest } from './wkInterceptableRequest';
import { WKWorkers } from './wkWorkers';
import { Page, PageDelegate, PageBinding } from '../page';
import * as path from 'path';
import { Protocol } from './protocol';
import * as dialog from '../dialog';
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
import * as types from '../types';
import * as accessibility from '../accessibility';
import { getAccessibilityTree } from './wkAccessibility';
import { WKProvisionalPage } from './wkProvisionalPage'; import { WKProvisionalPage } from './wkProvisionalPage';
import { WKBrowserContext } from './wkBrowser'; import { WKWorkers } from './wkWorkers';
import * as jpeg from 'jpeg-js';
import * as png from 'pngjs';
import { JSHandle } from '../javascript';
import { assert, createGuid, debugAssert, headersArrayToObject } from '../../utils/utils';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -113,9 +113,9 @@ export class WKPage implements PageDelegate {
for (const [key, value] of this._browserContext._permissions) for (const [key, value] of this._browserContext._permissions)
this._grantPermissions(key, value); this._grantPermissions(key, value);
} }
if (this._browserContext._screencastOptions) { if (this._browserContext._options._recordVideos) {
const contextOptions = this._browserContext._screencastOptions; const contextOptions = this._browserContext._options._recordVideos;
const outputFile = path.join(contextOptions.dir, createGuid() + '.webm'); const outputFile = path.join(this._browserContext._browser._options._videosPath!, createGuid() + '.webm');
const options = Object.assign({}, contextOptions, {outputFile}); const options = Object.assign({}, contextOptions, {outputFile});
promises.push(this.startScreencast(options)); promises.push(this.startScreencast(options));
} }
@ -721,7 +721,11 @@ export class WKPage implements PageDelegate {
width: options.width, width: options.width,
height: options.height, height: options.height,
}) as any; }) as any;
this._browserContext._browser._screencastStarted(screencastId, options.outputFile, this._page); const video = this._browserContext._browser._videoStarted(screencastId, options.outputFile);
this.pageOrError().then(pageOrError => {
if (pageOrError instanceof Page)
pageOrError.emit(Page.Events.VideoStarted, video);
}).catch(() => {});
} catch (e) { } catch (e) {
this._recordingVideoFile = null; this._recordingVideoFile = null;
throw e; throw e;

View File

@ -22,7 +22,6 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { TestServer } from '../utils/testserver'; import { TestServer } from '../utils/testserver';
declare global { declare global {
interface TestState { interface TestState {
videoPlayer: VideoPlayer; videoPlayer: VideoPlayer;
@ -244,19 +243,15 @@ describe('screencast', suite => {
} }
}); });
it('should sutomatically start/finish when new page is created/closed', test => { it('should automatically start/finish when new page is created/closed', test => {
test.flaky(options.FIREFOX, 'Even slow is not slow enough'); test.flaky(options.FIREFOX, 'Even slow is not slow enough');
}, async ({browser, tmpDir, toImpl}) => { }, async ({browserType, tmpDir}) => {
// Use server side of the context. All the code below also uses server side APIs. const browser = await browserType.launch({_videosPath: tmpDir});
const context = toImpl(await browser.newContext()); const context = await browser.newContext({_recordVideos: {width: 320, height: 240}});
await context._enableScreencast({width: 320, height: 240, dir: tmpDir});
expect(context._screencastOptions).toBeTruthy();
const [screencast, newPage] = await Promise.all([ const [screencast, newPage] = await Promise.all([
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>, new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
context.newPage(), context.newPage(),
]); ]);
expect(screencast.page === newPage).toBe(true);
const [videoFile] = await Promise.all([ const [videoFile] = await Promise.all([
screencast.path(), screencast.path(),
@ -264,50 +259,48 @@ describe('screencast', suite => {
]); ]);
expect(path.dirname(videoFile)).toBe(tmpDir); expect(path.dirname(videoFile)).toBe(tmpDir);
await context.close(); await context.close();
await browser.close();
}); });
it('should finish when contex closes', async ({browser, tmpDir, toImpl}) => { it('should finish when contex closes', async ({browserType, tmpDir}) => {
// Use server side of the context. All the code below also uses server side APIs. const browser = await browserType.launch({_videosPath: tmpDir});
const context = toImpl(await browser.newContext()); const context = await browser.newContext({_recordVideos: {width: 320, height: 240}});
await context._enableScreencast({width: 320, height: 240, dir: tmpDir});
expect(context._screencastOptions).toBeTruthy();
const [screencast, newPage] = await Promise.all([ const [video] = await Promise.all([
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>, new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
context.newPage(), context.newPage(),
]); ]);
expect(screencast.page === newPage).toBe(true);
const [videoFile] = await Promise.all([ const [videoFile] = await Promise.all([
screencast.path(), video.path(),
context.close(), context.close(),
]); ]);
expect(path.dirname(videoFile)).toBe(tmpDir); expect(path.dirname(videoFile)).toBe(tmpDir);
await browser.close();
}); });
it('should fire start event for popups', async ({browser, tmpDir, server, toImpl}) => { it('should fire start event for popups', async ({browserType, tmpDir, server}) => {
// Use server side of the context. All the code below also uses server side APIs. const browser = await browserType.launch({_videosPath: tmpDir});
const context = toImpl(await browser.newContext()); const context = await browser.newContext({_recordVideos: {width: 320, height: 240}});
await context._enableScreencast({width: 640, height: 480, dir: tmpDir});
expect(context._screencastOptions).toBeTruthy();
const [page] = await Promise.all([ const [page] = await Promise.all([
context.newPage(), context.newPage(),
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>, new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
]); ]);
await page.mainFrame().goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [video, popup] = await Promise.all([
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
new Promise<Page>(resolve => context.on('page', resolve)),
page.evaluate(() => { window.open('about:blank'); })
]);
const [videoFile] = await Promise.all([
video.path(),
popup.close()
]);
expect(path.dirname(videoFile)).toBe(tmpDir);
const [screencast, popup] = await Promise.all([ await browser.close();
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>,
new Promise(resolve => context.on('page', resolve)) as Promise<any>,
page.mainFrame()._evaluateExpression(() => {
const win = window.open('about:blank');
win.close();
}, true)
]);
expect(screencast.page === popup).toBe(true);
expect(path.dirname(await screencast.path())).toBe(tmpDir);
await context.close();
}); });
it('should scale frames down to the requested size ', async ({page, videoPlayer, tmpDir, server, toImpl}) => { it('should scale frames down to the requested size ', async ({page, videoPlayer, tmpDir, server, toImpl}) => {
@ -350,4 +343,4 @@ describe('screencast', suite => {
expectAll(pixels, almostRed); expectAll(pixels, almostRed);
} }
}); });
}); });