testplane
Version:
Tests framework based on mocha and wdio
372 lines • 17 kB
JavaScript
;
const _ = require("lodash");
const fs = require("fs-extra");
const { option, section } = require("gemini-configparser");
const defaults = require("./defaults");
const optionsBuilder = require("./options-builder");
const utils = require("./utils");
const { WEBDRIVER_PROTOCOL, DEVTOOLS_PROTOCOL, SAVE_HISTORY_MODE, ENV_PREFIXES } = require("../constants/config");
const { BROWSERS_SUPPORT_BIDI } = require("../constants/browser");
const { isSupportIsolation } = require("../utils/browser");
const { TimeTravelMode } = require("./types");
const { extractSelectivityEnabledEnvVariable } = require("./utils");
const is = utils.is;
function provideRootDefault(name) {
return () => _.get(defaults, name);
}
exports.getTopLevel = () => {
return buildBrowserOptions(provideRootDefault, {
desiredCapabilities: optionsBuilder(provideRootDefault).optionalObject("desiredCapabilities"),
});
};
exports.getPerBrowser = () => {
return buildBrowserOptions(provideTopLevelDefault, {
desiredCapabilities: option({
defaultValue: defaults.desiredCapabilities,
parseEnv: JSON.parse,
parseCli: JSON.parse,
validate: (value, config) => {
if (_.isNull(value) && _.isNull(config.desiredCapabilities)) {
throw new Error('Each browser must have "desiredCapabilities" option');
}
const { browserName, browserVersion, webSocketUrl } = value;
const browserSupportBidi = BROWSERS_SUPPORT_BIDI.find(b => b.name === browserName);
if (webSocketUrl === true && browserSupportBidi && browserVersion) {
const browserVersionMajor = browserVersion.split(".")[0];
if (Number(browserVersionMajor) < browserSupportBidi.minVersion) {
throw new Error(`BiDi protocol is not supported in ${browserName}@${browserVersion}, use ${browserName}@${browserSupportBidi.minVersion}.0 and higher`);
}
}
utils.assertOptionalObject(value, "desiredCapabilities");
},
map: (value, config) => _.extend({}, config.desiredCapabilities, value),
}),
});
};
function provideTopLevelDefault(name) {
return config => {
const value = _.get(config, name);
if (_.isUndefined(value)) {
throw new Error(`"${name}" should be set at the top level or per-browser option`);
}
return value;
};
}
function buildBrowserOptions(defaultFactory, extra) {
const options = optionsBuilder(defaultFactory);
return _.extend(extra, {
gridUrl: options.string("gridUrl"),
baseUrl: option({
defaultValue: defaultFactory("baseUrl"),
validate: is("string", "baseUrl"),
map: (value, config) => {
return config.baseUrl && !value.match(/^https?:\/\//)
? [config.baseUrl.replace(/\/$/, ""), value.replace(/^\//, "")].join("/")
: value;
},
}),
browserWSEndpoint: option({
defaultValue: defaultFactory("browserWSEndpoint"),
validate: value => {
if (_.isNull(value)) {
return;
}
is("string", "browserWSEndpoint")(value);
if (!/wss?:\/\//.test(value)) {
throw new Error(`"browserWSEndpoint" must start with "ws://" or "wss://" prefix`);
}
},
}),
automationProtocol: option({
defaultValue: defaultFactory("automationProtocol"),
validate: value => {
is("string", "automationProtocol")(value);
if (value !== WEBDRIVER_PROTOCOL && value !== DEVTOOLS_PROTOCOL) {
throw new Error(`"automationProtocol" must be "${WEBDRIVER_PROTOCOL}" or "${DEVTOOLS_PROTOCOL}"`);
}
},
}),
sessionEnvFlags: option({
defaultValue: defaultFactory("sessionEnvFlags"),
validate: value => {
if (!_.isPlainObject(value)) {
throw new Error('"sessionEnvFlags" must be an object');
}
if (_.isEmpty(value)) {
return;
}
const availableSessionEnvFlags = [
"isW3C",
"isChrome",
"isMobile",
"isIOS",
"isAndroid",
"isSauce",
"isSeleniumStandalone",
];
Object.keys(value).forEach(key => {
if (!availableSessionEnvFlags.includes(key)) {
throw new Error(`keys of "sessionEnvFlags" must be one of: ${availableSessionEnvFlags.join(", ")}`);
}
if (!_.isBoolean(value[key])) {
throw new Error('values of "sessionEnvFlags" must be boolean');
}
});
},
}),
sessionsPerBrowser: options.positiveInteger("sessionsPerBrowser"),
testsPerSession: options.positiveIntegerOrInfinity("testsPerSession"),
retry: options.nonNegativeInteger("retry"),
shouldRetry: options.optionalFunction("shouldRetry"),
httpTimeout: options.nonNegativeInteger("httpTimeout"),
urlHttpTimeout: options.optionalNonNegativeInteger("urlHttpTimeout"),
pageLoadTimeout: options.optionalNonNegativeInteger("pageLoadTimeout"),
sessionRequestTimeout: options.optionalNonNegativeInteger("sessionRequestTimeout"),
sessionQuitTimeout: options.optionalNonNegativeInteger("sessionQuitTimeout"),
testTimeout: options.optionalNonNegativeInteger("testTimeout"),
waitTimeout: options.positiveInteger("waitTimeout"),
waitInterval: options.positiveInteger("waitInterval"),
saveHistoryMode: option({
defaultValue: defaultFactory("saveHistoryMode"),
validate: value => {
const availableValues = Object.values(SAVE_HISTORY_MODE);
if (!availableValues.includes(value)) {
throw new Error(`"saveHistoryMode" must be one of: ${availableValues.join(", ")}`);
}
},
}),
takeScreenshotOnFails: option({
defaultValue: defaultFactory("takeScreenshotOnFails"),
parseEnv: JSON.parse,
parseCli: JSON.parse,
validate: value => {
if (!_.isPlainObject(value)) {
throw new Error('"takeScreenshotOnFails" must be an object');
}
const allowedProps = ["assertViewFail", "testFail"];
const unknownProps = _.keys(value).filter(prop => !allowedProps.includes(prop));
if (unknownProps.length) {
throw new Error(`"takeScreenshotOnFails" contains unknown properties: ${unknownProps}. Allowed: ${allowedProps}.`);
}
},
map: (value, config) => ({
...defaultFactory("takeScreenshotOnFails")(config),
...value,
}),
}),
takeScreenshotOnFailsTimeout: options.optionalNonNegativeInteger("takeScreenshotOnFailsTimeout"),
takeScreenshotOnFailsMode: options.enumeration("takeScreenshotOnFailsMode", ["fullpage", "viewport"]),
prepareBrowser: options.optionalFunction("prepareBrowser"),
screenshotsDir: option({
defaultValue: defaultFactory("screenshotsDir"),
validate: value => {
if (!_.isString(value) && !_.isFunction(value)) {
throw new Error('"screenshotsDir" must be a string or function');
}
},
map: (value, config, __, { isSetByUser }) => {
if (isSetByUser) {
return value;
}
const topLevelScreenshotsDir = _.get(config, "screenshotsDir");
if (topLevelScreenshotsDir) {
return topLevelScreenshotsDir;
}
const deprecatedScreensPath = "hermione/screens";
return fs.existsSync(deprecatedScreensPath) && !fs.existsSync(value) ? deprecatedScreensPath : value;
},
}),
calibrate: options.boolean("calibrate"),
compositeImage: options.boolean("compositeImage"),
strictTestsOrder: options.boolean("strictTestsOrder"),
screenshotMode: options.enumeration("screenshotMode", ["fullpage", "viewport", "auto"], {
map: (value, config, currentNode) => {
if (value !== defaults.screenshotMode) {
return value;
}
// Chrome mobile returns screenshots that are larger than visible viewport due to a bug:
// https://bugs.chromium.org/p/chromedriver/issues/detail?id=2853
// Due to this, screenshot is cropped incorrectly.
const capabilities = _.get(currentNode, "desiredCapabilities");
const isAndroid = capabilities &&
Boolean((capabilities.platformName && capabilities.platformName.match(/Android/i)) ||
(capabilities.browserName && capabilities.browserName.match(/Android/i)));
return isAndroid ? "viewport" : value;
},
}),
screenshotDelay: options.nonNegativeInteger("screenshotDelay"),
tolerance: option({
defaultValue: defaultFactory("tolerance"),
parseEnv: Number,
parseCli: Number,
validate: value => utils.assertNonNegativeNumber(value, "tolerance"),
}),
antialiasingTolerance: option({
defaultValue: defaultFactory("antialiasingTolerance"),
parseEnv: Number,
parseCli: Number,
validate: value => utils.assertNonNegativeNumber(value, "antialiasingTolerance"),
}),
disableAnimation: options.boolean("disableAnimation"),
compareOpts: options.optionalObject("compareOpts"),
buildDiffOpts: options.optionalObject("buildDiffOpts"),
assertViewOpts: option({
defaultValue: defaultFactory("assertViewOpts"),
parseEnv: JSON.parse,
parseCli: JSON.parse,
validate: value => utils.assertOptionalObject(value, "assertViewOpts"),
map: value => {
return value === defaults.assertViewOpts ? value : { ...defaults.assertViewOpts, ...value };
},
}),
openAndWaitOpts: option({
defaultValue: defaultFactory("openAndWaitOpts"),
parseEnv: JSON.parse,
parseCli: JSON.parse,
validate: value => utils.assertOptionalObject(value, "openAndWaitOpts"),
map: value => ({ ...defaults.openAndWaitOpts, ...value }),
}),
meta: options.optionalObject("meta"),
windowSize: option({
defaultValue: defaultFactory("windowSize"),
validate: value => {
if (_.isNull(value)) {
return;
}
if (_.isObject(value)) {
if (_.isNumber(value.width) && _.isNumber(value.height)) {
return;
}
else {
throw new Error('"windowSize" must be an object with "width" and "height" keys');
}
}
if (!_.isString(value)) {
throw new Error('"windowSize" must be string, object or null');
}
else if (!/^\d+x\d+$/.test(value)) {
throw new Error('"windowSize" should have form of <width>x<height> (i.e. 1600x1200)');
}
},
map: value => {
if (_.isNull(value) || _.isObject(value)) {
return value;
}
const [width, height] = value.split("x").map(v => parseInt(v, 10));
return { width, height };
},
}),
orientation: option({
defaultValue: defaultFactory("orientation"),
validate: value => {
if (_.isNull(value)) {
return;
}
is("string", "orientation")(value);
if (value !== "landscape" && value !== "portrait") {
throw new Error('"orientation" must be "landscape" or "portrait"');
}
},
}),
waitOrientationChange: options.boolean("waitOrientationChange"),
resetCursor: options.boolean("resetCursor"),
outputDir: options.optionalString("outputDir"),
agent: options.optionalObject("agent"),
headers: options.optionalObject("headers"),
transformRequest: options.optionalFunction("transformRequest"),
transformResponse: options.optionalFunction("transformResponse"),
strictSSL: options.optionalBoolean("strictSSL"),
user: options.optionalString("user"),
key: options.optionalString("key"),
region: options.optionalString("region"),
headless: option({
defaultValue: defaultFactory("headless"),
parseCli: value => {
try {
return utils.parseBoolean(value);
}
catch (_) {
return value;
}
},
parseEnv: value => {
try {
return utils.parseBoolean(value);
}
catch (_) {
return value;
}
},
validate: value => {
if (_.isNull(value) || _.isBoolean(value)) {
return;
}
if (typeof value !== "string") {
throw new Error('"headless" option should be boolean or string with "new" or "old" values');
}
if (value !== "old" && value !== "new") {
throw new Error(`"headless" option should be "new" or "old", but got "${value}"`);
}
},
}),
isolation: option({
defaultValue: defaultFactory("isolation"),
parseCli: value => utils.parseBoolean(value, "isolation"),
parseEnv: value => utils.parseBoolean(value, "isolation"),
validate: value => _.isNull(value) || is("boolean", "isolation")(value),
map: (value, config, currentNode, meta) => {
if (meta.isSetByUser || !_.isNull(value)) {
return value;
}
const caps = _.get(currentNode, "desiredCapabilities");
return caps ? isSupportIsolation(caps.browserName, caps.browserVersion) : value;
},
}),
passive: options.boolean("passive"),
timeTravel: option({
defaultValue: defaultFactory("timeTravel"),
parseEnv: JSON.parse,
parseCli: JSON.parse,
validate: value => {
const validateMode = mode => {
if (!Object.values(TimeTravelMode).includes(mode)) {
throw new Error(`TimeTravel mode must be one of the following strings: ${Object.values(TimeTravelMode).join(", ")}. Got: ${JSON.stringify(mode)}.`);
}
};
if (typeof value === "string") {
validateMode(value);
return true;
}
validateMode(value.mode);
return true;
},
map: value => {
if (typeof value === "string") {
return { mode: value };
}
return value;
},
}),
selectivity: option({
defaultValue: defaultFactory("selectivity"),
parseEnv: JSON.parse,
parseCli: JSON.parse,
validate: value => utils.assertOptionalObject(value, "selectivity"),
map: value => ({
...defaults.selectivity,
...value,
...extractSelectivityEnabledEnvVariable(ENV_PREFIXES),
}),
}),
stateOpts: section({
path: option({
defaultValue: defaultFactory("stateOpts.path"),
}),
cookies: options.optionalBoolean("stateOpts.cookies"),
localStorage: options.optionalBoolean("stateOpts.localStorage"),
sessionStorage: options.optionalBoolean("stateOpts.sessionStorage"),
keepFile: options.optionalBoolean("stateOpts.keepFile"),
}),
});
}
//# sourceMappingURL=browser-options.js.map