mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
api(video): restore the missing video path accessor (#4132)
This commit is contained in:
parent
9daedaca08
commit
5a7685665a
26
docs/api.md
26
docs/api.md
@ -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"
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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
37
src/client/video.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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) }));
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user