api(video): simplify video api (#3924)

- This leaves just `recordVideos` and `videoSize` options on the context.
- Videos are saved to `artifactsPath`. We also save their ids to trace.
- `context.close()` waits for the processed videos.
This commit is contained in:
Dmitry Gozman 2020-09-18 17:36:43 -07:00 committed by GitHub
parent 4e2d75d9f7
commit df777344a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 246 additions and 500 deletions

View File

@ -221,8 +221,8 @@ Indicates that the browser is connected.
- `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
- `logger` <[Logger]> Logger sink for Playwright logging.
- `relativeArtifactsPath` <[string]> Specifies a folder for artifacts like downloads, videos and traces, relative to `artifactsPath` from [`browserType.launch`](#browsertypelaunchoptions). Defaults to `.`.
- `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for new pages.
- `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `recordVideos` <[boolean]> Enables video recording for all pages to the `relativeArtifactsPath` folder.
- `videoSize` <[Object]> Specifies dimensions of the automatically recorded video. Can only be used if `recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `width` <[number]> Video frame width.
- `height` <[number]> Video frame height.
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
@ -269,8 +269,8 @@ Creates a new browser context. It won't share cookies/cache with other browser c
- `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
- `logger` <[Logger]> Logger sink for Playwright logging.
- `relativeArtifactsPath` <[string]> Specifies a folder for artifacts like downloads, videos and traces, relative to `artifactsPath` from [`browserType.launch`](#browsertypelaunchoptions). Defaults to `.`.
- `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for the new page.
- `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `recordVideos` <[boolean]> Enables video recording for all pages to the `relativeArtifactsPath` folder.
- `videoSize` <[Object]> Specifies dimensions of the automatically recorded video. Can only be used if `recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `width` <[number]> Video frame width.
- `height` <[number]> Video frame height.
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
@ -701,7 +701,6 @@ page.removeListener('request', logRequest);
```
<!-- GEN:toc -->
- [event: '_videostarted'](#event-_videostarted)
- [event: 'close'](#event-close-1)
- [event: 'console'](#event-console)
- [event: 'crash'](#event-crash)
@ -788,35 +787,6 @@ page.removeListener('request', logRequest);
- [page.workers()](#pageworkers)
<!-- GEN:stop -->
#### event: '_videostarted'
- <[Object]> Video object. Provides access to the video after it has been written to a file.
**experimental**
Emitted when video recording has started for this page. The event will fire only if [`_recordVideos`](#browsernewcontextoptions) option is configured on the parent context.
An example of recording a video for single page.
```js
const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
(async () => {
const browser = await webkit.launch({
_videosPath: __dirname // Save videos to custom directory
});
const context = await browser.newContext({
_recordVideos: true,
_videoSize: { width: 640, height: 360 }
});
const page = await context.newPage();
const video = await page.waitForEvent('_videostarted');
await page.goto('https://github.com/microsoft/playwright');
// Video recording will stop automaticall when the page closes.
await page.close();
// Wait for the path to the video. It will become available
// after the video has been completely written to the the file.
console.log('Recorded video: ' + await video.path());
})();
```
#### event: 'close'
Emitted when the page closes.
@ -4205,7 +4175,6 @@ This methods attaches Playwright to an existing browser instance.
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
- `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `artifactsPath` <[string]> Specifies a folder for various artifacts like downloads, videos and traces. If not specified, artifacts are not collected.
- `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `chromiumSandbox` <[boolean]> Enable Chromium sandboxing. Defaults to `true`.
- `firefoxUserPrefs` <[Object]<[string], [string]|[number]|[boolean]>> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
@ -4282,9 +4251,8 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
- `password` <[string]>
- `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
- `relativeArtifactsPath` <[string]> Specifies a folder for artifacts like downloads, videos and traces, relative to `artifactsPath`. Defaults to `.`.
- `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for new pages.
- `_videoSize` <[Object]> **experimental** Specifies dimensions of the automatically recorded video. Can only be used if `_recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `recordVideos` <[boolean]> Enables video recording for all pages to the `relativeArtifactsPath` folder.
- `videoSize` <[Object]> Specifies dimensions of the automatically recorded video. Can only be used if `recordVideos` is true. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `width` <[number]> Video frame width.
- `height` <[number]> Video frame height.
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
@ -4306,7 +4274,6 @@ Launches browser that uses persistent storage located at `userDataDir` and retur
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
- `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `artifactsPath` <[string]> Specifies a folder for various artifacts like downloads, videos and traces. If not specified, artifacts are not collected.
- `_videosPath` <[string]> **experimental** If specified, recorded videos are saved into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
- `chromiumSandbox` <[boolean]> Enable Chromium sandboxing. Defaults to `true`.
- `firefoxUserPrefs` <[Object]<[string], [string]|[number]|[boolean]>> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.

View File

@ -34,22 +34,19 @@ const fs = require('fs');
for (const browserType of success) {
try {
const browser = await playwright[browserType].launch({
_videosPath: __dirname,
artifactsPath: __dirname,
});
const context = await browser.newContext({
_recordVideos: true,
_videoSize: {width: 320, height: 240},
recordVideos: true,
videoSize: {width: 320, height: 240},
});
const page = await context.newPage();
const video = await page.waitForEvent('_videostarted');
await context.newPage();
// Wait fo 1 second to actually record something.
await new Promise(x => setTimeout(x, 1000));
const [videoFile] = await Promise.all([
video.path(),
context.close(),
]);
await context.close();
await browser.close();
if (!fs.existsSync(videoFile)) {
const videoFile = fs.readdirSync(__dirname).find(name => name.endsWith('webm'));
if (!videoFile) {
console.error(`ERROR: Package "${requireName}", browser "${browserType}" should have created screencast!`);
process.exit(1);
}

View File

@ -40,7 +40,6 @@ import { WebKitBrowser } from './webkitBrowser';
import { FirefoxBrowser } from './firefoxBrowser';
import { debugLogger } from '../utils/debugLogger';
import { SelectorsOwner } from './selectors';
import { Video } from './video';
import { isUnderTest } from '../utils/utils';
class Root extends ChannelOwner<channels.Channel, {}> {
@ -221,9 +220,6 @@ export class Connection {
case 'Route':
result = new Route(parent, type, guid, initializer);
break;
case 'Video':
result = new Video(parent, type, guid, initializer);
break;
case 'Stream':
result = new Stream(parent, type, guid, initializer);
break;

View File

@ -50,7 +50,6 @@ export const Events = {
Load: 'load',
Popup: 'popup',
Worker: 'worker',
_VideoStarted: '_videostarted',
},
Worker: {

View File

@ -42,7 +42,6 @@ import * as util from 'util';
import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types';
import { evaluationScript, urlMatches } from './clientHelper';
import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils';
import { Video } from './video';
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
width?: string | number,
@ -123,7 +122,6 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this._channel.on('response', ({ response }) => this.emit(Events.Page.Response, Response.from(response)));
this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request)));
this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));
this._channel.on('videoStarted', params => this._onVideoStarted(params));
if (this._browserContext._browserName === 'chromium') {
this.coverage = new ChromiumCoverage(this._channel);
@ -177,10 +175,6 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this.emit(Events.Page.Worker, worker);
}
private _onVideoStarted(params: channels.PageVideoStartedEvent): void {
this.emit(Events.Page._VideoStarted, Video.from(params.video));
}
_onClose() {
this._closed = true;
this._browserContext._pages.delete(this);

View File

@ -83,7 +83,6 @@ export type LaunchServerOptions = {
},
downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean,
port?: number,
logger?: Logger,

View File

@ -1,70 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* 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 { Readable } from 'stream';
import * as channels from '../protocol/channels';
import * as fs from 'fs';
import { mkdirIfNeeded } from '../utils/utils';
import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { Stream } from './stream';
export class Video extends ChannelOwner<channels.VideoChannel, channels.VideoInitializer> {
private _browser: Browser | null;
static from(channel: channels.VideoChannel): Video {
return (channel as any)._object;
}
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.VideoInitializer) {
super(parent, type, guid, initializer);
this._browser = (parent as BrowserContext)._browser;
}
async path(): Promise<string> {
if (this._browser && this._browser._isRemote)
throw new Error(`Path is not available when using browserType.connect().`);
return (await this._channel.path()).value;
}
async saveAs(path: string): Promise<void> {
return this._wrapApiCall('video.saveAs', async () => {
if (!this._browser || !this._browser._isRemote) {
await this._channel.saveAs({ path });
return;
}
const stream = await this.createReadStream();
if (!stream)
throw new Error('Failed to copy video from server');
await mkdirIfNeeded(path);
await new Promise((resolve, reject) => {
stream.pipe(fs.createWriteStream(path))
.on('finish' as any, resolve)
.on('error' as any, reject);
});
});
}
async createReadStream(): Promise<Readable | null> {
const result = await this._channel.stream();
if (!result.stream)
return null;
const stream = Stream.from(result.stream);
return stream.stream();
}
}

View File

@ -30,7 +30,6 @@ import { serializeResult, parseArgument } from './jsHandleDispatcher';
import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
import { FileChooser } from '../server/fileChooser';
import { CRCoverage } from '../server/chromium/crCoverage';
import { VideoDispatcher } from './videoDispatcher';
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
private _page: Page;
@ -66,7 +65,6 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
}));
page.on(Page.Events.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) }));
page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) }));
page.on(Page.Events.VideoStarted, screencast => this._dispatchEvent('videoStarted', { video: new VideoDispatcher(this._scope, screencast) }));
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
}

View File

@ -1,47 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* 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 * as fs from 'fs';
import * as util from 'util';
import * as channels from '../protocol/channels';
import { Video } from '../server/browserContext';
import { mkdirIfNeeded } from '../utils/utils';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { StreamDispatcher } from './streamDispatcher';
export class VideoDispatcher extends Dispatcher<Video, channels.VideoInitializer> implements channels.VideoChannel {
constructor(scope: DispatcherScope, screencast: Video) {
super(scope, screencast, 'Video', {});
}
async path(): Promise<channels.VideoPathResult> {
return { value: await this._object.path() };
}
async saveAs(params: channels.VideoSaveAsParams): Promise<channels.VideoSaveAsResult> {
const fileName = await this._object.path();
await mkdirIfNeeded(params.path);
await util.promisify(fs.copyFile)(fileName, params.path);
}
async stream(): Promise<channels.VideoStreamResult> {
const fileName = await this._object.path();
const readable = fs.createReadStream(fileName);
await new Promise(f => readable.on('readable', f));
return { stream: new StreamDispatcher(this._scope, readable) };
}
}

View File

@ -169,7 +169,6 @@ export type BrowserTypeLaunchParams = {
},
downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string,
firefoxUserPrefs?: any,
chromiumSandbox?: boolean,
slowMo?: number,
@ -197,7 +196,6 @@ export type BrowserTypeLaunchOptions = {
},
downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string,
firefoxUserPrefs?: any,
chromiumSandbox?: boolean,
slowMo?: number,
@ -229,7 +227,6 @@ export type BrowserTypeLaunchPersistentContextParams = {
},
downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean,
slowMo?: number,
noDefaultViewport?: boolean,
@ -289,7 +286,6 @@ export type BrowserTypeLaunchPersistentContextOptions = {
},
downloadsPath?: string,
artifactsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean,
slowMo?: number,
noDefaultViewport?: boolean,
@ -381,8 +377,8 @@ export type BrowserNewContextParams = {
acceptDownloads?: boolean,
relativeArtifactsPath?: string,
recordTrace?: boolean,
_recordVideos?: boolean,
_videoSize?: {
recordVideos?: boolean,
videoSize?: {
width: number,
height: number,
},
@ -421,8 +417,8 @@ export type BrowserNewContextOptions = {
acceptDownloads?: boolean,
relativeArtifactsPath?: string,
recordTrace?: boolean,
_recordVideos?: boolean,
_videoSize?: {
recordVideos?: boolean,
videoSize?: {
width: number,
height: number,
},
@ -675,7 +671,6 @@ export interface PageChannel extends Channel {
on(event: 'requestFinished', callback: (params: PageRequestFinishedEvent) => void): this;
on(event: 'response', callback: (params: PageResponseEvent) => void): this;
on(event: 'route', callback: (params: PageRouteEvent) => void): this;
on(event: 'videoStarted', callback: (params: PageVideoStartedEvent) => void): this;
on(event: 'worker', callback: (params: PageWorkerEvent) => void): this;
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
@ -758,9 +753,6 @@ export type PageRouteEvent = {
route: RouteChannel,
request: RequestChannel,
};
export type PageVideoStartedEvent = {
video: VideoChannel,
};
export type PageWorkerEvent = {
worker: WorkerChannel,
};
@ -2154,31 +2146,6 @@ export type DialogDismissParams = {};
export type DialogDismissOptions = {};
export type DialogDismissResult = void;
// ----------- Video -----------
export type VideoInitializer = {};
export interface VideoChannel extends Channel {
path(params?: VideoPathParams, metadata?: Metadata): Promise<VideoPathResult>;
saveAs(params: VideoSaveAsParams, metadata?: Metadata): Promise<VideoSaveAsResult>;
stream(params?: VideoStreamParams, metadata?: Metadata): Promise<VideoStreamResult>;
}
export type VideoPathParams = {};
export type VideoPathOptions = {};
export type VideoPathResult = {
value: string,
};
export type VideoSaveAsParams = {
path: string,
};
export type VideoSaveAsOptions = {
};
export type VideoSaveAsResult = void;
export type VideoStreamParams = {};
export type VideoStreamOptions = {};
export type VideoStreamResult = {
stream?: StreamChannel,
};
// ----------- Download -----------
export type DownloadInitializer = {
url: string,

View File

@ -221,7 +221,6 @@ BrowserType:
password: string?
downloadsPath: string?
artifactsPath: string?
_videosPath: string?
firefoxUserPrefs: json?
chromiumSandbox: boolean?
slowMo: number?
@ -261,7 +260,6 @@ BrowserType:
password: string?
downloadsPath: string?
artifactsPath: string?
_videosPath: string?
chromiumSandbox: boolean?
slowMo: number?
noDefaultViewport: boolean?
@ -373,8 +371,8 @@ Browser:
acceptDownloads: boolean?
relativeArtifactsPath: string?
recordTrace: boolean?
_recordVideos: boolean?
_videoSize:
recordVideos: boolean?
videoSize:
type: object?
properties:
width: number
@ -914,10 +912,6 @@ Page:
route: Route
request: Request
videoStarted:
parameters:
video: Video
worker:
parameters:
worker: Worker
@ -1815,26 +1809,6 @@ Dialog:
Video:
type: interface
commands:
path:
returns:
value: string
# Blocks path until saved to the local |path|.
saveAs:
parameters:
path: string
stream:
returns:
stream: Stream?
Download:
type: interface

View File

@ -122,7 +122,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
})),
downloadsPath: tOptional(tString),
artifactsPath: tOptional(tString),
_videosPath: tOptional(tString),
firefoxUserPrefs: tOptional(tAny),
chromiumSandbox: tOptional(tBoolean),
slowMo: tOptional(tNumber),
@ -151,7 +150,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
})),
downloadsPath: tOptional(tString),
artifactsPath: tOptional(tString),
_videosPath: tOptional(tString),
chromiumSandbox: tOptional(tBoolean),
slowMo: tOptional(tNumber),
noDefaultViewport: tOptional(tBoolean),
@ -223,8 +221,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
acceptDownloads: tOptional(tBoolean),
relativeArtifactsPath: tOptional(tString),
recordTrace: tOptional(tBoolean),
_recordVideos: tOptional(tBoolean),
_videoSize: tOptional(tObject({
recordVideos: tOptional(tBoolean),
videoSize: tOptional(tObject({
width: tNumber,
height: tNumber,
})),
@ -821,11 +819,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
promptText: tOptional(tString),
});
scheme.DialogDismissParams = tOptional(tObject({}));
scheme.VideoPathParams = tOptional(tObject({}));
scheme.VideoSaveAsParams = tObject({
path: tString,
});
scheme.VideoStreamParams = tOptional(tObject({}));
scheme.DownloadPathParams = tOptional(tObject({}));
scheme.DownloadSaveAsParams = tObject({
path: tString,

View File

@ -21,7 +21,6 @@ import { EventEmitter } from 'events';
import { Download } from './download';
import { ProxySettings } from './types';
import { ChildProcess } from 'child_process';
import { makeWaitForNextTask } from '../utils/utils';
export interface BrowserProcess {
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
@ -34,7 +33,6 @@ export type BrowserOptions = types.UIOptions & {
name: string,
artifactsPath?: string,
downloadsPath?: string,
_videosPath?: string,
headful?: boolean,
persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
browserProcess: BrowserProcess,
@ -50,7 +48,7 @@ export abstract class Browser extends EventEmitter {
private _downloads = new Map<string, Download>();
_defaultContext: BrowserContext | null = null;
private _startedClosing = false;
private readonly _idToVideo = new Map<string, Video>();
readonly _idToVideo = new Map<string, Video>();
constructor(options: BrowserOptions) {
super();
@ -89,20 +87,19 @@ export abstract class Browser extends EventEmitter {
this._downloads.delete(uuid);
}
_videoStarted(videoId: string, file: string, pageOrError: Promise<Page | Error>) {
const video = new Video(file);
_videoStarted(context: BrowserContext, videoId: string, path: string, pageOrError: Promise<Page | Error>) {
const video = new Video(context, videoId, path);
this._idToVideo.set(videoId, video);
pageOrError.then(pageOrError => {
// Emit the event in another task to ensure that newPage response is handled before.
if (pageOrError instanceof Page)
makeWaitForNextTask()(() => pageOrError.emit(Page.Events.VideoStarted, video));
pageOrError.emit(Page.Events.VideoStarted, video);
});
}
_videoFinished(videoId: string) {
const video = this._idToVideo.get(videoId);
const video = this._idToVideo.get(videoId)!;
this._idToVideo.delete(videoId);
video!._finishCallback();
video._finishCallback();
}
_didClose() {

View File

@ -17,7 +17,8 @@
import { EventEmitter } from 'events';
import { TimeoutSettings } from '../utils/timeoutSettings';
import { Browser } from './browser';
import { mkdirIfNeeded } from '../utils/utils';
import { Browser, BrowserOptions } from './browser';
import * as dom from './dom';
import { Download } from './download';
import * as frames from './frames';
@ -30,17 +31,17 @@ import * as types from './types';
import * as path from 'path';
export class Video {
private readonly _path: string;
readonly _videoId: string;
readonly _path: string;
readonly _context: BrowserContext;
readonly _finishedPromise: Promise<void>;
_finishCallback: () => void = () => {};
private readonly _finishedPromise: Promise<void>;
constructor(path: string) {
this._path = path;
this._finishedPromise = new Promise(fulfill => this._finishCallback = fulfill);
}
async path(): Promise<string> {
await this._finishedPromise;
return this._path;
constructor(context: BrowserContext, videoId: string, path: string) {
this._videoId = videoId;
this._path = path;
this._context = context;
this._finishedPromise = new Promise(fulfill => this._finishCallback = fulfill);
}
}
@ -122,6 +123,11 @@ export abstract class BrowserContext extends EventEmitter {
await listener.onContextCreated(this);
}
async _ensureArtifactsPath() {
if (this._artifactsPath)
await mkdirIfNeeded(path.join(this._artifactsPath, 'dummy'));
}
_browserClosed() {
for (const page of this.pages())
page._didClose();
@ -262,7 +268,14 @@ export abstract class BrowserContext extends EventEmitter {
if (this._closedStatus === 'open') {
this._closedStatus = 'closing';
await this._doClose();
await Promise.all([...this._downloads].map(d => d.delete()));
const promises: Promise<any>[] = [];
for (const download of this._downloads)
promises.push(download.delete());
for (const video of this._browser._idToVideo.values()) {
if (video._context === this)
promises.push(video._finishedPromise);
}
await Promise.all(promises);
for (const listener of contextListeners)
await listener.onContextDestroyed(this);
this._didCloseInternal();
@ -278,7 +291,7 @@ export function assertBrowserContextIsNotOwned(context: BrowserContext) {
}
}
export function validateBrowserContextOptions(options: types.BrowserContextOptions) {
export function validateBrowserContextOptions(options: types.BrowserContextOptions, browserOptions: BrowserOptions) {
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
if (options.noDefaultViewport && options.isMobile !== undefined)
@ -286,6 +299,10 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio
if (!options.viewport && !options.noDefaultViewport)
options.viewport = { width: 1280, height: 720 };
verifyGeolocation(options.geolocation);
if (options.recordTrace && !browserOptions.artifactsPath)
throw new Error(`"recordTrace" option requires "artifactsPath" to be specified`);
if (options.recordVideos && !browserOptions.artifactsPath)
throw new Error(`"recordVideos" option requires "artifactsPath" to be specified`);
}
export function verifyGeolocation(geolocation?: types.Geolocation) {

View File

@ -34,7 +34,6 @@ const mkdirAsync = util.promisify(fs.mkdir);
const mkdtempAsync = util.promisify(fs.mkdtemp);
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
const VIDEOS_FOLDER = path.join(os.tmpdir(), 'playwright_videos-');
type WebSocketNotPipe = { webSocketRegex: RegExp, stream: 'stdout' | 'stderr' };
@ -77,7 +76,6 @@ export abstract class BrowserType {
async launchPersistentContext(userDataDir: string, options: types.LaunchPersistentOptions = {}): Promise<BrowserContext> {
options = validateLaunchOptions(options);
const persistent: types.BrowserContextOptions = options;
validateBrowserContextOptions(persistent);
const controller = new ProgressController();
controller.setLogName('browser');
const browser = await controller.run(progress => {
@ -88,7 +86,7 @@ export abstract class BrowserType {
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<Browser> {
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
const { browserProcess, downloadsPath, _videosPath, transport } = await this._launchProcess(progress, options, !!persistent, userDataDir);
const { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, userDataDir);
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browserOptions: BrowserOptions = {
@ -98,10 +96,11 @@ export abstract class BrowserType {
headful: !options.headless,
artifactsPath: options.artifactsPath,
downloadsPath,
_videosPath,
browserProcess,
proxy: options.proxy,
};
if (persistent)
validateBrowserContextOptions(persistent, browserOptions);
copyTestHooks(options, browserOptions);
const browser = await this._connectToTransport(transport, browserOptions);
// We assume no control when using custom arguments, and do not prepare the default context in that case.
@ -110,7 +109,7 @@ export abstract class BrowserType {
return browser;
}
private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, _videosPath: string, transport: ConnectionTransport }> {
private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, transport: ConnectionTransport }> {
const {
ignoreDefaultArgs,
ignoreAllDefaultArgs,
@ -135,9 +134,8 @@ export abstract class BrowserType {
}
return dir;
};
// TODO: use artifactsPath for downloads and videos.
// TODO: use artifactsPath for downloads.
const downloadsPath = await ensurePath(DOWNLOADS_FOLDER, options.downloadsPath);
const _videosPath = await ensurePath(VIDEOS_FOLDER, options._videosPath);
if (!userDataDir) {
userDataDir = await mkdtempAsync(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
@ -211,7 +209,7 @@ export abstract class BrowserType {
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
transport = new PipeTransport(stdio[3], stdio[4]);
}
return { browserProcess, downloadsPath, _videosPath, transport };
return { browserProcess, downloadsPath, transport };
}
abstract _defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[];

View File

@ -98,7 +98,7 @@ export class CRBrowser extends Browser {
}
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
validateBrowserContextOptions(options);
validateBrowserContextOptions(options, this._options);
const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true });
const context = new CRBrowserContext(this, browserContextId, options);
await context._initialize();

View File

@ -458,13 +458,15 @@ class FrameSession {
promises.push(this._evaluateOnNewDocument(source));
for (const source of this._crPage._page._evaluateOnNewDocumentSources)
promises.push(this._evaluateOnNewDocument(source));
if (this._crPage._browserContext._options._recordVideos) {
const size = this._crPage._browserContext._options._videoSize || this._crPage._browserContext._options.viewport || { width: 1280, height: 720 };
if (this._isMainFrame() && this._crPage._browserContext._options.recordVideos) {
const size = this._crPage._browserContext._options.videoSize || this._crPage._browserContext._options.viewport || { width: 1280, height: 720 };
const screencastId = createGuid();
const outputFile = path.join(this._crPage._browserContext._browser._options._videosPath!, screencastId + '.webm');
promises.push(this._startScreencast(screencastId, {
...size,
outputFile,
const outputFile = path.join(this._crPage._browserContext._artifactsPath!, screencastId + '.webm');
promises.push(this._crPage._browserContext._ensureArtifactsPath().then(() => {
return this._startScreencast(screencastId, {
...size,
outputFile,
});
}));
}
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
@ -764,7 +766,7 @@ class FrameSession {
this._screencastState = 'started';
this._videoRecorder = videoRecorder;
this._screencastId = screencastId;
this._crPage._browserContext._browser._videoStarted(screencastId, options.outputFile, this._crPage.pageOrError());
this._crPage._browserContext._browser._videoStarted(this._crPage._browserContext, screencastId, options.outputFile, this._crPage.pageOrError());
} catch (e) {
videoRecorder.stop().catch(() => {});
throw e;

View File

@ -99,7 +99,7 @@ export class FFBrowser extends Browser {
}
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
validateBrowserContextOptions(options);
validateBrowserContextOptions(options, this._options);
if (options.isMobile)
throw new Error('options.isMobile is not supported in Firefox');
const { browserContextId } = await this._connection.send('Browser.createBrowserContext', { removeOnDetach: true });
@ -229,13 +229,15 @@ export class FFBrowserContext extends BrowserContext {
promises.push(this.setOffline(this._options.offline));
if (this._options.colorScheme)
promises.push(this._browser._connection.send('Browser.setColorScheme', { browserContextId, colorScheme: this._options.colorScheme }));
if (this._options._recordVideos) {
const size = this._options._videoSize || this._options.viewport || { width: 1280, height: 720 };
await this._browser._connection.send('Browser.setScreencastOptions', {
...size,
dir: this._browser._options._videosPath!,
browserContextId: this._browserContextId
});
if (this._options.recordVideos) {
const size = this._options.videoSize || this._options.viewport || { width: 1280, height: 720 };
promises.push(this._ensureArtifactsPath().then(() => {
return this._browser._connection.send('Browser.setScreencastOptions', {
...size,
dir: this._artifactsPath!,
browserContextId: this._browserContextId
});
}));
}
await Promise.all(promises);

View File

@ -31,7 +31,6 @@ import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
import { FFNetworkManager } from './ffNetworkManager';
import { Protocol } from './protocol';
import { rewriteErrorMessage } from '../../utils/stackTrace';
import { Video } from '../browserContext';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -50,7 +49,6 @@ export class FFPage implements PageDelegate {
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
private _eventListeners: RegisteredListener[];
private _workers = new Map<string, { frameId: string, session: FFSession }>();
private readonly _idToScreencast = new Map<string, Video>();
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
this._session = session;
@ -258,7 +256,7 @@ export class FFPage implements PageDelegate {
}
_onScreencastStarted(event: Protocol.Page.screencastStartedPayload) {
this._browserContext._browser._videoStarted(event.screencastId, event.file, this.pageOrError());
this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this.pageOrError());
}
async exposeBinding(binding: PageBinding) {

View File

@ -238,8 +238,8 @@ export type BrowserContextOptions = {
hasTouch?: boolean,
colorScheme?: ColorScheme,
acceptDownloads?: boolean,
_recordVideos?: boolean,
_videoSize?: Size,
recordVideos?: boolean,
videoSize?: Size,
recordTrace?: boolean,
relativeArtifactsPath?: string,
};
@ -261,7 +261,6 @@ type LaunchOptionsBase = {
proxy?: ProxySettings,
artifactsPath?: string,
downloadsPath?: string,
_videosPath?: string,
chromiumSandbox?: boolean,
slowMo?: number,
};

View File

@ -74,7 +74,7 @@ export class WKBrowser extends Browser {
}
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
validateBrowserContextOptions(options);
validateBrowserContextOptions(options, this._options);
const { browserContextId } = await this._browserSession.send('Playwright.createContext');
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
const context = new WKBrowserContext(this, browserContextId, options);

View File

@ -113,12 +113,14 @@ export class WKPage implements PageDelegate {
for (const [key, value] of this._browserContext._permissions)
this._grantPermissions(key, value);
}
if (this._browserContext._options._recordVideos) {
const size = this._browserContext._options._videoSize || this._browserContext._options.viewport || { width: 1280, height: 720 };
const outputFile = path.join(this._browserContext._browser._options._videosPath!, createGuid() + '.webm');
promises.push(this.startScreencast({
...size,
outputFile,
if (this._browserContext._options.recordVideos) {
const size = this._browserContext._options.videoSize || this._browserContext._options.viewport || { width: 1280, height: 720 };
const outputFile = path.join(this._browserContext._artifactsPath!, createGuid() + '.webm');
promises.push(this._browserContext._ensureArtifactsPath().then(() => {
return this.startScreencast({
...size,
outputFile,
});
}));
}
await Promise.all(promises);
@ -723,7 +725,7 @@ export class WKPage implements PageDelegate {
width: options.width,
height: options.height,
}) as any;
this._browserContext._browser._videoStarted(screencastId, options.outputFile, this.pageOrError());
this._browserContext._browser._videoStarted(this._browserContext, screencastId, options.outputFile, this.pageOrError());
} catch (e) {
this._recordingVideoFile = null;
throw e;

View File

@ -115,7 +115,7 @@ export class Snapshotter {
return frameResult;
const frameSnapshot = {
frameId: frame._id,
url: frame.url(),
url: removeHash(frame.url()),
html: '<body>Snapshot is not available</body>',
resourceOverrides: [],
};
@ -190,7 +190,7 @@ export class Snapshotter {
const snapshot: FrameSnapshot = {
frameId: frame._id,
url: frame.url(),
url: removeHash(frame.url()),
html: data.html,
resourceOverrides: [],
};
@ -216,6 +216,16 @@ export class Snapshotter {
}
}
function removeHash(url: string) {
try {
const u = new URL(url);
u.hash = '';
return u.toString();
} catch (e) {
return url;
}
}
type FrameSnapshotAndMapping = {
snapshot: FrameSnapshot,
mapping: Map<Frame, string>,

View File

@ -51,6 +51,13 @@ export type PageDestroyedTraceEvent = {
pageId: string,
};
export type PageVideoTraceEvent = {
type: 'page-video',
contextId: string,
pageId: string,
fileName: string,
};
export type ActionTraceEvent = {
type: 'action',
contextId: string,
@ -75,6 +82,7 @@ export type TraceEvent =
ContextDestroyedTraceEvent |
PageCreatedTraceEvent |
PageDestroyedTraceEvent |
PageVideoTraceEvent |
NetworkResourceTraceEvent |
ActionTraceEvent;

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
import { ActionListener, ActionMetadata, BrowserContext, ContextListener, contextListeners } from '../server/browserContext';
import { ActionListener, ActionMetadata, BrowserContext, ContextListener, contextListeners, Video } from '../server/browserContext';
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes';
import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent, PageVideoTraceEvent } from './traceTypes';
import * as path from 'path';
import * as util from 'util';
import * as fs from 'fs';
@ -42,10 +42,8 @@ class Tracer implements ContextListener {
async onContextCreated(context: BrowserContext): Promise<void> {
if (!context._options.recordTrace)
return;
if (!context._artifactsPath)
throw new Error(`"recordTrace" option requires "artifactsPath" to be specified`);
const traceStorageDir = path.join(context._browser._options.artifactsPath!, '.playwright-shared');
const traceFile = path.join(context._artifactsPath, 'playwright.trace');
const traceFile = path.join(context._artifactsPath!, 'playwright.trace');
const contextTracer = new ContextTracer(context, traceStorageDir, traceFile);
this._contextTracers.set(context, contextTracer);
}
@ -147,6 +145,18 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
};
this._appendTraceEvent(event);
page.on(Page.Events.VideoStarted, (video: Video) => {
if (this._disposed)
return;
const event: PageVideoTraceEvent = {
type: 'page-video',
contextId: this._contextId,
pageId,
fileName: path.basename(video._path),
};
this._appendTraceEvent(event);
});
page.once(Page.Events.Close, () => {
this._pageToId.delete(page);
if (this._disposed)

View File

@ -279,6 +279,9 @@ defineTestFixture('context', async ({browser, testOutputDir}, runTest, info) =>
const contextOptions: BrowserContextOptions = {
relativeArtifactsPath: path.relative(config.outputDir, testOutputDir),
recordTrace: !!options.TRACING,
// TODO: enable videos. Currently, long videos are slowly processed by Chromium
// and (sometimes) Firefox, which causes test timeouts.
// recordVideos: !!options.TRACING,
};
const context = await browser.newContext(contextOptions);
await runTest(context);

View File

@ -15,59 +15,57 @@
*/
import { options, playwrightFixtures } from './playwright.fixtures';
import type { Page } from '..';
import type { Page, Browser } from '..';
import fs from 'fs';
import path from 'path';
import { TestServer } from '../utils/testserver';
import { mkdirIfNeeded } from '../lib/utils/utils';
type WorkerState = {
videoDir: string;
videoPlayerBrowser: Browser,
};
type TestState = {
videoPlayer: VideoPlayer;
videoFile: string;
relativeArtifactsPath: string;
videoDir: string;
};
const fixtures = playwrightFixtures.declareWorkerFixtures<WorkerState>().declareTestFixtures<TestState>();
const { it, expect, describe, defineTestFixture, defineWorkerFixture, overrideWorkerFixture } = fixtures;
defineWorkerFixture('videoDir', async ({}, test, config) => {
await test(path.join(config.outputDir, 'screencast'));
});
overrideWorkerFixture('browser', async ({browserType, defaultBrowserOptions, videoDir}, test) => {
overrideWorkerFixture('browser', async ({browserType, defaultBrowserOptions}, test, config) => {
const browser = await browserType.launch({
...defaultBrowserOptions,
// Make sure videos are stored on the same volume as the test output dir.
_videosPath: videoDir,
artifactsPath: path.join(config.outputDir, '.screencast'),
});
await test(browser);
await browser.close();
});
defineTestFixture('videoPlayer', async ({playwright, context, server}, test) => {
defineWorkerFixture('videoPlayerBrowser', async ({playwright}, runTest) => {
// WebKit on Mac & Windows cannot replay webm/vp8 video, is unrelyable
// on Linux (times out) and in Firefox, so we always launch chromium for
// playback.
const chromium = await playwright.chromium.launch();
context = await chromium.newContext();
const page = await context.newPage();
const player = new VideoPlayer(page, server);
await test(player);
if (chromium)
await chromium.close();
else
await page.close();
const browser = await playwright.chromium.launch();
await runTest(browser);
await browser.close();
});
defineTestFixture('videoFile', async ({browserType, videoDir}, runTest, info) => {
defineTestFixture('videoPlayer', async ({videoPlayerBrowser, server}, test) => {
const page = await videoPlayerBrowser.newPage();
await test(new VideoPlayer(page, server));
await page.close();
});
defineTestFixture('relativeArtifactsPath', async ({browserType}, runTest, info) => {
const { test } = info;
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
const videoFile = path.join(videoDir, `${browserType.name()}-${sanitizedTitle}-${test.results.length}_v.webm`);
await mkdirIfNeeded(videoFile);
await runTest(videoFile);
const relativeArtifactsPath = `${browserType.name()}-${sanitizedTitle}-${test.results.length}`;
await runTest(relativeArtifactsPath);
});
defineTestFixture('videoDir', async ({relativeArtifactsPath}, runTest, info) => {
await runTest(path.join(info.config.outputDir, '.screencast', relativeArtifactsPath));
});
function almostRed(r, g, b, alpha) {
@ -112,9 +110,20 @@ function expectAll(pixels, rgbaPredicate) {
}
}
async function findVideo(videoDir: string) {
const files = await fs.promises.readdir(videoDir);
return path.join(videoDir, files.find(file => file.endsWith('webm')));
}
async function findVideos(videoDir: string) {
const files = await fs.promises.readdir(videoDir);
return files.filter(file => file.endsWith('webm')).map(file => path.join(videoDir, file));
}
class VideoPlayer {
private readonly _page: Page;
private readonly _server: TestServer;
constructor(page: Page, server: TestServer) {
this._page = page;
this._server = server;
@ -189,19 +198,29 @@ class VideoPlayer {
describe('screencast', suite => {
suite.slow();
}, () => {
it('should capture static page', async ({browser, videoPlayer, videoFile}) => {
const context = await browser.newContext({ _recordVideos: true, _videoSize: { width: 320, height: 240 } });
it('should require artifactsPath', async ({browserType, defaultBrowserOptions}) => {
const browser = await browserType.launch({
...defaultBrowserOptions,
artifactsPath: undefined,
});
const error = await browser.newContext({ recordVideos: true }).catch(e => e);
expect(error.message).toContain('"recordVideos" option requires "artifactsPath" to be specified');
await browser.close();
});
it('should capture static page', async ({browser, videoPlayer, relativeArtifactsPath, videoDir}) => {
const context = await browser.newContext({
relativeArtifactsPath,
recordVideos: true,
videoSize: { width: 320, height: 240 }
});
const page = await context.newPage();
const video = await page.waitForEvent('_videostarted') as any;
await page.evaluate(() => document.body.style.backgroundColor = 'red');
await new Promise(r => setTimeout(r, 1000));
await page.close();
const tmpPath = await video.path();
expect(fs.existsSync(tmpPath)).toBe(true);
fs.renameSync(tmpPath, videoFile);
await context.close();
const videoFile = await findVideo(videoDir);
await videoPlayer.load(videoFile);
const duration = await videoPlayer.duration();
expect(duration).toBeGreaterThan(0);
@ -216,21 +235,21 @@ describe('screencast', suite => {
it('should capture navigation', (test, parameters) => {
test.flaky();
}, async ({browser, server, videoPlayer, videoFile}) => {
const context = await browser.newContext({ _recordVideos: true, _videoSize: { width: 1280, height: 720 } });
}, async ({browser, server, videoPlayer, relativeArtifactsPath, videoDir}) => {
const context = await browser.newContext({
relativeArtifactsPath,
recordVideos: true,
videoSize: { width: 1280, height: 720 }
});
const page = await context.newPage();
const video = await page.waitForEvent('_videostarted') as any;
await page.goto(server.PREFIX + '/background-color.html#rgb(0,0,0)');
await new Promise(r => setTimeout(r, 1000));
await page.goto(server.CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)');
await new Promise(r => setTimeout(r, 1000));
await page.close();
const tmpPath = await video.path();
expect(fs.existsSync(tmpPath)).toBe(true);
fs.renameSync(tmpPath, videoFile);
await context.close();
const videoFile = await findVideo(videoDir);
await videoPlayer.load(videoFile);
const duration = await videoPlayer.duration();
expect(duration).toBeGreaterThan(0);
@ -250,21 +269,22 @@ describe('screencast', suite => {
it('should capture css transformation', (test, parameters) => {
test.fail(options.WEBKIT(parameters) && options.WIN(parameters), 'Does not work on WebKit Windows');
}, async ({browser, server, videoPlayer, videoFile}) => {
}, async ({browser, server, videoPlayer, relativeArtifactsPath, videoDir}) => {
const size = {width: 320, height: 240};
// Set viewport equal to screencast frame size to avoid scaling.
const context = await browser.newContext({ _recordVideos: true, _videoSize: size, viewport: size });
const context = await browser.newContext({
relativeArtifactsPath,
recordVideos: true,
videoSize: size,
viewport: size,
});
const page = await context.newPage();
const video = await page.waitForEvent('_videostarted') as any;
await page.goto(server.PREFIX + '/rotate-z.html');
await new Promise(r => setTimeout(r, 1000));
await page.close();
const tmpPath = await video.path();
expect(fs.existsSync(tmpPath)).toBe(true);
fs.renameSync(tmpPath, videoFile);
await context.close();
const videoFile = await findVideo(videoDir);
await videoPlayer.load(videoFile);
const duration = await videoPlayer.duration();
expect(duration).toBeGreaterThan(0);
@ -276,73 +296,35 @@ describe('screencast', suite => {
}
});
it('should automatically start/finish when new page is created/closed', async ({browser, videoDir}) => {
const context = await browser.newContext({ _recordVideos: true, _videoSize: { width: 320, height: 240 }});
const [screencast, newPage] = await Promise.all([
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
context.newPage(),
]);
const [videoFile] = await Promise.all([
screencast.path(),
newPage.close(),
]);
expect(path.dirname(videoFile)).toBe(videoDir);
await context.close();
});
it('should finish when contex closes', async ({browser, videoDir}) => {
const context = await browser.newContext({ _recordVideos: true, _videoSize: { width: 320, height: 240 } });
const [video] = await Promise.all([
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
context.newPage(),
]);
const [videoFile] = await Promise.all([
video.path(),
context.close(),
]);
expect(path.dirname(videoFile)).toBe(videoDir);
});
it('should fire striclty after context.newPage', async ({browser}) => {
const context = await browser.newContext({ _recordVideos: true, _videoSize: { width: 320, height: 240 } });
const page = await context.newPage();
// Should not hang.
await page.waitForEvent('_videostarted');
await context.close();
});
it('should fire start event for popups', async ({browser, videoDir, server}) => {
const context = await browser.newContext({ _recordVideos: true, _videoSize: { width: 320, height: 240 } });
const [page] = await Promise.all([
context.newPage(),
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
]);
await page.goto(server.EMPTY_PAGE);
const [video, popup] = await Promise.all([
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
new Promise<Page>(resolve => context.on('page', resolve)),
page.evaluate(() => { window.open('about:blank'); })
]);
const [videoFile] = await Promise.all([
video.path(),
popup.close()
]);
expect(path.dirname(videoFile)).toBe(videoDir);
});
it('should scale frames down to the requested size ', async ({browser, videoPlayer, videoFile, server}) => {
it('should work for popups', async ({browser, relativeArtifactsPath, videoDir, server}) => {
const context = await browser.newContext({
relativeArtifactsPath,
recordVideos: true,
videoSize: { width: 320, height: 240 }
});
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await Promise.all([
page.waitForEvent('popup'),
page.evaluate(() => { window.open('about:blank'); }),
]);
await new Promise(r => setTimeout(r, 1000));
await context.close();
const videoFiles = await findVideos(videoDir);
expect(videoFiles.length).toBe(2);
});
it('should scale frames down to the requested size ', async ({browser, videoPlayer, relativeArtifactsPath, videoDir, server}) => {
const context = await browser.newContext({
relativeArtifactsPath,
recordVideos: true,
viewport: {width: 640, height: 480},
// Set size to 1/2 of the viewport.
_recordVideos: true,
_videoSize: { width: 320, height: 240 },
videoSize: { width: 320, height: 240 },
});
const page = await context.newPage();
const video = await page.waitForEvent('_videostarted') as any;
await page.goto(server.PREFIX + '/checkerboard.html');
// Update the picture to ensure enough frames are generated.
@ -354,12 +336,9 @@ describe('screencast', suite => {
container.firstElementChild.classList.add('red');
});
await new Promise(r => setTimeout(r, 1000));
await page.close();
const tmp = await video.path();
expect(fs.existsSync(tmp)).toBe(true);
fs.renameSync(tmp, videoFile);
await context.close();
const videoFile = await findVideo(videoDir);
await videoPlayer.load(videoFile);
const duration = await videoPlayer.duration();
expect(duration).toBeGreaterThan(0);
@ -383,83 +362,37 @@ describe('screencast', suite => {
}
});
it('should use viewport as default size', async ({browser, videoPlayer, videoFile}) => {
it('should use viewport as default size', async ({browser, videoPlayer, relativeArtifactsPath, videoDir}) => {
const size = {width: 800, height: 600};
const context = await browser.newContext({_recordVideos: true, viewport: size});
const context = await browser.newContext({
relativeArtifactsPath,
recordVideos: true,
viewport: size,
});
const [video] = await Promise.all([
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
context.newPage(),
]);
await context.newPage();
await new Promise(r => setTimeout(r, 1000));
const [tmpPath] = await Promise.all([
video.path(),
context.close(),
]);
await context.close();
expect(fs.existsSync(tmpPath)).toBe(true);
fs.renameSync(tmpPath, videoFile);
const videoFile = await findVideo(videoDir);
await videoPlayer.load(videoFile);
expect(await videoPlayer.videoWidth()).toBe(size.width);
expect(await videoPlayer.videoHeight()).toBe(size.height);
});
it('should be 1280x720 by default', async ({browser, videoPlayer, videoFile}) => {
const context = await browser.newContext({_recordVideos: true});
it('should be 1280x720 by default', async ({browser, videoPlayer, relativeArtifactsPath, videoDir}) => {
const context = await browser.newContext({
relativeArtifactsPath,
recordVideos: true,
});
const [video] = await Promise.all([
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
context.newPage(),
]);
await context.newPage();
await new Promise(r => setTimeout(r, 1000));
const [tmpPath] = await Promise.all([
video.path(),
context.close(),
]);
await context.close();
expect(fs.existsSync(tmpPath)).toBe(true);
fs.renameSync(tmpPath, videoFile);
const videoFile = await findVideo(videoDir);
await videoPlayer.load(videoFile);
expect(await videoPlayer.videoWidth()).toBe(1280);
expect(await videoPlayer.videoHeight()).toBe(720);
});
it('should create read stream', async ({browser, server}) => {
const context = await browser.newContext({_recordVideos: true});
const page = await context.newPage();
const video = await page.waitForEvent('_videostarted') as any;
await page.goto(server.PREFIX + '/grid.html');
await new Promise(r => setTimeout(r, 1000));
const [stream, path] = await Promise.all([
video.createReadStream(),
video.path(),
// TODO: make it work with dead context!
page.close(),
]);
const bufs = [];
stream.on('data', data => bufs.push(data));
await new Promise(f => stream.on('end', f));
const streamedData = Buffer.concat(bufs);
expect(fs.readFileSync(path).compare(streamedData)).toBe(0);
});
it('should saveAs', async ({browser, server, tmpDir}) => {
const context = await browser.newContext({_recordVideos: true});
const page = await context.newPage();
const video = await page.waitForEvent('_videostarted') as any;
await page.goto(server.PREFIX + '/grid.html');
await new Promise(r => setTimeout(r, 1000));
const saveAsPath = path.join(tmpDir, 'v.webm');
const [videoPath] = await Promise.all([
video.path(),
video.saveAs(saveAsPath),
// TODO: make it work with dead context!
page.close(),
]);
expect(fs.readFileSync(videoPath).compare(fs.readFileSync(saveAsPath))).toBe(0);
});
});
});