2020-08-11 07:22:57 +03:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2020-08-17 05:19:52 +03:00
|
|
|
|
2020-08-20 07:32:12 +03:00
|
|
|
import { options } from './playwright.fixtures';
|
2020-08-21 02:04:27 +03:00
|
|
|
import { registerFixture } from '../test-runner';
|
2020-08-27 00:16:35 +03:00
|
|
|
import type { Page } from '..';
|
2020-08-11 07:22:57 +03:00
|
|
|
|
2020-08-14 17:22:54 +03:00
|
|
|
import fs from 'fs';
|
|
|
|
import path from 'path';
|
|
|
|
import url from 'url';
|
2020-08-11 07:22:57 +03:00
|
|
|
|
2020-08-12 20:18:41 +03:00
|
|
|
|
2020-08-12 01:50:53 +03:00
|
|
|
declare global {
|
2020-08-20 22:51:05 +03:00
|
|
|
interface TestState {
|
2020-08-12 20:18:41 +03:00
|
|
|
videoPlayer: VideoPlayer;
|
2020-08-14 02:00:23 +03:00
|
|
|
}
|
2020-08-12 01:50:53 +03:00
|
|
|
}
|
2020-08-11 07:22:57 +03:00
|
|
|
|
2020-08-12 20:18:41 +03:00
|
|
|
registerFixture('videoPlayer', async ({playwright, context}, test) => {
|
|
|
|
let firefox;
|
2020-08-20 22:51:05 +03:00
|
|
|
if (options.WEBKIT && !LINUX) {
|
2020-08-12 20:18:41 +03:00
|
|
|
// WebKit on Mac & Windows cannot replay webm/vp8 video, so we launch Firefox.
|
|
|
|
firefox = await playwright.firefox.launch();
|
|
|
|
context = await firefox.newContext();
|
|
|
|
}
|
|
|
|
|
2020-08-13 03:51:07 +03:00
|
|
|
const page = await context.newPage();
|
|
|
|
const player = new VideoPlayer(page);
|
|
|
|
await test(player);
|
|
|
|
if (firefox)
|
|
|
|
await firefox.close();
|
|
|
|
else
|
|
|
|
await page.close();
|
2020-08-11 07:22:57 +03:00
|
|
|
});
|
|
|
|
|
2020-08-12 20:18:41 +03:00
|
|
|
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) {
|
2020-08-28 14:20:29 +03:00
|
|
|
const checkPixel = i => {
|
2020-08-12 20:18:41 +03:00
|
|
|
const r = pixels[i];
|
|
|
|
const g = pixels[i + 1];
|
|
|
|
const b = pixels[i + 2];
|
|
|
|
const alpha = pixels[i + 3];
|
|
|
|
rgbaPredicate(r, g, b, alpha);
|
2020-08-28 14:20:29 +03:00
|
|
|
};
|
2020-08-12 20:18:41 +03:00
|
|
|
try {
|
2020-08-28 14:20:29 +03:00
|
|
|
for (let i = 0, n = pixels.length; i < n; i += 4)
|
2020-08-12 20:18:41 +03:00
|
|
|
checkPixel(i);
|
2020-08-28 14:20:29 +03:00
|
|
|
} catch (e) {
|
2020-08-12 20:18:41 +03:00
|
|
|
// Log pixel values on failure.
|
2020-08-13 20:45:06 +03:00
|
|
|
e.message += `\n\nActual pixels=[${pixels}]`;
|
2020-08-12 20:18:41 +03:00
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class VideoPlayer {
|
|
|
|
private readonly _page: Page;
|
|
|
|
constructor(page: Page) {
|
|
|
|
this._page = page;
|
|
|
|
}
|
|
|
|
|
|
|
|
async load(videoFile) {
|
|
|
|
await this._page.goto(url.pathToFileURL(videoFile).href);
|
2020-08-28 14:20:29 +03:00
|
|
|
await this._page.$eval('video', (v: HTMLVideoElement) => {
|
2020-08-12 20:18:41 +03:00
|
|
|
return new Promise(fulfil => {
|
|
|
|
// In case video playback autostarts.
|
|
|
|
v.pause();
|
|
|
|
v.onplaying = fulfil;
|
|
|
|
v.play();
|
|
|
|
});
|
|
|
|
});
|
2020-08-28 14:20:29 +03:00
|
|
|
await this._page.$eval('video', (v: HTMLVideoElement) => {
|
2020-08-12 20:18:41 +03:00
|
|
|
v.pause();
|
|
|
|
const result = new Promise(f => v.onseeked = f);
|
|
|
|
v.currentTime = v.duration;
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async duration() {
|
2020-08-28 14:20:29 +03:00
|
|
|
return await this._page.$eval('video', (v: HTMLVideoElement) => v.duration);
|
2020-08-12 20:18:41 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async videoWidth() {
|
2020-08-28 14:20:29 +03:00
|
|
|
return await this._page.$eval('video', (v: HTMLVideoElement) => v.videoWidth);
|
2020-08-12 20:18:41 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async videoHeight() {
|
2020-08-28 14:20:29 +03:00
|
|
|
return await this._page.$eval('video', (v: HTMLVideoElement) => v.videoHeight);
|
2020-08-12 20:18:41 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async seek(timestamp) {
|
2020-08-28 14:20:29 +03:00
|
|
|
await this._page.$eval('video', (v: HTMLVideoElement, timestamp) => {
|
2020-08-12 20:18:41 +03:00
|
|
|
v.pause();
|
|
|
|
const result = new Promise(f => v.onseeked = f);
|
|
|
|
v.currentTime = timestamp;
|
|
|
|
return result;
|
|
|
|
}, timestamp);
|
|
|
|
}
|
|
|
|
|
2020-08-13 02:07:01 +03:00
|
|
|
async seekFirstNonEmptyFrame() {
|
|
|
|
let time = 0;
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
await this.seek(time);
|
|
|
|
const pixels = await this.pixels();
|
|
|
|
if (!pixels.every(p => p === 255))
|
|
|
|
return;
|
|
|
|
time += 0.1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-12 20:18:41 +03:00
|
|
|
async seekLastNonEmptyFrame() {
|
|
|
|
const duration = await this.duration();
|
|
|
|
let time = duration - 0.01;
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
await this.seek(time);
|
|
|
|
const pixels = await this.pixels();
|
|
|
|
if (!pixels.every(p => p === 0))
|
|
|
|
return;
|
|
|
|
time -= 0.1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-13 19:12:13 +03:00
|
|
|
async pixels(point = {x: 0, y: 0}) {
|
2020-08-28 14:20:29 +03:00
|
|
|
const pixels = await this._page.$eval('video', (video: HTMLVideoElement, point) => {
|
|
|
|
const canvas = document.createElement('canvas');
|
2020-08-12 20:18:41 +03:00
|
|
|
if (!video.videoWidth || !video.videoHeight)
|
2020-08-28 14:20:29 +03:00
|
|
|
throw new Error('Video element is empty');
|
2020-08-12 20:18:41 +03:00
|
|
|
canvas.width = video.videoWidth;
|
|
|
|
canvas.height = video.videoHeight;
|
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
context.drawImage(video, 0, 0);
|
2020-08-13 19:12:13 +03:00
|
|
|
const imgd = context.getImageData(point.x, point.y, 10, 10);
|
2020-08-12 20:18:41 +03:00
|
|
|
return Array.from(imgd.data);
|
2020-08-13 19:12:13 +03:00
|
|
|
}, point);
|
2020-08-12 20:18:41 +03:00
|
|
|
return pixels;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-28 20:03:52 +03:00
|
|
|
it.fixme(options.CHROMIUM).flaky(options.FIREFOX && MAC)('should capture static page', async ({page, tmpDir, videoPlayer, toImpl}) => {
|
2020-08-11 07:22:57 +03:00
|
|
|
if (!toImpl)
|
|
|
|
return;
|
2020-08-14 03:32:27 +03:00
|
|
|
const videoFile = path.join(tmpDir, 'v.webm');
|
2020-08-11 07:22:57 +03:00
|
|
|
await page.evaluate(() => document.body.style.backgroundColor = 'red');
|
2020-08-19 22:45:31 +03:00
|
|
|
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
|
2020-08-11 07:22:57 +03:00
|
|
|
// TODO: in WebKit figure out why video size is not reported correctly for
|
|
|
|
// static pictures.
|
2020-08-20 22:51:05 +03:00
|
|
|
if (options.HEADLESS && options.WEBKIT)
|
2020-08-11 07:22:57 +03:00
|
|
|
await page.setViewportSize({width: 1270, height: 950});
|
|
|
|
await new Promise(r => setTimeout(r, 300));
|
2020-08-19 22:45:31 +03:00
|
|
|
await toImpl(page)._delegate.stopScreencast();
|
2020-08-11 07:22:57 +03:00
|
|
|
expect(fs.existsSync(videoFile)).toBe(true);
|
|
|
|
|
2020-08-12 20:18:41 +03:00
|
|
|
await videoPlayer.load(videoFile);
|
|
|
|
const duration = await videoPlayer.duration();
|
|
|
|
expect(duration).toBeGreaterThan(0);
|
2020-08-11 07:22:57 +03:00
|
|
|
|
2020-08-12 20:18:41 +03:00
|
|
|
expect(await videoPlayer.videoWidth()).toBe(640);
|
|
|
|
expect(await videoPlayer.videoHeight()).toBe(480);
|
|
|
|
|
|
|
|
await videoPlayer.seekLastNonEmptyFrame();
|
|
|
|
const pixels = await videoPlayer.pixels();
|
|
|
|
expectAll(pixels, almostRed);
|
|
|
|
});
|
|
|
|
|
2020-08-28 14:20:29 +03:00
|
|
|
it.fixme(options.CHROMIUM).flaky(options.WEBKIT)('should capture navigation', async ({page, tmpDir, server, videoPlayer, toImpl}) => {
|
2020-08-12 20:18:41 +03:00
|
|
|
if (!toImpl)
|
|
|
|
return;
|
2020-08-14 03:32:27 +03:00
|
|
|
const videoFile = path.join(tmpDir, 'v.webm');
|
2020-08-12 20:18:41 +03:00
|
|
|
await page.goto(server.PREFIX + '/background-color.html#rgb(0,0,0)');
|
2020-08-19 22:45:31 +03:00
|
|
|
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
|
2020-08-12 20:18:41 +03:00
|
|
|
// TODO: in WebKit figure out why video size is not reported correctly for
|
|
|
|
// static pictures.
|
2020-08-20 22:51:05 +03:00
|
|
|
if (options.HEADLESS && options.WEBKIT)
|
2020-08-12 20:18:41 +03:00
|
|
|
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));
|
2020-08-19 22:45:31 +03:00
|
|
|
await toImpl(page)._delegate.stopScreencast();
|
2020-08-12 20:18:41 +03:00
|
|
|
expect(fs.existsSync(videoFile)).toBe(true);
|
|
|
|
|
|
|
|
await videoPlayer.load(videoFile);
|
|
|
|
const duration = await videoPlayer.duration();
|
2020-08-11 07:22:57 +03:00
|
|
|
expect(duration).toBeGreaterThan(0);
|
2020-08-12 20:18:41 +03:00
|
|
|
|
|
|
|
{
|
2020-08-13 02:07:01 +03:00
|
|
|
await videoPlayer.seekFirstNonEmptyFrame();
|
2020-08-12 20:18:41 +03:00
|
|
|
const pixels = await videoPlayer.pixels();
|
|
|
|
expectAll(pixels, almostBlack);
|
2020-08-11 07:22:57 +03:00
|
|
|
}
|
2020-08-12 20:18:41 +03:00
|
|
|
|
|
|
|
{
|
|
|
|
await videoPlayer.seekLastNonEmptyFrame();
|
|
|
|
const pixels = await videoPlayer.pixels();
|
|
|
|
expectAll(pixels, almostGrey);
|
2020-08-11 07:22:57 +03:00
|
|
|
}
|
|
|
|
});
|
2020-08-13 19:12:13 +03:00
|
|
|
|
2020-08-14 04:01:06 +03:00
|
|
|
// Accelerated compositing is disabled in WebKit on Windows.
|
2020-08-28 14:20:29 +03:00
|
|
|
it.fixme(options.CHROMIUM || (options.WEBKIT && WIN)).flaky(options.WEBKIT && LINUX)('should capture css transformation', async ({page, tmpDir, server, videoPlayer, toImpl}) => {
|
2020-08-13 19:12:13 +03:00
|
|
|
if (!toImpl)
|
|
|
|
return;
|
2020-08-14 03:32:27 +03:00
|
|
|
const videoFile = path.join(tmpDir, 'v.webm');
|
2020-08-13 19:12:13 +03:00
|
|
|
await page.goto(server.PREFIX + '/rotate-z.html');
|
2020-08-19 22:45:31 +03:00
|
|
|
await toImpl(page)._delegate.startScreencast({outputFile: videoFile, width: 640, height: 480});
|
2020-08-13 19:12:13 +03:00
|
|
|
// TODO: in WebKit figure out why video size is not reported correctly for
|
|
|
|
// static pictures.
|
2020-08-20 22:51:05 +03:00
|
|
|
if (options.HEADLESS && options.WEBKIT)
|
2020-08-13 19:12:13 +03:00
|
|
|
await page.setViewportSize({width: 1270, height: 950});
|
|
|
|
await new Promise(r => setTimeout(r, 300));
|
2020-08-19 22:45:31 +03:00
|
|
|
await toImpl(page)._delegate.stopScreencast();
|
2020-08-13 19:12:13 +03:00
|
|
|
expect(fs.existsSync(videoFile)).toBe(true);
|
|
|
|
|
|
|
|
await videoPlayer.load(videoFile);
|
|
|
|
const duration = await videoPlayer.duration();
|
|
|
|
expect(duration).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
{
|
|
|
|
await videoPlayer.seekLastNonEmptyFrame();
|
|
|
|
const pixels = await videoPlayer.pixels({x: 95, y: 45});
|
|
|
|
expectAll(pixels, almostRed);
|
|
|
|
}
|
|
|
|
});
|
2020-08-19 22:45:31 +03:00
|
|
|
|
2020-08-28 14:20:29 +03:00
|
|
|
it.slow().fixme(options.CHROMIUM)('should fire start/stop events when page created/closed', async ({browser, tmpDir, server, toImpl}) => {
|
2020-08-19 22:45:31 +03:00
|
|
|
if (!toImpl)
|
2020-08-28 14:20:29 +03:00
|
|
|
return;
|
2020-08-19 22:45:31 +03:00
|
|
|
// Use server side of the context. All the code below also uses server side APIs.
|
|
|
|
const context = toImpl(await browser.newContext());
|
2020-08-21 05:49:30 +03:00
|
|
|
await context._enableScreencast({width: 640, height: 480, dir: tmpDir});
|
2020-08-19 22:45:31 +03:00
|
|
|
expect(context._screencastOptions).toBeTruthy();
|
|
|
|
|
2020-08-25 23:07:32 +03:00
|
|
|
const [screencast, newPage] = await Promise.all([
|
2020-08-19 22:45:31 +03:00
|
|
|
new Promise(resolve => context.on('screencaststarted', resolve)) as Promise<any>,
|
|
|
|
context.newPage(),
|
|
|
|
]);
|
2020-08-25 23:07:32 +03:00
|
|
|
expect(screencast.page === newPage).toBe(true);
|
2020-08-19 22:45:31 +03:00
|
|
|
|
2020-08-25 23:07:32 +03:00
|
|
|
const [videoFile] = await Promise.all([
|
|
|
|
screencast.path(),
|
2020-08-19 22:45:31 +03:00
|
|
|
newPage.close(),
|
|
|
|
]);
|
2020-08-25 23:07:32 +03:00
|
|
|
expect(path.dirname(videoFile)).toBe(tmpDir);
|
2020-08-19 22:45:31 +03:00
|
|
|
await context.close();
|
|
|
|
});
|
2020-08-25 03:23:54 +03:00
|
|
|
|
2020-08-28 14:20:29 +03:00
|
|
|
it.fixme(options.CHROMIUM)('should fire start event for popups', async ({browser, tmpDir, server, toImpl}) => {
|
2020-08-25 03:23:54 +03:00
|
|
|
if (!toImpl)
|
2020-08-28 14:20:29 +03:00
|
|
|
return;
|
2020-08-25 03:23:54 +03:00
|
|
|
// 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);
|
2020-08-25 23:07:32 +03:00
|
|
|
const [screencast, popup] = await Promise.all([
|
2020-08-25 03:23:54 +03:00
|
|
|
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)
|
|
|
|
]);
|
2020-08-25 23:07:32 +03:00
|
|
|
expect(screencast.page === popup).toBe(true);
|
|
|
|
expect(path.dirname(await screencast.path())).toBe(tmpDir);
|
2020-08-25 03:23:54 +03:00
|
|
|
await context.close();
|
|
|
|
});
|