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

This commit is contained in:
Pavel Feldman 2020-10-13 22:15:51 -07:00 committed by GitHub
parent 9daedaca08
commit 5a7685665a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 120 additions and 16 deletions

View File

@ -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)
@ -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)
@ -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.
@ -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.
@ -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"

View File

@ -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);

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -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';
@ -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);
@ -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));
}

View File

@ -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);
@ -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;
@ -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') {
@ -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;

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

@ -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;
}
}

View File

@ -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';
@ -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) }));
}

View File

@ -125,6 +125,7 @@ export interface RemoteBrowserChannel extends Channel {
export type RemoteBrowserVideoEvent = {
context: BrowserContextChannel,
stream: StreamChannel,
relativePath: string,
};
// ----------- Selectors -----------
@ -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>;
@ -765,6 +767,9 @@ export type PageRouteEvent = {
route: RouteChannel,
request: RequestChannel,
};
export type PageVideoEvent = {
relativePath: string,
};
export type PageWorkerEvent = {
worker: WorkerChannel,
};

View File

@ -175,6 +175,7 @@ RemoteBrowser:
parameters:
context: BrowserContext
stream: Stream
relativePath: string
Selectors:
@ -927,6 +928,10 @@ Page:
route: Route
request: Request
video:
parameters:
relativePath: string
worker:
parameters:
worker: Worker

View File

@ -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);
}

View File

@ -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({