mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-11 20:37:54 +03:00
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:
parent
4e2d75d9f7
commit
df777344a3
45
docs/api.md
45
docs/api.md
@ -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`.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -50,7 +50,6 @@ export const Events = {
|
||||
Load: 'load',
|
||||
Popup: 'popup',
|
||||
Worker: 'worker',
|
||||
_VideoStarted: '_videostarted',
|
||||
},
|
||||
|
||||
Worker: {
|
||||
|
@ -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);
|
||||
|
@ -83,7 +83,6 @@ export type LaunchServerOptions = {
|
||||
},
|
||||
downloadsPath?: string,
|
||||
artifactsPath?: string,
|
||||
_videosPath?: string,
|
||||
chromiumSandbox?: boolean,
|
||||
port?: number,
|
||||
logger?: Logger,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) }));
|
||||
}
|
||||
|
||||
|
@ -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) };
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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) {
|
||||
|
@ -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[];
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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>,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user