const fs = require('fs') const os = require('os') const path = require('path') const chalk = require('chalk') const getFunctionArguments = require('fn-args') const deepClone = require('lodash.clonedeep') const { convertColorToRGBA, isColorProperty } = require('./colorUtils') const Fuse = require('fuse.js') function deepMerge(target, source) { const merge = require('lodash.merge') return merge(target, source) } module.exports.genTestId = test => { return this.clearString(require('crypto').createHash('sha256').update(test.fullTitle()).digest('base64').slice(0, -2)) } module.exports.deepMerge = deepMerge module.exports.deepClone = deepClone module.exports.isGenerator = function (fn) { return fn.constructor.name === 'GeneratorFunction' } const isFunction = (module.exports.isFunction = function (fn) { return typeof fn === 'function' }) const isAsyncFunction = (module.exports.isAsyncFunction = function (fn) { if (!fn) return false return fn[Symbol.toStringTag] === 'AsyncFunction' }) module.exports.fileExists = function (filePath) { return fs.existsSync(filePath) } module.exports.isFile = function (filePath) { let filestat try { filestat = fs.statSync(filePath) } catch (err) { if (err.code === 'ENOENT') return false } if (!filestat) return false return filestat.isFile() } module.exports.getParamNames = function (fn) { if (fn.isSinonProxy) return [] return getFunctionArguments(fn) } module.exports.installedLocally = function () { return path.resolve(`${__dirname}/../`).indexOf(process.cwd()) === 0 } module.exports.methodsOfObject = function (obj, className) { const methods = [] const standard = ['constructor', 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'bind', 'apply', 'call', 'isPrototypeOf', 'propertyIsEnumerable'] function pushToMethods(prop) { try { if (!isFunction(obj[prop]) && !isAsyncFunction(obj[prop])) return } catch (err) { // can't access property return } if (standard.indexOf(prop) >= 0) return if (prop.indexOf('_') === 0) return methods.push(prop) } while (obj.constructor.name !== className) { Object.getOwnPropertyNames(obj).forEach(pushToMethods) obj = Object.getPrototypeOf(obj) if (!obj || !obj.constructor) break } return methods } module.exports.template = function (template, data) { return template.replace(/{{([^{}]*)}}/g, (a, b) => { const r = data[b] if (r === undefined) return '' return r.toString() }) } /** * Make first char uppercase. * @param {string} str * @returns {string | undefined} */ module.exports.ucfirst = function (str) { if (str) return str.charAt(0).toUpperCase() + str.substr(1) } /** * Make first char lowercase. * @param {string} str * @returns {string | undefined} */ module.exports.lcfirst = function (str) { if (str) return str.charAt(0).toLowerCase() + str.substr(1) } module.exports.chunkArray = function (arr, chunk) { let i let j const tmp = [] for (i = 0, j = arr.length; i < j; i += chunk) { tmp.push(arr.slice(i, i + chunk)) } return tmp } module.exports.clearString = function (str) { if (!str) return '' /* Replace forbidden symbols in string */ if (str.endsWith('.')) { str = str.slice(0, -1) } return str .replace(/ /g, '_') .replace(/"/g, "'") .replace(/\//g, '_') .replace(//g, ')') .replace(/:/g, '_') .replace(/\\/g, '_') .replace(/\|/g, '_') .replace(/\?/g, '.') .replace(/\*/g, '^') .replace(/'/g, '') } module.exports.decodeUrl = function (url) { /* Replace forbidden symbols in string */ return decodeURIComponent(decodeURIComponent(decodeURIComponent(url))) } module.exports.xpathLocator = { /** * @param {string} string * @returns {string} */ literal: string => { if (string.indexOf("'") > -1) { string = string .split("'", -1) .map(substr => `'${substr}'`) .join(',"\'",') return `concat(${string})` } return `'${string}'` }, /** * Combines passed locators into one disjunction one. * @param {string[]} locators * @returns {string} */ combine: locators => locators.join(' | '), } module.exports.test = { grepLines(array, startString, endString) { let startIndex = 0 let endIndex array.every((elem, index) => { if (elem === startString) { startIndex = index return true } if (elem === endString) { endIndex = index return false } return true }) return array.slice(startIndex + 1, endIndex) }, submittedData(dataFile) { return function (key) { if (!fs.existsSync(dataFile)) { const waitTill = new Date(new Date().getTime() + 1 * 1000) // wait for one sec for file to be created while (waitTill > new Date()) {} } if (!fs.existsSync(dataFile)) { throw new Error('Data file was not created in time') } const data = JSON.parse(fs.readFileSync(dataFile, 'utf8')) if (key) { return data.form[key] } return data } }, } function toCamelCase(name) { if (typeof name !== 'string') { return name } return name.replace(/-(\w)/gi, (_word, letter) => { return letter.toUpperCase() }) } module.exports.toCamelCase = toCamelCase function convertFontWeightToNumber(name) { const fontWeightPatterns = [ { num: 100, pattern: /^Thin$/i }, { num: 200, pattern: /^(Extra|Ultra)-?light$/i }, { num: 300, pattern: /^Light$/i }, { num: 400, pattern: /^(Normal|Regular|Roman|Book)$/i }, { num: 500, pattern: /^Medium$/i }, { num: 600, pattern: /^(Semi|Demi)-?bold$/i }, { num: 700, pattern: /^Bold$/i }, { num: 800, pattern: /^(Extra|Ultra)-?bold$/i }, { num: 900, pattern: /^(Black|Heavy)$/i }, ] if (/^[1-9]00$/.test(name)) { return Number(name) } const matches = fontWeightPatterns.filter(fontWeight => fontWeight.pattern.test(name)) if (matches.length) { return String(matches[0].num) } return name } function isFontWeightProperty(prop) { return prop === 'fontWeight' } module.exports.convertCssPropertiesToCamelCase = function (props) { const output = {} Object.keys(props).forEach(key => { const keyCamel = toCamelCase(key) if (isFontWeightProperty(keyCamel)) { output[keyCamel] = convertFontWeightToNumber(props[key]) } else if (isColorProperty(keyCamel)) { output[keyCamel] = convertColorToRGBA(props[key]) } else { output[keyCamel] = props[key] } }) return output } module.exports.deleteDir = function (dir_path) { if (fs.existsSync(dir_path)) { fs.readdirSync(dir_path).forEach(function (entry) { const entry_path = path.join(dir_path, entry) if (fs.lstatSync(entry_path).isDirectory()) { this.deleteDir(entry_path) } else { fs.unlinkSync(entry_path) } }) fs.rmdirSync(dir_path) } } /** * Returns absolute filename to save screenshot. * @param fileName {string} - filename. */ module.exports.screenshotOutputFolder = function (fileName) { const fileSep = path.sep if (!fileName.includes(fileSep) || fileName.includes('record_')) { return path.resolve(global.output_dir, fileName) } return path.resolve(global.codecept_dir, fileName) } module.exports.relativeDir = function (fileName) { return fileName.replace(global.codecept_dir, '').replace(/^\//, '') } module.exports.beautify = function (code) { const format = require('js-beautify').js return format(code, { indent_size: 2, space_in_empty_paren: true }) } function shouldAppendBaseUrl(url) { return !/^\w+\:\/\//.test(url) } function trimUrl(url) { const firstChar = url.substr(1) if (firstChar === '/') { url = url.slice(1) } return url } function joinUrl(baseUrl, url) { return shouldAppendBaseUrl(url) ? `${baseUrl}/${trimUrl(url)}` : url } module.exports.appendBaseUrl = function (baseUrl = '', oneOrMoreUrls) { if (typeof baseUrl !== 'string') { throw new Error(`Invalid value for baseUrl: ${baseUrl}`) } if (!(typeof oneOrMoreUrls === 'string' || Array.isArray(oneOrMoreUrls))) { throw new Error(`Expected type of Urls is 'string' or 'array', Found '${typeof oneOrMoreUrls}'.`) } // Remove '/' if it's at the end of baseUrl const lastChar = baseUrl.substr(-1) if (lastChar === '/') { baseUrl = baseUrl.slice(0, -1) } if (!Array.isArray(oneOrMoreUrls)) { return joinUrl(baseUrl, oneOrMoreUrls) } return oneOrMoreUrls.map(url => joinUrl(baseUrl, url)) } /** * Recursively search key in object and replace it's value. * * @param {*} obj source object for replacing * @param {string} key key to search * @param {*} value value to set for key */ module.exports.replaceValueDeep = function replaceValueDeep(obj, key, value) { if (!obj) return if (obj instanceof Array) { for (const i in obj) { replaceValueDeep(obj[i], key, value) } } if (Object.prototype.hasOwnProperty.call(obj, key)) { obj[key] = value } if (typeof obj === 'object' && obj !== null) { const children = Object.values(obj) for (const child of children) { replaceValueDeep(child, key, value) } } return obj } module.exports.ansiRegExp = function ({ onlyFirst = false } = {}) { const pattern = ['[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'].join('|') return new RegExp(pattern, onlyFirst ? undefined : 'g') } module.exports.tryOrDefault = function (fn, defaultValue) { try { return fn() } catch (_) { return defaultValue } } function normalizeKeyReplacer(match, prefix, key, suffix, offset, string) { if (typeof key !== 'string') { return string } const normalizedKey = key.charAt(0).toUpperCase() + key.substr(1).toLowerCase() let position = '' if (typeof prefix === 'string') { position = prefix } else if (typeof suffix === 'string') { position = suffix } return normalizedKey + position.charAt(0).toUpperCase() + position.substr(1).toLowerCase() } /** * Transforms `key` into normalized to OS key. * @param {string} key * @returns {string} */ module.exports.getNormalizedKeyAttributeValue = function (key) { // Use operation modifier key based on operating system key = key.replace(/(Ctrl|Control|Cmd|Command)[ _]?Or[ _]?(Ctrl|Control|Cmd|Command)/i, os.platform() === 'darwin' ? 'Meta' : 'Control') // Selection of keys (https://www.w3.org/TR/uievents-key/#named-key-attribute-values) // which can be written in various ways and should be normalized. // For example 'LEFT ALT', 'ALT_Left', 'alt left' or 'LeftAlt' will be normalized as 'AltLeft'. key = key.replace(/^\s*(?:(Down|Left|Right|Up)[ _]?)?(Arrow|Alt|Ctrl|Control|Cmd|Command|Meta|Option|OS|Page|Shift|Super)(?:[ _]?(Down|Left|Right|Up|Gr(?:aph)?))?\s*$/i, normalizeKeyReplacer) // Map alias to corresponding key value key = key.replace(/^(Add|Divide|Decimal|Multiply|Subtract)$/, 'Numpad$1') key = key.replace(/^AltGr$/, 'AltGraph') key = key.replace(/^(Cmd|Command|Os|Super)/, 'Meta') key = key.replace('Ctrl', 'Control') key = key.replace('Option', 'Alt') key = key.replace(/^(NumpadComma|Separator)$/, 'Comma') return key } const modifierKeys = ['Alt', 'AltGraph', 'AltLeft', 'AltRight', 'Control', 'ControlLeft', 'ControlRight', 'Meta', 'MetaLeft', 'MetaRight', 'Shift', 'ShiftLeft', 'ShiftRight'] module.exports.modifierKeys = modifierKeys module.exports.isModifierKey = function (key) { return modifierKeys.includes(key) } module.exports.requireWithFallback = function (...packages) { const exists = function (pkg) { try { require.resolve(pkg) } catch (e) { return false } return true } for (const pkg of packages) { if (exists(pkg)) { return require(pkg) } } throw new Error(`Cannot find modules ${packages.join(',')}`) } module.exports.isNotSet = function (obj) { if (obj === null) return true if (obj === undefined) return true return false } module.exports.emptyFolder = async directoryPath => { require('child_process').execSync(`rm -rf ${directoryPath}/*`) } module.exports.printObjectProperties = obj => { if (typeof obj !== 'object' || obj === null) { return obj } let result = '' for (const [key, value] of Object.entries(obj)) { result += `${key}: "${value}"; ` } return `{${result}}` } module.exports.normalizeSpacesInString = string => { return string.replace(/\s+/g, ' ') } module.exports.humanizeFunction = function (fn) { const fnStr = fn.toString().trim() // Remove arrow function syntax, async, and parentheses let simplified = fnStr .replace(/^async\s*/, '') .replace(/^\([^)]*\)\s*=>/, '') .replace(/^function\s*\([^)]*\)/, '') // Remove curly braces and any whitespace around them .replace(/{\s*(.*)\s*}/, '$1') // Remove return statement .replace(/return\s+/, '') // Remove trailing semicolon .replace(/;$/, '') .trim() if (simplified.length > 100) { simplified = simplified.slice(0, 97) + '...' } return simplified } /** * Searches through a given data source using the Fuse.js library for fuzzy searching. * * @function searchWithFusejs * @param {Array|Object} source - The data source to search through. This can be an array of objects or strings. * @param {string} searchString - The search query string to match against the source. * @param {Object} [opts] - Optional configuration object for Fuse.js. * @param {boolean} [opts.includeScore=true] - Whether to include the score of the match in the results. * @param {number} [opts.threshold=0.6] - Determines the match threshold; lower values mean stricter matching. * @param {boolean} [opts.caseSensitive=false] - Whether the search should be case-sensitive. * @param {number} [opts.distance=100] - Determines how far apart the search term is allowed to be from the target. * @param {number} [opts.maxPatternLength=32] - The maximum length of the search pattern. Patterns longer than this are ignored. * @param {boolean} [opts.ignoreLocation=false] - Whether the location of the match is ignored when scoring. * @param {boolean} [opts.ignoreFieldNorm=false] - When true, the field's length is not considered when scoring. * @param {Array} [opts.keys=[]] - List of keys to search in the objects of the source array. * @param {boolean} [opts.shouldSort=true] - Whether the results should be sorted by score. * @param {string} [opts.sortFn] - A custom sorting function for sorting results. * @param {number} [opts.minMatchCharLength=1] - The minimum number of characters that must match. * @param {boolean} [opts.useExtendedSearch=false] - Enables extended search capabilities. * * @returns {Array} - An array of search results. Each result contains an item and, if `includeScore` is true, a score. * * @example * const data = [ * { title: "Old Man's War", author: "John Scalzi" }, * { title: "The Lock Artist", author: "Steve Hamilton" }, * ]; * * const options = { * keys: ['title', 'author'], * includeScore: true, * threshold: 0.4, * caseSensitive: false, * distance: 50, * ignoreLocation: true, * }; * * const results = searchWithFusejs(data, 'lock', options); * console.log(results); */ module.exports.searchWithFusejs = function (source, searchString, opts) { const fuse = new Fuse(source, opts) return fuse.search(searchString) } module.exports.humanizeString = function (string) { // split strings by words, then make them all lowercase const _result = string .replace(/([a-z](?=[A-Z]))/g, '$1 ') .split(' ') .map(word => word.toLowerCase()) _result[0] = _result[0] === 'i' ? this.ucfirst(_result[0]) : _result[0] return _result.join(' ').trim() } module.exports.serializeError = function (error) { if (error) { const { stack, uncaught, message, actual, expected } = error return { stack, uncaught, message, actual, expected } } return null } module.exports.base64EncodeFile = function (filePath) { return Buffer.from(fs.readFileSync(filePath)).toString('base64') } module.exports.markdownToAnsi = function (markdown) { return ( markdown // Headers (# Text) - make blue and bold .replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, text) => { return chalk.bold.blue(`${hashes} ${text}`) }) // Bullet points - replace with yellow bullet character .replace(/^[-*]\s+(.+)$/gm, (_, text) => { return `${chalk.yellow('•')} ${text}` }) // Bold (**text**) - make bold .replace(/\*\*(.+?)\*\*/g, (_, text) => { return chalk.bold(text) }) // Italic (*text*) - make italic (dim in terminals) .replace(/\*(.+?)\*/g, (_, text) => { return chalk.italic(text) }) ) }