Skip to content

Commit 08a2061

Browse files
committed
api(video): restore the missing video path accessor
1 parent 9daedac commit 08a2061

13 files changed

+120
-16
lines changed

docs/api.md

+26
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- [class: ConsoleMessage](#class-consolemessage)
1515
- [class: Dialog](#class-dialog)
1616
- [class: Download](#class-download)
17+
- [class: Video](#class-video)
1718
- [class: FileChooser](#class-filechooser)
1819
- [class: Keyboard](#class-keyboard)
1920
- [class: Mouse](#class-mouse)
@@ -787,6 +788,7 @@ page.removeListener('request', logRequest);
787788
- [page.uncheck(selector, [options])](#pageuncheckselector-options)
788789
- [page.unroute(url[, handler])](#pageunrouteurl-handler)
789790
- [page.url()](#pageurl)
791+
- [page.video()](#pagevideo)
790792
- [page.viewportSize()](#pageviewportsize)
791793
- [page.waitForEvent(event[, optionsOrPredicate])](#pagewaitforeventevent-optionsorpredicate)
792794
- [page.waitForFunction(pageFunction[, arg, options])](#pagewaitforfunctionpagefunction-arg-options)
@@ -1900,6 +1902,11 @@ Removes a route created with [page.route(url, handler)](#pagerouteurl-handler).
19001902

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

1905+
#### page.video()
1906+
- returns: <[null]|[Video]>
1907+
1908+
Video object associated with this page.
1909+
19031910
#### page.viewportSize()
19041911
- returns: <[null]|[Object]>
19051912
- `width` <[number]> page width in pixels.
@@ -3429,6 +3436,24 @@ Returns suggested filename for this download. It is typically computed by the br
34293436
Returns downloaded url.
34303437

34313438

3439+
### class: Video
3440+
3441+
When browser context is created with the `videosPath` option, each page has a video object associated with it.
3442+
3443+
```js
3444+
console.log(await page.video().path());
3445+
```
3446+
3447+
<!-- GEN:toc -->
3448+
- [video.path()](#videopath)
3449+
<!-- GEN:stop -->
3450+
3451+
#### video.path()
3452+
- returns: <[string]>
3453+
3454+
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.
3455+
3456+
34323457
### class: FileChooser
34333458

34343459
[FileChooser] objects are dispatched by the page in the ['filechooser'](#event-filechooser) event.
@@ -4818,6 +4843,7 @@ const { chromium } = require('playwright');
48184843
[URL]: https://nodejs.org/api/url.html
48194844
[USKeyboardLayout]: ../src/usKeyboardLayout.ts "USKeyboardLayout"
48204845
[UnixTime]: https://en.wikipedia.org/wiki/Unix_time "Unix Time"
4846+
[Video]: #class-video "Video"
48214847
[WebKitBrowser]: #class-webkitbrowser "WebKitBrowser"
48224848
[WebSocket]: #class-websocket "WebSocket"
48234849
[Worker]: #class-worker "Worker"

src/browserServerImpl.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ class ConnectedBrowser extends BrowserDispatcher {
181181
const readable = fs.createReadStream(video._path);
182182
await new Promise(f => readable.on('readable', f));
183183
const stream = new StreamDispatcher(this._remoteBrowser!._scope, readable);
184-
this._remoteBrowser!._dispatchEvent('video', { stream, context: contextDispatcher });
184+
this._remoteBrowser!._dispatchEvent('video', {
185+
stream,
186+
context: contextDispatcher,
187+
relativePath: video._relativePath
188+
});
185189
await new Promise<void>(resolve => {
186190
readable.on('close', resolve);
187191
readable.on('end', resolve);

src/client/api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export { JSHandle } from './jsHandle';
3232
export { Request, Response, Route } from './network';
3333
export { Page } from './page';
3434
export { Selectors } from './selectors';
35+
export { Video } from './video';
3536
export { Worker } from './worker';
3637

3738
export { ChromiumBrowser } from './chromiumBrowser';

src/client/browser.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel, channels.Brow
5858
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
5959
};
6060
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
61-
if (this._isRemote)
62-
context._videosPathForRemote = options.videosPath;
61+
context._options = contextOptions;
6362
this._contexts.add(context);
6463
context._logger = logger || this._logger;
6564
return context;

src/client/browserContext.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
3737
_timeoutSettings = new TimeoutSettings();
3838
_ownerPage: Page | undefined;
3939
private _closedPromise: Promise<void>;
40-
_videosPathForRemote?: string;
40+
_options: channels.BrowserNewContextParams = {};
4141

4242
static from(context: channels.BrowserContextChannel): BrowserContext {
4343
return (context as any)._object;

src/client/browserType.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { TimeoutSettings } from '../utils/timeoutSettings';
2929
import { ChildProcess } from 'child_process';
3030
import { envObjectToArray } from './clientHelper';
3131
import { validateHeaders } from './network';
32-
import { assert, makeWaitForNextTask, headersObjectToArray, createGuid, mkdirIfNeeded } from '../utils/utils';
32+
import { assert, makeWaitForNextTask, headersObjectToArray, mkdirIfNeeded } from '../utils/utils';
3333
import { SelectorsOwner, sharedSelectors } from './selectors';
3434
import { kBrowserClosedError } from '../utils/errors';
3535
import { Stream } from './stream';
@@ -108,6 +108,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
108108
};
109109
const result = await this._channel.launchPersistentContext(persistentOptions);
110110
const context = BrowserContext.from(result.context);
111+
context._options = persistentOptions;
111112
context._logger = logger;
112113
return context;
113114
}, logger);
@@ -188,16 +189,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
188189
export class RemoteBrowser extends ChannelOwner<channels.RemoteBrowserChannel, channels.RemoteBrowserInitializer> {
189190
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RemoteBrowserInitializer) {
190191
super(parent, type, guid, initializer);
191-
this._channel.on('video', ({ context, stream }) => this._onVideo(BrowserContext.from(context), Stream.from(stream)));
192+
this._channel.on('video', ({ context, stream, relativePath }) => this._onVideo(BrowserContext.from(context), Stream.from(stream), relativePath));
192193
}
193194

194-
private async _onVideo(context: BrowserContext, stream: Stream) {
195-
if (!context._videosPathForRemote) {
196-
stream._channel.close().catch(e => null);
197-
return;
198-
}
199-
200-
const videoFile = path.join(context._videosPathForRemote, createGuid() + '.webm');
195+
private async _onVideo(context: BrowserContext, stream: Stream, relativePath: string) {
196+
const videoFile = path.join(context._options.videosPath!, relativePath);
201197
await mkdirIfNeeded(videoFile);
202198
stream.stream().pipe(fs.createWriteStream(videoFile));
203199
}

src/client/page.ts

+12
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOpt
4444
import { evaluationScript, urlMatches } from './clientHelper';
4545
import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils';
4646
import { isSafeCloseError } from '../utils/errors';
47+
import { Video } from './video';
4748

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

8688
static from(page: channels.PageChannel): Page {
8789
return (page as any)._object;
@@ -125,6 +127,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
125127
this._channel.on('requestFinished', ({ request }) => this.emit(Events.Page.RequestFinished, Request.from(request)));
126128
this._channel.on('response', ({ response }) => this.emit(Events.Page.Response, Response.from(response)));
127129
this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request)));
130+
this._channel.on('video', ({ relativePath }) => this.video()!._setRelativePath(relativePath));
128131
this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));
129132

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

232+
video(): Video | null {
233+
if (this._video)
234+
return this._video;
235+
if (!this._browserContext._options.videosPath)
236+
return null;
237+
this._video = new Video(this);
238+
return this._video;
239+
}
240+
229241
private _attributeToPage<T>(func: () => T): T {
230242
try {
231243
this._isPageCall = true;

src/client/video.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as path from 'path';
18+
import { Page } from './page';
19+
20+
export class Video {
21+
private _page: Page;
22+
private _pathCallback: ((path: string) => void) | undefined;
23+
private _pathPromise: Promise<string>;
24+
25+
constructor(page: Page) {
26+
this._page = page;
27+
this._pathPromise = new Promise(f => this._pathCallback = f);
28+
}
29+
30+
_setRelativePath(relativePath: string) {
31+
this._pathCallback!(path.join(this._page.context()._options.videosPath!, relativePath));
32+
}
33+
34+
path(): Promise<string> {
35+
return this._pathPromise;
36+
}
37+
}

src/dispatchers/pageDispatcher.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { BrowserContext, runAction } from '../server/browserContext';
17+
import { BrowserContext, runAction, Video } from '../server/browserContext';
1818
import { Frame } from '../server/frames';
1919
import { Request } from '../server/network';
2020
import { Page, Worker } from '../server/page';
@@ -66,6 +66,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
6666
}));
6767
page.on(Page.Events.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) }));
6868
page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) }));
69+
page.on(Page.Events.VideoStarted, (video: Video) => this._dispatchEvent('video', { relativePath: video._relativePath }));
6970
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
7071
}
7172

src/protocol/channels.ts

+5
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export interface RemoteBrowserChannel extends Channel {
125125
export type RemoteBrowserVideoEvent = {
126126
context: BrowserContextChannel,
127127
stream: StreamChannel,
128+
relativePath: string,
128129
};
129130

130131
// ----------- Selectors -----------
@@ -683,6 +684,7 @@ export interface PageChannel extends Channel {
683684
on(event: 'requestFinished', callback: (params: PageRequestFinishedEvent) => void): this;
684685
on(event: 'response', callback: (params: PageResponseEvent) => void): this;
685686
on(event: 'route', callback: (params: PageRouteEvent) => void): this;
687+
on(event: 'video', callback: (params: PageVideoEvent) => void): this;
686688
on(event: 'worker', callback: (params: PageWorkerEvent) => void): this;
687689
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
688690
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
@@ -765,6 +767,9 @@ export type PageRouteEvent = {
765767
route: RouteChannel,
766768
request: RequestChannel,
767769
};
770+
export type PageVideoEvent = {
771+
relativePath: string,
772+
};
768773
export type PageWorkerEvent = {
769774
worker: WorkerChannel,
770775
};

src/protocol/protocol.yml

+5
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ RemoteBrowser:
175175
parameters:
176176
context: BrowserContext
177177
stream: Stream
178+
relativePath: string
178179

179180

180181
Selectors:
@@ -927,6 +928,10 @@ Page:
927928
route: Route
928929
request: Request
929930

931+
video:
932+
parameters:
933+
relativePath: string
934+
930935
worker:
931936
parameters:
932937
worker: Worker

src/server/browserContext.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,16 @@ import * as path from 'path';
3333
export class Video {
3434
readonly _videoId: string;
3535
readonly _path: string;
36+
readonly _relativePath: string;
3637
readonly _context: BrowserContext;
3738
readonly _finishedPromise: Promise<void>;
3839
private _finishCallback: () => void = () => {};
3940
private _callbackOnFinish?: () => Promise<void>;
4041

41-
constructor(context: BrowserContext, videoId: string, path: string) {
42+
constructor(context: BrowserContext, videoId: string, p: string) {
4243
this._videoId = videoId;
43-
this._path = path;
44+
this._path = p;
45+
this._relativePath = path.relative(context._options.videosPath!, p);
4446
this._context = context;
4547
this._finishedPromise = new Promise(fulfill => this._finishCallback = fulfill);
4648
}

test/screencast.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,22 @@ describe('screencast', suite => {
180180
}
181181
});
182182

183+
it('should emit video event', async ({browser, testInfo}) => {
184+
const videosPath = testInfo.outputPath('');
185+
const size = { width: 320, height: 240 };
186+
const context = await browser.newContext({
187+
videosPath,
188+
viewport: size,
189+
videoSize: size
190+
});
191+
const page = await context.newPage();
192+
await page.evaluate(() => document.body.style.backgroundColor = 'red');
193+
await new Promise(r => setTimeout(r, 1000));
194+
await context.close();
195+
const path = await page.video()!.path();
196+
expect(path).toContain(videosPath);
197+
});
198+
183199
it('should capture navigation', async ({browser, server, testInfo}) => {
184200
const videosPath = testInfo.outputPath('');
185201
const context = await browser.newContext({

0 commit comments

Comments
 (0)