Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement video recording #3257

Merged
merged 3 commits into from
Jan 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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