Skip to content

Commit

Permalink
Implement video recording (#3257)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreyBelym committed Jan 25, 2019
1 parent 8f29a87 commit daa552b
Show file tree
Hide file tree
Showing 49 changed files with 1,079 additions and 217 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
},
"devDependencies": {
"@belym.a.2105/broken-link-checker": "^0.7.9",
"@ffmpeg-installer/ffmpeg": "^1.0.17",
"@types/chai": "^3.5.2",
"babel-eslint": "^7.1.1",
"babel-plugin-add-module-exports": "^0.2.0",
Expand Down
32 changes: 21 additions & 11 deletions src/browser/provider/built-in/chrome/cdp.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ async function setEmulation (runtimeInfo) {
await resizeWindow({ width: config.width, height: config.height }, runtimeInfo);
}

async function getScreenshotData (client) {
const { visualViewport } = await client.Page.getLayoutMetrics();

const clipRegion = {
x: visualViewport.pageX,
y: visualViewport.pageY,
width: visualViewport.clientWidth,
height: visualViewport.clientHeight,
scale: visualViewport.scale
};

return await client.Page.captureScreenshot({ fromSurface: true, clip: clipRegion });
}

export async function createClient (runtimeInfo) {
const { browserId, config, cdpPort } = runtimeInfo;

Expand Down Expand Up @@ -99,20 +113,16 @@ export async function updateMobileViewportSize (runtimeInfo) {
runtimeInfo.viewportSize.height = windowDimensions.outerHeight;
}

export async function takeScreenshot (path, { client }) {
const { visualViewport } = await client.Page.getLayoutMetrics();
export async function getVideoFrameData ({ client }) {
const frameData = await getScreenshotData(client);

const clipRegion = {
x: visualViewport.pageX,
y: visualViewport.pageY,
width: visualViewport.clientWidth,
height: visualViewport.clientHeight,
scale: visualViewport.scale
};
return Buffer.from(frameData.data, 'base64');
}

const screenshot = await client.Page.captureScreenshot({ fromSurface: true, clip: clipRegion });
export async function takeScreenshot (path, { client }) {
const screenshotData = await getScreenshotData(client);

await writeFile(path, screenshot.data, { encoding: 'base64' });
await writeFile(path, screenshotData.data, { encoding: 'base64' });
}

export async function resizeWindow (newDimensions, runtimeInfo) {
Expand Down
5 changes: 5 additions & 0 deletions src/browser/provider/built-in/chrome/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ export default {
await this.resizeWindow(browserId, maximumSize.width, maximumSize.height, maximumSize.width, maximumSize.height);
},

async getVideoFrameData (browserId) {
return await cdp.getVideoFrameData(this.openedBrowsers[browserId]);
},

async hasCustomActionForBrowser (browserId) {
const { config, client } = this.openedBrowsers[browserId];

Expand All @@ -97,6 +101,7 @@ export default {
hasMaximizeWindow: !!client && config.headless,
hasTakeScreenshot: !!client,
hasChromelessScreenshots: !!client,
hasGetVideoFrameData: !!client,
hasCanResizeWindowToDimensions: false
};
},
Expand Down
7 changes: 7 additions & 0 deletions src/browser/provider/built-in/firefox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,20 @@ export default {
await this.resizeWindow(browserId, maximumSize.width, maximumSize.height);
},

async getVideoFrameData (browserId) {
const { marionetteClient } = this.openedBrowsers[browserId];

return await marionetteClient.getVideoFrameData();
},

async hasCustomActionForBrowser (browserId) {
const { config, marionetteClient } = this.openedBrowsers[browserId];

return {
hasCloseBrowser: true,
hasTakeScreenshot: !!marionetteClient,
hasChromelessScreenshots: !!marionetteClient,
hasGetVideoFrameData: !!marionetteClient,
hasResizeWindow: !!marionetteClient && config.headless,
hasMaximizeWindow: !!marionetteClient && config.headless,
hasCanResizeWindowToDimensions: false
Expand Down
14 changes: 12 additions & 2 deletions src/browser/provider/built-in/firefox/marionette-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ module.exports = class MarionetteClient {
return responsePacket.body[3];
}

async _getScreenshotData () {
return await this._getResponse({ command: COMMANDS.takeScreenshot });
}

async connect () {
await this._connectSocket(this.port, this.host);

Expand All @@ -169,9 +173,15 @@ module.exports = class MarionetteClient {
}

async takeScreenshot (path) {
const screenshot = await this._getResponse({ command: COMMANDS.takeScreenshot });
const screenshotData = await this._getScreenshotData();

await writeFile(path, screenshotData.value, { encoding: 'base64' });
}

async getVideoFrameData () {
const frameData = await this._getScreenshotData();

await writeFile(path, screenshot.value, { encoding: 'base64' });
return Buffer.from(frameData.value, 'base64');
}

async setWindowSize (width, height) {
Expand Down
4 changes: 4 additions & 0 deletions src/browser/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ export default class BrowserProvider {
await this.plugin.takeScreenshot(browserId, screenshotPath, pageWidth, pageHeight);
}

async getVideoFrameData (browserId) {
return this.plugin.getVideoFrameData(browserId);
}

async hasCustomActionForBrowser (browserId) {
return this.plugin.hasCustomActionForBrowser(browserId);
}
Expand Down
7 changes: 7 additions & 0 deletions src/browser/provider/plugin-host.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export default class BrowserProviderPluginHost {
hasCloseBrowser: this.hasOwnProperty('closeBrowser'),
hasResizeWindow: this.hasOwnProperty('resizeWindow'),
hasTakeScreenshot: this.hasOwnProperty('takeScreenshot'),
hasGetVideoFrameData: this.hasOwnProperty('getVideoFrameData'),
hasCanResizeWindowToDimensions: this.hasOwnProperty('canResizeWindowToDimensions'),
hasMaximizeWindow: this.hasOwnProperty('maximizeWindow'),
hasChromelessScreenshots: false
Expand All @@ -131,6 +132,12 @@ export default class BrowserProviderPluginHost {
this.reportWarning(browserId, WARNING_MESSAGE.maximizeNotSupportedByBrowserProvider, this[name]);
}

async getVideoFrameData (browserId) {
const browserAlias = BrowserConnection.getById(browserId).browserInfo.alias;

this.reportWarning(browserId, WARNING_MESSAGE.videoNotSupportedByBrowserProvider, browserAlias);
}

async reportJobResult (/*browserId, status, data*/) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/browser/provider/pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export default {
if (!await provider.isValidBrowserName(browserName))
throw new GeneralError(MESSAGE.cantFindBrowser, alias);

return browserInfo;
return { alias, ...browserInfo };
},

addProvider (providerName, providerObject) {
Expand Down
13 changes: 11 additions & 2 deletions src/cli/argument-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import MESSAGE from '../errors/runtime/message';
import { assertType, is } from '../errors/runtime/type-assertions';
import getViewPortWidth from '../utils/get-viewport-width';
import { wordWrap, splitQuotedText } from '../utils/string';
import parseSslOptions from '../utils/parse-ssl-options';
import { getSSLOptions, getVideoOptions } from '../utils/get-options';
import getFilterFn from '../utils/get-filter-fn';

const REMOTE_ALIAS_RE = /^remote(?::(\d*))?$/;
Expand Down Expand Up @@ -89,6 +89,9 @@ export default class CLIArgumentParser {
.option('--proxy <host>', 'specify the host of the proxy server')
.option('--proxy-bypass <rules>', 'specify a comma-separated list of rules that define URLs accessed bypassing the proxy server')
.option('--ssl <options>', 'specify SSL options to run TestCafe proxy server over the HTTPS protocol')
.option('--video <path>', ' record videos of test runs')
.option('--video-options <option=value[,...]>', 'specify video recording options')
.option('--video-encoding-options <option=value[,...]>', 'specify encoding options')
.option('--disable-page-reloads', 'disable page reloads between tests')
.option('--dev', 'enables mechanisms to log and diagnose errors')
.option('--qr-code', 'outputs QR-code that repeats URLs used to connect the remote browsers')
Expand Down Expand Up @@ -177,7 +180,7 @@ export default class CLIArgumentParser {

async _parseSslOptions () {
if (this.opts.ssl)
this.opts.ssl = await parseSslOptions(this.opts.ssl);
this.opts.ssl = await getSSLOptions(this.opts.ssl);
}

async _parseReporters () {
Expand All @@ -200,6 +203,11 @@ export default class CLIArgumentParser {
this.src = this.program.args.slice(1);
}

async _parseVideoOptions () {
this.opts.videoOptions = typeof this.opts.videoOptions === 'string' ? await getVideoOptions(this.opts.videoOptions) : null;
this.opts.videoEncodingOptions = typeof this.opts.videoEncodingOptions === 'string' ? await getVideoOptions(this.opts.videoEncodingOptions) : null;
}

_getProviderName () {
this.opts.providerName = this.opts.listBrowsers === true ? void 0 : this.opts.listBrowsers;
}
Expand Down Expand Up @@ -227,6 +235,7 @@ export default class CLIArgumentParser {
this._parseConcurrency();
this._parseFileList();

await this._parseVideoOptions();
await this._parseSslOptions();
await this._parseReporters();
}
Expand Down
1 change: 1 addition & 0 deletions src/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ async function runTests (argParser) {
.reporter(argParser.opts.reporter)
.concurrency(argParser.opts.concurrency)
.filter(argParser.filter)
.video(opts.video, opts.videoOptions, opts.videoEncodingOptions)
.screenshots(opts.screenshots, opts.screenshotsOnFails, opts.screenshotPathPattern)
.startApp(opts.app, opts.appInitDelay);

Expand Down
57 changes: 44 additions & 13 deletions src/configuration/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import Promise from 'pinkie';
import { fsObjectExists, readFile } from '../utils/promisified-functions';
import debug from 'debug';
import { stat, readFile } from '../utils/promisified-functions';
import Option from './option';
import optionSource from './option-source';
import { cloneDeep, castArray } from 'lodash';
import { ensureOptionValue as ensureSslOptionValue } from '../utils/parse-ssl-options';
import { getSSLOptions } from '../utils/get-options';
import OPTION_NAMES from './option-names';
import getFilterFn from '../utils/get-filter-fn';
import resolvePathRelativelyCwd from '../utils/resolve-path-relatively-cwd';
import JSON5 from 'json5';
import warningMessage from '../notifications/warning-message';
import renderTemplate from '../utils/render-template';
import prepareReporters from '../utils/prepare-reporters';
import WARNING_MESSAGES from '../notifications/warning-message';

import {
DEFAULT_TIMEOUT,
DEFAULT_SPEED_VALUE,
Expand All @@ -19,6 +20,8 @@ import {
DEFAULT_CONCURRENCY_VALUE
} from './default-values';

const DEBUG_LOGGER = debug('testcafe:configuration');

const CONFIGURATION_FILENAME = '.testcaferc.json';

const OPTION_FLAG_NAMES = [
Expand Down Expand Up @@ -52,26 +55,56 @@ export default class Configuration {
return result;
}

static async _isConfigurationFileExists (path) {
try {
await stat(path);

return true;
}
catch (error) {
DEBUG_LOGGER(renderTemplate(WARNING_MESSAGES.cannotFindConfigurationFile, path, error.stack));

return false;
}
}

static _showConsoleWarning (message) {
process.stdout.write(message + '\n');
}

static _showWarningForError (error, warningTemplate, ...args) {
const message = renderTemplate(warningTemplate, ...args);

Configuration._showConsoleWarning(message);

DEBUG_LOGGER(message);
DEBUG_LOGGER(error);
}

async _load () {
if (!await fsObjectExists(this.filePath))
if (!await Configuration._isConfigurationFileExists(this.filePath))
return;

let configurationFileContent = null;

try {
configurationFileContent = await readFile(this.filePath);
}
catch (e) {
console.log(warningMessage.errorReadConfigFile); // eslint-disable-line no-console
catch (error) {
Configuration._showWarningForError(error, WARNING_MESSAGES.cannotReadConfigFile);

return;
}

try {
const optionsObj = JSON5.parse(configurationFileContent);

this._options = Configuration._fromObj(optionsObj);
}
catch (e) {
console.log(warningMessage.errorConfigFileCannotBeParsed); // eslint-disable-line no-console
catch (error) {
Configuration._showWarningForError(error, WARNING_MESSAGES.cannotParseConfigFile);

return;
}

await this._normalizeOptionsAfterLoad();
Expand Down Expand Up @@ -120,9 +153,7 @@ export default class Configuration {
if (!sslOptions)
return;

await Promise.all(Object.entries(sslOptions.value).map(async ([key, value]) => {
sslOptions.value[key] = await ensureSslOptionValue(key, value);
}));
sslOptions.value = await getSSLOptions(sslOptions.value);
}

_ensureOption (name, value, source) {
Expand Down Expand Up @@ -199,7 +230,7 @@ export default class Configuration {
const optionsStr = this._overridenOptions.map(option => `"${option}"`).join(', ');
const optionsSuffix = this._overridenOptions.length > 1 ? 's' : '';

console.log(renderTemplate(warningMessage.configOptionsWereOverriden, optionsStr, optionsSuffix)); // eslint-disable-line no-console
Configuration._showConsoleWarning(renderTemplate(WARNING_MESSAGES.configOptionsWereOverriden, optionsStr, optionsSuffix));

this._overridenOptions = [];
}
Expand Down
5 changes: 4 additions & 1 deletion src/configuration/option-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,8 @@ export default {
stopOnFirstFail: 'stopOnFirstFail',
selectorTimeout: 'selectorTimeout',
assertionTimeout: 'assertionTimeout',
pageLoadTimeout: 'pageLoadTimeout'
pageLoadTimeout: 'pageLoadTimeout',
videoPath: 'videoPath',
videoOptions: 'videoOptions',
videoEncodingOptions: 'videoEncodingOptions'
};
Loading

0 comments on commit daa552b

Please sign in to comment.