nightwatch
Version:
Easy to use Node.js based End-to-End testing solution for browser based apps and websites, using the W3C WebDriver API.
530 lines (424 loc) • 13.1 kB
JavaScript
const path = require('path');
const fs = require('fs');
const mkpath = require('mkpath');
const BrowserName = require('./browsername.js');
const LocateStrategy = require('./locatestrategy.js');
const Logger = require('./logger.js');
const PeriodicPromise = require('./periodic-promise.js');
const createPromise = require('./createPromise.js');
const Screenshots = require('./screenshots.js');
const TimedCallback = require('./timed-callback.js');
const formatRegExp = /%[sdj%]/g;
const indentRegex = /^/gm;
const testSuiteNameRegxp = /(_|-|\.)*([A-Z]*)/g;
const nameSeparatorRegxp = /(\s|\/)/;
const PrimitiveTypes = {
OBJECT: 'object',
FUNCTION: 'function',
BOOLEAN: 'boolean',
NUMBER: 'number',
STRING: 'string',
UNDEFINED: 'undefined'
};
class Utils {
static isObject(obj) {
return obj !== null && typeof obj == 'object';
}
static isFunction(fn) {
return typeof fn == PrimitiveTypes.FUNCTION;
}
static isBoolean(value) {
return typeof value == PrimitiveTypes.BOOLEAN;
}
static isNumber(value) {
return typeof value == PrimitiveTypes.NUMBER;
}
static isString(value) {
return typeof value == PrimitiveTypes.STRING;
}
static isUndefined(value) {
return typeof value == PrimitiveTypes.UNDEFINED;
}
static isES6AsyncFn(fn) {
return Utils.isFunction(fn) && fn.constructor.name === 'AsyncFunction';
}
static enforceType(value, type) {
type = type.toLowerCase();
switch (type) {
case PrimitiveTypes.STRING:
case PrimitiveTypes.BOOLEAN:
case PrimitiveTypes.NUMBER:
case PrimitiveTypes.FUNCTION:
if (typeof value != type) {
throw new Error(`Invalid type ${typeof value} for value "${value}". Expecting "${type}" instead.`);
}
return;
}
throw new Error(`Invalid type ${type} for ${value}`);
}
static convertBoolean(value) {
if (Utils.isString(value) && (!value || value === 'false' || value === '0')) {
return false;
}
return Boolean(value);
}
static get symbols() {
let ok = String.fromCharCode(10004);
let fail = String.fromCharCode(10006);
if (process.platform === 'win32') {
ok = '\u221A';
fail = '\u00D7';
}
return {
ok: ok,
fail: fail
};
}
static formatElapsedTime(timeMs, includeMs = false) {
let seconds = timeMs/1000;
return (seconds < 1 && timeMs + 'ms') ||
(seconds > 1 && seconds < 60 && (seconds + 's')) ||
(Math.floor(seconds/60) + 'm' + ' ' + Math.floor(seconds%60) + 's' + (includeMs ? (' / ' + timeMs + 'ms') : ''));
}
/**
* Wrap a synchronous function, turning it into an async fn with a callback as
* the last argument if necessary. `asyncArgCount` is the expected argument
* count if `fn` is already asynchronous.
*
* @param {number} asyncArgCount
* @param {function} fn
* @param {object} [context]
*/
static makeFnAsync(asyncArgCount, fn, context) {
if (fn.length === asyncArgCount) {
return fn;
}
return function(...args) {
let done = args.pop();
context = context || null;
fn.apply(context, args);
done();
};
}
static makePromise(handler, context, args) {
const result = Reflect.apply(handler, context, args);
if (result instanceof Promise) {
return result;
}
return Promise.resolve(result);
}
static checkFunction(name, parent) {
return parent && (typeof parent[name] == 'function') && parent[name] || false;
}
static getTestSuiteName(moduleName) {
let words;
moduleName = moduleName.replace(testSuiteNameRegxp, function(match, $0, $1, offset, string) {
if (!match) {
return '';
}
return (offset > 0 && (string.charAt(offset-1) !== ' ') ? ' ':'') + $1;
});
words = moduleName.split(nameSeparatorRegxp).map(function(word, index, matches) {
if (word === '/') {
return '/';
}
return word.charAt(0).toUpperCase() + word.substr(1);
});
return words.join('');
}
/**
* A smaller version of util.format that doesn't support json and
* if a placeholder is missing, it is omitted instead of appended
*
* @param f
* @returns {string}
*/
static format(f) {
let i = 1;
let args = arguments;
let len = args.length;
return String(f).replace(formatRegExp, function(x) {
if (x === '%%') {
return '%';
}
if (i >= len) {
return x;
}
switch (x) {
case '%s':
return String(args[i++]);
case '%d':
return Number(args[i++]);
default:
return x;
}
});
}
static getModuleKey(filePath, srcFolders, fullPaths) {
let modulePathParts = filePath.split(path.sep);
let diffInFolder = '';
let folder = '';
let parentFolder = '';
let moduleName = modulePathParts.pop();
filePath = modulePathParts.join(path.sep);
if (srcFolders) {
for (let i = 0; i < srcFolders.length; i++) {
folder = path.resolve(srcFolders[i]);
if (fullPaths.length > 1) {
parentFolder = folder.split(path.sep).pop();
}
if (filePath.indexOf(folder) === 0) {
diffInFolder = filePath.substring(folder.length + 1);
break;
}
}
}
return path.join(parentFolder, diffInFolder, moduleName);
}
static stackTraceFilter(parts) {
let stack = parts.reduce(function(list, line) {
if (contains(line, [
'node_modules',
'(node.js:',
'(timers.js:',
'(events.js:',
'(util.js:',
'(net.js:'
])) {
return list;
}
list.push(line);
return list;
}, []);
return stack.join('\n');
}
static addDetailedError(err) {
let detailedErr;
if (err instanceof TypeError) {
if (/\.page\..+ is not a function$/.test(err.message)) {
detailedErr = '- verify if page objects are setup correctly, check "page_objects_path" in your config';
} else if (/\w is not a function$/.test(err.message)) {
detailedErr = '- writing an ES6 async test case? - keep in mind that commands return a Promise; \n - writing unit tests? - make sure to specify "unit_tests_mode=true" in your config.';
}
}
if (detailedErr) {
err.detailedErr = detailedErr;
}
}
/**
* @param {Error} err
*/
static errorToStackTrace(err) {
if (!Utils.isErrorObject(err)) {
err = new Error(err);
}
Utils.addDetailedError(err);
const colors = require('./logger.js').colors;
let headline = err.message ? `${err.name}: ${err.message}` : err.name;
if (err.detailedErr) {
headline += `\n ${err.detailedErr}`;
}
headline = colors.red(headline.replace(indentRegex, ' '));
let stackTrace = Utils.filterStack(err);
stackTrace = colors.stack_trace(stackTrace.replace(indentRegex, ' '));
return `${headline}\n${stackTrace}`;
}
static filterStack(err) {
if (err instanceof Error) {
const stackTrace = err.stack.split('\n').slice(1);
return Utils.stackTraceFilter(stackTrace);
}
return '';
}
static showStackTrace(stack) {
let parts = stack.split('\n');
let headline = parts.shift();
const colors = require('./logger.js').colors;
console.error(colors.red(headline.replace(indentRegex, ' ')));
if (parts.length > 0) {
let result = Utils.stackTraceFilter(parts);
console.error(colors.stack_trace(result.replace(indentRegex, ' ')));
}
}
static isErrorObject(err) {
return err instanceof Error || Object.prototype.toString.call(err) === '[object Error]';
}
// util to replace deprecated fs.existsSync
static dirExistsSync(path) {
try {
return fs.statSync(path).isDirectory(); // eslint-disable-next-line no-empty
} catch (e) {}
return false;
}
static fileExistsSync(path) {
try {
return fs.statSync(path).isFile(); // eslint-disable-next-line no-empty
} catch (e) {}
return false;
}
static fileExists(path) {
return Utils.checkPath(path)
.then(function(stats) {
return stats.isFile();
})
.catch(function(err) {
return false;
});
}
static isFileNameValid(fileName) {
return path.extname(fileName) === '.js';
}
static checkPath(source, originalErr = null, followSymlinks = true) {
return new Promise(function(resolve, reject) {
fs[followSymlinks ? 'stat' : 'lstat'](source, function(err, stats) {
if (err) {
return reject(err.code === 'ENOENT' && originalErr || err);
}
resolve(stats);
});
});
}
static isFolder(source) {
return Utils.checkPath(source, null, false).then(stats => stats.isDirectory());
}
static readDir(source) {
return new Promise(function(resolve, reject) {
fs.readdir(source, function(err, list) {
if (err) {
return reject(err);
}
resolve(list);
});
});
}
/**
*
* @param {string} sourcePath
* @param {Array} namespace
* @param {function} loadFn
*/
static readFolderRecursively(sourcePath, namespace = [], loadFn) {
const resources = fs.readdirSync(sourcePath);
resources.forEach(resource => {
const isFolder = fs.lstatSync(path.join(sourcePath, resource)).isDirectory();
if (isFolder) {
const pathFolder = path.join(sourcePath, resource);
let ns = namespace.slice(0);
ns.push(resource);
Utils.readFolderRecursively(pathFolder, ns, loadFn);
return;
}
loadFn(sourcePath, resource, namespace);
});
}
static getConfigFolder(argv) {
if (!argv || !argv.config) {
return '';
}
return path.dirname(argv.config);
}
/**
*
* @param {Array} arr
* @param {number} maxDepth
* @param {Boolean} includeEmpty
* @returns {Array}
*/
static flattenArrayDeep(arr, maxDepth = 4, includeEmpty = false) {
if (!Array.isArray(arr)) {
throw new Error(`Utils.flattenArrayDeep excepts an array to be passed. Received: "${arr === null ? arr : typeof arr}".`);
}
return (function flatten(currentArray, currentDepth, initialValue = []) {
currentDepth = currentDepth + 1;
return currentArray.reduce(function(prev, value) {
if (Array.isArray(value)) {
let result = prev.concat(value);
if (Array.isArray(result) && currentDepth <= maxDepth) {
return flatten(result, currentDepth);
}
return result;
}
currentDepth = 0;
if (!includeEmpty && (value === null || value === undefined || value === '')) {
return prev;
}
prev.push(value);
return prev;
}, initialValue);
})(arr, 0);
}
/**
* Strips out all control characters from a string
* However, excludes newline and carriage return
*
* @param {string} input String to remove invisible chars from
* @returns {string} Initial input string but without invisible chars
*/
static stripControlChars(input) {
return input && input.replace(
// eslint-disable-next-line no-control-regex
/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g,
''
);
}
static relativeUrl(url) {
return !(url.includes('://'));
}
static uriJoin(baseUrl, uriPath) {
let result = baseUrl;
if (baseUrl.endsWith('/')) {
result = result.substring(0, result.length - 1);
}
if (!uriPath.startsWith('/')) {
result = result + '/';
}
return result + uriPath;
}
static replaceParams(url, params = {}) {
return Object.keys(params).reduce(function(prev, param) {
prev = prev.replace(`:${param}`, params[param]);
return prev;
}, url);
}
static createFolder(dirPath) {
return new Promise((resolve, reject) => {
mkpath(dirPath, function(err) {
if (err) {
return reject(err);
}
resolve();
});
});
}
static containsMultiple(arrayOrString, valueToFind, separator = ',') {
if (typeof valueToFind == 'string') {
valueToFind = valueToFind.split(separator);
}
if (Array.isArray(valueToFind)) {
if (valueToFind.length > 1) {
return valueToFind.every(item => arrayOrString.includes(item));
}
valueToFind = valueToFind[0];
}
return arrayOrString.includes(valueToFind);
}
}
function contains(str, text) {
if (Array.isArray(text)) {
for (let i = 0; i < text.length; i++) {
if (contains(str, text[i])) {
return true;
}
}
}
return str.includes(text);
}
module.exports = Utils;
module.exports.PrimitiveTypes = PrimitiveTypes;
module.exports.BrowserName = BrowserName;
module.exports.LocateStrategy = LocateStrategy;
module.exports.Logger = Logger;
module.exports.createPromise = createPromise;
module.exports.PeriodicPromise = PeriodicPromise;
module.exports.Screenshots = Screenshots;
module.exports.TimedCallback = TimedCallback;