feat(screencast): add start/stop events on context (#3483)

This commit is contained in:
Yury Semikhatsky 2020-08-19 12:45:31 -07:00 committed by GitHub
parent 73cd6ecef3
commit 83de0071c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 109 additions and 28 deletions

View file

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

View file

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

View file

@ -23,6 +23,8 @@ export const Events = {
BrowserContext: {
Close: 'close',
Page: 'page',
ScreencastStarted: 'screencaststarted',
ScreencastStopped: 'screencaststopped',
},
BrowserServer: {

View file

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

View file

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

View file

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

View file

@ -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[]> {

View file

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

View file

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

View file

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