@revoloo/cypress6
Version:
Cypress.io end to end testing tool
389 lines (292 loc) • 9.37 kB
JavaScript
// See: ./errorScenarios.md for details about error messages and stack traces
const _ = require('lodash')
const chai = require('chai')
const $dom = require('../dom')
const $utils = require('./utils')
const $stackUtils = require('./stack_utils')
const $errorMessages = require('./error_messages')
const ERROR_PROPS = 'message type name stack sourceMappedStack parsedStack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending docsUrl codeFrame'.split(' ')
const ERR_PREPARED_FOR_SERIALIZATION = Symbol('ERR_PREPARED_FOR_SERIALIZATION')
if (!Error.captureStackTrace) {
Error.captureStackTrace = (err, fn) => {
const stack = (new Error()).stack
err.stack = $stackUtils.stackWithLinesDroppedFromMarker(stack, fn.name)
}
}
const prepareErrorForSerialization = (err) => {
if (err[ERR_PREPARED_FOR_SERIALIZATION]) {
return err
}
if (err.type === 'existence' || $dom.isDom(err.actual) || $dom.isDom(err.expected)) {
err.showDiff = false
}
if (err.showDiff === true) {
if (err.actual) {
err.actual = chai.util.inspect(err.actual)
}
if (err.expected) {
err.expected = chai.util.inspect(err.expected)
}
} else {
delete err.actual
delete err.expected
delete err.showDiff
}
err[ERR_PREPARED_FOR_SERIALIZATION] = true
return err
}
const wrapErr = (err) => {
if (!err) return
prepareErrorForSerialization(err)
return $utils.reduceProps(err, ERROR_PROPS)
}
const isAssertionErr = (err = {}) => {
return err.name === 'AssertionError'
}
const isChaiValidationErr = (err = {}) => {
return _.startsWith(err.message, 'Invalid Chai property')
}
const isCypressErr = (err = {}) => {
return err.name === 'CypressError'
}
const mergeErrProps = (origErr, ...newProps) => {
return _.extend(origErr, ...newProps)
}
const stackWithReplacedProps = (err, props) => {
const {
message: originalMessage,
name: originalName,
stack: originalStack,
} = err
const {
message: newMessage,
name: newName,
} = props
// if stack doesn't already exist, leave it as is
if (!originalStack) return originalStack
let stack
if (newMessage) {
stack = originalStack.replace(originalMessage, newMessage)
}
if (newName) {
stack = originalStack.replace(originalName, newName)
}
if (originalMessage) {
return stack
}
// if message is undefined or an empty string, the error (in Chrome at least)
// is 'Error\n\n<stack>' and it results in wrongly prepending the
// new message, looking like '<newMsg>Error\n\n<stack>'
const message = newMessage || err.message
const name = newName || err.name
return originalStack.replace(originalName, `${name}: ${message}`)
}
const modifyErrMsg = (err, newErrMsg, cb) => {
err.stack = $stackUtils.normalizedStack(err)
const newMessage = cb(err.message, newErrMsg)
const newStack = stackWithReplacedProps(err, { message: newMessage })
err.message = newMessage
err.stack = newStack
return err
}
const appendErrMsg = (err, errMsg) => {
return modifyErrMsg(err, errMsg, (msg1, msg2) => {
// we don't want to just throw in extra
// new lines if there isn't even a msg
if (!msg1) return msg2
if (!msg2) return msg1
return `${msg1}\n\n${msg2}`
})
}
const makeErrFromObj = (obj) => {
const err2 = new Error(obj.message)
err2.name = obj.name
err2.stack = obj.stack
_.each(obj, (val, prop) => {
if (!err2[prop]) {
err2[prop] = val
}
})
return err2
}
const throwErr = (err, options = {}) => {
if (_.isString(err)) {
err = cypressErr({ message: err })
}
let { onFail, errProps } = options
// assume onFail is a command if
//# onFail is present and isnt a function
if (onFail && !_.isFunction(onFail)) {
const command = onFail
//# redefine onFail and automatically
//# hook this into our command
onFail = (err) => {
return command.error(err)
}
}
if (onFail) {
err.onFail = onFail
}
if (errProps) {
_.extend(err, errProps)
}
throw err
}
const throwErrByPath = (errPath, options = {}) => {
const err = errByPath(errPath, options.args)
// gets rid of internal stack lines that just build the error
if (Error.captureStackTrace) {
Error.captureStackTrace(err, throwErrByPath)
}
return throwErr(err, options)
}
const warnByPath = (errPath, options = {}) => {
const errObj = errByPath(errPath, options.args)
let err = errObj.message
if (errObj.docsUrl) {
err += `\n\n${errObj.docsUrl}`
}
$utils.warning(err)
}
class InternalCypressError extends Error {
constructor (message) {
super(message)
this.name = 'InternalCypressError'
if (Error.captureStackTrace) {
Error.captureStackTrace(this, InternalCypressError)
}
}
}
class CypressError extends Error {
constructor (message) {
super(message)
this.name = 'CypressError'
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CypressError)
}
}
setUserInvocationStack (stack) {
this.userInvocationStack = stack
return this
}
}
const getUserInvocationStack = (err) => {
return err.userInvocationStack
}
const internalErr = (err) => {
const newErr = new InternalCypressError(err.message)
return mergeErrProps(newErr, err)
}
const cypressErr = (err) => {
const newErr = new CypressError(err.message)
return mergeErrProps(newErr, err)
}
const cypressErrByPath = (errPath, options = {}) => {
const errObj = errByPath(errPath, options.args)
return cypressErr(errObj)
}
const replaceErrMsgTokens = (errMessage, args) => {
if (!errMessage) return errMessage
const replace = (str, argValue, argKey) => {
return str.replace(new RegExp(`\{\{${argKey}\}\}`, 'g'), argValue)
}
const getMsg = function (args = {}) {
return _.reduce(args, (message, argValue, argKey) => {
if (_.isArray(message)) {
return _.map(message, (str) => replace(str, argValue, argKey))
}
return replace(message, argValue, argKey)
}, errMessage)
}
// replace more than 2 newlines with exactly 2 newlines
return $utils.normalizeNewLines(getMsg(args), 2)
}
const errByPath = (msgPath, args) => {
let msgValue = _.get($errorMessages, msgPath)
if (!msgValue) {
return internalErr({ message: `Error message path '${msgPath}' does not exist` })
}
let msgObj = msgValue
if (_.isFunction(msgValue)) {
msgObj = msgValue(args)
}
if (_.isString(msgObj)) {
msgObj = {
message: msgObj,
}
}
return cypressErr({
message: replaceErrMsgTokens(msgObj.message, args),
docsUrl: msgObj.docsUrl ? replaceErrMsgTokens(msgObj.docsUrl, args) : undefined,
})
}
const createUncaughtException = (type, err) => {
// FIXME: `fromSpec` is a dirty hack to get uncaught exceptions in `top` to say they're from the spec
const errPath = (type === 'spec' || err.fromSpec) ? 'uncaught.fromSpec' : 'uncaught.fromApp'
let uncaughtErr = errByPath(errPath, {
errMsg: err.message,
})
modifyErrMsg(err, uncaughtErr.message, () => uncaughtErr.message)
err.docsUrl = _.compact([uncaughtErr.docsUrl, err.docsUrl])
return err
}
// stacks from command failures and assertion failures have the right message
// but the stack points to cypress internals. here we replace the internal
// cypress stack with the invocation stack, which points to the user's code
const stackAndCodeFrameIndex = (err, userInvocationStack) => {
if (!userInvocationStack) return { stack: err.stack }
if (isCypressErr(err) || isChaiValidationErr(err)) {
return $stackUtils.stackWithUserInvocationStackSpliced(err, userInvocationStack)
}
return { stack: $stackUtils.replacedStack(err, userInvocationStack) }
}
const preferredStackAndCodeFrameIndex = (err, userInvocationStack) => {
let { stack, index } = stackAndCodeFrameIndex(err, userInvocationStack)
stack = $stackUtils.stackWithContentAppended(err, stack)
stack = $stackUtils.stackWithReplacementMarkerLineRemoved(stack)
return { stack, index }
}
const enhanceStack = ({ err, userInvocationStack, projectRoot }) => {
const { stack, index } = preferredStackAndCodeFrameIndex(err, userInvocationStack)
const { sourceMapped, parsed } = $stackUtils.getSourceStack(stack, projectRoot)
err.stack = stack
err.sourceMappedStack = sourceMapped
err.parsedStack = parsed
err.codeFrame = $stackUtils.getCodeFrame(err, index)
return err
}
// all errors flow through this function before they're finally thrown
// or used to reject promises
const processErr = (errObj = {}, config) => {
let docsUrl = errObj.docsUrl
if (config('isInteractive') || !docsUrl) {
return errObj
}
// backup, and then delete the docsUrl property in runMode
// so that it does not add the 'Learn More' link to the UI
// for screenshots or videos
delete errObj.docsUrl
docsUrl = _(docsUrl).castArray().compact().join('\n\n')
// append the docs url when not interactive so it appears in the stdout
return appendErrMsg(errObj, docsUrl)
}
module.exports = {
appendErrMsg,
createUncaughtException,
cypressErr,
cypressErrByPath,
enhanceStack,
errByPath,
isAssertionErr,
isChaiValidationErr,
isCypressErr,
makeErrFromObj,
mergeErrProps,
modifyErrMsg,
processErr,
throwErr,
throwErrByPath,
warnByPath,
wrapErr,
getUserInvocationStack,
}