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.
|
* 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);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ export const Events = {
|
||||||
BrowserContext: {
|
BrowserContext: {
|
||||||
Close: 'close',
|
Close: 'close',
|
||||||
Page: 'page',
|
Page: 'page',
|
||||||
|
ScreencastStarted: 'screencaststarted',
|
||||||
|
ScreencastStopped: 'screencaststopped',
|
||||||
},
|
},
|
||||||
|
|
||||||
BrowserServer: {
|
BrowserServer: {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
11
src/types.ts
11
src/types.ts
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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[]> {
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue