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`'.
|
- `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.
|
- `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 `.`.
|
- `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.
|
- `recordVideos` <[boolean]> Enables video recording for all pages to the `relativeArtifactsPath` folder.
|
||||||
- `_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.
|
- `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.
|
- `width` <[number]> Video frame width.
|
||||||
- `height` <[number]> Video frame height.
|
- `height` <[number]> Video frame height.
|
||||||
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
|
- `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`'.
|
- `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.
|
- `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 `.`.
|
- `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.
|
- `recordVideos` <[boolean]> Enables video recording for all pages to the `relativeArtifactsPath` folder.
|
||||||
- `_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.
|
- `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.
|
- `width` <[number]> Video frame width.
|
||||||
- `height` <[number]> Video frame height.
|
- `height` <[number]> Video frame height.
|
||||||
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
|
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
|
||||||
|
|
@ -701,7 +701,6 @@ page.removeListener('request', logRequest);
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- GEN:toc -->
|
<!-- GEN:toc -->
|
||||||
- [event: '_videostarted'](#event-_videostarted)
|
|
||||||
- [event: 'close'](#event-close-1)
|
- [event: 'close'](#event-close-1)
|
||||||
- [event: 'console'](#event-console)
|
- [event: 'console'](#event-console)
|
||||||
- [event: 'crash'](#event-crash)
|
- [event: 'crash'](#event-crash)
|
||||||
|
|
@ -788,35 +787,6 @@ page.removeListener('request', logRequest);
|
||||||
- [page.workers()](#pageworkers)
|
- [page.workers()](#pageworkers)
|
||||||
<!-- GEN:stop -->
|
<!-- 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'
|
#### event: 'close'
|
||||||
|
|
||||||
Emitted when the page closes.
|
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.
|
- `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.
|
- `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.
|
- `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`.
|
- `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).
|
- `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`.
|
- `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]>
|
- `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`'.
|
- `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 `.`.
|
- `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]> Enables video recording for all pages to the `relativeArtifactsPath` folder.
|
||||||
- `_recordVideos` <[boolean]> **experimental** Enables automatic video recording for new pages.
|
- `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.
|
||||||
- `_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.
|
|
||||||
- `width` <[number]> Video frame width.
|
- `width` <[number]> Video frame width.
|
||||||
- `height` <[number]> Video frame height.
|
- `height` <[number]> Video frame height.
|
||||||
- `recordTrace` <[boolean]> Enables trace recording to the `relativeArtifactsPath` folder.
|
- `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.
|
- `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.
|
- `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.
|
- `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`.
|
- `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).
|
- `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`.
|
- `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) {
|
for (const browserType of success) {
|
||||||
try {
|
try {
|
||||||
const browser = await playwright[browserType].launch({
|
const browser = await playwright[browserType].launch({
|
||||||
_videosPath: __dirname,
|
artifactsPath: __dirname,
|
||||||
});
|
});
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
_recordVideos: true,
|
recordVideos: true,
|
||||||
_videoSize: {width: 320, height: 240},
|
videoSize: {width: 320, height: 240},
|
||||||
});
|
});
|
||||||
const page = await context.newPage();
|
await context.newPage();
|
||||||
const video = await page.waitForEvent('_videostarted');
|
|
||||||
// Wait fo 1 second to actually record something.
|
// Wait fo 1 second to actually record something.
|
||||||
await new Promise(x => setTimeout(x, 1000));
|
await new Promise(x => setTimeout(x, 1000));
|
||||||
const [videoFile] = await Promise.all([
|
await context.close();
|
||||||
video.path(),
|
|
||||||
context.close(),
|
|
||||||
]);
|
|
||||||
await browser.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!`);
|
console.error(`ERROR: Package "${requireName}", browser "${browserType}" should have created screencast!`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ import { WebKitBrowser } from './webkitBrowser';
|
||||||
import { FirefoxBrowser } from './firefoxBrowser';
|
import { FirefoxBrowser } from './firefoxBrowser';
|
||||||
import { debugLogger } from '../utils/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import { SelectorsOwner } from './selectors';
|
import { SelectorsOwner } from './selectors';
|
||||||
import { Video } from './video';
|
|
||||||
import { isUnderTest } from '../utils/utils';
|
import { isUnderTest } from '../utils/utils';
|
||||||
|
|
||||||
class Root extends ChannelOwner<channels.Channel, {}> {
|
class Root extends ChannelOwner<channels.Channel, {}> {
|
||||||
|
|
@ -221,9 +220,6 @@ export class Connection {
|
||||||
case 'Route':
|
case 'Route':
|
||||||
result = new Route(parent, type, guid, initializer);
|
result = new Route(parent, type, guid, initializer);
|
||||||
break;
|
break;
|
||||||
case 'Video':
|
|
||||||
result = new Video(parent, type, guid, initializer);
|
|
||||||
break;
|
|
||||||
case 'Stream':
|
case 'Stream':
|
||||||
result = new Stream(parent, type, guid, initializer);
|
result = new Stream(parent, type, guid, initializer);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ export const Events = {
|
||||||
Load: 'load',
|
Load: 'load',
|
||||||
Popup: 'popup',
|
Popup: 'popup',
|
||||||
Worker: 'worker',
|
Worker: 'worker',
|
||||||
_VideoStarted: '_videostarted',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
Worker: {
|
Worker: {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ import * as util from 'util';
|
||||||
import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types';
|
import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types';
|
||||||
import { evaluationScript, urlMatches } from './clientHelper';
|
import { evaluationScript, urlMatches } from './clientHelper';
|
||||||
import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils';
|
import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils';
|
||||||
import { Video } from './video';
|
|
||||||
|
|
||||||
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
||||||
width?: string | number,
|
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('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('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request)));
|
||||||
this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));
|
this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));
|
||||||
this._channel.on('videoStarted', params => this._onVideoStarted(params));
|
|
||||||
|
|
||||||
if (this._browserContext._browserName === 'chromium') {
|
if (this._browserContext._browserName === 'chromium') {
|
||||||
this.coverage = new ChromiumCoverage(this._channel);
|
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);
|
this.emit(Events.Page.Worker, worker);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onVideoStarted(params: channels.PageVideoStartedEvent): void {
|
|
||||||
this.emit(Events.Page._VideoStarted, Video.from(params.video));
|
|
||||||
}
|
|
||||||
|
|
||||||
_onClose() {
|
_onClose() {
|
||||||
this._closed = true;
|
this._closed = true;
|
||||||
this._browserContext._pages.delete(this);
|
this._browserContext._pages.delete(this);
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ export type LaunchServerOptions = {
|
||||||
},
|
},
|
||||||
downloadsPath?: string,
|
downloadsPath?: string,
|
||||||
artifactsPath?: string,
|
artifactsPath?: string,
|
||||||
_videosPath?: string,
|
|
||||||
chromiumSandbox?: boolean,
|
chromiumSandbox?: boolean,
|
||||||
port?: number,
|
port?: number,
|
||||||
logger?: Logger,
|
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 { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
|
||||||
import { FileChooser } from '../server/fileChooser';
|
import { FileChooser } from '../server/fileChooser';
|
||||||
import { CRCoverage } from '../server/chromium/crCoverage';
|
import { CRCoverage } from '../server/chromium/crCoverage';
|
||||||
import { VideoDispatcher } from './videoDispatcher';
|
|
||||||
|
|
||||||
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
|
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
|
||||||
private _page: Page;
|
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.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.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) }));
|
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,
|
downloadsPath?: string,
|
||||||
artifactsPath?: string,
|
artifactsPath?: string,
|
||||||
_videosPath?: string,
|
|
||||||
firefoxUserPrefs?: any,
|
firefoxUserPrefs?: any,
|
||||||
chromiumSandbox?: boolean,
|
chromiumSandbox?: boolean,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
|
|
@ -197,7 +196,6 @@ export type BrowserTypeLaunchOptions = {
|
||||||
},
|
},
|
||||||
downloadsPath?: string,
|
downloadsPath?: string,
|
||||||
artifactsPath?: string,
|
artifactsPath?: string,
|
||||||
_videosPath?: string,
|
|
||||||
firefoxUserPrefs?: any,
|
firefoxUserPrefs?: any,
|
||||||
chromiumSandbox?: boolean,
|
chromiumSandbox?: boolean,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
|
|
@ -229,7 +227,6 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
||||||
},
|
},
|
||||||
downloadsPath?: string,
|
downloadsPath?: string,
|
||||||
artifactsPath?: string,
|
artifactsPath?: string,
|
||||||
_videosPath?: string,
|
|
||||||
chromiumSandbox?: boolean,
|
chromiumSandbox?: boolean,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
noDefaultViewport?: boolean,
|
noDefaultViewport?: boolean,
|
||||||
|
|
@ -289,7 +286,6 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
||||||
},
|
},
|
||||||
downloadsPath?: string,
|
downloadsPath?: string,
|
||||||
artifactsPath?: string,
|
artifactsPath?: string,
|
||||||
_videosPath?: string,
|
|
||||||
chromiumSandbox?: boolean,
|
chromiumSandbox?: boolean,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
noDefaultViewport?: boolean,
|
noDefaultViewport?: boolean,
|
||||||
|
|
@ -381,8 +377,8 @@ export type BrowserNewContextParams = {
|
||||||
acceptDownloads?: boolean,
|
acceptDownloads?: boolean,
|
||||||
relativeArtifactsPath?: string,
|
relativeArtifactsPath?: string,
|
||||||
recordTrace?: boolean,
|
recordTrace?: boolean,
|
||||||
_recordVideos?: boolean,
|
recordVideos?: boolean,
|
||||||
_videoSize?: {
|
videoSize?: {
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
|
|
@ -421,8 +417,8 @@ export type BrowserNewContextOptions = {
|
||||||
acceptDownloads?: boolean,
|
acceptDownloads?: boolean,
|
||||||
relativeArtifactsPath?: string,
|
relativeArtifactsPath?: string,
|
||||||
recordTrace?: boolean,
|
recordTrace?: boolean,
|
||||||
_recordVideos?: boolean,
|
recordVideos?: boolean,
|
||||||
_videoSize?: {
|
videoSize?: {
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
},
|
},
|
||||||
|
|
@ -675,7 +671,6 @@ export interface PageChannel extends Channel {
|
||||||
on(event: 'requestFinished', callback: (params: PageRequestFinishedEvent) => void): this;
|
on(event: 'requestFinished', callback: (params: PageRequestFinishedEvent) => void): this;
|
||||||
on(event: 'response', callback: (params: PageResponseEvent) => void): this;
|
on(event: 'response', callback: (params: PageResponseEvent) => void): this;
|
||||||
on(event: 'route', callback: (params: PageRouteEvent) => 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;
|
on(event: 'worker', callback: (params: PageWorkerEvent) => void): this;
|
||||||
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
|
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
|
||||||
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
|
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
|
||||||
|
|
@ -758,9 +753,6 @@ export type PageRouteEvent = {
|
||||||
route: RouteChannel,
|
route: RouteChannel,
|
||||||
request: RequestChannel,
|
request: RequestChannel,
|
||||||
};
|
};
|
||||||
export type PageVideoStartedEvent = {
|
|
||||||
video: VideoChannel,
|
|
||||||
};
|
|
||||||
export type PageWorkerEvent = {
|
export type PageWorkerEvent = {
|
||||||
worker: WorkerChannel,
|
worker: WorkerChannel,
|
||||||
};
|
};
|
||||||
|
|
@ -2154,31 +2146,6 @@ export type DialogDismissParams = {};
|
||||||
export type DialogDismissOptions = {};
|
export type DialogDismissOptions = {};
|
||||||
export type DialogDismissResult = void;
|
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 -----------
|
// ----------- Download -----------
|
||||||
export type DownloadInitializer = {
|
export type DownloadInitializer = {
|
||||||
url: string,
|
url: string,
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,6 @@ BrowserType:
|
||||||
password: string?
|
password: string?
|
||||||
downloadsPath: string?
|
downloadsPath: string?
|
||||||
artifactsPath: string?
|
artifactsPath: string?
|
||||||
_videosPath: string?
|
|
||||||
firefoxUserPrefs: json?
|
firefoxUserPrefs: json?
|
||||||
chromiumSandbox: boolean?
|
chromiumSandbox: boolean?
|
||||||
slowMo: number?
|
slowMo: number?
|
||||||
|
|
@ -261,7 +260,6 @@ BrowserType:
|
||||||
password: string?
|
password: string?
|
||||||
downloadsPath: string?
|
downloadsPath: string?
|
||||||
artifactsPath: string?
|
artifactsPath: string?
|
||||||
_videosPath: string?
|
|
||||||
chromiumSandbox: boolean?
|
chromiumSandbox: boolean?
|
||||||
slowMo: number?
|
slowMo: number?
|
||||||
noDefaultViewport: boolean?
|
noDefaultViewport: boolean?
|
||||||
|
|
@ -373,8 +371,8 @@ Browser:
|
||||||
acceptDownloads: boolean?
|
acceptDownloads: boolean?
|
||||||
relativeArtifactsPath: string?
|
relativeArtifactsPath: string?
|
||||||
recordTrace: boolean?
|
recordTrace: boolean?
|
||||||
_recordVideos: boolean?
|
recordVideos: boolean?
|
||||||
_videoSize:
|
videoSize:
|
||||||
type: object?
|
type: object?
|
||||||
properties:
|
properties:
|
||||||
width: number
|
width: number
|
||||||
|
|
@ -914,10 +912,6 @@ Page:
|
||||||
route: Route
|
route: Route
|
||||||
request: Request
|
request: Request
|
||||||
|
|
||||||
videoStarted:
|
|
||||||
parameters:
|
|
||||||
video: Video
|
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
parameters:
|
parameters:
|
||||||
worker: Worker
|
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:
|
Download:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
})),
|
})),
|
||||||
downloadsPath: tOptional(tString),
|
downloadsPath: tOptional(tString),
|
||||||
artifactsPath: tOptional(tString),
|
artifactsPath: tOptional(tString),
|
||||||
_videosPath: tOptional(tString),
|
|
||||||
firefoxUserPrefs: tOptional(tAny),
|
firefoxUserPrefs: tOptional(tAny),
|
||||||
chromiumSandbox: tOptional(tBoolean),
|
chromiumSandbox: tOptional(tBoolean),
|
||||||
slowMo: tOptional(tNumber),
|
slowMo: tOptional(tNumber),
|
||||||
|
|
@ -151,7 +150,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
})),
|
})),
|
||||||
downloadsPath: tOptional(tString),
|
downloadsPath: tOptional(tString),
|
||||||
artifactsPath: tOptional(tString),
|
artifactsPath: tOptional(tString),
|
||||||
_videosPath: tOptional(tString),
|
|
||||||
chromiumSandbox: tOptional(tBoolean),
|
chromiumSandbox: tOptional(tBoolean),
|
||||||
slowMo: tOptional(tNumber),
|
slowMo: tOptional(tNumber),
|
||||||
noDefaultViewport: tOptional(tBoolean),
|
noDefaultViewport: tOptional(tBoolean),
|
||||||
|
|
@ -223,8 +221,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
acceptDownloads: tOptional(tBoolean),
|
acceptDownloads: tOptional(tBoolean),
|
||||||
relativeArtifactsPath: tOptional(tString),
|
relativeArtifactsPath: tOptional(tString),
|
||||||
recordTrace: tOptional(tBoolean),
|
recordTrace: tOptional(tBoolean),
|
||||||
_recordVideos: tOptional(tBoolean),
|
recordVideos: tOptional(tBoolean),
|
||||||
_videoSize: tOptional(tObject({
|
videoSize: tOptional(tObject({
|
||||||
width: tNumber,
|
width: tNumber,
|
||||||
height: tNumber,
|
height: tNumber,
|
||||||
})),
|
})),
|
||||||
|
|
@ -821,11 +819,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
promptText: tOptional(tString),
|
promptText: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.DialogDismissParams = tOptional(tObject({}));
|
scheme.DialogDismissParams = tOptional(tObject({}));
|
||||||
scheme.VideoPathParams = tOptional(tObject({}));
|
|
||||||
scheme.VideoSaveAsParams = tObject({
|
|
||||||
path: tString,
|
|
||||||
});
|
|
||||||
scheme.VideoStreamParams = tOptional(tObject({}));
|
|
||||||
scheme.DownloadPathParams = tOptional(tObject({}));
|
scheme.DownloadPathParams = tOptional(tObject({}));
|
||||||
scheme.DownloadSaveAsParams = tObject({
|
scheme.DownloadSaveAsParams = tObject({
|
||||||
path: tString,
|
path: tString,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import { EventEmitter } from 'events';
|
||||||
import { Download } from './download';
|
import { Download } from './download';
|
||||||
import { ProxySettings } from './types';
|
import { ProxySettings } from './types';
|
||||||
import { ChildProcess } from 'child_process';
|
import { ChildProcess } from 'child_process';
|
||||||
import { makeWaitForNextTask } from '../utils/utils';
|
|
||||||
|
|
||||||
export interface BrowserProcess {
|
export interface BrowserProcess {
|
||||||
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
||||||
|
|
@ -34,7 +33,6 @@ export type BrowserOptions = types.UIOptions & {
|
||||||
name: string,
|
name: string,
|
||||||
artifactsPath?: string,
|
artifactsPath?: string,
|
||||||
downloadsPath?: string,
|
downloadsPath?: string,
|
||||||
_videosPath?: string,
|
|
||||||
headful?: boolean,
|
headful?: boolean,
|
||||||
persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
|
persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
|
||||||
browserProcess: BrowserProcess,
|
browserProcess: BrowserProcess,
|
||||||
|
|
@ -50,7 +48,7 @@ export abstract class Browser extends EventEmitter {
|
||||||
private _downloads = new Map<string, Download>();
|
private _downloads = new Map<string, Download>();
|
||||||
_defaultContext: BrowserContext | null = null;
|
_defaultContext: BrowserContext | null = null;
|
||||||
private _startedClosing = false;
|
private _startedClosing = false;
|
||||||
private readonly _idToVideo = new Map<string, Video>();
|
readonly _idToVideo = new Map<string, Video>();
|
||||||
|
|
||||||
constructor(options: BrowserOptions) {
|
constructor(options: BrowserOptions) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -89,20 +87,19 @@ export abstract class Browser extends EventEmitter {
|
||||||
this._downloads.delete(uuid);
|
this._downloads.delete(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
_videoStarted(videoId: string, file: string, pageOrError: Promise<Page | Error>) {
|
_videoStarted(context: BrowserContext, videoId: string, path: string, pageOrError: Promise<Page | Error>) {
|
||||||
const video = new Video(file);
|
const video = new Video(context, videoId, path);
|
||||||
this._idToVideo.set(videoId, video);
|
this._idToVideo.set(videoId, video);
|
||||||
pageOrError.then(pageOrError => {
|
pageOrError.then(pageOrError => {
|
||||||
// Emit the event in another task to ensure that newPage response is handled before.
|
|
||||||
if (pageOrError instanceof Page)
|
if (pageOrError instanceof Page)
|
||||||
makeWaitForNextTask()(() => pageOrError.emit(Page.Events.VideoStarted, video));
|
pageOrError.emit(Page.Events.VideoStarted, video);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_videoFinished(videoId: string) {
|
_videoFinished(videoId: string) {
|
||||||
const video = this._idToVideo.get(videoId);
|
const video = this._idToVideo.get(videoId)!;
|
||||||
this._idToVideo.delete(videoId);
|
this._idToVideo.delete(videoId);
|
||||||
video!._finishCallback();
|
video._finishCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
_didClose() {
|
_didClose() {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
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 * as dom from './dom';
|
||||||
import { Download } from './download';
|
import { Download } from './download';
|
||||||
import * as frames from './frames';
|
import * as frames from './frames';
|
||||||
|
|
@ -30,17 +31,17 @@ import * as types from './types';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export class Video {
|
export class Video {
|
||||||
private readonly _path: string;
|
readonly _videoId: string;
|
||||||
|
readonly _path: string;
|
||||||
|
readonly _context: BrowserContext;
|
||||||
|
readonly _finishedPromise: Promise<void>;
|
||||||
_finishCallback: () => 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> {
|
constructor(context: BrowserContext, videoId: string, path: string) {
|
||||||
await this._finishedPromise;
|
this._videoId = videoId;
|
||||||
return this._path;
|
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);
|
await listener.onContextCreated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _ensureArtifactsPath() {
|
||||||
|
if (this._artifactsPath)
|
||||||
|
await mkdirIfNeeded(path.join(this._artifactsPath, 'dummy'));
|
||||||
|
}
|
||||||
|
|
||||||
_browserClosed() {
|
_browserClosed() {
|
||||||
for (const page of this.pages())
|
for (const page of this.pages())
|
||||||
page._didClose();
|
page._didClose();
|
||||||
|
|
@ -262,7 +268,14 @@ export abstract class BrowserContext extends EventEmitter {
|
||||||
if (this._closedStatus === 'open') {
|
if (this._closedStatus === 'open') {
|
||||||
this._closedStatus = 'closing';
|
this._closedStatus = 'closing';
|
||||||
await this._doClose();
|
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)
|
for (const listener of contextListeners)
|
||||||
await listener.onContextDestroyed(this);
|
await listener.onContextDestroyed(this);
|
||||||
this._didCloseInternal();
|
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)
|
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
|
||||||
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
|
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
|
||||||
if (options.noDefaultViewport && options.isMobile !== undefined)
|
if (options.noDefaultViewport && options.isMobile !== undefined)
|
||||||
|
|
@ -286,6 +299,10 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio
|
||||||
if (!options.viewport && !options.noDefaultViewport)
|
if (!options.viewport && !options.noDefaultViewport)
|
||||||
options.viewport = { width: 1280, height: 720 };
|
options.viewport = { width: 1280, height: 720 };
|
||||||
verifyGeolocation(options.geolocation);
|
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) {
|
export function verifyGeolocation(geolocation?: types.Geolocation) {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ const mkdirAsync = util.promisify(fs.mkdir);
|
||||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||||
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
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 DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
|
||||||
const VIDEOS_FOLDER = path.join(os.tmpdir(), 'playwright_videos-');
|
|
||||||
|
|
||||||
type WebSocketNotPipe = { webSocketRegex: RegExp, stream: 'stdout' | 'stderr' };
|
type WebSocketNotPipe = { webSocketRegex: RegExp, stream: 'stdout' | 'stderr' };
|
||||||
|
|
||||||
|
|
@ -77,7 +76,6 @@ export abstract class BrowserType {
|
||||||
async launchPersistentContext(userDataDir: string, options: types.LaunchPersistentOptions = {}): Promise<BrowserContext> {
|
async launchPersistentContext(userDataDir: string, options: types.LaunchPersistentOptions = {}): Promise<BrowserContext> {
|
||||||
options = validateLaunchOptions(options);
|
options = validateLaunchOptions(options);
|
||||||
const persistent: types.BrowserContextOptions = options;
|
const persistent: types.BrowserContextOptions = options;
|
||||||
validateBrowserContextOptions(persistent);
|
|
||||||
const controller = new ProgressController();
|
const controller = new ProgressController();
|
||||||
controller.setLogName('browser');
|
controller.setLogName('browser');
|
||||||
const browser = await controller.run(progress => {
|
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> {
|
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<Browser> {
|
||||||
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
|
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)
|
if ((options as any).__testHookBeforeCreateBrowser)
|
||||||
await (options as any).__testHookBeforeCreateBrowser();
|
await (options as any).__testHookBeforeCreateBrowser();
|
||||||
const browserOptions: BrowserOptions = {
|
const browserOptions: BrowserOptions = {
|
||||||
|
|
@ -98,10 +96,11 @@ export abstract class BrowserType {
|
||||||
headful: !options.headless,
|
headful: !options.headless,
|
||||||
artifactsPath: options.artifactsPath,
|
artifactsPath: options.artifactsPath,
|
||||||
downloadsPath,
|
downloadsPath,
|
||||||
_videosPath,
|
|
||||||
browserProcess,
|
browserProcess,
|
||||||
proxy: options.proxy,
|
proxy: options.proxy,
|
||||||
};
|
};
|
||||||
|
if (persistent)
|
||||||
|
validateBrowserContextOptions(persistent, browserOptions);
|
||||||
copyTestHooks(options, browserOptions);
|
copyTestHooks(options, browserOptions);
|
||||||
const browser = await this._connectToTransport(transport, 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.
|
// 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;
|
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 {
|
const {
|
||||||
ignoreDefaultArgs,
|
ignoreDefaultArgs,
|
||||||
ignoreAllDefaultArgs,
|
ignoreAllDefaultArgs,
|
||||||
|
|
@ -135,9 +134,8 @@ export abstract class BrowserType {
|
||||||
}
|
}
|
||||||
return dir;
|
return dir;
|
||||||
};
|
};
|
||||||
// TODO: use artifactsPath for downloads and videos.
|
// TODO: use artifactsPath for downloads.
|
||||||
const downloadsPath = await ensurePath(DOWNLOADS_FOLDER, options.downloadsPath);
|
const downloadsPath = await ensurePath(DOWNLOADS_FOLDER, options.downloadsPath);
|
||||||
const _videosPath = await ensurePath(VIDEOS_FOLDER, options._videosPath);
|
|
||||||
|
|
||||||
if (!userDataDir) {
|
if (!userDataDir) {
|
||||||
userDataDir = await mkdtempAsync(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
|
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];
|
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
||||||
transport = new PipeTransport(stdio[3], stdio[4]);
|
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[];
|
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> {
|
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||||
validateBrowserContextOptions(options);
|
validateBrowserContextOptions(options, this._options);
|
||||||
const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true });
|
const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true });
|
||||||
const context = new CRBrowserContext(this, browserContextId, options);
|
const context = new CRBrowserContext(this, browserContextId, options);
|
||||||
await context._initialize();
|
await context._initialize();
|
||||||
|
|
|
||||||
|
|
@ -458,13 +458,15 @@ class FrameSession {
|
||||||
promises.push(this._evaluateOnNewDocument(source));
|
promises.push(this._evaluateOnNewDocument(source));
|
||||||
for (const source of this._crPage._page._evaluateOnNewDocumentSources)
|
for (const source of this._crPage._page._evaluateOnNewDocumentSources)
|
||||||
promises.push(this._evaluateOnNewDocument(source));
|
promises.push(this._evaluateOnNewDocument(source));
|
||||||
if (this._crPage._browserContext._options._recordVideos) {
|
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 size = this._crPage._browserContext._options.videoSize || this._crPage._browserContext._options.viewport || { width: 1280, height: 720 };
|
||||||
const screencastId = createGuid();
|
const screencastId = createGuid();
|
||||||
const outputFile = path.join(this._crPage._browserContext._browser._options._videosPath!, screencastId + '.webm');
|
const outputFile = path.join(this._crPage._browserContext._artifactsPath!, screencastId + '.webm');
|
||||||
promises.push(this._startScreencast(screencastId, {
|
promises.push(this._crPage._browserContext._ensureArtifactsPath().then(() => {
|
||||||
...size,
|
return this._startScreencast(screencastId, {
|
||||||
outputFile,
|
...size,
|
||||||
|
outputFile,
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
|
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
|
||||||
|
|
@ -764,7 +766,7 @@ class FrameSession {
|
||||||
this._screencastState = 'started';
|
this._screencastState = 'started';
|
||||||
this._videoRecorder = videoRecorder;
|
this._videoRecorder = videoRecorder;
|
||||||
this._screencastId = screencastId;
|
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) {
|
} catch (e) {
|
||||||
videoRecorder.stop().catch(() => {});
|
videoRecorder.stop().catch(() => {});
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export class FFBrowser extends Browser {
|
||||||
}
|
}
|
||||||
|
|
||||||
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||||
validateBrowserContextOptions(options);
|
validateBrowserContextOptions(options, this._options);
|
||||||
if (options.isMobile)
|
if (options.isMobile)
|
||||||
throw new Error('options.isMobile is not supported in Firefox');
|
throw new Error('options.isMobile is not supported in Firefox');
|
||||||
const { browserContextId } = await this._connection.send('Browser.createBrowserContext', { removeOnDetach: true });
|
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));
|
promises.push(this.setOffline(this._options.offline));
|
||||||
if (this._options.colorScheme)
|
if (this._options.colorScheme)
|
||||||
promises.push(this._browser._connection.send('Browser.setColorScheme', { browserContextId, colorScheme: this._options.colorScheme }));
|
promises.push(this._browser._connection.send('Browser.setColorScheme', { browserContextId, colorScheme: this._options.colorScheme }));
|
||||||
if (this._options._recordVideos) {
|
if (this._options.recordVideos) {
|
||||||
const size = this._options._videoSize || this._options.viewport || { width: 1280, height: 720 };
|
const size = this._options.videoSize || this._options.viewport || { width: 1280, height: 720 };
|
||||||
await this._browser._connection.send('Browser.setScreencastOptions', {
|
promises.push(this._ensureArtifactsPath().then(() => {
|
||||||
...size,
|
return this._browser._connection.send('Browser.setScreencastOptions', {
|
||||||
dir: this._browser._options._videosPath!,
|
...size,
|
||||||
browserContextId: this._browserContextId
|
dir: this._artifactsPath!,
|
||||||
});
|
browserContextId: this._browserContextId
|
||||||
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
|
||||||
import { FFNetworkManager } from './ffNetworkManager';
|
import { FFNetworkManager } from './ffNetworkManager';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
||||||
import { Video } from '../browserContext';
|
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
|
||||||
|
|
@ -50,7 +49,6 @@ export class FFPage implements PageDelegate {
|
||||||
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
private _workers = new Map<string, { frameId: string, session: FFSession }>();
|
private _workers = new Map<string, { frameId: string, session: FFSession }>();
|
||||||
private readonly _idToScreencast = new Map<string, Video>();
|
|
||||||
|
|
||||||
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
|
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
|
|
@ -258,7 +256,7 @@ export class FFPage implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onScreencastStarted(event: Protocol.Page.screencastStartedPayload) {
|
_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) {
|
async exposeBinding(binding: PageBinding) {
|
||||||
|
|
|
||||||
|
|
@ -238,8 +238,8 @@ export type BrowserContextOptions = {
|
||||||
hasTouch?: boolean,
|
hasTouch?: boolean,
|
||||||
colorScheme?: ColorScheme,
|
colorScheme?: ColorScheme,
|
||||||
acceptDownloads?: boolean,
|
acceptDownloads?: boolean,
|
||||||
_recordVideos?: boolean,
|
recordVideos?: boolean,
|
||||||
_videoSize?: Size,
|
videoSize?: Size,
|
||||||
recordTrace?: boolean,
|
recordTrace?: boolean,
|
||||||
relativeArtifactsPath?: string,
|
relativeArtifactsPath?: string,
|
||||||
};
|
};
|
||||||
|
|
@ -261,7 +261,6 @@ type LaunchOptionsBase = {
|
||||||
proxy?: ProxySettings,
|
proxy?: ProxySettings,
|
||||||
artifactsPath?: string,
|
artifactsPath?: string,
|
||||||
downloadsPath?: string,
|
downloadsPath?: string,
|
||||||
_videosPath?: string,
|
|
||||||
chromiumSandbox?: boolean,
|
chromiumSandbox?: boolean,
|
||||||
slowMo?: number,
|
slowMo?: number,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ export class WKBrowser extends Browser {
|
||||||
}
|
}
|
||||||
|
|
||||||
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||||
validateBrowserContextOptions(options);
|
validateBrowserContextOptions(options, this._options);
|
||||||
const { browserContextId } = await this._browserSession.send('Playwright.createContext');
|
const { browserContextId } = await this._browserSession.send('Playwright.createContext');
|
||||||
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
|
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
|
||||||
const context = new WKBrowserContext(this, browserContextId, options);
|
const context = new WKBrowserContext(this, browserContextId, options);
|
||||||
|
|
|
||||||
|
|
@ -113,12 +113,14 @@ export class WKPage implements PageDelegate {
|
||||||
for (const [key, value] of this._browserContext._permissions)
|
for (const [key, value] of this._browserContext._permissions)
|
||||||
this._grantPermissions(key, value);
|
this._grantPermissions(key, value);
|
||||||
}
|
}
|
||||||
if (this._browserContext._options._recordVideos) {
|
if (this._browserContext._options.recordVideos) {
|
||||||
const size = this._browserContext._options._videoSize || this._browserContext._options.viewport || { width: 1280, height: 720 };
|
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');
|
const outputFile = path.join(this._browserContext._artifactsPath!, createGuid() + '.webm');
|
||||||
promises.push(this.startScreencast({
|
promises.push(this._browserContext._ensureArtifactsPath().then(() => {
|
||||||
...size,
|
return this.startScreencast({
|
||||||
outputFile,
|
...size,
|
||||||
|
outputFile,
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
@ -723,7 +725,7 @@ export class WKPage implements PageDelegate {
|
||||||
width: options.width,
|
width: options.width,
|
||||||
height: options.height,
|
height: options.height,
|
||||||
}) as any;
|
}) as any;
|
||||||
this._browserContext._browser._videoStarted(screencastId, options.outputFile, this.pageOrError());
|
this._browserContext._browser._videoStarted(this._browserContext, screencastId, options.outputFile, this.pageOrError());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._recordingVideoFile = null;
|
this._recordingVideoFile = null;
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ export class Snapshotter {
|
||||||
return frameResult;
|
return frameResult;
|
||||||
const frameSnapshot = {
|
const frameSnapshot = {
|
||||||
frameId: frame._id,
|
frameId: frame._id,
|
||||||
url: frame.url(),
|
url: removeHash(frame.url()),
|
||||||
html: '<body>Snapshot is not available</body>',
|
html: '<body>Snapshot is not available</body>',
|
||||||
resourceOverrides: [],
|
resourceOverrides: [],
|
||||||
};
|
};
|
||||||
|
|
@ -190,7 +190,7 @@ export class Snapshotter {
|
||||||
|
|
||||||
const snapshot: FrameSnapshot = {
|
const snapshot: FrameSnapshot = {
|
||||||
frameId: frame._id,
|
frameId: frame._id,
|
||||||
url: frame.url(),
|
url: removeHash(frame.url()),
|
||||||
html: data.html,
|
html: data.html,
|
||||||
resourceOverrides: [],
|
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 = {
|
type FrameSnapshotAndMapping = {
|
||||||
snapshot: FrameSnapshot,
|
snapshot: FrameSnapshot,
|
||||||
mapping: Map<Frame, string>,
|
mapping: Map<Frame, string>,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,13 @@ export type PageDestroyedTraceEvent = {
|
||||||
pageId: string,
|
pageId: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PageVideoTraceEvent = {
|
||||||
|
type: 'page-video',
|
||||||
|
contextId: string,
|
||||||
|
pageId: string,
|
||||||
|
fileName: string,
|
||||||
|
};
|
||||||
|
|
||||||
export type ActionTraceEvent = {
|
export type ActionTraceEvent = {
|
||||||
type: 'action',
|
type: 'action',
|
||||||
contextId: string,
|
contextId: string,
|
||||||
|
|
@ -75,6 +82,7 @@ export type TraceEvent =
|
||||||
ContextDestroyedTraceEvent |
|
ContextDestroyedTraceEvent |
|
||||||
PageCreatedTraceEvent |
|
PageCreatedTraceEvent |
|
||||||
PageDestroyedTraceEvent |
|
PageDestroyedTraceEvent |
|
||||||
|
PageVideoTraceEvent |
|
||||||
NetworkResourceTraceEvent |
|
NetworkResourceTraceEvent |
|
||||||
ActionTraceEvent;
|
ActionTraceEvent;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
* limitations under the License.
|
* 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 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 path from 'path';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
@ -42,10 +42,8 @@ class Tracer implements ContextListener {
|
||||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||||
if (!context._options.recordTrace)
|
if (!context._options.recordTrace)
|
||||||
return;
|
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 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);
|
const contextTracer = new ContextTracer(context, traceStorageDir, traceFile);
|
||||||
this._contextTracers.set(context, contextTracer);
|
this._contextTracers.set(context, contextTracer);
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +145,18 @@ class ContextTracer implements SnapshotterDelegate, ActionListener {
|
||||||
};
|
};
|
||||||
this._appendTraceEvent(event);
|
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, () => {
|
page.once(Page.Events.Close, () => {
|
||||||
this._pageToId.delete(page);
|
this._pageToId.delete(page);
|
||||||
if (this._disposed)
|
if (this._disposed)
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,9 @@ defineTestFixture('context', async ({browser, testOutputDir}, runTest, info) =>
|
||||||
const contextOptions: BrowserContextOptions = {
|
const contextOptions: BrowserContextOptions = {
|
||||||
relativeArtifactsPath: path.relative(config.outputDir, testOutputDir),
|
relativeArtifactsPath: path.relative(config.outputDir, testOutputDir),
|
||||||
recordTrace: !!options.TRACING,
|
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);
|
const context = await browser.newContext(contextOptions);
|
||||||
await runTest(context);
|
await runTest(context);
|
||||||
|
|
|
||||||
|
|
@ -15,59 +15,57 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { options, playwrightFixtures } from './playwright.fixtures';
|
import { options, playwrightFixtures } from './playwright.fixtures';
|
||||||
import type { Page } from '..';
|
import type { Page, Browser } from '..';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { TestServer } from '../utils/testserver';
|
import { TestServer } from '../utils/testserver';
|
||||||
import { mkdirIfNeeded } from '../lib/utils/utils';
|
|
||||||
|
|
||||||
type WorkerState = {
|
type WorkerState = {
|
||||||
videoDir: string;
|
videoPlayerBrowser: Browser,
|
||||||
};
|
};
|
||||||
type TestState = {
|
type TestState = {
|
||||||
videoPlayer: VideoPlayer;
|
videoPlayer: VideoPlayer;
|
||||||
videoFile: string;
|
relativeArtifactsPath: string;
|
||||||
|
videoDir: string;
|
||||||
};
|
};
|
||||||
const fixtures = playwrightFixtures.declareWorkerFixtures<WorkerState>().declareTestFixtures<TestState>();
|
const fixtures = playwrightFixtures.declareWorkerFixtures<WorkerState>().declareTestFixtures<TestState>();
|
||||||
const { it, expect, describe, defineTestFixture, defineWorkerFixture, overrideWorkerFixture } = fixtures;
|
const { it, expect, describe, defineTestFixture, defineWorkerFixture, overrideWorkerFixture } = fixtures;
|
||||||
|
|
||||||
defineWorkerFixture('videoDir', async ({}, test, config) => {
|
overrideWorkerFixture('browser', async ({browserType, defaultBrowserOptions}, test, config) => {
|
||||||
await test(path.join(config.outputDir, 'screencast'));
|
|
||||||
});
|
|
||||||
|
|
||||||
overrideWorkerFixture('browser', async ({browserType, defaultBrowserOptions, videoDir}, test) => {
|
|
||||||
const browser = await browserType.launch({
|
const browser = await browserType.launch({
|
||||||
...defaultBrowserOptions,
|
...defaultBrowserOptions,
|
||||||
// Make sure videos are stored on the same volume as the test output dir.
|
// 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 test(browser);
|
||||||
await browser.close();
|
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
|
// WebKit on Mac & Windows cannot replay webm/vp8 video, is unrelyable
|
||||||
// on Linux (times out) and in Firefox, so we always launch chromium for
|
// on Linux (times out) and in Firefox, so we always launch chromium for
|
||||||
// playback.
|
// playback.
|
||||||
const chromium = await playwright.chromium.launch();
|
const browser = await playwright.chromium.launch();
|
||||||
context = await chromium.newContext();
|
await runTest(browser);
|
||||||
|
await browser.close();
|
||||||
const page = await context.newPage();
|
|
||||||
const player = new VideoPlayer(page, server);
|
|
||||||
await test(player);
|
|
||||||
if (chromium)
|
|
||||||
await chromium.close();
|
|
||||||
else
|
|
||||||
await page.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 { test } = info;
|
||||||
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
|
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
|
||||||
const videoFile = path.join(videoDir, `${browserType.name()}-${sanitizedTitle}-${test.results.length}_v.webm`);
|
const relativeArtifactsPath = `${browserType.name()}-${sanitizedTitle}-${test.results.length}`;
|
||||||
await mkdirIfNeeded(videoFile);
|
await runTest(relativeArtifactsPath);
|
||||||
await runTest(videoFile);
|
});
|
||||||
|
|
||||||
|
defineTestFixture('videoDir', async ({relativeArtifactsPath}, runTest, info) => {
|
||||||
|
await runTest(path.join(info.config.outputDir, '.screencast', relativeArtifactsPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
function almostRed(r, g, b, alpha) {
|
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 {
|
class VideoPlayer {
|
||||||
private readonly _page: Page;
|
private readonly _page: Page;
|
||||||
private readonly _server: TestServer;
|
private readonly _server: TestServer;
|
||||||
|
|
||||||
constructor(page: Page, server: TestServer) {
|
constructor(page: Page, server: TestServer) {
|
||||||
this._page = page;
|
this._page = page;
|
||||||
this._server = server;
|
this._server = server;
|
||||||
|
|
@ -189,19 +198,29 @@ class VideoPlayer {
|
||||||
describe('screencast', suite => {
|
describe('screencast', suite => {
|
||||||
suite.slow();
|
suite.slow();
|
||||||
}, () => {
|
}, () => {
|
||||||
it('should capture static page', async ({browser, videoPlayer, videoFile}) => {
|
it('should require artifactsPath', async ({browserType, defaultBrowserOptions}) => {
|
||||||
const context = await browser.newContext({ _recordVideos: true, _videoSize: { width: 320, height: 240 } });
|
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 page = await context.newPage();
|
||||||
const video = await page.waitForEvent('_videostarted') as any;
|
|
||||||
|
|
||||||
await page.evaluate(() => document.body.style.backgroundColor = 'red');
|
await page.evaluate(() => document.body.style.backgroundColor = 'red');
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
await page.close();
|
await context.close();
|
||||||
|
|
||||||
const tmpPath = await video.path();
|
|
||||||
expect(fs.existsSync(tmpPath)).toBe(true);
|
|
||||||
fs.renameSync(tmpPath, videoFile);
|
|
||||||
|
|
||||||
|
const videoFile = await findVideo(videoDir);
|
||||||
await videoPlayer.load(videoFile);
|
await videoPlayer.load(videoFile);
|
||||||
const duration = await videoPlayer.duration();
|
const duration = await videoPlayer.duration();
|
||||||
expect(duration).toBeGreaterThan(0);
|
expect(duration).toBeGreaterThan(0);
|
||||||
|
|
@ -216,21 +235,21 @@ describe('screencast', suite => {
|
||||||
|
|
||||||
it('should capture navigation', (test, parameters) => {
|
it('should capture navigation', (test, parameters) => {
|
||||||
test.flaky();
|
test.flaky();
|
||||||
}, async ({browser, server, videoPlayer, videoFile}) => {
|
}, async ({browser, server, videoPlayer, relativeArtifactsPath, videoDir}) => {
|
||||||
const context = await browser.newContext({ _recordVideos: true, _videoSize: { width: 1280, height: 720 } });
|
const context = await browser.newContext({
|
||||||
|
relativeArtifactsPath,
|
||||||
|
recordVideos: true,
|
||||||
|
videoSize: { width: 1280, height: 720 }
|
||||||
|
});
|
||||||
const page = await context.newPage();
|
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 page.goto(server.PREFIX + '/background-color.html#rgb(0,0,0)');
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
await page.goto(server.CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)');
|
await page.goto(server.CROSS_PROCESS_PREFIX + '/background-color.html#rgb(100,100,100)');
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
await page.close();
|
await context.close();
|
||||||
|
|
||||||
const tmpPath = await video.path();
|
|
||||||
expect(fs.existsSync(tmpPath)).toBe(true);
|
|
||||||
fs.renameSync(tmpPath, videoFile);
|
|
||||||
|
|
||||||
|
const videoFile = await findVideo(videoDir);
|
||||||
await videoPlayer.load(videoFile);
|
await videoPlayer.load(videoFile);
|
||||||
const duration = await videoPlayer.duration();
|
const duration = await videoPlayer.duration();
|
||||||
expect(duration).toBeGreaterThan(0);
|
expect(duration).toBeGreaterThan(0);
|
||||||
|
|
@ -250,21 +269,22 @@ describe('screencast', suite => {
|
||||||
|
|
||||||
it('should capture css transformation', (test, parameters) => {
|
it('should capture css transformation', (test, parameters) => {
|
||||||
test.fail(options.WEBKIT(parameters) && options.WIN(parameters), 'Does not work on WebKit Windows');
|
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};
|
const size = {width: 320, height: 240};
|
||||||
// Set viewport equal to screencast frame size to avoid scaling.
|
// 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 page = await context.newPage();
|
||||||
const video = await page.waitForEvent('_videostarted') as any;
|
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/rotate-z.html');
|
await page.goto(server.PREFIX + '/rotate-z.html');
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
await page.close();
|
await context.close();
|
||||||
|
|
||||||
const tmpPath = await video.path();
|
|
||||||
expect(fs.existsSync(tmpPath)).toBe(true);
|
|
||||||
fs.renameSync(tmpPath, videoFile);
|
|
||||||
|
|
||||||
|
const videoFile = await findVideo(videoDir);
|
||||||
await videoPlayer.load(videoFile);
|
await videoPlayer.load(videoFile);
|
||||||
const duration = await videoPlayer.duration();
|
const duration = await videoPlayer.duration();
|
||||||
expect(duration).toBeGreaterThan(0);
|
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}) => {
|
it('should work for popups', async ({browser, relativeArtifactsPath, videoDir, server}) => {
|
||||||
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}) => {
|
|
||||||
const context = await browser.newContext({
|
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},
|
viewport: {width: 640, height: 480},
|
||||||
// Set size to 1/2 of the viewport.
|
// 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 page = await context.newPage();
|
||||||
const video = await page.waitForEvent('_videostarted') as any;
|
|
||||||
|
|
||||||
await page.goto(server.PREFIX + '/checkerboard.html');
|
await page.goto(server.PREFIX + '/checkerboard.html');
|
||||||
// Update the picture to ensure enough frames are generated.
|
// Update the picture to ensure enough frames are generated.
|
||||||
|
|
@ -354,12 +336,9 @@ describe('screencast', suite => {
|
||||||
container.firstElementChild.classList.add('red');
|
container.firstElementChild.classList.add('red');
|
||||||
});
|
});
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
await page.close();
|
await context.close();
|
||||||
|
|
||||||
const tmp = await video.path();
|
|
||||||
expect(fs.existsSync(tmp)).toBe(true);
|
|
||||||
fs.renameSync(tmp, videoFile);
|
|
||||||
|
|
||||||
|
const videoFile = await findVideo(videoDir);
|
||||||
await videoPlayer.load(videoFile);
|
await videoPlayer.load(videoFile);
|
||||||
const duration = await videoPlayer.duration();
|
const duration = await videoPlayer.duration();
|
||||||
expect(duration).toBeGreaterThan(0);
|
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 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([
|
await context.newPage();
|
||||||
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
|
|
||||||
context.newPage(),
|
|
||||||
]);
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
const [tmpPath] = await Promise.all([
|
await context.close();
|
||||||
video.path(),
|
|
||||||
context.close(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(fs.existsSync(tmpPath)).toBe(true);
|
const videoFile = await findVideo(videoDir);
|
||||||
fs.renameSync(tmpPath, videoFile);
|
|
||||||
await videoPlayer.load(videoFile);
|
await videoPlayer.load(videoFile);
|
||||||
expect(await videoPlayer.videoWidth()).toBe(size.width);
|
expect(await videoPlayer.videoWidth()).toBe(size.width);
|
||||||
expect(await videoPlayer.videoHeight()).toBe(size.height);
|
expect(await videoPlayer.videoHeight()).toBe(size.height);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be 1280x720 by default', async ({browser, videoPlayer, videoFile}) => {
|
it('should be 1280x720 by default', async ({browser, videoPlayer, relativeArtifactsPath, videoDir}) => {
|
||||||
const context = await browser.newContext({_recordVideos: true});
|
const context = await browser.newContext({
|
||||||
|
relativeArtifactsPath,
|
||||||
|
recordVideos: true,
|
||||||
|
});
|
||||||
|
|
||||||
const [video] = await Promise.all([
|
await context.newPage();
|
||||||
new Promise<any>(r => context.on('page', page => page.on('_videostarted', r))),
|
|
||||||
context.newPage(),
|
|
||||||
]);
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
const [tmpPath] = await Promise.all([
|
await context.close();
|
||||||
video.path(),
|
|
||||||
context.close(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(fs.existsSync(tmpPath)).toBe(true);
|
const videoFile = await findVideo(videoDir);
|
||||||
fs.renameSync(tmpPath, videoFile);
|
|
||||||
await videoPlayer.load(videoFile);
|
await videoPlayer.load(videoFile);
|
||||||
expect(await videoPlayer.videoWidth()).toBe(1280);
|
expect(await videoPlayer.videoWidth()).toBe(1280);
|
||||||
expect(await videoPlayer.videoHeight()).toBe(720);
|
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 a new issue