@revoloo/cypress6
Version:
Cypress.io end to end testing tool
399 lines (313 loc) • 9.09 kB
JavaScript
const _ = require('lodash')
const capitalize = require('underscore.string/capitalize')
const methods = require('methods')
const moment = require('moment')
const $ = require('jquery')
const $jquery = require('../dom/jquery')
const $Location = require('./location')
const tagOpen = /\[([a-z\s='"-]+)\]/g
const tagClosed = /\[\/([a-z]+)\]/g
const quotesRe = /('|")/g
const defaultOptions = {
delay: 10,
force: false,
timeout: null,
interval: null,
multiple: false,
waitForAnimations: true,
animationDistanceThreshold: 5,
scrollBehavior: 'top',
}
const USER_FRIENDLY_TYPE_DETECTORS = _.map([
[_.isUndefined, 'undefined'],
[_.isNull, 'null'],
[_.isBoolean, 'boolean'],
[_.isNumber, 'number'],
[_.isString, 'string'],
[_.isRegExp, 'regexp'],
[_.isSymbol, 'symbol'],
[_.isElement, 'element'],
[_.isError, 'error'],
[_.isSet, 'set'],
[_.isWeakSet, 'set'],
[_.isMap, 'map'],
[_.isWeakMap, 'map'],
[_.isFunction, 'function'],
[_.isArrayLikeObject, 'array'],
[_.isBuffer, 'buffer'],
[_.isDate, 'date'],
[_.isObject, 'object'],
[_.stubTrue, 'unknown'],
], ([fn, type]) => {
return [fn, _.constant(type)]
})
module.exports = {
warning (msg) {
// eslint-disable-next-line no-console
return console.warn(`Cypress Warning: ${msg}`)
},
log (...msgs) {
// eslint-disable-next-line no-console
return console.log(...msgs)
},
monkeypatchBefore (origFn, fn) {
return function () {
const newArgs = fn.apply(this, arguments)
if (newArgs !== undefined) {
return origFn.apply(this, newArgs)
}
return origFn.apply(this, arguments)
}
},
unwrapFirst (val) {
// this method returns the first item in an array
// and if its still a jquery object, then we return
// the first() jquery element
const item = [].concat(val)[0]
if ($jquery.isJquery(item)) {
return item.first()
}
return item
},
switchCase (value, casesObj, defaultKey = 'default') {
if (_.has(casesObj, value)) {
return _.result(casesObj, value)
}
if (_.has(casesObj, defaultKey)) {
return _.result(casesObj, defaultKey)
}
const keys = _.keys(casesObj)
throw new Error(`The switch/case value: '${value}' did not match any cases: ${keys.join(', ')}.`)
},
reduceProps (obj, props = []) {
if (!obj) {
return null
}
return _.reduce(props, (memo, prop) => {
if (_.has(obj, prop) || obj[prop] !== undefined) {
memo[prop] = _.result(obj, prop)
}
return memo
}, {})
},
normalizeObjWithLength (obj) {
// lodash shits the bed if our object has a 'length'
// property so we have to normalize that
if (_.has(obj, 'length')) {
obj.Length = obj.length
delete obj.length
}
return obj
},
// return a new object if the obj
// contains the properties of filter
// and the values are different
filterOutOptions (obj, filter = {}) {
_.defaults(filter, defaultOptions)
this.normalizeObjWithLength(filter)
const whereFilterHasSameKeyButDifferentValue = (value, key) => {
const upperKey = capitalize(key)
return (_.has(filter, key) || _.has(filter, upperKey)) &&
filter[key] !== value
}
obj = _.pickBy(obj, whereFilterHasSameKeyButDifferentValue)
if (_.isEmpty(obj)) {
return undefined
}
return obj
},
stringifyActualObj (obj) {
obj = this.normalizeObjWithLength(obj)
const str = _.reduce(obj, (memo, value, key) => {
memo.push(`${`${key}`.toLowerCase()}: ${this.stringifyActual(value)}`)
return memo
}, [])
return `{${str.join(', ')}}`
},
stringifyActual (value) {
const $dom = require('../dom')
if ($dom.isDom(value)) {
return $dom.stringify(value, 'short')
}
if (_.isFunction(value)) {
return 'function(){}'
}
if (_.isArray(value)) {
const len = value.length
if (len > 3) {
return `Array[${len}]`
}
return `[${_.map(value, _.bind(this.stringifyActual, this)).join(', ')}]`
}
if (_.isRegExp(value)) {
return value.toString()
}
if (_.isObject(value)) {
// Cannot use $dom.isJquery here because it causes infinite recursion.
if (value instanceof $) {
return `jQuery{${value.length}}`
}
const len = _.keys(value).length
if (len > 2) {
return `Object{${len}}`
}
return this.stringifyActualObj(value)
}
if (_.isSymbol(value)) {
return 'Symbol'
}
if (_.isUndefined(value)) {
return undefined
}
return `${value}`
},
// give us some user-friendly "types"
stringifyFriendlyTypeof: _.cond(USER_FRIENDLY_TYPE_DETECTORS),
stringify (values) {
// if we already have an array
// then nest it again so that
// its formatted properly
values = [].concat(values)
return _
.chain(values)
.map(_.bind(this.stringifyActual, this))
.without(undefined)
.join(', ')
.value()
},
stringifyArg (arg) {
if (_.isString(arg) || _.isNumber(arg) || _.isBoolean(arg)) {
return JSON.stringify(arg)
}
if (_.isNull(arg)) {
return 'null'
}
if (_.isUndefined(arg)) {
return 'undefined'
}
return this.stringifyActual(arg)
},
plural (obj, plural, singular) {
obj = _.isNumber(obj) ? obj : obj.length
if (obj > 1) {
return plural
}
return singular
},
convertHtmlTags (html) {
return html
.replace(tagOpen, '<$1>')
.replace(tagClosed, '</$1>')
},
isInstanceOf (instance, constructor) {
try {
return instance instanceof constructor
} catch (e) {
return false
}
},
escapeQuotes (text) {
// convert to str and escape any single
// or double quotes
return (`${text}`).replace(quotesRe, '\\$1')
},
normalizeNumber (num) {
const parsed = Number(num)
// return num if this isNaN else return parsed
if (_.isNaN(parsed)) {
return num
}
return parsed
},
isValidHttpMethod (str) {
return _.isString(str) && _.includes(methods, str.toLowerCase())
},
addTwentyYears () {
return moment().add(20, 'years').unix()
},
locReload (forceReload, win) {
return win.location.reload(forceReload)
},
locHref (url, win) {
win.location.href = url
},
locToString (win) {
return win.location.toString()
},
locExisting () {
return $Location.create(window.location.href)
},
iframeSrc ($autIframe, url) {
return $autIframe.prop('src', url)
},
getDistanceBetween (point1, point2) {
const deltaX = point1.x - point2.x
const deltaY = point1.y - point2.y
return Math.sqrt((deltaX * deltaX) + (deltaY * deltaY))
},
getTestFromRunnable (r) {
return r.ctx.currentTest || r
},
memoize (func, cacheInstance = new Map()) {
const memoized = function (...args) {
const key = args[0]
const { cache } = memoized
if (cache.has(key)) {
return cache.get(key)
}
const result = func.apply(this, args)
memoized.cache = cache.set(key, result) || cache
return result
}
memoized.cache = cacheInstance
return memoized
},
indent (str, indentAmount) {
const indentStr = _.repeat(' ', indentAmount)
str = str.replace(/\n/g, `\n${indentStr}`)
return `${indentStr}${str}`
},
// normalize more than {maxNewLines} new lines into
// exactly {replacementNumLines} new lines
normalizeNewLines (str, maxNewLines, replacementNumLines) {
const moreThanMaxNewLinesRe = new RegExp(`\\n{${maxNewLines},}`)
const replacementWithNumLines = replacementNumLines ?? maxNewLines
return _.chain(str)
.split(moreThanMaxNewLinesRe)
.compact()
.join(_.repeat('\n', replacementWithNumLines))
.value()
},
/**
* Correctly decodes Unicode string in encoded in base64
* @see https://github.com/cypress-io/cypress/issues/5435
* @see https://github.com/cypress-io/cypress/issues/7507
* @see https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
*
* @example
```
Buffer.from(JSON.stringify({state: '🙂'})).toString('base64')
// 'eyJzdGF0ZSI6IvCfmYIifQ=='
// "window.atob" does NOT work
// atob('eyJzdGF0ZSI6IvCfmYIifQ==')
// "{"state":"ð"}"
// but this function works
b64DecodeUnicode('eyJzdGF0ZSI6IvCfmYIifQ==')
'{"state":"🙂"}'
```
*/
decodeBase64Unicode (str) {
return decodeURIComponent(atob(str).split('').map((char) => {
return `%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`
}).join(''))
},
/**
* Correctly encodes Unicode string to base64
* @see https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
*/
encodeBase64Unicode (str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode(`0x${p1}`)
}))
},
}