UNPKG

nightwatch

Version:

Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.

567 lines (462 loc) 13.8 kB
const path = require('path'); const fs = require('fs'); const mkpath = require('mkpath'); const glob = require('glob'); const lodashMerge = require('lodash.merge'); const {By} = require('selenium-webdriver'); const Logger = require('./logger'); const BrowserName = require('./browsername.js'); const LocateStrategy = require('./locatestrategy.js'); const PeriodicPromise = require('./periodic-promise.js'); const createPromise = require('./createPromise.js'); const isErrorObject = require('./isErrorObject.js'); const Screenshots = require('./screenshots.js'); const TimedCallback = require('./timed-callback.js'); const getFreePort = require('./getFreePort.js'); const requireModule = require('./requireModule.js'); const getAllClassMethodNames = require('./getAllClassMethodNames.js'); const printVersionInfo = require('./printVersionInfo.js'); const {filterStack, filterStackTrace, showStackTrace, stackTraceFilter, errorToStackTrace} = require('./stackTrace.js'); const formatRegExp = /%[sdj%]/g; 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 get tsFileExt() { return '.ts'; } static get jsFileExt() { return '.js'; } 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 isDefined(value) { return !Utils.isUndefined(value); } 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 }; } /** * @param {object|string} definition * @return {object} */ static convertToElementSelector(definition) { const selector = Utils.isString(definition) ? {selector: definition} : definition; return selector; } static isElementGlobal(selector) { return selector.webElementLocator instanceof By; } /** * @param {object} definition * @param {object} [props] * @return {object} */ static setElementSelectorProps(definition, props = {}) { const selector = Utils.convertToElementSelector(definition); if (!selector || Utils.isElementGlobal(selector)) { return selector; } Object.keys(props).forEach(function(key) { selector[key] = props[key]; }); return selector; } 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 psuedo-async fn with a callback as * the last argument if necessary. `asyncArgCount` is the expected argument * count if `fn` is already asynchronous. * * @deprecated * @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(message, selector, timeMS) { return String(message).replace(formatRegExp, function(exp) { if (exp === '%%') { return '%'; } switch (exp) { case '%s': return String(selector); case '%d': return Number(timeMS); default: return exp; } }); } 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 getOriginalStackTrace(commandFn) { let originalStackTrace; if (commandFn.stackTrace) { originalStackTrace = commandFn.stackTrace; } else { let err = new Error; Error.captureStackTrace(err, commandFn); originalStackTrace = err.stack; } return originalStackTrace; } // 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 isTsFile(fileName){ return (path.extname(fileName) === Utils.tsFileExt); } static isFileNameValid(fileName) { return [ Utils.jsFileExt, '.mjs', '.cjs', Utils.tsFileExt ].includes(path.extname(fileName)); } static checkPath(source, originalErr = null, followSymlinks = true) { return new Promise(function(resolve, reject) { if (glob.hasMagic(source)) { return resolve(); } fs[followSymlinks ? 'stat' : 'lstat'](source, function(err, stats) { if (err) { return reject(err.code === 'ENOENT' && originalErr || err); } resolve(stats); }); }); } /** * @param {string} source * @return {Promise} */ static isFolder(source) { return Utils.checkPath(source, null, false).then(stats => stats.isDirectory()); } /** * @param {string} source * @return {Promise} */ static readDir(source) { return new Promise(function(resolve, reject) { const callback = function(err, list) { if (err) { return reject(err); } resolve(list); }; glob.hasMagic(source) ? glob(source, callback) : fs.readdir(source, callback); }); } /** * * @param {string} sourcePath * @param {Array} namespace * @param {function} loadFn * @param {function} readSyncFn */ static readFolderRecursively(sourcePath, namespace = [], loadFn, readSyncFn) { let resources; if (glob.hasMagic(sourcePath)) { resources = glob.sync(sourcePath); } else if (Utils.isFunction(readSyncFn)) { const result = readSyncFn(sourcePath); sourcePath = result.sourcePath; resources = result.resources; } else { resources = fs.readdirSync(sourcePath); } resources.sort(); // makes the list predictable resources.forEach(resource => { if (path.isAbsolute(resource)) { sourcePath = path.dirname(resource); resource = path.basename(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 getPluginPath(pluginName) { return path.resolve(require.resolve(pluginName, { paths: [process.cwd()] })); } static singleSourceFile(argv = {}) { const {test, _source} = argv; if (Utils.isString(test)) { return Utils.fileExistsSync(test); } return Array.isArray(_source) && _source.length === 1 && Utils.fileExistsSync(_source[0]); } 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); } static alwaysDisplayError(err) { return [ 'TypeError', 'SyntaxError', 'ReferenceError', 'RangeError' ].includes(err.name); } static shouldReplaceStack(err) { return !Utils.alwaysDisplayError(err); } } lodashMerge(Utils, { PrimitiveTypes, BrowserName, LocateStrategy, Logger, isErrorObject, requireModule, createPromise, getAllClassMethodNames, filterStack, filterStackTrace, showStackTrace, stackTraceFilter, errorToStackTrace, PeriodicPromise, Screenshots, TimedCallback, getFreePort, printVersionInfo }); module.exports = Utils;