feat(screencast): add start/stop events on context (#3483)
This commit is contained in:
parent
73cd6ecef3
commit
83de0071c9
|
|
@ -15,8 +15,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { helper } from './helper';
|
||||
import * as network from './network';
|
||||
import * as path from 'path';
|
||||
import { Page, PageBinding } from './page';
|
||||
import { TimeoutSettings } from './timeoutSettings';
|
||||
import * as frames from './frames';
|
||||
|
|
@ -28,10 +30,20 @@ import { EventEmitter } from 'events';
|
|||
import { Progress } from './progress';
|
||||
import { DebugController } from './debug/debugController';
|
||||
|
||||
export class Screencast {
|
||||
readonly path: string;
|
||||
readonly page: Page;
|
||||
constructor(path: string, page: Page) {
|
||||
this.path = path;
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BrowserContext extends EventEmitter {
|
||||
readonly _timeoutSettings = new TimeoutSettings();
|
||||
readonly _pageBindings = new Map<string, PageBinding>();
|
||||
readonly _options: types.BrowserContextOptions;
|
||||
_screencastOptions: types.ContextScreencastOptions | null = null;
|
||||
_requestInterceptor?: network.RouteHandler;
|
||||
private _isPersistentContext: boolean;
|
||||
private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
|
||||
|
|
@ -142,6 +154,15 @@ export abstract class BrowserContext extends EventEmitter {
|
|||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
_enableScreencast(options: types.ContextScreencastOptions) {
|
||||
this._screencastOptions = options;
|
||||
fs.mkdirSync(path.dirname(options.dir), {recursive: true});
|
||||
}
|
||||
|
||||
_disableScreencast() {
|
||||
this._screencastOptions = null;
|
||||
}
|
||||
|
||||
async _loadDefaultContext(progress: Progress) {
|
||||
if (!this.pages().length) {
|
||||
const waitForEvent = helper.waitForEvent(progress, this, Events.BrowserContext.Page);
|
||||
|
|
|
|||
|
|
@ -209,11 +209,11 @@ export class CRPage implements PageDelegate {
|
|||
await this._mainFrameSession._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
|
||||
}
|
||||
|
||||
async startVideoRecording(options: types.VideoRecordingOptions): Promise<void> {
|
||||
async startScreencast(options: types.PageScreencastOptions): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async stopVideoRecording(): Promise<void> {
|
||||
async stopScreencast(): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export const Events = {
|
|||
BrowserContext: {
|
||||
Close: 'close',
|
||||
Page: 'page',
|
||||
ScreencastStarted: 'screencaststarted',
|
||||
ScreencastStopped: 'screencaststopped',
|
||||
},
|
||||
|
||||
BrowserServer: {
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ export class FFPage implements PageDelegate {
|
|||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async startVideoRecording(options: types.VideoRecordingOptions): Promise<void> {
|
||||
async startScreencast(options: types.PageScreencastOptions): Promise<void> {
|
||||
this._session.send('Page.startVideoRecording', {
|
||||
file: options.outputFile,
|
||||
width: options.width,
|
||||
|
|
@ -355,7 +355,7 @@ export class FFPage implements PageDelegate {
|
|||
});
|
||||
}
|
||||
|
||||
async stopVideoRecording(): Promise<void> {
|
||||
async stopScreencast(): Promise<void> {
|
||||
await this._session.send('Page.stopVideoRecording');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ export interface PageDelegate {
|
|||
canScreenshotOutsideViewport(): boolean;
|
||||
resetViewport(): Promise<void>; // Only called if canScreenshotOutsideViewport() returns false.
|
||||
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
|
||||
startVideoRecording(options: types.VideoRecordingOptions): Promise<void>;
|
||||
stopVideoRecording(): Promise<void>;
|
||||
startScreencast(options: types.PageScreencastOptions): Promise<void>;
|
||||
stopScreencast(): Promise<void>;
|
||||
takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer>;
|
||||
|
||||
isElementHandle(remoteObject: any): boolean;
|
||||
|
|
|
|||
11
src/types.ts
11
src/types.ts
|
|
@ -60,13 +60,20 @@ export type ScreenshotOptions = ElementScreenshotOptions & {
|
|||
clip?: Rect,
|
||||
};
|
||||
|
||||
export type VideoRecordingOptions = {
|
||||
outputFile: string,
|
||||
export type ScreencastOptions = {
|
||||
width: number,
|
||||
height: number,
|
||||
scale?: number,
|
||||
};
|
||||
|
||||
export type PageScreencastOptions = ScreencastOptions & {
|
||||
outputFile: string,
|
||||
};
|
||||
|
||||
export type ContextScreencastOptions = ScreencastOptions & {
|
||||
dir: string,
|
||||
};
|
||||
|
||||
export type URLMatch = string | RegExp | ((url: URL) => boolean);
|
||||
|
||||
export type Credentials = {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Events } from '../events';
|
|||
import { helper, RegisteredListener, assert } from '../helper';
|
||||
import * as network from '../network';
|
||||
import { Page, PageBinding } from '../page';
|
||||
import * as path from 'path';
|
||||
import { ConnectionTransport } from '../transport';
|
||||
import * as types from '../types';
|
||||
import { Protocol } from './protocol';
|
||||
|
|
@ -247,12 +248,17 @@ export class WKBrowserContext extends BrowserContext {
|
|||
const { pageProxyId } = await this._browser._browserSession.send('Playwright.createPage', { browserContextId: this._browserContextId });
|
||||
const wkPage = this._browser._wkPages.get(pageProxyId)!;
|
||||
const result = await wkPage.pageOrError();
|
||||
if (result instanceof Page) {
|
||||
if (result.isClosed())
|
||||
throw new Error('Page has been closed.');
|
||||
return result;
|
||||
if (!(result instanceof Page))
|
||||
throw result;
|
||||
if (result.isClosed())
|
||||
throw new Error('Page has been closed.');
|
||||
if (result._browserContext._screencastOptions) {
|
||||
const contextOptions = result._browserContext._screencastOptions;
|
||||
const outputFile = path.join(contextOptions.dir, helper.guid() + '.webm');
|
||||
const options = Object.assign({}, contextOptions, {outputFile});
|
||||
await wkPage.startScreencast(options);
|
||||
}
|
||||
throw result;
|
||||
return result;
|
||||
}
|
||||
|
||||
async _doCookies(urls: string[]): Promise<types.NetworkCookie[]> {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Screencast } from '../browserContext';
|
||||
import * as frames from '../frames';
|
||||
import { helper, RegisteredListener, assert, debugAssert } from '../helper';
|
||||
import * as dom from '../dom';
|
||||
|
|
@ -68,6 +69,7 @@ export class WKPage implements PageDelegate {
|
|||
// Holds window features for the next popup being opened via window.open,
|
||||
// until the popup page proxy arrives.
|
||||
private _nextWindowOpenPopupFeatures?: string[];
|
||||
private _recordingVideoFile: string | null = null;
|
||||
|
||||
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) {
|
||||
this._pageProxySession = pageProxySession;
|
||||
|
|
@ -689,7 +691,9 @@ export class WKPage implements PageDelegate {
|
|||
}
|
||||
|
||||
async closePage(runBeforeUnload: boolean): Promise<void> {
|
||||
this._pageProxySession.sendMayFail('Target.close', {
|
||||
if (this._recordingVideoFile)
|
||||
await this.stopScreencast();
|
||||
await this._pageProxySession.sendMayFail('Target.close', {
|
||||
targetId: this._session.sessionId,
|
||||
runBeforeUnload
|
||||
});
|
||||
|
|
@ -703,17 +707,31 @@ export class WKPage implements PageDelegate {
|
|||
await this._session.send('Page.setDefaultBackgroundColorOverride', { color });
|
||||
}
|
||||
|
||||
async startVideoRecording(options: types.VideoRecordingOptions): Promise<void> {
|
||||
this._pageProxySession.send('Screencast.startVideoRecording', {
|
||||
file: options.outputFile,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
scale: options.scale,
|
||||
});
|
||||
async startScreencast(options: types.PageScreencastOptions): Promise<void> {
|
||||
if (this._recordingVideoFile)
|
||||
throw new Error('Already recording');
|
||||
this._recordingVideoFile = options.outputFile;
|
||||
try {
|
||||
await this._pageProxySession.send('Screencast.startVideoRecording', {
|
||||
file: options.outputFile,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
scale: options.scale,
|
||||
});
|
||||
this._browserContext.emit(Events.BrowserContext.ScreencastStarted, new Screencast(options.outputFile, this._initializedPage!));
|
||||
} catch (e) {
|
||||
this._recordingVideoFile = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async stopVideoRecording(): Promise<void> {
|
||||
async stopScreencast(): Promise<void> {
|
||||
if (!this._recordingVideoFile)
|
||||
throw new Error('No video recording in progress');
|
||||
const fileName = this._recordingVideoFile;
|
||||
this._recordingVideoFile = null;
|
||||
await this._pageProxySession.send('Screencast.stopVideoRecording');
|
||||
this._browserContext.emit(Events.BrowserContext.ScreencastStopped, new Screencast(fileName, this._initializedPage!));
|
||||
}
|
||||
|
||||
async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ if (browserName !== 'chromium') {
|
|||
if (browserName === 'webkit')
|
||||
api.delete('browserContext.clearPermissions');
|
||||
|
||||
// Screencast APIs that are not publicly available.
|
||||
api.delete('browserContext.emit("screencaststarted")');
|
||||
api.delete('browserContext.emit("screencaststopped")');
|
||||
|
||||
const coverageDir = path.join(__dirname, '..', 'coverage-report', 'coverage');
|
||||
|
||||
const coveredMethods = new Set();
|
||||
|
|
|
|||
|
|
@ -179,13 +179,13 @@ it.fail(options.CHROMIUM)('should capture static page', async({page, tmpDir, vid
|
|||
return;
|
||||
const videoFile = path.join(tmpDir, 'v.webm');
|
||||
await page.evaluate(() => document.body.style.backgroundColor = 'red');
|
||||
await toImpl(page)._delegate.startVideoRecording({outputFile: videoFile, width: 640, height: 480});
|
||||
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
|
||||
// TODO: in WebKit figure out why video size is not reported correctly for
|
||||
// static pictures.
|
||||
if (HEADLESS && options.WEBKIT)
|
||||
await page.setViewportSize({width: 1270, height: 950});
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
await toImpl(page)._delegate.stopVideoRecording();
|
||||
await toImpl(page)._delegate.stopScreencast();
|
||||
expect(fs.existsSync(videoFile)).toBe(true);
|
||||
|
||||
await videoPlayer.load(videoFile);
|
||||
|
|
@ -205,7 +205,7 @@ it.fail(options.CHROMIUM)('should capture navigation', async({page, tmpDir, serv
|
|||
return;
|
||||
const videoFile = path.join(tmpDir, 'v.webm');
|
||||
await page.goto(server.PREFIX + '/background-color.html#rgb(0,0,0)');
|
||||
await toImpl(page)._delegate.startVideoRecording({outputFile: videoFile, width: 640, height: 480});
|
||||
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
|
||||
// TODO: in WebKit figure out why video size is not reported correctly for
|
||||
// static pictures.
|
||||
if (HEADLESS && options.WEBKIT)
|
||||
|
|
@ -213,7 +213,7 @@ it.fail(options.CHROMIUM)('should capture navigation', async({page, tmpDir, serv
|
|||
await new Promise(r => setTimeout(r, 300));
|
||||
await page.goto(server.CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
await toImpl(page)._delegate.stopVideoRecording();
|
||||
await toImpl(page)._delegate.stopScreencast();
|
||||
expect(fs.existsSync(videoFile)).toBe(true);
|
||||
|
||||
await videoPlayer.load(videoFile);
|
||||
|
|
@ -239,13 +239,13 @@ it.fail(options.CHROMIUM || (options.WEBKIT && WIN))('should capture css transfo
|
|||
return;
|
||||
const videoFile = path.join(tmpDir, 'v.webm');
|
||||
await page.goto(server.PREFIX + '/rotate-z.html');
|
||||
await toImpl(page)._delegate.startVideoRecording({outputFile: videoFile, width: 640, height: 480});
|
||||
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
|
||||
// TODO: in WebKit figure out why video size is not reported correctly for
|
||||
// static pictures.
|
||||
if (HEADLESS && options.WEBKIT)
|
||||
await page.setViewportSize({width: 1270, height: 950});
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
await toImpl(page)._delegate.stopVideoRecording();
|
||||
await toImpl(page)._delegate.stopScreencast();
|
||||
expect(fs.existsSync(videoFile)).toBe(true);
|
||||
|
||||
await videoPlayer.load(videoFile);
|
||||
|
|
@ -258,3 +258,26 @@ it.fail(options.CHROMIUM || (options.WEBKIT && WIN))('should capture css transfo
|
|||
expectAll(pixels, almostRed);
|
||||
}
|
||||
});
|
||||
|
||||
it.fail(options.CHROMIUM || options.FFOX)('should fire start/stop events when page created/closed', async({browser, tmpDir, server, toImpl}) => {
|
||||
if (!toImpl)
|
||||
return;
|
||||
// Use server side of the context. All the code below also uses server side APIs.
|
||||
const context = toImpl(await browser.newContext());
|
||||
context._enableScreencast({width: 640, height: 480, dir: tmpDir});
|
||||
expect(context._screencastOptions).toBeTruthy();
|
||||
|
||||
const [startEvent, newPage] = await Promise.all([
|
||||
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>,
|
||||
context.newPage(),
|
||||
]);
|
||||
expect(startEvent.page === newPage).toBe(true);
|
||||
expect(startEvent.path).toBeTruthy();
|
||||
|
||||
const [stopEvent] = await Promise.all([
|
||||
new Promise(resolve => context.on('screencaststopped', resolve)) as Promise<any>,
|
||||
newPage.close(),
|
||||
]);
|
||||
expect(stopEvent.page === newPage).toBe(true);
|
||||
await context.close();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue