UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

547 lines (436 loc) 16.5 kB
/* eslint-disable prefer-rest-params */ // tests in driver/cypress/integration/commands/assertions_spec.js const _ = require('lodash') const $ = require('jquery') const chai = require('chai') const sinonChai = require('@cypress/sinon-chai') const $dom = require('../dom') const $utils = require('../cypress/utils') const $errUtils = require('../cypress/error_utils') const $stackUtils = require('../cypress/stack_utils') const $chaiJquery = require('../cypress/chai_jquery') const chaiInspect = require('./chai/inspect') // all words between single quotes const allPropertyWordsBetweenSingleQuotes = /('.*?')/g const allBetweenFourStars = /\*\*.*\*\*/ const allNumberStrings = /'([0-9]+)'/g const allEmptyStrings = /''/g const allSingleQuotes = /'/g const allEscapedSingleQuotes = /\\'/g const allQuoteMarkers = /__quote__/g const allWordsBetweenCurlyBraces = /(#{.+?})/g const allQuadStars = /\*\*\*\*/g const leadingWhitespaces = /\*\*'\s*/g const trailingWhitespaces = /\s*'\*\*/g const whitespace = /\s/g const valueHasLeadingOrTrailingWhitespaces = /\*\*'\s+|\s+'\*\*/g let assertProto = null let matchProto = null let lengthProto = null let containProto = null let existProto = null let getMessage = null let chaiUtils = null chai.use(sinonChai) chai.use((chai, u) => { chaiUtils = u $chaiJquery(chai, chaiUtils, { onInvalid (method, obj) { $errUtils.throwErrByPath('chai.invalid_jquery_obj', { args: { assertion: method, subject: $utils.stringifyActual(obj), }, }) }, onError (err, method, obj, negated) { switch (method) { case 'visible': if (!negated) { // add reason hidden unless we expect the element to be hidden const reason = $dom.getReasonIsHidden(obj) err.message += `\n\n${reason}` } break default: break } // always rethrow the error! throw err }, }) assertProto = chai.Assertion.prototype.assert matchProto = chai.Assertion.prototype.match lengthProto = chai.Assertion.prototype.__methods.length.method containProto = chai.Assertion.prototype.__methods.contain.method existProto = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'exist').get const { objDisplay } = chai.util; ({ getMessage } = chai.util) const _inspect = chai.util.inspect const { inspect, setFormatValueHook } = chaiInspect.create(chai) // prevent tunneling into Window objects (can throw cross-origin errors) setFormatValueHook((ctx, val) => { // https://github.com/cypress-io/cypress/issues/5270 // When name attribute exists in <iframe>, // Firefox returns [object Window] but Chrome returns [object Object] // So, we try throwing an error and check the error message. try { val && val.document val && val.inspect } catch (e) { if (e.stack.indexOf('cross-origin') !== -1 || // chrome e.message.indexOf('cross-origin') !== -1) { // firefox return `[window]` } } }) // remove any single quotes between our **, // except escaped quotes, empty strings and number strings. const removeOrKeepSingleQuotesBetweenStars = (message) => { // remove any single quotes between our **, preserving escaped quotes // and if an empty string, put the quotes back return message.replace(allBetweenFourStars, (match) => { if (valueHasLeadingOrTrailingWhitespaces.test(match)) { // Above we used \s+, but below we use \s*. // It's because of the strings like ' love' that have empty spaces on one side only. match = match .replace(leadingWhitespaces, (match) => { return match.replace(`**'`, '**__quote__') .replace(whitespace, '&nbsp;') }) .replace(trailingWhitespaces, (match) => { return match.replace(`'**`, '__quote__**') .replace(whitespace, '&nbsp;') }) } return match .replace(allEscapedSingleQuotes, '__quote__') // preserve escaped quotes .replace(allNumberStrings, '__quote__$1__quote__') // preserve number strings (e.g. '42') .replace(allEmptyStrings, '__quote____quote__') // preserve empty strings (e.g. '') .replace(allSingleQuotes, '') .replace(allQuoteMarkers, '\'') // put escaped quotes back }) } const replaceArgMessages = (args, str) => { return _.reduce(args, (memo, value, index) => { if (_.isString(value)) { value = value .replace(allWordsBetweenCurlyBraces, '**$1**') .replace(allEscapedSingleQuotes, '__quote__') .replace(allPropertyWordsBetweenSingleQuotes, '**$1**') // when a value has ** in it, **** are sometimes created after the process above. // remove them with this. .replace(allQuadStars, '**') memo.push(value) } else { memo.push(value) } return memo } , []) } const restoreAsserts = function () { chai.util.inspect = _inspect chai.util.getMessage = getMessage chai.util.objDisplay = objDisplay chai.Assertion.prototype.assert = assertProto chai.Assertion.prototype.match = matchProto chai.Assertion.prototype.__methods.length.method = lengthProto chai.Assertion.prototype.__methods.contain.method = containProto return Object.defineProperty(chai.Assertion.prototype, 'exist', { get: existProto }) } const overrideChaiInspect = () => { return chai.util.inspect = inspect } const overrideChaiObjDisplay = () => { return chai.util.objDisplay = function (obj) { const str = chai.util.inspect(obj) const type = Object.prototype.toString.call(obj) if (chai.config.truncateThreshold && (str.length >= chai.config.truncateThreshold)) { if (type === '[object Function]') { if (!obj.name || (obj.name === '')) { return '[Function]' } return `[Function: ${obj.name}]` } if (type === '[object Array]') { return `[ Array(${obj.length}) ]` } if (type === '[object Object]') { const keys = Object.keys(obj) const kstr = keys.length > 2 ? `${keys.splice(0, 2).join(', ')}, ...` : keys.join(', ') return `{ Object (${kstr}) }` } return str } return str } } const overrideChaiAsserts = function (specWindow, state, assertFn) { chai.Assertion.prototype.assert = createPatchedAssert(specWindow, state, assertFn) const _origGetmessage = function (obj, args) { const negate = chaiUtils.flag(obj, 'negate') const val = chaiUtils.flag(obj, 'object') const expected = args[3] const actual = chaiUtils.getActual(obj, args) let msg = (negate ? args[2] : args[1]) const flagMsg = chaiUtils.flag(obj, 'message') if (typeof msg === 'function') { msg = msg() } msg = msg || '' msg = msg .replace(/#\{this\}/g, () => { return chaiUtils.objDisplay(val) }) .replace(/#\{act\}/g, () => { return chaiUtils.objDisplay(actual) }) .replace(/#\{exp\}/g, () => { return chaiUtils.objDisplay(expected) }) return (flagMsg ? `${flagMsg}: ${msg}` : msg) } chaiUtils.getMessage = function (assert, args) { const obj = assert._obj // if we are formatting a DOM object if ($dom.isDom(obj)) { // replace object with our formatted one assert._obj = $dom.stringify(obj, 'short') } const msg = _origGetmessage.call(this, assert, args) // restore the real obj if we changed it if (obj !== assert._obj) { assert._obj = obj } return msg } chai.Assertion.overwriteMethod('match', (_super) => { return (function (regExp) { if (_.isRegExp(regExp) || $dom.isDom(this._obj)) { return _super.apply(this, arguments) } const err = $errUtils.cypressErrByPath('chai.match_invalid_argument', { args: { regExp } }) err.retry = false throw err }) }) const containFn1 = (_super) => { return (function (text) { let obj = this._obj if (!($dom.isElement(obj))) { return _super.apply(this, arguments) } const escText = $utils.escapeQuotes(text) const selector = `:contains('${escText}'), [type='submit'][value~='${escText}']` // the assert checks below only work if $dom.isJquery(obj) // https://github.com/cypress-io/cypress/issues/3549 if (!($dom.isJquery(obj))) { obj = $(obj) } this.assert( obj.is(selector) || !!obj.find(selector).length, 'expected #{this} to contain #{exp}', 'expected #{this} not to contain #{exp}', text, ) }) } const containFn2 = (_super) => { return (function () { return _super.apply(this, arguments) }) } chai.Assertion.overwriteChainableMethod('contain', containFn1, containFn2) chai.Assertion.overwriteChainableMethod('length', (_super) => { return (function (length) { let obj = this._obj if (!($dom.isJquery(obj) || $dom.isElement(obj))) { return _super.apply(this, arguments) } length = $utils.normalizeNumber(length) // filter out anything not currently in our document if ($dom.isDetached(obj)) { obj = (this._obj = obj.filter((index, el) => { return $dom.isAttached(el) })) } const node = obj && obj.length ? $dom.stringify(obj, 'short') : obj.selector // if our length assertion fails we need to check to // ensure that the length argument is a finite number // because if its not, we need to bail on retrying try { return this.assert( obj.length === length, `expected '${node}' to have a length of \#{exp} but got \#{act}`, `expected '${node}' to not have a length of \#{act}`, length, obj.length, ) } catch (e1) { e1.node = node e1.negated = chaiUtils.flag(this, 'negate') e1.type = 'length' if (_.isFinite(length)) { const getLongLengthMessage = function (len1, len2) { if (len1 > len2) { return `Too many elements found. Found '${len1}', expected '${len2}'.` } return `Not enough elements found. Found '${len1}', expected '${len2}'.` } // if the user has specified a custom message, // for example: expect($subject, 'Should have length').to.have.length(1) // prefer that over our message const message = chaiUtils.flag(this, 'message') ? e1.message : getLongLengthMessage(obj.length, length) $errUtils.modifyErrMsg(e1, message, () => message) throw e1 } const e2 = $errUtils.cypressErrByPath('chai.length_invalid_argument', { args: { length } }) e2.retry = false throw e2 } }) }, (_super) => { return (function () { return _super.apply(this, arguments) }) }) return chai.Assertion.overwriteProperty('exist', (_super) => { return (function () { const obj = this._obj if (!($dom.isJquery(obj) || $dom.isElement(obj))) { try { return _super.apply(this, arguments) } catch (e) { e.type = 'existence' throw e } } else { let isAttached if (!obj.length) { this._obj = null } const node = obj && obj.length ? $dom.stringify(obj, 'short') : obj.selector try { return this.assert( (isAttached = $dom.isAttached(obj)), 'expected \#{act} to exist in the DOM', 'expected \#{act} not to exist in the DOM', node, node, ) } catch (e1) { e1.node = node e1.negated = chaiUtils.flag(this, 'negate') e1.type = 'existence' const getLongExistsMessage = function (obj) { // if we expected not for an element to exist if (isAttached) { return `Expected ${node} not to exist in the DOM, but it was continuously found.` } return `Expected to find element: \`${obj.selector}\`, but never found it.` } const newMessage = getLongExistsMessage(obj) $errUtils.modifyErrMsg(e1, newMessage, () => newMessage) throw e1 } } }) }) } const captureUserInvocationStack = (specWindow, state, ssfi) => { // we need a user invocation stack with the top line being the point where // the error occurred for the sake of the code frame // in chrome, stack lines from another frame don't appear in the // error. specWindow.Error works for our purposes because it // doesn't include anything extra (chai.Assertion error doesn't work // because it doesn't have lines from the spec iframe) // in firefox, specWindow.Error has too many extra lines at the // beginning, but chai.AssertionError helps us winnow those down const chaiInvocationStack = $stackUtils.hasCrossFrameStacks(specWindow) && (new chai.AssertionError('uis', {}, ssfi)).stack const userInvocationStack = $stackUtils.captureUserInvocationStack(specWindow.Error, chaiInvocationStack) state('currentAssertionUserInvocationStack', userInvocationStack) } const createPatchedAssert = (specWindow, state, assertFn) => { return (function (...args) { let err const passed = chaiUtils.test(this, args) const value = chaiUtils.flag(this, 'object') const expected = args[3] const customArgs = replaceArgMessages(args, this._obj) let message = chaiUtils.getMessage(this, customArgs) const actual = chaiUtils.getActual(this, customArgs) message = removeOrKeepSingleQuotesBetweenStars(message) try { assertProto.apply(this, args) } catch (e) { err = e } assertFn(passed, message, value, actual, expected, err) if (!err) return // when assert() is used instead of expect(), we override the method itself // below in `overrideAssert` and prefer the user invocation stack // that we capture there if (!state('assertUsed')) { captureUserInvocationStack(specWindow, state, chaiUtils.flag(this, 'ssfi')) } throw err }) } const overrideExpect = (specWindow, state) => { // only override assertions for this specific // expect function instance so we do not affect // the outside world return (val, message) => { captureUserInvocationStack(specWindow, state, overrideExpect) // make the assertion return new chai.Assertion(val, message) } } const overrideAssert = function (specWindow, state) { const fn = (express, errmsg) => { state('assertUsed', true) captureUserInvocationStack(specWindow, state, fn) return chai.assert(express, errmsg) } const fns = _.functions(chai.assert) _.each(fns, (name) => { fn[name] = function () { state('assertUsed', true) captureUserInvocationStack(specWindow, state, overrideAssert) return chai.assert[name].apply(this, arguments) } }) return fn } const setSpecWindowGlobals = function (specWindow, state) { const expect = overrideExpect(specWindow, state) const assert = overrideAssert(specWindow, state) specWindow.chai = chai specWindow.expect = expect specWindow.assert = assert return { chai, expect, assert, } } const create = function (specWindow, state, assertFn) { restoreAsserts() overrideChaiInspect() overrideChaiObjDisplay() overrideChaiAsserts(specWindow, state, assertFn) return setSpecWindowGlobals(specWindow, state) } module.exports = { replaceArgMessages, removeOrKeepSingleQuotesBetweenStars, setSpecWindowGlobals, restoreAsserts, overrideExpect, overrideChaiAsserts, create, } })