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. * limitations under the License.
*/ */
import * as fs from 'fs';
import { helper } from './helper'; import { helper } from './helper';
import * as network from './network'; import * as network from './network';
import * as path from 'path';
import { Page, PageBinding } from './page'; import { Page, PageBinding } from './page';
import { TimeoutSettings } from './timeoutSettings'; import { TimeoutSettings } from './timeoutSettings';
import * as frames from './frames'; import * as frames from './frames';
@ -28,10 +30,20 @@ import { EventEmitter } from 'events';
import { Progress } from './progress'; import { Progress } from './progress';
import { DebugController } from './debug/debugController'; 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 { export abstract class BrowserContext extends EventEmitter {
readonly _timeoutSettings = new TimeoutSettings(); readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>(); readonly _pageBindings = new Map<string, PageBinding>();
readonly _options: types.BrowserContextOptions; readonly _options: types.BrowserContextOptions;
_screencastOptions: types.ContextScreencastOptions | null = null;
_requestInterceptor?: network.RouteHandler; _requestInterceptor?: network.RouteHandler;
private _isPersistentContext: boolean; private _isPersistentContext: boolean;
private _closedStatus: 'open' | 'closing' | 'closed' = 'open'; private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
@ -142,6 +154,15 @@ export abstract class BrowserContext extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout); 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) { async _loadDefaultContext(progress: Progress) {
if (!this.pages().length) { if (!this.pages().length) {
const waitForEvent = helper.waitForEvent(progress, this, Events.BrowserContext.Page); 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 }); 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'); throw new Error('Not implemented');
} }
async stopVideoRecording(): Promise<void> { async stopScreencast(): Promise<void> {
throw new Error('Not implemented'); throw new Error('Not implemented');
} }

View file

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

View file

@ -346,7 +346,7 @@ export class FFPage implements PageDelegate {
throw new Error('Not implemented'); throw new Error('Not implemented');
} }
async startVideoRecording(options: types.VideoRecordingOptions): Promise<void> { async startScreencast(options: types.PageScreencastOptions): Promise<void> {
this._session.send('Page.startVideoRecording', { this._session.send('Page.startVideoRecording', {
file: options.outputFile, file: options.outputFile,
width: options.width, 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'); await this._session.send('Page.stopVideoRecording');
} }

View file

@ -57,8 +57,8 @@ export interface PageDelegate {
canScreenshotOutsideViewport(): boolean; canScreenshotOutsideViewport(): boolean;
resetViewport(): Promise<void>; // Only called if canScreenshotOutsideViewport() returns false. resetViewport(): Promise<void>; // Only called if canScreenshotOutsideViewport() returns false.
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>; setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
startVideoRecording(options: types.VideoRecordingOptions): Promise<void>; startScreencast(options: types.PageScreencastOptions): Promise<void>;
stopVideoRecording(): Promise<void>; stopScreencast(): Promise<void>;
takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer>; takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer>;
isElementHandle(remoteObject: any): boolean; isElementHandle(remoteObject: any): boolean;

View file

@ -60,13 +60,20 @@ export type ScreenshotOptions = ElementScreenshotOptions & {
clip?: Rect, clip?: Rect,
}; };
export type VideoRecordingOptions = { export type ScreencastOptions = {
outputFile: string,
width: number, width: number,
height: number, height: number,
scale?: 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 URLMatch = string | RegExp | ((url: URL) => boolean);
export type Credentials = { export type Credentials = {

View file

@ -21,6 +21,7 @@ import { Events } from '../events';
import { helper, RegisteredListener, assert } from '../helper'; import { helper, RegisteredListener, assert } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding } from '../page'; import { Page, PageBinding } from '../page';
import * as path from 'path';
import { ConnectionTransport } from '../transport'; import { ConnectionTransport } from '../transport';
import * as types from '../types'; import * as types from '../types';
import { Protocol } from './protocol'; 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 { pageProxyId } = await this._browser._browserSession.send('Playwright.createPage', { browserContextId: this._browserContextId });
const wkPage = this._browser._wkPages.get(pageProxyId)!; const wkPage = this._browser._wkPages.get(pageProxyId)!;
const result = await wkPage.pageOrError(); const result = await wkPage.pageOrError();
if (result instanceof Page) { if (!(result instanceof Page))
if (result.isClosed()) throw result;
throw new Error('Page has been closed.'); if (result.isClosed())
return result; 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[]> { async _doCookies(urls: string[]): Promise<types.NetworkCookie[]> {

View file

@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Screencast } from '../browserContext';
import * as frames from '../frames'; import * as frames from '../frames';
import { helper, RegisteredListener, assert, debugAssert } from '../helper'; import { helper, RegisteredListener, assert, debugAssert } from '../helper';
import * as dom from '../dom'; 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, // Holds window features for the next popup being opened via window.open,
// until the popup page proxy arrives. // until the popup page proxy arrives.
private _nextWindowOpenPopupFeatures?: string[]; private _nextWindowOpenPopupFeatures?: string[];
private _recordingVideoFile: string | null = null;
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) { constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) {
this._pageProxySession = pageProxySession; this._pageProxySession = pageProxySession;
@ -689,7 +691,9 @@ export class WKPage implements PageDelegate {
} }
async closePage(runBeforeUnload: boolean): Promise<void> { 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, targetId: this._session.sessionId,
runBeforeUnload runBeforeUnload
}); });
@ -703,17 +707,31 @@ export class WKPage implements PageDelegate {
await this._session.send('Page.setDefaultBackgroundColorOverride', { color }); await this._session.send('Page.setDefaultBackgroundColorOverride', { color });
} }
async startVideoRecording(options: types.VideoRecordingOptions): Promise<void> { async startScreencast(options: types.PageScreencastOptions): Promise<void> {
this._pageProxySession.send('Screencast.startVideoRecording', { if (this._recordingVideoFile)
file: options.outputFile, throw new Error('Already recording');
width: options.width, this._recordingVideoFile = options.outputFile;
height: options.height, try {
scale: options.scale, 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'); 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> { 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') if (browserName === 'webkit')
api.delete('browserContext.clearPermissions'); 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 coverageDir = path.join(__dirname, '..', 'coverage-report', 'coverage');
const coveredMethods = new Set(); const coveredMethods = new Set();

View file

@ -179,13 +179,13 @@ it.fail(options.CHROMIUM)('should capture static page', async({page, tmpDir, vid
return; return;
const videoFile = path.join(tmpDir, 'v.webm'); const videoFile = path.join(tmpDir, 'v.webm');
await page.evaluate(() => document.body.style.backgroundColor = 'red'); 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 // TODO: in WebKit figure out why video size is not reported correctly for
// static pictures. // static pictures.
if (HEADLESS && options.WEBKIT) if (HEADLESS && options.WEBKIT)
await page.setViewportSize({width: 1270, height: 950}); await page.setViewportSize({width: 1270, height: 950});
await new Promise(r => setTimeout(r, 300)); await new Promise(r => setTimeout(r, 300));
await toImpl(page)._delegate.stopVideoRecording(); await toImpl(page)._delegate.stopScreencast();
expect(fs.existsSync(videoFile)).toBe(true); expect(fs.existsSync(videoFile)).toBe(true);
await videoPlayer.load(videoFile); await videoPlayer.load(videoFile);
@ -205,7 +205,7 @@ it.fail(options.CHROMIUM)('should capture navigation', async({page, tmpDir, serv
return; return;
const videoFile = path.join(tmpDir, 'v.webm'); const videoFile = path.join(tmpDir, 'v.webm');
await page.goto(server.PREFIX + '/background-color.html#rgb(0,0,0)'); 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 // TODO: in WebKit figure out why video size is not reported correctly for
// static pictures. // static pictures.
if (HEADLESS && options.WEBKIT) 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 new Promise(r => setTimeout(r, 300));
await page.goto(server.CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)'); await page.goto(server.CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)');
await new Promise(r => setTimeout(r, 300)); await new Promise(r => setTimeout(r, 300));
await toImpl(page)._delegate.stopVideoRecording(); await toImpl(page)._delegate.stopScreencast();
expect(fs.existsSync(videoFile)).toBe(true); expect(fs.existsSync(videoFile)).toBe(true);
await videoPlayer.load(videoFile); await videoPlayer.load(videoFile);
@ -239,13 +239,13 @@ it.fail(options.CHROMIUM || (options.WEBKIT && WIN))('should capture css transfo
return; return;
const videoFile = path.join(tmpDir, 'v.webm'); const videoFile = path.join(tmpDir, 'v.webm');
await page.goto(server.PREFIX + '/rotate-z.html'); 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 // TODO: in WebKit figure out why video size is not reported correctly for
// static pictures. // static pictures.
if (HEADLESS && options.WEBKIT) if (HEADLESS && options.WEBKIT)
await page.setViewportSize({width: 1270, height: 950}); await page.setViewportSize({width: 1270, height: 950});
await new Promise(r => setTimeout(r, 300)); await new Promise(r => setTimeout(r, 300));
await toImpl(page)._delegate.stopVideoRecording(); await toImpl(page)._delegate.stopScreencast();
expect(fs.existsSync(videoFile)).toBe(true); expect(fs.existsSync(videoFile)).toBe(true);
await videoPlayer.load(videoFile); await videoPlayer.load(videoFile);
@ -258,3 +258,26 @@ it.fail(options.CHROMIUM || (options.WEBKIT && WIN))('should capture css transfo
expectAll(pixels, almostRed); 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();
});