Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Sep 5, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ 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.
- `_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]>>

Creates a new browser context. It won't share cookies/cache with other browser contexts.
Expand Down Expand Up @@ -262,6 +265,9 @@ 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.
- `_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]>>

Creates a new page in a new browser context. Closing this page will close the context as well.
Expand Down Expand Up @@ -684,6 +690,7 @@ page.removeListener('request', logRequest);
```

<!-- GEN:toc -->
- [event: '_videostarted'](#event-videostarted)
- [event: 'close'](#event-close-1)
- [event: 'console'](#event-console)
- [event: 'crash'](#event-crash)
Expand Down Expand Up @@ -770,6 +777,12 @@ page.removeListener('request', logRequest);
- [page.workers()](#pageworkers)
<!-- 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'

Emitted when the page closes.
Expand Down Expand Up @@ -4157,6 +4170,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.
- `_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).
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
Expand Down Expand Up @@ -4231,6 +4245,10 @@ 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`'.
- `_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.

Launches browser that uses persistent storage located at `userDataDir` and returns the only context. Closing this context will automatically close the browser.
Expand All @@ -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.
- `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.
- `_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).
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
Expand Down
2 changes: 2 additions & 0 deletions src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { TimeoutSettings } from '../utils/timeoutSettings';
import { Waiter } from './waiter';
import { URLMatch, Headers, WaitForEventOptions } from './types';
import { isDevMode, headersObjectToArray } from '../utils/utils';
import { Video } from './video';

export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> {
_pages = new Set<Page>();
Expand All @@ -38,6 +39,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
_ownerPage: Page | undefined;
private _isClosedOrClosing = false;
private _closedPromise: Promise<void>;
private _idToScreencast = new Map<string, Video>();

static from(context: channels.BrowserContextChannel): BrowserContext {
return (context as any)._object;
Expand Down
2 changes: 1 addition & 1 deletion src/client/channelOwner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
const base = new EventEmitter();
this._channel = new Proxy(base, {
get: (obj: any, prop) => {
if (String(prop).startsWith('_'))
if (String(prop).startsWith('_') && String(prop) !== '_enableScreencast' && String(prop) !== '_disableScreencast')
return obj[prop];
if (prop === 'then')
return obj.then;
Expand Down
4 changes: 4 additions & 0 deletions src/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { WebKitBrowser } from './webkitBrowser';
import { FirefoxBrowser } from './firefoxBrowser';
import { debugLogger } from '../utils/debugLogger';
import { SelectorsOwner } from './selectors';
import { Video } from './video';

class Root extends ChannelOwner<channels.Channel, {}> {
constructor(connection: Connection) {
Expand Down Expand Up @@ -207,6 +208,9 @@ export class Connection {
case 'Route':
result = new Route(parent, type, guid, initializer);
break;
case 'Video':
result = new Video(parent, type, guid, initializer);
break;
case 'Stream':
result = new Stream(parent, type, guid, initializer);
break;
Expand Down
1 change: 1 addition & 0 deletions src/client/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const Events = {
Load: 'load',
Popup: 'popup',
Worker: 'worker',
_VideoStarted: '_videostarted',
},

Worker: {
Expand Down
6 changes: 6 additions & 0 deletions src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import * as util from 'util';
import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types';
import { evaluationScript, urlMatches } from './clientHelper';
import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils';
import { Video } from './video';

type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
width?: string | number,
Expand Down Expand Up @@ -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('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request)));
this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));
this._channel.on('videoStarted', params => this._onVideoStarted(params));

if (this._browserContext._browserName === 'chromium') {
this.coverage = new ChromiumCoverage(this._channel);
Expand Down Expand Up @@ -175,6 +177,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this.emit(Events.Page.Worker, worker);
}

private _onVideoStarted(params: channels.PageVideoStartedEvent): void {
this.emit(Events.Page._VideoStarted, Video.from(params.video));
}

_onClose() {
this._closed = true;
this._browserContext._pages.delete(this);
Expand Down
1 change: 1 addition & 0 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export type LaunchServerOptions = {
password?: string
},
downloadsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean,
port?: number,
logger?: Logger,
Expand Down
39 changes: 39 additions & 0 deletions src/client/video.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion src/dispatchers/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export class DispatcherConnection {
onmessage = (message: object) => {};
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;
this.onmessage({ guid, method, params: this._replaceDispatchersWithGuids(params, allowDispatchers) });
}
Expand Down
4 changes: 3 additions & 1 deletion src/dispatchers/pageDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { serializeResult, parseArgument } from './jsHandleDispatcher';
import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
import { FileChooser } from '../server/fileChooser';
import { CRCoverage } from '../server/chromium/crCoverage';
import { VideoDispatcher } from './videoDispatcher';

export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
private _page: Page;
Expand All @@ -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.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded'));
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', {
element: new ElementHandleDispatcher(this._scope, fileChooser.element()),
isMultiple: fileChooser.isMultiple()
Expand All @@ -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.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) }));
page.on(Page.Events.ScreencastStarted, screencast => this._dispatchEvent('videoStarted', { video: new VideoDispatcher(this._scope, screencast) }));
yury-s marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we should narrow down the scopes as much as we can (page in this case)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The video can survive the page and finish after it has closed.

page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
}

Expand Down
29 changes: 29 additions & 0 deletions src/dispatchers/videoDispatcher.ts
Original file line number Diff line number Diff line change
@@ -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 { Screencast } from '../server/browserContext';
import { Dispatcher, DispatcherScope } from './dispatcher';

export class VideoDispatcher extends Dispatcher<Screencast, channels.VideoInitializer> implements channels.VideoChannel {
constructor(scope: DispatcherScope, screencast: Screencast) {
super(scope, screencast, 'Video', {});
}

async path(): Promise<channels.VideoPathResult> {
return { value: await this._object.path() };
}
}
27 changes: 27 additions & 0 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export type BrowserTypeLaunchParams = {
password?: string,
},
downloadsPath?: string,
_videosPath?: string,
firefoxUserPrefs?: any,
chromiumSandbox?: boolean,
slowMo?: number,
Expand All @@ -190,6 +191,7 @@ export type BrowserTypeLaunchOptions = {
password?: string,
},
downloadsPath?: string,
_videosPath?: string,
firefoxUserPrefs?: any,
chromiumSandbox?: boolean,
slowMo?: number,
Expand Down Expand Up @@ -220,6 +222,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
password?: string,
},
downloadsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean,
slowMo?: number,
noDefaultViewport?: boolean,
Expand Down Expand Up @@ -276,6 +279,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
password?: string,
},
downloadsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean,
slowMo?: number,
noDefaultViewport?: boolean,
Expand Down Expand Up @@ -363,6 +367,10 @@ export type BrowserNewContextParams = {
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
acceptDownloads?: boolean,
_recordVideos?: {
width: number,
height: number,
},
};
export type BrowserNewContextOptions = {
noDefaultViewport?: boolean,
Expand Down Expand Up @@ -396,6 +404,10 @@ export type BrowserNewContextOptions = {
hasTouch?: boolean,
colorScheme?: 'dark' | 'light' | 'no-preference',
acceptDownloads?: boolean,
_recordVideos?: {
width: number,
height: number,
},
};
export type BrowserNewContextResult = {
context: BrowserContextChannel,
Expand Down Expand Up @@ -645,6 +657,7 @@ export interface PageChannel extends Channel {
on(event: 'requestFinished', callback: (params: PageRequestFinishedEvent) => void): this;
on(event: 'response', callback: (params: PageResponseEvent) => 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;
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams): Promise<PageSetDefaultTimeoutNoReplyResult>;
Expand Down Expand Up @@ -727,6 +740,9 @@ export type PageRouteEvent = {
route: RouteChannel,
request: RequestChannel,
};
export type PageVideoStartedEvent = {
video: VideoChannel,
};
export type PageWorkerEvent = {
worker: WorkerChannel,
};
Expand Down Expand Up @@ -2107,6 +2123,17 @@ export type DialogDismissParams = {};
export type DialogDismissOptions = {};
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 -----------
export type DownloadInitializer = {
url: string,
Expand Down
Loading