playwright/test/screencast.spec.ts
2020-09-02 13:59:15 -07:00

360 lines
12 KiB
TypeScript

/**
* 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.
*/
import { options } from './playwright.fixtures';
import { registerFixture } from '@playwright/test-runner';
import type { Page } from '..';
import fs from 'fs';
import path from 'path';
import { TestServer } from '../utils/testserver';
declare global {
interface TestState {
videoPlayer: VideoPlayer;
}
}
registerFixture('videoPlayer', async ({playwright, context, server}, test) => {
let firefox;
if (options.WEBKIT && !LINUX) {
// WebKit on Mac & Windows cannot replay webm/vp8 video, so we launch Firefox.
firefox = await playwright.firefox.launch();
context = await firefox.newContext();
}
const page = await context.newPage();
const player = new VideoPlayer(page, server);
await test(player);
if (firefox)
await firefox.close();
else
await page.close();
});
function almostRed(r, g, b, alpha) {
expect(r).toBeGreaterThan(240);
expect(g).toBeLessThan(50);
expect(b).toBeLessThan(50);
expect(alpha).toBe(255);
}
function almostBlack(r, g, b, alpha) {
expect(r).toBeLessThan(10);
expect(g).toBeLessThan(10);
expect(b).toBeLessThan(10);
expect(alpha).toBe(255);
}
function almostGrey(r, g, b, alpha) {
expect(r).toBeGreaterThanOrEqual(90);
expect(g).toBeGreaterThanOrEqual(90);
expect(b).toBeGreaterThanOrEqual(90);
expect(r).toBeLessThan(110);
expect(g).toBeLessThan(110);
expect(b).toBeLessThan(110);
expect(alpha).toBe(255);
}
function expectAll(pixels, rgbaPredicate) {
const checkPixel = i => {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const alpha = pixels[i + 3];
rgbaPredicate(r, g, b, alpha);
};
try {
for (let i = 0, n = pixels.length; i < n; i += 4)
checkPixel(i);
} catch (e) {
// Log pixel values on failure.
e.message += `\n\nActual pixels=[${pixels}]`;
throw e;
}
}
class VideoPlayer {
private readonly _page: Page;
private readonly _server: TestServer;
constructor(page: Page, server: TestServer) {
this._page = page;
this._server = server;
}
async load(videoFile: string) {
const servertPath = '/v.webm';
this._server.setRoute(servertPath, (req, response) => {
this._server.serveFile(req, response, videoFile);
});
await this._page.goto(this._server.PREFIX + '/player.html');
}
async duration() {
return await this._page.$eval('video', (v: HTMLVideoElement) => v.duration);
}
async videoWidth() {
return await this._page.$eval('video', (v: HTMLVideoElement) => v.videoWidth);
}
async videoHeight() {
return await this._page.$eval('video', (v: HTMLVideoElement) => v.videoHeight);
}
async seekFirstNonEmptyFrame() {
await this._page.evaluate(async () => await (window as any).playToTheEnd());
while (true) {
await this._page.evaluate(async () => await (window as any).playOneFrame());
const ended = await this._page.$eval('video', (video: HTMLVideoElement) => video.ended);
if (ended)
throw new Error('All frames are empty');
const pixels = await this.pixels();
if (!pixels.every(p => p === 255))
return;
}
}
async countFrames() {
return await this._page.evaluate(async () => await (window as any).countFrames());
}
async currentTime() {
return await this._page.$eval('video', (v: HTMLVideoElement) => v.currentTime);
}
async playOneFrame() {
return await this._page.evaluate(async () => await (window as any).playOneFrame());
}
async seekLastFrame() {
const isWPE = LINUX && options.WEBKIT && options.HEADLESS;
return await this._page.evaluate(async x => await (window as any).seekLastFrame(x), isWPE);
}
async pixels(point = {x: 0, y: 0}) {
const pixels = await this._page.$eval('video', (video: HTMLVideoElement, point) => {
const canvas = document.createElement('canvas');
if (!video.videoWidth || !video.videoHeight)
throw new Error('Video element is empty');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0);
const imgd = context.getImageData(point.x, point.y, 10, 10);
return Array.from(imgd.data);
}, point);
return pixels;
}
}
describe('screencast', suite => {
suite.slow();
suite.skip(options.WIRE);
}, () => {
it('should capture static page', test => {
test.flaky(options.CHROMIUM && LINUX && !options.HEADLESS);
test.flaky(options.WEBKIT && LINUX);
}, async ({page, tmpDir, videoPlayer, toImpl}) => {
const videoFile = path.join(tmpDir, 'v.webm');
await page.evaluate(() => document.body.style.backgroundColor = 'red');
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 (options.HEADLESS && options.WEBKIT)
await page.setViewportSize({width: 1270, height: 950});
await new Promise(r => setTimeout(r, 300));
await toImpl(page)._delegate.stopScreencast();
expect(fs.existsSync(videoFile)).toBe(true);
await videoPlayer.load(videoFile);
const duration = await videoPlayer.duration();
expect(duration).toBeGreaterThan(0);
expect(await videoPlayer.videoWidth()).toBe(640);
expect(await videoPlayer.videoHeight()).toBe(480);
await videoPlayer.seekLastFrame();
const pixels = await videoPlayer.pixels();
expectAll(pixels, almostRed);
});
it('should capture navigation', test => {
test.flaky(options.CHROMIUM && MAC);
test.flaky(options.FIREFOX);
test.flaky(options.WEBKIT);
}, async ({page, tmpDir, server, videoPlayer, toImpl}) => {
const videoFile = path.join(tmpDir, 'v.webm');
await page.goto(server.PREFIX + '/background-color.html#rgb(0,0,0)');
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 (options.HEADLESS && options.WEBKIT)
await page.setViewportSize({width: 1270, height: 950});
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.stopScreencast();
expect(fs.existsSync(videoFile)).toBe(true);
await videoPlayer.load(videoFile);
const duration = await videoPlayer.duration();
expect(duration).toBeGreaterThan(0);
{
await videoPlayer.seekFirstNonEmptyFrame();
const pixels = await videoPlayer.pixels();
expectAll(pixels, almostBlack);
}
{
await videoPlayer.seekLastFrame();
const pixels = await videoPlayer.pixels();
expectAll(pixels, almostGrey);
}
});
it('should capture css transformation', test => {
test.fixme(options.WEBKIT && WIN, 'Accelerated compositing is disabled in WebKit on Windows.');
test.flaky(options.WEBKIT && LINUX);
}, async ({page, tmpDir, server, videoPlayer, toImpl}) => {
const videoFile = path.join(tmpDir, 'v.webm');
// Set viewport equal to screencast frame size to avoid scaling.
await page.setViewportSize({width: 640, height: 480});
await page.goto(server.PREFIX + '/rotate-z.html');
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
// 300 is not enough for Chromium headful.
await new Promise(r => setTimeout(r, 500));
await toImpl(page)._delegate.stopScreencast();
expect(fs.existsSync(videoFile)).toBe(true);
await videoPlayer.load(videoFile);
const duration = await videoPlayer.duration();
expect(duration).toBeGreaterThan(0);
{
await videoPlayer.seekLastFrame();
const pixels = await videoPlayer.pixels({x: 95, y: 45});
expectAll(pixels, almostRed);
}
});
it('should sutomatically start/finish when new page is created/closed', test => {
test.flaky(options.FIREFOX, 'Even slow is not slow enough');
}, async ({browser, tmpDir, toImpl}) => {
// Use server side of the context. All the code below also uses server side APIs.
const context = toImpl(await browser.newContext());
await context._enableScreencast({width: 320, height: 240, dir: tmpDir});
expect(context._screencastOptions).toBeTruthy();
const [screencast, newPage] = await Promise.all([
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>,
context.newPage(),
]);
expect(screencast.page === newPage).toBe(true);
const [videoFile] = await Promise.all([
screencast.path(),
newPage.close(),
]);
expect(path.dirname(videoFile)).toBe(tmpDir);
await context.close();
});
it('should finish when contex closes', async ({browser, tmpDir, toImpl}) => {
// Use server side of the context. All the code below also uses server side APIs.
const context = toImpl(await browser.newContext());
await context._enableScreencast({width: 320, height: 240, dir: tmpDir});
expect(context._screencastOptions).toBeTruthy();
const [screencast, newPage] = await Promise.all([
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>,
context.newPage(),
]);
expect(screencast.page === newPage).toBe(true);
const [videoFile] = await Promise.all([
screencast.path(),
context.close(),
]);
expect(path.dirname(videoFile)).toBe(tmpDir);
});
it('should fire start event for popups', async ({browser, tmpDir, server, toImpl}) => {
// Use server side of the context. All the code below also uses server side APIs.
const context = toImpl(await browser.newContext());
await context._enableScreencast({width: 640, height: 480, dir: tmpDir});
expect(context._screencastOptions).toBeTruthy();
const page = await context.newPage();
await page.mainFrame().goto(server.EMPTY_PAGE);
const [screencast, popup] = await Promise.all([
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>,
new Promise(resolve => context.on('page', resolve)) as Promise<any>,
page.mainFrame()._evaluateExpression(() => {
const win = window.open('about:blank');
win.close();
}, true)
]);
expect(screencast.page === popup).toBe(true);
expect(path.dirname(await screencast.path())).toBe(tmpDir);
await context.close();
});
it('should scale frames down to the requested size ', test => {
test.fixme(options.CHROMIUM && options.HEADLESS, 'Window is not resized when changing viewport');
}, async ({page, videoPlayer, tmpDir, server, toImpl}) => {
// Set size to 1/2 of the viewport.
await page.setViewportSize({width: 640, height: 480});
const videoFile = path.join(tmpDir, 'v.webm');
await page.goto(server.PREFIX + '/checkerboard.html');
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 320, height: 240});
// Update the picture to ensure enough frames are generated.
await page.$eval('.container', container => {
container.firstElementChild.classList.remove('red');
});
await new Promise(r => setTimeout(r, 300));
await page.$eval('.container', container => {
container.firstElementChild.classList.add('red');
});
await new Promise(r => setTimeout(r, 300));
await toImpl(page)._delegate.stopScreencast();
expect(fs.existsSync(videoFile)).toBe(true);
await videoPlayer.load(videoFile);
const duration = await videoPlayer.duration();
expect(duration).toBeGreaterThan(0);
await videoPlayer.seekLastFrame();
{
const pixels = await videoPlayer.pixels({x: 0, y: 0});
expectAll(pixels, almostRed);
}
{
const pixels = await videoPlayer.pixels({x: 300, y: 0});
expectAll(pixels, almostGrey);
}
{
const pixels = await videoPlayer.pixels({x: 0, y: 200});
expectAll(pixels, almostGrey);
}
{
const pixels = await videoPlayer.pixels({x: 300, y: 200});
expectAll(pixels, almostRed);
}
});
});