diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 3774258f3c..b99f011403 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -208,6 +208,14 @@ export class CRPage implements PageDelegate { await this._mainFrameSession._client.send('Emulation.setDefaultBackgroundColorOverride', { color }); } + async startVideoRecording(options: types.VideoRecordingOptions): Promise { + throw new Error('Not implemented'); + } + + async stopVideoRecording(): Promise { + throw new Error('Not implemented'); + } + async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { const { visualViewport } = await this._mainFrameSession._client.send('Page.getLayoutMetrics'); if (!documentRect) { diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 142717a6d1..fd03f8cabe 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -347,6 +347,19 @@ export class FFPage implements PageDelegate { throw new Error('Not implemented'); } + async startVideoRecording(options: types.VideoRecordingOptions): Promise { + this._session.send('Page.startVideoRecording', { + file: options.outputFile, + width: options.width, + height: options.height, + scale: options.scale, + }); + } + + async stopVideoRecording(): Promise { + await this._session.send('Page.stopVideoRecording'); + } + async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { if (!documentRect) { const context = await this._page.mainFrame()._utilityContext(); diff --git a/src/page.ts b/src/page.ts index 0c74873014..350a5eb897 100644 --- a/src/page.ts +++ b/src/page.ts @@ -58,6 +58,8 @@ export interface PageDelegate { canScreenshotOutsideViewport(): boolean; resetViewport(): Promise; // Only called if canScreenshotOutsideViewport() returns false. setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise; + startVideoRecording(options: types.VideoRecordingOptions): Promise; + stopVideoRecording(): Promise; takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise; isElementHandle(remoteObject: any): boolean; diff --git a/src/types.ts b/src/types.ts index e973520884..512780425f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,6 +61,13 @@ export type ScreenshotOptions = ElementScreenshotOptions & { clip?: Rect, }; +export type VideoRecordingOptions = { + outputFile: string, + width: number, + height: number, + scale?: number, +}; + export type URLMatch = string | RegExp | ((url: URL) => boolean); export type Credentials = { diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 7cecbe8d09..780807c2a5 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -704,6 +704,19 @@ export class WKPage implements PageDelegate { await this._session.send('Page.setDefaultBackgroundColorOverride', { color }); } + async startVideoRecording(options: types.VideoRecordingOptions): Promise { + this._pageProxySession.send('Screencast.startVideoRecording', { + file: options.outputFile, + width: options.width, + height: options.height, + scale: options.scale, + }); + } + + async stopVideoRecording(): Promise { + await this._pageProxySession.send('Screencast.stopVideoRecording'); + } + async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { const rect = (documentRect || viewportRect)!; const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport' }); diff --git a/test/screencast.spec.js b/test/screencast.spec.js new file mode 100644 index 0000000000..e4a58d4bde --- /dev/null +++ b/test/screencast.spec.js @@ -0,0 +1,117 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * 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. + */ + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const url = require('url'); +const {mkdtempAsync, removeFolderAsync} = require('./utils'); + +const {FFOX, CHROMIUM, WEBKIT, MAC, LINUX, WIN, HEADLESS, USES_HOOKS} = testOptions; + +registerFixture('persistentDirectory', async ({}, test) => { + const persistentDirectory = await mkdtempAsync(path.join(os.tmpdir(), 'playwright-test-')); + try { + await test(persistentDirectory); + } finally { + await removeFolderAsync(persistentDirectory); + } +}); + +registerFixture('firefox', async ({playwright}, test) => { + if (WEBKIT && !LINUX) { + const firefox = await playwright.firefox.launch(); + try { + await test(firefox); + } finally { + await firefox.close(); + } + } else { + await test(null); + } +}); + +it.fail(CHROMIUM)('should capture static page', async({page, persistentDirectory, firefox, toImpl}) => { + if (!toImpl) + return; + const videoFile = path.join(persistentDirectory, 'v.webm'); + await page.evaluate(() => document.body.style.backgroundColor = 'red'); + await toImpl(page)._delegate.startVideoRecording({outputFile: videoFile, width: 640, height: 480}); + // TODO: in WebKit figure out why video size is not reported correctly for + // static pictures. + if (HEADLESS && WEBKIT) + await page.setViewportSize({width: 1270, height: 950}); + await new Promise(r => setTimeout(r, 300)); + await toImpl(page)._delegate.stopVideoRecording(); + expect(fs.existsSync(videoFile)).toBe(true); + + if (WEBKIT && !LINUX) { + // WebKit on Mac & Windows cannot replay webm/vp8 video, so we launch Firefox. + const context = await firefox.newContext(); + page = await context.newPage(); + } + + await page.goto(url.pathToFileURL(videoFile).href); + await page.$eval('video', v => { + return new Promise(fulfil => { + // In case video playback autostarts. + v.pause(); + v.onplaying = fulfil; + v.play(); + }); + }); + await page.$eval('video', v => { + v.pause(); + const result = new Promise(f => v.onseeked = f); + v.currentTime = v.duration - 0.01; + return result; + }); + + const duration = await page.$eval('video', v => v.duration); + expect(duration).toBeGreaterThan(0); + const videoWidth = await page.$eval('video', v => v.videoWidth); + expect(videoWidth).toBe(640); + const videoHeight = await page.$eval('video', v => v.videoHeight); + expect(videoHeight).toBe(480); + + const pixels = await page.$eval('video', video => { + let canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const context = canvas.getContext('2d'); + context.drawImage(video, 0, 0); + const imgd = context.getImageData(0, 0, 10, 10); + return Array.from(imgd.data); + }); + const expectAlmostRed = (i) => { + const r = pixels[i]; + const g = pixels[i + 1]; + const b = pixels[i + 2]; + const alpha = pixels[i + 3]; + expect(r).toBeGreaterThan(240); + expect(g).toBeLessThan(50); + expect(b).toBeLessThan(50); + expect(alpha).toBe(255); + } + try { + for (var i = 0, n = pixels.length; i < n; i += 4) + expectAlmostRed(i); + } catch(e) { + // Log pixel values on failure. + console.log(pixels); + throw e; + } +});