Skip to content

api(video): restore the missing video path accessor #4132

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

Merged
merged 1 commit into from
Oct 14, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [class: ConsoleMessage](#class-consolemessage)
- [class: Dialog](#class-dialog)
- [class: Download](#class-download)
- [class: Video](#class-video)
- [class: FileChooser](#class-filechooser)
- [class: Keyboard](#class-keyboard)
- [class: Mouse](#class-mouse)
Expand Down Expand Up @@ -787,6 +788,7 @@ page.removeListener('request', logRequest);
- [page.uncheck(selector, [options])](#pageuncheckselector-options)
- [page.unroute(url[, handler])](#pageunrouteurl-handler)
- [page.url()](#pageurl)
- [page.video()](#pagevideo)
- [page.viewportSize()](#pageviewportsize)
- [page.waitForEvent(event[, optionsOrPredicate])](#pagewaitforeventevent-optionsorpredicate)
- [page.waitForFunction(pageFunction[, arg, options])](#pagewaitforfunctionpagefunction-arg-options)
Expand Down Expand Up @@ -1900,6 +1902,11 @@ Removes a route created with [page.route(url, handler)](#pagerouteurl-handler).

This is a shortcut for [page.mainFrame().url()](#frameurl)

#### page.video()
- returns: <[null]|[Video]>

Video object associated with this page.

#### page.viewportSize()
- returns: <[null]|[Object]>
- `width` <[number]> page width in pixels.
Expand Down Expand Up @@ -3429,6 +3436,24 @@ Returns suggested filename for this download. It is typically computed by the br
Returns downloaded url.


### class: Video

When browser context is created with the `videosPath` option, each page has a video object associated with it.

```js
console.log(await page.video().path());
```

<!-- GEN:toc -->
- [video.path()](#videopath)
<!-- GEN:stop -->

#### video.path()
- returns: <[string]>

Returns the file system path this video will be recorded to. The video is guaranteed to be written to the filesystem upon closing the browser context.


### class: FileChooser

[FileChooser] objects are dispatched by the page in the ['filechooser'](#event-filechooser) event.
Expand Down Expand Up @@ -4818,6 +4843,7 @@ const { chromium } = require('playwright');
[URL]: https://nodejs.org/api/url.html
[USKeyboardLayout]: ../src/usKeyboardLayout.ts "USKeyboardLayout"
[UnixTime]: https://en.wikipedia.org/wiki/Unix_time "Unix Time"
[Video]: #class-video "Video"
[WebKitBrowser]: #class-webkitbrowser "WebKitBrowser"
[WebSocket]: #class-websocket "WebSocket"
[Worker]: #class-worker "Worker"
Expand Down
6 changes: 5 additions & 1 deletion src/browserServerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,11 @@ class ConnectedBrowser extends BrowserDispatcher {
const readable = fs.createReadStream(video._path);
await new Promise(f => readable.on('readable', f));
const stream = new StreamDispatcher(this._remoteBrowser!._scope, readable);
this._remoteBrowser!._dispatchEvent('video', { stream, context: contextDispatcher });
this._remoteBrowser!._dispatchEvent('video', {
stream,
context: contextDispatcher,
relativePath: video._relativePath
});
await new Promise<void>(resolve => {
readable.on('close', resolve);
readable.on('end', resolve);
Expand Down
1 change: 1 addition & 0 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { JSHandle } from './jsHandle';
export { Request, Response, Route } from './network';
export { Page } from './page';
export { Selectors } from './selectors';
export { Video } from './video';
export { Worker } from './worker';

export { ChromiumBrowser } from './chromiumBrowser';
Expand Down
3 changes: 1 addition & 2 deletions src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel, channels.Brow
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
};
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
if (this._isRemote)
context._videosPathForRemote = options.videosPath;
context._options = contextOptions;
this._contexts.add(context);
context._logger = logger || this._logger;
return context;
Expand Down
2 changes: 1 addition & 1 deletion src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
_timeoutSettings = new TimeoutSettings();
_ownerPage: Page | undefined;
private _closedPromise: Promise<void>;
_videosPathForRemote?: string;
_options: channels.BrowserNewContextParams = {};

static from(context: channels.BrowserContextChannel): BrowserContext {
return (context as any)._object;
Expand Down
14 changes: 5 additions & 9 deletions src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { TimeoutSettings } from '../utils/timeoutSettings';
import { ChildProcess } from 'child_process';
import { envObjectToArray } from './clientHelper';
import { validateHeaders } from './network';
import { assert, makeWaitForNextTask, headersObjectToArray, createGuid, mkdirIfNeeded } from '../utils/utils';
import { assert, makeWaitForNextTask, headersObjectToArray, mkdirIfNeeded } from '../utils/utils';
import { SelectorsOwner, sharedSelectors } from './selectors';
import { kBrowserClosedError } from '../utils/errors';
import { Stream } from './stream';
Expand Down Expand Up @@ -108,6 +108,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
};
const result = await this._channel.launchPersistentContext(persistentOptions);
const context = BrowserContext.from(result.context);
context._options = persistentOptions;
context._logger = logger;
return context;
}, logger);
Expand Down Expand Up @@ -188,16 +189,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
export class RemoteBrowser extends ChannelOwner<channels.RemoteBrowserChannel, channels.RemoteBrowserInitializer> {
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RemoteBrowserInitializer) {
super(parent, type, guid, initializer);
this._channel.on('video', ({ context, stream }) => this._onVideo(BrowserContext.from(context), Stream.from(stream)));
this._channel.on('video', ({ context, stream, relativePath }) => this._onVideo(BrowserContext.from(context), Stream.from(stream), relativePath));
}

private async _onVideo(context: BrowserContext, stream: Stream) {
if (!context._videosPathForRemote) {
stream._channel.close().catch(e => null);
return;
}

const videoFile = path.join(context._videosPathForRemote, createGuid() + '.webm');
private async _onVideo(context: BrowserContext, stream: Stream, relativePath: string) {
const videoFile = path.join(context._options.videosPath!, relativePath);
await mkdirIfNeeded(videoFile);
stream.stream().pipe(fs.createWriteStream(videoFile));
}
Expand Down
12 changes: 12 additions & 0 deletions src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOpt
import { evaluationScript, urlMatches } from './clientHelper';
import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils';
import { isSafeCloseError } from '../utils/errors';
import { Video } from './video';

const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
const mkdirAsync = util.promisify(fs.mkdir);
Expand Down Expand Up @@ -82,6 +83,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
readonly _bindings = new Map<string, FunctionWithSource>();
readonly _timeoutSettings: TimeoutSettings;
_isPageCall = false;
private _video: Video | null = null;

static from(page: channels.PageChannel): Page {
return (page as any)._object;
Expand Down Expand Up @@ -125,6 +127,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this._channel.on('requestFinished', ({ request }) => this.emit(Events.Page.RequestFinished, Request.from(request)));
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('video', ({ relativePath }) => this.video()!._setRelativePath(relativePath));
this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));

if (this._browserContext._browserName === 'chromium') {
Expand Down Expand Up @@ -226,6 +229,15 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this._channel.setDefaultTimeoutNoReply({ timeout });
}

video(): Video | null {
if (this._video)
return this._video;
if (!this._browserContext._options.videosPath)
return null;
this._video = new Video(this);
return this._video;
}

private _attributeToPage<T>(func: () => T): T {
try {
this._isPageCall = true;
Expand Down
37 changes: 37 additions & 0 deletions src/client/video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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 path from 'path';
import { Page } from './page';

export class Video {
private _page: Page;
private _pathCallback: ((path: string) => void) | undefined;
private _pathPromise: Promise<string>;

constructor(page: Page) {
this._page = page;
this._pathPromise = new Promise(f => this._pathCallback = f);
}

_setRelativePath(relativePath: string) {
this._pathCallback!(path.join(this._page.context()._options.videosPath!, relativePath));
}

path(): Promise<string> {
return this._pathPromise;
}
}
3 changes: 2 additions & 1 deletion src/dispatchers/pageDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { BrowserContext, runAction } from '../server/browserContext';
import { BrowserContext, runAction, Video } from '../server/browserContext';
import { Frame } from '../server/frames';
import { Request } from '../server/network';
import { Page, Worker } from '../server/page';
Expand Down Expand Up @@ -66,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.VideoStarted, (video: Video) => this._dispatchEvent('video', { relativePath: video._relativePath }));
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
}

Expand Down
5 changes: 5 additions & 0 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export interface RemoteBrowserChannel extends Channel {
export type RemoteBrowserVideoEvent = {
context: BrowserContextChannel,
stream: StreamChannel,
relativePath: string,
};

// ----------- Selectors -----------
Expand Down Expand Up @@ -683,6 +684,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: 'video', callback: (params: PageVideoEvent) => void): this;
on(event: 'worker', callback: (params: PageWorkerEvent) => void): this;
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
Expand Down Expand Up @@ -765,6 +767,9 @@ export type PageRouteEvent = {
route: RouteChannel,
request: RequestChannel,
};
export type PageVideoEvent = {
relativePath: string,
};
export type PageWorkerEvent = {
worker: WorkerChannel,
};
Expand Down
5 changes: 5 additions & 0 deletions src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ RemoteBrowser:
parameters:
context: BrowserContext
stream: Stream
relativePath: string


Selectors:
Expand Down Expand Up @@ -927,6 +928,10 @@ Page:
route: Route
request: Request

video:
parameters:
relativePath: string

worker:
parameters:
worker: Worker
Expand Down
6 changes: 4 additions & 2 deletions src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ import * as path from 'path';
export class Video {
readonly _videoId: string;
readonly _path: string;
readonly _relativePath: string;
readonly _context: BrowserContext;
readonly _finishedPromise: Promise<void>;
private _finishCallback: () => void = () => {};
private _callbackOnFinish?: () => Promise<void>;

constructor(context: BrowserContext, videoId: string, path: string) {
constructor(context: BrowserContext, videoId: string, p: string) {
this._videoId = videoId;
this._path = path;
this._path = p;
this._relativePath = path.relative(context._options.videosPath!, p);
this._context = context;
this._finishedPromise = new Promise(fulfill => this._finishCallback = fulfill);
}
Expand Down
16 changes: 16 additions & 0 deletions test/screencast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,22 @@ describe('screencast', suite => {
}
});

it('should emit video event', async ({browser, testInfo}) => {
const videosPath = testInfo.outputPath('');
const size = { width: 320, height: 240 };
const context = await browser.newContext({
videosPath,
viewport: size,
videoSize: size
});
const page = await context.newPage();
await page.evaluate(() => document.body.style.backgroundColor = 'red');
await new Promise(r => setTimeout(r, 1000));
await context.close();
const path = await page.video()!.path();
expect(path).toContain(videosPath);
});

it('should capture navigation', async ({browser, server, testInfo}) => {
const videosPath = testInfo.outputPath('');
const context = await browser.newContext({
Expand Down