From 36866da64d86dc7e162489e357d7fc814ff025bb Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Tue, 17 Mar 2020 00:33:56 -0400 Subject: [PATCH] Error Improvements (#6724) * redesign and improve reporter error display - add markdown support - collapse stacktrace - separate docs url and add link in reporter Co-authored-by: Jennifer Shehane * $utils -> $errUtils * derp * serializeError -> wrapErr * cloneErr -> makeErrFromObj * yarn.lock * fix unit tests * move err-model * fix styles * fix/improve error logging * fix non-converted bits * transfer missed changes * fix issues * remove obselete spec * make type test more reliable * use should, get retries * update snapshots * update e2e network error test * update more snapshots * update error whitespace * update snapshot * try something out * nevermind * fix tooltip * add some logging * remove whitespace * remove spying on window * update snapshot * fix test * update snapshot * fix merge: snapshot stacktraces * fix noStackTrace and update snapshot * update snapshot * fix yarn.lock * don't show diff if retrying an existence error * url -> URL * don't add newline after docs url and update a few snapshots * keep opening stack trace from collapsing test * remove unnecessary global cy reference * fix tests * put e2e timeout increase back in the right spot for exit: false * don't show diff when assertion contains an element also, keep mocha from messing up extracting error name when it includes a colon * use backticks for hook error title * fix appending error message when original message is falsy * don't show diff on existence failures * update snapshots * fix finish/done being called twice due to not returning * prevent error print button click from propagating * use correct error methods and remove need for workaround * create better abstraction around creating cypress error from path, refactor * fix throwErr and tests Co-authored-by: Jennifer Shehane Co-authored-by: Brian Mann Co-authored-by: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Co-authored-by: Jennifer Shehane Co-authored-by: Brian Mann Co-authored-by: Ben Kucera <14625260+Bkucera@users.noreply.github.com> --- packages/driver/src/cy/actionability.js | 2 +- packages/driver/src/cy/chai.js | 26 +- .../src/cy/commands/actions/check.coffee | 2 +- .../src/cy/commands/actions/scroll.coffee | 2 +- .../driver/src/cy/commands/angular.coffee | 2 +- .../driver/src/cy/commands/asserting.coffee | 4 +- .../driver/src/cy/commands/commands.coffee | 4 +- .../driver/src/cy/commands/connectors.coffee | 38 +- packages/driver/src/cy/commands/cookies.js | 2 +- packages/driver/src/cy/commands/files.coffee | 11 +- packages/driver/src/cy/commands/querying.js | 4 +- .../driver/src/cy/commands/screenshot.coffee | 2 +- .../driver/src/cy/commands/traversals.coffee | 4 +- packages/driver/src/cy/commands/window.coffee | 2 +- packages/driver/src/cy/commands/xhr.coffee | 2 +- packages/driver/src/cy/ensures.coffee | 6 +- packages/driver/src/cy/errors.coffee | 29 +- packages/driver/src/cy/retries.coffee | 21 +- packages/driver/src/cypress.js | 11 +- packages/driver/src/cypress/cy.js | 17 +- .../driver/src/cypress/error_messages.coffee | 1778 +++++++++++------ packages/driver/src/cypress/error_utils.js | 306 +-- packages/driver/src/cypress/log.js | 2 +- packages/driver/src/dom/visibility.js | 36 +- .../driver/test/cypress/fixtures/dom.html | 17 +- .../commands/actions/check_spec.coffee | 33 +- .../commands/actions/clear_spec.js | 32 +- .../commands/actions/click_spec.js | 67 +- .../commands/actions/focus_spec.coffee | 34 +- .../commands/actions/hover_spec.coffee | 4 +- .../commands/actions/scroll_spec.coffee | 50 +- .../commands/actions/select_spec.coffee | 29 +- .../commands/actions/submit_spec.coffee | 8 +- .../commands/actions/trigger_spec.coffee | 21 +- .../commands/actions/type_errors_spec.js | 99 +- .../integration/commands/agents_spec.coffee | 14 +- .../integration/commands/aliasing_spec.coffee | 21 +- .../integration/commands/angular_spec.coffee | 6 +- .../integration/commands/assertions_spec.js | 31 +- .../integration/commands/clock_spec.coffee | 18 +- .../integration/commands/commands_spec.coffee | 4 +- .../commands/connectors_spec.coffee | 220 +- .../integration/commands/cookies_spec.coffee | 45 +- .../integration/commands/exec_spec.coffee | 26 +- .../integration/commands/files_spec.coffee | 44 +- .../integration/commands/fixtures_spec.coffee | 6 +- .../commands/local_storage_spec.coffee | 3 +- .../integration/commands/location_spec.coffee | 14 + .../integration/commands/misc_spec.coffee | 4 +- .../commands/navigation_spec.coffee | 104 +- .../integration/commands/querying_spec.js | 42 +- .../integration/commands/request_spec.coffee | 70 +- .../commands/screenshot_spec.coffee | 81 +- .../integration/commands/task_spec.coffee | 22 +- .../commands/traversals_spec.coffee | 8 +- .../integration/commands/waiting_spec.coffee | 134 +- .../integration/commands/window_spec.coffee | 22 +- .../integration/commands/xhr_spec.coffee | 101 +- .../cypress/integration/cy/timeouts_spec.js | 45 + .../integration/cypress/browser_spec.coffee | 2 +- .../integration/cypress/cy_spec.coffee | 26 +- .../integration/cypress/cypress_spec.coffee | 8 +- .../cypress/error_utils_spec.coffee | 52 +- .../cypress/screenshot_spec.coffee | 143 +- .../cypress/selector_playground_spec.coffee | 33 +- .../integration/dom/visibility_spec.ts | 71 +- .../integration/e2e/promises_spec.coffee | 13 +- .../integration/e2e/return_value_spec.coffee | 9 +- .../e2e/uncaught_errors_spec.coffee | 17 +- .../cypress/integration/errors_spec.js | 46 - .../cypress/integration/test_errors_spec.js | 173 ++ packages/reporter/package.json | 2 +- .../src/collapsible/collapsible.spec.tsx | 6 +- .../reporter/src/collapsible/collapsible.tsx | 19 +- .../reporter/src/commands/command-model.ts | 2 +- packages/reporter/src/commands/commands.scss | 21 +- .../src/{lib => errors}/err-model.spec.ts | 8 +- packages/reporter/src/errors/err-model.ts | 46 + packages/reporter/src/errors/errors.scss | 129 ++ .../reporter/src/errors/test-error.spec.tsx | 48 - packages/reporter/src/errors/test-error.tsx | 73 +- .../reporter/src/hooks/hook-model.spec.ts | 20 +- packages/reporter/src/hooks/hook-model.ts | 4 +- packages/reporter/src/lib/base.scss | 12 + packages/reporter/src/lib/err-model.ts | 35 - packages/reporter/src/lib/events.spec.ts | 31 +- packages/reporter/src/lib/events.ts | 14 +- packages/reporter/src/lib/variables.scss | 8 + .../reporter/src/runnables/runnables.scss | 29 +- packages/reporter/src/test/test-model.spec.ts | 12 +- packages/reporter/src/test/test-model.ts | 2 +- packages/reporter/src/test/test.tsx | 7 +- packages/runner/src/lib/event-manager.js | 23 +- .../1_async_timeouts_spec.coffee.js | 4 +- ...caught_uncaught_hook_errors_spec.coffee.js | 20 +- .../1_commands_outside_of_test_spec.coffee.js | 10 +- .../__snapshots__/3_config_spec.coffee.js | 2 +- .../__snapshots__/3_issue_1669_spec.coffee.js | 2 +- .../__snapshots__/3_issue_173_spec.coffee.js | 2 +- .../__snapshots__/3_issue_674_spec.coffee.js | 8 +- .../3_js_error_handling_spec.coffee.js | 10 +- .../4_form_submissions_spec.coffee.js | 7 +- .../__snapshots__/4_request_spec.coffee.js | 20 +- .../4_return_value_spec.coffee.js | 10 +- .../5_screenshots_spec.coffee.js | 6 +- .../5_spec_isolation_spec.coffee.js | 14 +- .../__snapshots__/5_stdout_spec.coffee.js | 114 +- .../5_task_not_registered_spec.coffee.js | 2 +- .../__snapshots__/6_task_spec.coffee.js | 4 +- .../6_uncaught_spec_errors_spec.coffee.js | 2 +- .../__snapshots__/6_visit_spec.coffee.js | 48 +- .../6_web_security_spec.coffee.js | 9 +- .../__snapshots__/7_record_spec.coffee.js | 4 +- .../__snapshots__/8_reporters_spec.coffee.js | 18 +- packages/server/lib/reporter.coffee | 20 + packages/server/test/e2e/5_stdout_spec.coffee | 6 + .../e2e/8_network_error_handling_spec.coffee | 2 +- .../stdout_assertion_errors_spec.js | 19 + .../server/expected_stdout_failures.txt | 2 +- packages/server/test/support/helpers/e2e.js | 4 +- yarn.lock | 91 +- 121 files changed, 3340 insertions(+), 2056 deletions(-) create mode 100644 packages/driver/test/cypress/integration/cy/timeouts_spec.js delete mode 100644 packages/reporter/cypress/integration/errors_spec.js create mode 100644 packages/reporter/cypress/integration/test_errors_spec.js rename packages/reporter/src/{lib => errors}/err-model.spec.ts (85%) create mode 100644 packages/reporter/src/errors/err-model.ts delete mode 100644 packages/reporter/src/errors/test-error.spec.tsx delete mode 100644 packages/reporter/src/lib/err-model.ts create mode 100644 packages/server/test/support/fixtures/projects/e2e/cypress/integration/stdout_assertion_errors_spec.js diff --git a/packages/driver/src/cy/actionability.js b/packages/driver/src/cy/actionability.js index 6aa9dc3096dc..3689cc1e7525 100644 --- a/packages/driver/src/cy/actionability.js +++ b/packages/driver/src/cy/actionability.js @@ -251,7 +251,7 @@ const ensureNotAnimating = function (cy, $el, coordsHistory, animationDistanceTh // if we dont have at least 2 points // then automatically retry if (coordsHistory.length < 2) { - throw $errUtils.cypressErr('coordsHistory must be at least 2 sets of coords') + $errUtils.throwErrByPath('dom.animation_coords_history_invalid') } // verify that our element is not currently animating diff --git a/packages/driver/src/cy/chai.js b/packages/driver/src/cy/chai.js index cbed329059c4..9857e7078351 100644 --- a/packages/driver/src/cy/chai.js +++ b/packages/driver/src/cy/chai.js @@ -49,16 +49,12 @@ chai.use((chai, u) => { $chaiJquery(chai, chaiUtils, { onInvalid (method, obj) { - const err = $errUtils.cypressErr( - $errUtils.errMsgByPath( - 'chai.invalid_jquery_obj', { - assertion: method, - subject: $utils.stringifyActual(obj), - }, - ), - ) - - throw err + $errUtils.throwErrByPath('chai.invalid_jquery_obj', { + args: { + assertion: method, + subject: $utils.stringifyActual(obj), + }, + }) }, onError (err, method, obj, negated) { @@ -253,7 +249,7 @@ chai.use((chai, u) => { return _super.apply(this, arguments) } - const err = $errUtils.cypressErr($errUtils.errMsgByPath('chai.match_invalid_argument', { regExp })) + const err = $errUtils.cypressErrByPath('chai.match_invalid_argument', { args: { regExp } }) err.retry = false throw err @@ -340,11 +336,11 @@ chai.use((chai, u) => { return `Not enough elements found. Found '${len1}', expected '${len2}'.` } - e1.displayMessage = getLongLengthMessage(obj.length, length) + e1.message = getLongLengthMessage(obj.length, length) throw e1 } - const e2 = $errUtils.cypressErr($errUtils.errMsgByPath('chai.length_invalid_argument', { length })) + const e2 = $errUtils.cypressErrByPath('chai.length_invalid_argument', { args: { length } }) e2.retry = false throw e2 @@ -397,10 +393,10 @@ chai.use((chai, u) => { 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.` + return `Expected to find element: \`${obj.selector}\`, but never found it.` } - e1.displayMessage = getLongExistsMessage(obj) + e1.message = getLongExistsMessage(obj) throw e1 } } diff --git a/packages/driver/src/cy/commands/actions/check.coffee b/packages/driver/src/cy/commands/actions/check.coffee index d28856463cdb..6509e804a654 100644 --- a/packages/driver/src/cy/commands/actions/check.coffee +++ b/packages/driver/src/cy/commands/actions/check.coffee @@ -58,7 +58,7 @@ checkOrUncheck = (type, subject, values = [], options = {}) -> if not isAcceptableElement($el) node = $dom.stringify($el) word = $utils.plural(options.$el, "contains", "is") - phrase = if type is "check" then " and :radio" else "" + phrase = if type is "check" then " and `:radio`" else "" $errUtils.throwErrByPath("check_uncheck.invalid_element", { onFail: options._log args: { node, word, phrase, cmd: type } diff --git a/packages/driver/src/cy/commands/actions/scroll.coffee b/packages/driver/src/cy/commands/actions/scroll.coffee index 49895948ba0c..915b2e178b96 100644 --- a/packages/driver/src/cy/commands/actions/scroll.coffee +++ b/packages/driver/src/cy/commands/actions/scroll.coffee @@ -32,7 +32,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> ## ensure the subject is not window itself ## cause how are you gonna scroll the window into view... if subject is state("window") - $utils.throwErrByPath("scrollIntoView.subject_is_window") + $errUtils.throwErrByPath("scrollIntoView.subject_is_window") ## throw if we're trying to scroll to multiple elements if subject.length > 1 diff --git a/packages/driver/src/cy/commands/angular.coffee b/packages/driver/src/cy/commands/angular.coffee index c5a1e0dc7337..27d2723eaf8f 100644 --- a/packages/driver/src/cy/commands/angular.coffee +++ b/packages/driver/src/cy/commands/angular.coffee @@ -35,7 +35,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> cy.verifyUpcomingAssertions(getEl($elements), options, { onRetry: resolveElements onFail: (err) -> - err.displayMessage = "Could not find element for binding: '#{binding}'." + err.message = "Could not find element for binding: '#{binding}'." }) findByNgAttr = (name, attr, el, options) -> diff --git a/packages/driver/src/cy/commands/asserting.coffee b/packages/driver/src/cy/commands/asserting.coffee index 35ea2110a285..d55aac333f75 100644 --- a/packages/driver/src/cy/commands/asserting.coffee +++ b/packages/driver/src/cy/commands/asserting.coffee @@ -84,7 +84,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> options = {} if reEventually.test(chainers) - err = $errUtils.cypressErr("The 'eventually' assertion chainer has been deprecated. This is now the default behavior so you can safely remove this word and everything should work as before.") + err = $errUtils.cypressErrByPath('should.eventually_deprecated') err.retry = false throwAndLogErr(err) @@ -123,7 +123,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> newExp = _.reduce chainers, (memo, value) => if value not of memo - err = $errUtils.cypressErr("The chainer: '#{value}' was not found. Could not build assertion.") + err = $errUtils.cypressErrByPath('should.chainer_not_found', { args: { chainer: value } }) err.retry = false throwAndLogErr(err) diff --git a/packages/driver/src/cy/commands/commands.coffee b/packages/driver/src/cy/commands/commands.coffee index 80233288ed44..e0ed6dd2bad1 100644 --- a/packages/driver/src/cy/commands/commands.coffee +++ b/packages/driver/src/cy/commands/commands.coffee @@ -5,12 +5,12 @@ $errUtils = require("../../cypress/error_utils") command = (ctx, name, args...) -> if not ctx[name] - cmds = _.keys($Chainer.prototype).join(", ") + cmds = "\`#{_.keys($Chainer.prototype).join("`, `")}\`" $errUtils.throwErrByPath("miscellaneous.invalid_command", { args: { name, cmds } }) - ctx[name].apply(window, args) + ctx[name].apply(ctx, args) module.exports = (Commands, Cypress, cy, state, config) -> Commands.addChainer({ diff --git a/packages/driver/src/cy/commands/connectors.coffee b/packages/driver/src/cy/commands/connectors.coffee index 82e5e87e7f9d..e431f379d4e0 100644 --- a/packages/driver/src/cy/commands/connectors.coffee +++ b/packages/driver/src/cy/commands/connectors.coffee @@ -211,47 +211,45 @@ module.exports = (Commands, Cypress, cy, state, config) -> args: { cmd: name } }) - ## TODO: use the new error utils that are part of - ## the error message enhancements PR propertyNotOnSubjectErr = (prop) -> - $errUtils.cypressErr( - $errUtils.errMsgByPath("invoke_its.nonexistent_prop", { + $errUtils.cypressErrByPath("invoke_its.nonexistent_prop", { + args: { prop, cmd: name - }) - ) + } + }) propertyValueNullOrUndefinedErr = (prop, value) -> errMessagePath = if isCmdIts then "its" else "invoke" - $errUtils.cypressErr( - $errUtils.errMsgByPath("#{errMessagePath}.null_or_undefined_prop_value", { + $errUtils.cypressErrByPath("#{errMessagePath}.null_or_undefined_prop_value", { + args: { prop, value, - cmd: name - }) - ) + } + cmd: name + }) subjectNullOrUndefinedErr = (prop, value) -> errMessagePath = if isCmdIts then "its" else "invoke" - $errUtils.cypressErr( - $errUtils.errMsgByPath("#{errMessagePath}.subject_null_or_undefined", { + $errUtils.cypressErrByPath("#{errMessagePath}.subject_null_or_undefined", { + args: { prop, - value, cmd: name - }) - ) + value, + } + }) propertyNotOnPreviousNullOrUndefinedValueErr = (prop, value, previousProp) -> - $errUtils.cypressErr( - $errUtils.errMsgByPath("invoke_its.previous_prop_null_or_undefined", { + $errUtils.cypressErrByPath("invoke_its.previous_prop_null_or_undefined", { + args: { prop, value, previousProp, cmd: name - }) - ) + } + }) traverseObjectAtPath = (acc, pathsArray, index = 0) -> ## traverse at this depth diff --git a/packages/driver/src/cy/commands/cookies.js b/packages/driver/src/cy/commands/cookies.js index 79330c44758d..4d6bee8eb130 100644 --- a/packages/driver/src/cy/commands/cookies.js +++ b/packages/driver/src/cy/commands/cookies.js @@ -92,7 +92,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { $errUtils.throwErrByPath('cookies.backend_error', { args: { action, - command, + cmd: command, browserDisplayName: Cypress.browser.displayName, errMessage: err.message, errStack: err.stack, diff --git a/packages/driver/src/cy/commands/files.coffee b/packages/driver/src/cy/commands/files.coffee index bb325526849e..5f1a8bb648ff 100644 --- a/packages/driver/src/cy/commands/files.coffee +++ b/packages/driver/src/cy/commands/files.coffee @@ -2,6 +2,7 @@ _ = require("lodash") Promise = require("bluebird") $errUtils = require("../../cypress/error_utils") +$errMessages = require("../../cypress/error_messages") module.exports = (Commands, Cypress, cy, state, config) -> Commands.addAll({ @@ -50,16 +51,20 @@ module.exports = (Commands, Cypress, cy, state, config) -> onFail: (err) -> return unless err.type is "existence" - if contents? + { message, docsUrl } = if contents? ## file exists but it shouldn't - err.displayMessage = $errUtils.errMsgByPath("files.existent", { + $errUtils.errObjByPath($errMessages, "files.existent", { file, filePath }) else ## file doesn't exist but it should - err.displayMessage = $errUtils.errMsgByPath("files.nonexistent", { + $errUtils.errObjByPath($errMessages, "files.nonexistent", { file, filePath }) + + err.message = message + err.docsUrl = docsUrl + onRetry: verifyAssertions }) diff --git a/packages/driver/src/cy/commands/querying.js b/packages/driver/src/cy/commands/querying.js index 18124f7013c7..50f9977b86fb 100644 --- a/packages/driver/src/cy/commands/querying.js +++ b/packages/driver/src/cy/commands/querying.js @@ -68,7 +68,7 @@ module.exports = (Commands, Cypress, cy) => { get (selector, options = {}) { const ctx = this - if ((options === null) || Array.isArray(options) || (typeof options !== 'object')) { + if ((options === null) || _.isArray(options) || !_.isPlainObject(options)) { return $errUtils.throwErrByPath('get.invalid_options', { args: { options }, }) @@ -497,7 +497,7 @@ module.exports = (Commands, Cypress, cy) => { break case 'existence': - return err.displayMessage = getErr(err) + return err.message = getErr(err) default: break } diff --git a/packages/driver/src/cy/commands/screenshot.coffee b/packages/driver/src/cy/commands/screenshot.coffee index 73ca3a12665d..ea9e54ce11ef 100644 --- a/packages/driver/src/cy/commands/screenshot.coffee +++ b/packages/driver/src/cy/commands/screenshot.coffee @@ -352,7 +352,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> isWin = $dom.isWindow(subject) screenshotConfig = _.pick(options, "capture", "scale", "disableTimersAndAnimations", "blackout", "waitForCommandSynchronization", "padding", "clip", "onBeforeScreenshot", "onAfterScreenshot") - screenshotConfig = $Screenshot.validate(screenshotConfig, "cy.screenshot", options._log) + screenshotConfig = $Screenshot.validate(screenshotConfig, "screenshot", options._log) screenshotConfig = _.extend($Screenshot.getConfig(), screenshotConfig) ## set this regardless of options.log b/c its used by the diff --git a/packages/driver/src/cy/commands/traversals.coffee b/packages/driver/src/cy/commands/traversals.coffee index f5a9a81df5a5..e1226797ec84 100644 --- a/packages/driver/src/cy/commands/traversals.coffee +++ b/packages/driver/src/cy/commands/traversals.coffee @@ -59,5 +59,5 @@ module.exports = (Commands, Cypress, cy, state, config) -> onFail: (err) -> if err.type is "existence" node = $dom.stringify(subject, "short") - err.displayMessage += " Queried from element: #{node}" - }) + err.message += " Queried from element: #{node}" + }) diff --git a/packages/driver/src/cy/commands/window.coffee b/packages/driver/src/cy/commands/window.coffee index 4f3a7415491d..d5a62a70bf7c 100644 --- a/packages/driver/src/cy/commands/window.coffee +++ b/packages/driver/src/cy/commands/window.coffee @@ -178,7 +178,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> orientationIsValidAndLandscape = (orientation) => if orientation not in validOrientations - all = validOrientations.join("' or '") + all = validOrientations.join("` or `") $errUtils.throwErrByPath "viewport.invalid_orientation", { onFail: options._log args: { all, orientation } diff --git a/packages/driver/src/cy/commands/xhr.coffee b/packages/driver/src/cy/commands/xhr.coffee index 37b1eb451d2c..f7b1a5a4c032 100644 --- a/packages/driver/src/cy/commands/xhr.coffee +++ b/packages/driver/src/cy/commands/xhr.coffee @@ -168,7 +168,7 @@ startXhrServer = (cy, state, config) -> log.snapshot("response").end() onNetworkError: (xhr) -> - err = $errUtils.cypressErr($errUtils.errMsgByPath("xhr.network_error")) + err = $errUtils.cypressErrByPath("xhr.network_error") if log = logs[xhr.id] log.snapshot("failed").error(err) diff --git a/packages/driver/src/cy/ensures.coffee b/packages/driver/src/cy/ensures.coffee index 091eff0ea1dc..53850c58a37a 100644 --- a/packages/driver/src/cy/ensures.coffee +++ b/packages/driver/src/cy/ensures.coffee @@ -71,7 +71,9 @@ create = (state, expect) -> if types.length > 1 ## append a nice error message telling the user this - err = $errUtils.appendErrMsg(err, "All #{types.length} subject validations failed on this subject.") + errProps = $errUtils.appendErrMsg(err, "All #{types.length} subject validations failed on this subject.") + + $errUtils.mergeErrProps(err, errProps) throw err @@ -223,7 +225,7 @@ create = (state, expect) -> ## TODO: REFACTOR THIS TO CALL THE CHAI-OVERRIDES DIRECTLY ## OR GO THROUGH I18N - cy.ensureExistence($el) + ensureExistence($el) ensureElDoesNotHaveCSS = ($el, cssProperty, cssValue, onFail) -> cmd = state("current").get("name") diff --git a/packages/driver/src/cy/errors.coffee b/packages/driver/src/cy/errors.coffee index 812ae4f95854..cf80ad288e9b 100644 --- a/packages/driver/src/cy/errors.coffee +++ b/packages/driver/src/cy/errors.coffee @@ -1,5 +1,7 @@ +_ = require("lodash") $dom = require("../dom") $errUtils = require("../cypress/error_utils") +$errorMessages = require('../cypress/error_messages') crossOriginScriptRe = /^script error/i @@ -37,28 +39,37 @@ create = (state, config, log) -> msg = $errUtils.errMsgByPath("uncaught.cross_origin_script") createErrFromMsg = -> - new Error $errUtils.errMsgByPath("uncaught.error", { msg, source, lineno }) + new Error($errUtils.errMsgByPath("uncaught.error", { + msg, source, lineno + })) ## if we have the 5th argument it means we're in a super ## modern browser making this super simple to work with. err ?= createErrFromMsg() - err.name = "Uncaught " + err.name - - suffixMsg = switch type + uncaughtErrLookup = switch type when "app" then "uncaught.fromApp" when "spec" then "uncaught.fromSpec" - err = $errUtils.appendErrMsg(err, $errUtils.errMsgByPath(suffixMsg)) + uncaughtErrObj = $errUtils.errObjByPath($errorMessages, uncaughtErrLookup) + + err.name = "Uncaught " + err.name + + uncaughtErrProps = $errUtils.modifyErrMsg(err, uncaughtErrObj.message, (msg1, msg2) -> + return "#{msg1}\n\n#{msg2}" + ) + _.defaults(uncaughtErrProps, uncaughtErrObj) + + uncaughtErr = $errUtils.mergeErrProps(err, uncaughtErrProps) - err.onFail = -> + uncaughtErr.onFail = -> if l = current and current.getLastLog() - l.error(err) + l.error(uncaughtErr) ## normalize error message for firefox - $errUtils.normalizeErrorStack(err) + $errUtils.normalizeErrorStack(uncaughtErr) - return err + return uncaughtErr commandRunningFailed = (err) -> ## allow for our own custom onFail function diff --git a/packages/driver/src/cy/retries.coffee b/packages/driver/src/cy/retries.coffee index 0f672586dda6..8e13f595a81e 100644 --- a/packages/driver/src/cy/retries.coffee +++ b/packages/driver/src/cy/retries.coffee @@ -1,6 +1,7 @@ _ = require("lodash") Promise = require("bluebird") debug = require('debug')('cypress:driver:retries') + $utils = require("../cypress/utils") $errUtils = require("../cypress/error_utils") @@ -58,15 +59,19 @@ create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) - if assertions = options.assertions finishAssertions(assertions) - getErrMessage = (err) -> - _.get(err, 'displayMessage') or - _.get(err, 'message') or - err + { error, onFail } = options + + prependMsg = $errUtils.errMsgByPath("miscellaneous.retry_timed_out") + + retryErrProps = $errUtils.modifyErrMsg(error, prependMsg, (msg1, msg2) -> + return "#{msg2}#{msg1}" + ) + + retryErr = $errUtils.mergeErrProps(error, retryErrProps) - $errUtils.throwErrByPath "miscellaneous.retry_timed_out", { - onFail: (options.onFail or log) - args: { error: getErrMessage(options.error) } - } + $errUtils.throwErr(retryErr, { + onFail: onFail or log + }) runnableHasChanged = -> ## if we've changed runnables don't retry! diff --git a/packages/driver/src/cypress.js b/packages/driver/src/cypress.js index 0e00357f9d07..ad0c2161f026 100644 --- a/packages/driver/src/cypress.js +++ b/packages/driver/src/cypress.js @@ -54,7 +54,7 @@ $Log.command = () => { return $errUtils.throwErrByPath('miscellaneous.command_log_renamed') } -const throwDeprecatedCommandInterface = (key, method) => { +const throwDeprecatedCommandInterface = (key = 'commandName', method) => { let signature = '' switch (method) { @@ -326,9 +326,12 @@ class $Cypress { case 'runner:fail': { // mocha runner calculated a failure - const err = args[0].err + if (err.type === 'existence' || $dom.isDom(err.actual) || $dom.isDom(err.expected)) { + err.showDiff = false + } + if (err.actual) { err.actual = chai.util.inspect(err.actual) } @@ -504,7 +507,7 @@ class $Cypress { // attaching long stace traces // which otherwise make this err // unusably long - const err = $errUtils.cloneErr(e) + const err = $errUtils.makeErrFromObj(e) err.__stackCleaned__ = true err.backend = true @@ -526,7 +529,7 @@ class $Cypress { const e = reply.error if (e) { - const err = $errUtils.cloneErr(e) + const err = $errUtils.makeErrFromObj(e) err.automation = true diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index 840c64e57146..453f62b9fb3b 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -105,9 +105,9 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { const warnMixingPromisesAndCommands = function () { const title = state('runnable').fullTitle() - const msg = $errUtils.errMsgByPath('miscellaneous.mixing_promises_and_commands', title) - - return $utils.warning(msg) + $errUtils.warnByPath('miscellaneous.mixing_promises_and_commands', { + args: { title }, + }) } const $$ = function (selector, context) { @@ -678,7 +678,9 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { stopped = true - $errUtils.normalizeErrorStack(err) + err = $errUtils.normalizeErrorStack(err) + + err = $errUtils.processErr(err, config) // store the error on state now state('error', err) @@ -718,12 +720,13 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { rets = Cypress.action('cy:fail', err, state('runnable')) } catch (err2) { $errUtils.normalizeErrorStack(err2) + // and if any of these throw synchronously immediately error - finish(err2) + return finish(err2) } // bail if we had callbacks attached - if (rets.length) { + if (rets && rets.length) { return } @@ -1269,7 +1272,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { $utils.stringify(ret) $errUtils.throwErrByPath('miscellaneous.returned_value_and_commands', { - args: ret, + args: { returned: ret }, }) } diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index ede78696fff0..9663e68c460c 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -14,9 +14,9 @@ format = (data) -> formatConfigFile = (configFile) -> if configFile == false - return "'cypress.json' (currently disabled by --config-file=false)" + return "`cypress.json` (currently disabled by --config-file=false)" - return "'#{format(configFile)}'" + return "`#{format(configFile)}`" formatRedirect = (redirect) -> " - #{redirect}" @@ -31,7 +31,12 @@ formatProp = (memo, field) -> memo cmd = (command, args = "") -> - "cy.#{command}(#{args})" + prefix = if command.startsWith("Cypress") then "" else "cy." + + "`#{prefix}#{command}(#{args})`" + +getScreenshotDocsPath = (cmd) -> + if cmd is "Cypress.Screenshot.defaults" then "screenshot-api" else "screenshot" getRedirects = (obj, phrase) -> redirects = obj.redirects ? [] @@ -57,34 +62,58 @@ getHttpProps = (fields = []) -> module.exports = { add: - type_missing: "Cypress.add(key, fn, type) must include a type!" + type_missing: "`Cypress.add(key, fn, type)` must include a type!" agents: - deprecated_warning: "cy.agents() is deprecated. Use cy.stub() and cy.spy() instead." + deprecated_warning: "#{cmd('agents')} is deprecated. Use #{cmd('stub')} and #{cmd('spy')} instead." alias: - invalid: "Invalid alias: '{{name}}'.\nYou forgot the '@'. It should be written as: '@{{displayName}}'." - not_registered_with_available: "#{cmd('{{cmd}}')} could not find a registered alias for: '@{{displayName}}'.\nAvailable aliases are: '{{availableAliases}}'." - not_registered_without_available: "#{cmd('{{cmd}}')} could not find a registered alias for: '@{{displayName}}'.\nYou have not aliased anything yet." + invalid: "Invalid alias: `{{name}}`.\nYou forgot the `@`. It should be written as: `@{{displayName}}`." + not_registered_with_available: "#{cmd('{{cmd}}')} could not find a registered alias for: `@{{displayName}}`.\nAvailable aliases are: `{{availableAliases}}`." + not_registered_without_available: "#{cmd('{{cmd}}')} could not find a registered alias for: `@{{displayName}}`.\nYou have not aliased anything yet." as: - empty_string: "#{cmd('as')} cannot be passed an empty string." - invalid_type: "#{cmd('as')} can only accept a string." - invalid_first_token: "'{{alias}}' cannot be named starting with the '@' symbol. Try renaming the alias to '{{suggestedName}}', or something else that does not start with the '@' symbol." - reserved_word: "#{cmd('as')} cannot be aliased as: '{{alias}}'. This word is reserved." + empty_string: { + message: "#{cmd('as')} cannot be passed an empty string." + docsUrl: "https://on.cypress.io/as" + } + invalid_type: { + message: "#{cmd('as')} can only accept a string." + docsUrl: "https://on.cypress.io/as" + } + invalid_first_token: { + message: "`{{alias}}` cannot be named starting with the `@` symbol. Try renaming the alias to `{{suggestedName}}`, or something else that does not start with the `@` symbol." + docsUrl: "https://on.cypress.io/as" + } + reserved_word: { + message: "#{cmd('as')} cannot be aliased as: `{{alias}}`. This word is reserved." + docsUrl: "https://on.cypress.io/as" + } blur: - multiple_elements: "#{cmd('blur')} can only be called on a single element. Your subject contained {{num}} elements." - no_focused_element: "#{cmd('blur')} can only be called when there is a currently focused element." - timed_out: "#{cmd('blur')} timed out because your browser did not receive any blur events. This is a known bug in Chrome when it is not the currently focused window." - wrong_focused_element: "#{cmd('blur')} can only be called on the focused element. Currently the focused element is a: {{node}}" + multiple_elements: { + message: "#{cmd('blur')} can only be called on a single element. Your subject contained {{num}} elements." + docsUrl: "https://on.cypress.io/blur" + } + no_focused_element: { + message: "#{cmd('blur')} can only be called when there is a currently focused element." + docsUrl: "https://on.cypress.io/blur" + } + timed_out: { + message: "#{cmd('blur')} timed out because your browser did not receive any `blur` events. This is a known bug in Chrome when it is not the currently focused window." + docsUrl: "https://on.cypress.io/blur" + } + wrong_focused_element: { + message: "#{cmd('blur')} can only be called on the focused element. Currently the focused element is a: `{{node}}`" + docsUrl: "https://on.cypress.io/blur" + } browser: - invalid_arg: "Cypress.{{method}}() must be passed the name of a browser or an object to filter with. You passed: {{obj}}" + invalid_arg: "`Cypress.{{method}}()` must be passed the name of a browser or an object to filter with. You passed: `{{obj}}`" chai: - length_invalid_argument: "You must provide a valid number to a length assertion. You passed: '{{length}}'" - match_invalid_argument: "'match' requires its argument be a 'RegExp'. You passed: '{{regExp}}'" + length_invalid_argument: "You must provide a valid number to a `length` assertion. You passed: `{{length}}`" + match_invalid_argument: "`match` requires its argument be a `RegExp`. You passed: `{{regExp}}`" invalid_jquery_obj: (obj) -> """ You attempted to make a chai-jQuery assertion on an object that is neither a DOM object or a jQuery object. @@ -102,366 +131,510 @@ module.exports = { This can sometimes happen if a previous assertion changed the subject. """ - chain: - removed: """ - #{cmd('chain')} was an undocumented command that has now been removed. - - You can safely remove this from your code and it should work without it. - """ - check_uncheck: - invalid_element: "#{cmd('{{cmd}}')} can only be called on :checkbox{{phrase}}. Your subject {{word}} a: {{node}}" + invalid_element: { + message: "#{cmd('{{cmd}}')} can only be called on `:checkbox`{{phrase}}. Your subject {{word}} a: `{{node}}`" + docsUrl: "https://on.cypress.io/{{cmd}}" + } clear: - invalid_element: """ - #{cmd('clear')} failed because it requires a valid clearable element. - - The element cleared was: - - > {{node}} - - A clearable element matches one of the following selectors: - 'a[href]' - 'area[href]' - 'input' - 'select' - 'textarea' - 'button' - 'iframe' - '[tabindex]' - '[contenteditable]' - """ + invalid_element: { + message: """ + #{cmd('clear')} failed because it requires a valid clearable element. + + The element cleared was: + + > `{{node}}` + + A clearable element matches one of the following selectors: + 'a[href]' + 'area[href]' + 'input' + 'select' + 'textarea' + 'button' + 'iframe' + '[tabindex]' + '[contenteditable]' + """ + docsUrl: "https://on.cypress.io/clear" + } clearCookie: - invalid_argument: "#{cmd('clearCookie')} must be passed a string argument for name." + invalid_argument: { + message: "#{cmd('clearCookie')} must be passed a string argument for name." + docsUrl: "https://on.cypress.io/clearcookie" + } clearLocalStorage: - invalid_argument: "#{cmd('clearLocalStorage')} must be called with either a string or regular expression." + invalid_argument: { + message: "#{cmd('clearLocalStorage')} must be called with either a string or regular expression." + docsUrl: "https://on.cypress.io/clearlocalstorage" + } click: - multiple_elements: "#{cmd('{{cmd}}')} can only be called on a single element. Your subject contained {{num}} elements. Pass { multiple: true } if you want to serially click each element." - on_select_element: "#{cmd('{{cmd}}')} cannot be called on a ` element. Use #{cmd('select')} command instead to change the value." + docsUrl: "https://on.cypress.io/select" + } clock: - already_created: "#{cmd('clock')} can only be called once per test. Use the clock returned from the previous call." - invalid_1st_arg: "#{cmd('clock')} only accepts a number or an options object for its first argument. You passed: {{arg}}" - invalid_2nd_arg: "#{cmd('clock')} only accepts an array of function names or an options object for its second argument. You passed: {{arg}}" + invalid_1st_arg: { + message: "#{cmd('clock')} only accepts a number or an `options` object for its first argument. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/clock" + } + invalid_2nd_arg: { + message: "#{cmd('clock')} only accepts an array of function names or an `options` object for its second argument. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/clock" + } contains: - empty_string: "#{cmd('contains')} cannot be passed an empty string." - invalid_argument: "#{cmd('contains')} can only accept a string, number or regular expression." - length_option: "#{cmd('contains')} cannot be passed a length option because it will only ever return 1 element." - regex_conflict: "You passed a regular expression with the case-insensitive (i) flag and { matchCase: true } to #{cmd('contains')}. Those options conflict with each other, so please choose one or the other." + empty_string: { + message: "#{cmd('contains')} cannot be passed an empty string." + docsUrl: "https://on.cypress.io/contains" + } + invalid_argument: { + message: "#{cmd('contains')} can only accept a string, number or regular expression." + docsUrl: "https://on.cypress.io/contains" + } + length_option: { + message: "#{cmd('contains')} cannot be passed a `length` option because it will only ever return 1 element." + docsUrl: "https://on.cypress.io/contains" + } + regex_conflict: { + message: "You passed a regular expression with the case-insensitive (_i_) flag and `{ matchCase: true }` to #{cmd('contains')}. Those options conflict with each other, so please choose one or the other." + docsUrl: "https://on.cypress.io/contains" + } cookies: - backend_error: """ - #{cmd('{{command}}')} had an unexpected error {{action}} {{browserDisplayName}}. - - {{errMessage}} - {{errStack}} - """ - removed_method: """ - The Cypress.Cookies.{{method}}() method has been removed. + backend_error: (obj) -> { + message: """ + #{cmd('{{cmd}}')} had an unexpected error {{action}} {{browserDisplayName}}. + {{errMessage}} + {{errStack}} + """ + docsUrl: "https://on.cypress.io/#{_.toLower(obj.cmd)}" + } + + invalid_name: (obj) -> { + message: "#{cmd('{{cmd}}')} must be passed an RFC-6265-compliant cookie name. You passed:\n\n`{{name}}`" + docsUrl: "https://on.cypress.io/#{_.toLower(obj.cmd)}" + } + timed_out: (obj) -> { + message: "#{cmd('{{cmd}}')} timed out waiting `{{timeout}}ms` to complete." + docsUrl: "https://on.cypress.io/#{_.toLower(obj.cmd)}" + } + removed_method: { + message: """ + The `Cypress.Cookies.{{method}}()` method has been removed. Setting, getting, and clearing cookies is now an asynchronous operation. Replace this call with the appropriate command such as: - - cy.getCookie() - - cy.getCookies() - - cy.setCookie() - - cy.clearCookie() - - cy.clearCookies() - """ - timed_out: "#{cmd('{{cmd}}')} timed out waiting '{{timeout}}ms' to complete." + - `cy.getCookie()` + - `cy.getCookies()` + - `cy.setCookie()` + - `cy.clearCookie()` + - `cy.clearCookies()` + """ + } dom: - animating: """ - #{cmd('{{cmd}}')} could not be issued because this element is currently animating: + animating: { + message: """ + #{cmd('{{cmd}}')} could not be issued because this element is currently animating: - {{node}} + `{{node}}` - You can fix this problem by: - - Passing {force: true} which disables all error checking - - Passing {waitForAnimations: false} which disables waiting on animations - - Passing {animationDistanceThreshold: 20} which decreases the sensitivity - - https://on.cypress.io/element-is-animating - """ + You can fix this problem by: + - Passing `{force: true}` which disables all error checking + - Passing `{waitForAnimations: false}` which disables waiting on animations + - Passing `{animationDistanceThreshold: 20}` which decreases the sensitivity + """ + docsUrl: "https://on.cypress.io/element-is-animating" + } + animation_coords_history_invalid: "coordsHistory must be at least 2 sets of coords" animation_check_failed: "Not enough coord points provided to calculate distance." - center_hidden: """ - #{cmd('{{cmd}}')} failed because the center of this element is hidden from view: + center_hidden: { + message: """ + #{cmd('{{cmd}}')} failed because the center of this element is hidden from view: - {{node}} + `{{node}}` - Fix this problem, or use {force: true} to disable error checking. - - https://on.cypress.io/element-cannot-be-interacted-with - """ - covered: (obj) -> + Fix this problem, or use `{force: true}` to disable error checking. """ - #{cmd(obj.cmd)} failed because this element: - - #{obj.element1} + docsUrl: "https://on.cypress.io/element-cannot-be-interacted-with" + } + covered: { + message: """ + #{cmd('{{cmd}}')} failed because this element: - is being covered by another element: + `{{element1}}` - #{obj.element2} + is being covered by another element: - Fix this problem, or use {force: true} to disable error checking. + `{{element2}}` - https://on.cypress.io/element-cannot-be-interacted-with + Fix this problem, or use {force: true} to disable error checking. """ - pointer_events_none: (obj) -> - """ - #{cmd(obj.cmd)} failed because this element: - - #{obj.element} + docsUrl: "https://on.cypress.io/element-cannot-be-interacted-with" + } + pointer_events_none: (obj) -> { + message: """ + #{cmd(obj.cmd)} failed because this element: - has CSS 'pointer-events: none'#{if obj.elementInherited then ", inherited from this element:\n\n#{obj.elementInherited}\n" else ""} + `#{obj.element}` - 'pointer-events: none' prevents user mouse interaction. + has CSS `pointer-events: none`#{if obj.elementInherited then ", inherited from this element:\n\n`#{obj.elementInherited}`\n" else ""} - Fix this problem, or use {force: true} to disable error checking. + `pointer-events: none` prevents user mouse interaction. - https://on.cypress.io/element-cannot-be-interacted-with + Fix this problem, or use {force: true} to disable error checking. """ - disabled: """ - #{cmd('{{cmd}}')} failed because this element is disabled: - - {{node}} - - Fix this problem, or use {force: true} to disable error checking. - - https://on.cypress.io/element-cannot-be-interacted-with - """ - readonly: """ - #{cmd('{{cmd}}')} failed because this element is readonly: - - {{node}} + docsUrl: "https://on.cypress.io/element-cannot-be-interacted-with" + } + disabled: { + message: """ + #{cmd('{{cmd}}')} failed because this element is `disabled`: - Fix this problem, or use {force: true} to disable error checking. + `{{node}}` - https://on.cypress.io/element-cannot-be-interacted-with - """ - invalid_position_argument: "Invalid position argument: '{{position}}'. Position may only be {{validPositions}}." + Fix this problem, or use `{force: true}` to disable error checking. + """ + docsUrl: "https://on.cypress.io/element-cannot-be-interacted-with" + } + invalid_position_argument: { + message: "Invalid position argument: `{{position}}`. Position may only be {{validPositions}}." + docsUrl: "https://on.cypress.io/element-cannot-be-interacted-with" + } not_scrollable: """ #{cmd('{{cmd}}')} failed because this element is not scrollable:\n - {{node}}\n + `{{node}}`\n """ - not_visible: """ - #{cmd('{{cmd}}')} failed because this element is not visible: - - {{node}} - - {{reason}} + not_visible: { + message: """ + #{cmd('{{cmd}}')} failed because this element is not visible: - Fix this problem, or use {force: true} to disable error checking. + `{{node}}` - https://on.cypress.io/element-cannot-be-interacted-with - """ - readonly: """ - #{cmd('{{cmd}}')} failed because this element is readonly: + {{reason}} - {{node}} + Fix this problem, or use `{force: true}` to disable error checking. + """ + docsUrl: "https://on.cypress.io/element-cannot-be-interacted-with" + } + readonly: { + message: """ + #{cmd('{{cmd}}')} failed because this element is readonly: - Fix this problem, or use {force: true} to disable error checking. + `{{node}}` - https://on.cypress.io/element-cannot-be-interacted-with - """ + Fix this problem, or use `{force: true}` to disable error checking. + """ + docsUrl: "https://on.cypress.io/element-cannot-be-interacted-with" + } each: - invalid_argument: "#{cmd('each')} must be passed a callback function." - non_array: "#{cmd('each')} can only operate on an array like subject. Your subject was: '{{subject}}'" + invalid_argument: { + message: "#{cmd('each')} must be passed a callback function." + docsUrl: "https://on.cypress.io/each" + } + non_array: { + message: "#{cmd('each')} can only operate on an array like subject. Your subject was: `{{subject}}`" + docsUrl: "https://on.cypress.io/each" + } exec: - failed: """#{cmd('exec', '\'{{cmd}}\'')} failed with the following error: + failed: { + message: """#{cmd('exec', '\'{{cmd}}\'')} failed with the following error: - > "{{error}}" - """ - invalid_argument: "#{cmd('exec')} must be passed a non-empty string as its 1st argument. You passed: '{{cmd}}'." - non_zero_exit: """ - #{cmd('exec', '\'{{cmd}}\'')} failed because the command exited with a non-zero code. - - Pass {failOnNonZeroExit: false} to ignore exit code failures. - - Information about the failure: - Code: {{code}} - {{output}} - """ - timed_out: "#{cmd('exec', '\'{{cmd}}\'')} timed out after waiting {{timeout}}ms." + > "{{error}}" + """ + docsUrl: "https://on.cypress.io/exec" + } + invalid_argument: { + message: "#{cmd('exec')} must be passed a non-empty string as its 1st argument. You passed: '{{cmd}}'." + docsUrl: "https://on.cypress.io/exec" + } + non_zero_exit: { + message: """ + #{cmd('exec', '\'{{cmd}}\'')} failed because the command exited with a non-zero code. + + Pass `{failOnNonZeroExit: false}` to ignore exit code failures. + + Information about the failure: + Code: {{code}} + {{output}} + """ + docsUrl: "https://on.cypress.io/exec" + } + timed_out: { + message: "#{cmd('exec', '\'{{cmd}}\'')} timed out after waiting `{{timeout}}ms`." + docsUrl: "https://on.cypress.io/exec" + } files: - unexpected_error: """#{cmd('{{cmd}}', '"{{file}}"')} failed while trying to {{action}} the file at the following path: + unexpected_error: (obj) -> { + message: """#{cmd('{{cmd}}', '"{{file}}"')} failed while trying to {{action}} the file at the following path: - {{filePath}} + `{{filePath}}` - The following error occurred: + The following error occurred: - > "{{error}}" - """ - existent: """#{cmd('readFile', '"{{file}}"')} failed because the file exists when expected not to exist at the following path: - - {{filePath}} - """ - invalid_argument: "#{cmd('{{cmd}}')} must be passed a non-empty string as its 1st argument. You passed: '{{file}}'." - invalid_contents: "#{cmd('writeFile')} must be passed a non-empty string, an object, or an array as its 2nd argument. You passed: '{{contents}}'." - nonexistent: """#{cmd('readFile', '"{{file}}"')} failed because the file does not exist at the following path: - - {{filePath}} - """ - timed_out: "#{cmd('{{cmd}}', '"{{file}}"')} timed out after waiting {{timeout}}ms." + > "{{error}}" + """ + docsUrl: "https://on.cypress.io/#{_.toLower(obj.cmd)}" + } + existent: { + message: """#{cmd('readFile', '"{{file}}"')} failed because the file exists when expected not to exist at the following path: - fill: - invalid_1st_arg: "#{cmd('fill')} must be passed an object literal as its 1st argument" + `{{filePath}}` + """ + docsUrl: "https://on.cypress.io/readfile" + } + invalid_argument: (obj) -> { + message: "#{cmd('{{cmd}}')} must be passed a non-empty string as its 1st argument. You passed: `{{file}}`." + docsUrl: "https://on.cypress.io/#{_.toLower(obj.cmd)}" + } + invalid_contents: { + message: "#{cmd('writeFile')} must be passed a non-empty string, an object, or an array as its 2nd argument. You passed: `{{contents}}`." + docsUrl: "https://on.cypress.io/writefile" + } + nonexistent: { + message: """#{cmd('readFile', '"{{file}}"')} failed because the file does not exist at the following path: + + `{{filePath}}` + """ + docsUrl: "https://on.cypress.io/readfile" + } + timed_out: (obj) -> { + message: "#{cmd('{{cmd}}', '"{{file}}"')} timed out after waiting `{{timeout}}ms`." + docsUrl: "https://on.cypress.io/#{_.toLower(obj.cmd)}" + } fixture: - set_to_false: "#{cmd('fixture')} is not valid because you have configured 'fixturesFolder' to false." - timed_out: "#{cmd('fixture')} timed out waiting '{{timeout}}ms' to receive a fixture. No fixture was ever sent by the server." + set_to_false: { + message: "#{cmd('fixture')} is not valid because you have configured `fixturesFolder` to `false`." + docsUrl: "https://on.cypress.io/fixture" + } + timed_out: { + message: "#{cmd('fixture')} timed out waiting `{{timeout}}ms` to receive a fixture. No fixture was ever sent by the server." + docsUrl: "https://on.cypress.io/fixture" + } focus: - invalid_element: "#{cmd('focus')} can only be called on a valid focusable element. Your subject is a: {{node}}" - multiple_elements: "#{cmd('focus')} can only be called on a single element. Your subject contained {{num}} elements." - timed_out: "#{cmd('focus')} timed out because your browser did not receive any focus events. This is a known bug in Chrome when it is not the currently focused window." + invalid_element: { + message: "#{cmd('focus')} can only be called on a valid focusable element. Your subject is a: `{{node}}`" + docsUrl: "https://on.cypress.io/focus" + } + multiple_elements: { + message: "#{cmd('focus')} can only be called on a single element. Your subject contained {{num}} elements." + docsUrl: "https://on.cypress.io/focus" + } + timed_out: { + message: "#{cmd('focus')} timed out because your browser did not receive any `focus` events. This is a known bug in Chrome when it is not the currently focused window." + docsUrl: "https://on.cypress.io/focus" + } get: - alias_invalid: "'{{prop}}' is not a valid alias property. Only 'numbers' or 'all' is permitted." - alias_zero: "'0' is not a valid alias property. Are you trying to ask for the first response? If so write @{{alias}}.1" - invalid_options: "#{cmd('get')} only accepts an options object for its second argument. You passed {{options}}" + alias_invalid: { + message: "`{{prop}}` is not a valid alias property. Only `numbers` or `all` is permitted." + docsUrl: "https://on.cypress.io/get" + } + alias_zero: { + message: "`0` is not a valid alias property. Are you trying to ask for the first response? If so write `@{{alias}}.1`" + docsUrl: "https://on.cypress.io/get" + } + invalid_options: { + message: "#{cmd('get')} only accepts an options object for its second argument. You passed {{options}}" + docsUrl: "https://on.cypress.io/get" + } getCookie: - invalid_argument: "#{cmd('getCookie')} must be passed a string argument for name." + invalid_argument: { + message: "#{cmd('getCookie')} must be passed a string argument for name." + docsUrl: "https://on.cypress.io/getcookie" + } go: - invalid_argument: "#{cmd('go')} accepts only a string or number argument" - invalid_direction: "#{cmd('go')} accepts either 'forward' or 'back'. You passed: '{{str}}'" - invalid_number: "#{cmd('go')} cannot accept '0'. The number must be greater or less than '0'." + invalid_argument: { + message: "#{cmd('go')} accepts only a string or number argument" + docsUrl: "https://on.cypress.io/go" + } + invalid_direction: { + message: "#{cmd('go')} accepts either `forward` or `back`. You passed: `{{str}}`" + docsUrl: "https://on.cypress.io/go" + } + invalid_number: { + message: "#{cmd('go')} cannot accept `0`. The number must be greater or less than `0`." + docsUrl: "https://on.cypress.io/go" + } hover: - not_implemented: """ - #{cmd('hover')} is not currently implemented. - - However it is usually easy to workaround. - - Read the following document for a detailed explanation. - - https://on.cypress.io/hover - """ - - invoke: - prop_not_a_function: + not_implemented: { + message: """ + #{cmd('hover')} is not currently implemented.\n + However it is usually easy to workaround.\n + Read the following document for a detailed explanation.\n """ - #{cmd('invoke')} errored because the property: '{{prop}}' returned a '{{type}}' value instead of a function. #{cmd('invoke')} can only be used on properties that return callable functions. + docsUrl: "https://on.cypress.io/hover" + } + invoke: + prop_not_a_function: { + message: """ + #{cmd('invoke')} errored because the property: `{{prop}}` returned a `{{type}}` value instead of a function. #{cmd('invoke')} can only be used on properties that return callable functions. - #{cmd('invoke')} waited for the specified property '{{prop}}' to return a function, but it never did. + #{cmd('invoke')} waited for the specified property `{{prop}}` to return a function, but it never did. - If you want to assert on the property's value, then switch to use #{cmd('its')} and add an assertion such as: + If you want to assert on the property's value, then switch to use #{cmd('its')} and add an assertion such as: - cy.wrap({ foo: 'bar' }).its('foo').should('eq', 'bar') - """ - subject_null_or_undefined: - """ - #{cmd('invoke')} errored because your subject is: '{{value}}'. You cannot invoke any functions such as '{{prop}}' on a '{{value}}' value. + `cy.wrap({ foo: 'bar' }).its('foo').should('eq', 'bar')` + """ + docsUrl: "https://on.cypress.io/invoke" + } + subject_null_or_undefined: { + message: """ + #{cmd('invoke')} errored because your subject is: `{{value}}`. You cannot invoke any functions such as `{{prop}}` on a `{{value}}` value. - If you expect your subject to be '{{value}}', then add an assertion such as: + If you expect your subject to be `{{value}}`, then add an assertion such as: - cy.wrap({{value}}).should('be.{{value}}') - """ - null_or_undefined_prop_value: - """ - #{cmd('invoke')} errored because the property: '{{prop}}' is not a function, and instead returned a '{{value}}' value. + `cy.wrap({{value}}).should('be.{{value}}')` + """ + docsUrl: "https://on.cypress.io/invoke" + } + null_or_undefined_prop_value: { + message: """ + #{cmd('invoke')} errored because the property: `{{prop}}` is not a function, and instead returned a `{{value}}` value. - #{cmd('invoke')} waited for the specified property '{{prop}}' to become a callable function, but it never did. + #{cmd('invoke')} waited for the specified property `{{prop}}` to become a callable function, but it never did. - If you expect the property '{{prop}}' to be '{{value}}', then switch to use #{cmd('its')} and add an assertion such as: + If you expect the property `{{prop}}` to be `{{value}}`, then switch to use #{cmd('its')} and add an assertion such as: - cy.wrap({ foo: {{value}} }).its('foo').should('be.{{value}}') - """ + `cy.wrap({ foo: {{value}} }).its('foo').should('be.{{value}}')` + """ + docsUrl: "https://on.cypress.io/invoke" + } its: - subject_null_or_undefined: - """ - #{cmd('its')} errored because your subject is: '{{value}}'. You cannot access any properties such as '{{prop}}' on a '{{value}}' value. + subject_null_or_undefined: { + message: """ + #{cmd('its')} errored because your subject is: `{{value}}`. You cannot access any properties such as `{{prop}}` on a `{{value}}` value. - If you expect your subject to be '{{value}}', then add an assertion such as: + If you expect your subject to be `{{value}}`, then add an assertion such as: - cy.wrap({{value}}).should('be.{{value}}') - """ - null_or_undefined_prop_value: - """ - #{cmd('its')} errored because the property: '{{prop}}' returned a '{{value}}' value. + `cy.wrap({{value}}).should('be.{{value}}')` + """ + docsUrl: "https://on.cypress.io/its" + } + null_or_undefined_prop_value: { + message: """ + #{cmd('its')} errored because the property: `{{prop}}` returned a `{{value}}` value. - #{cmd('its')} waited for the specified property '{{prop}}' to become accessible, but it never did. + #{cmd('its')} waited for the specified property `{{prop}}` to become accessible, but it never did. - If you expect the property '{{prop}}' to be '{{value}}', then add an assertion such as: + If you expect the property `{{prop}}` to be `{{value}}`, then add an assertion such as: - cy.wrap({ foo: {{value}} }).its('foo').should('be.{{value}}') - """ + `cy.wrap({ foo: {{value}} }).its('foo').should('be.{{value}}')` + """ + docsUrl: "https://on.cypress.io/its" + } invoke_its: - nonexistent_prop: - """ - #{cmd('{{cmd}}')} errored because the property: '{{prop}}' does not exist on your subject. - - #{cmd('{{cmd}}')} waited for the specified property '{{prop}}' to exist, but it never did. - - If you do not expect the property '{{prop}}' to exist, then add an assertion such as: - - cy.wrap({ foo: 'bar' }).its('quux').should('not.exist') - """ - previous_prop_null_or_undefined: - """ - #{cmd('{{cmd}}')} errored because the property: '{{previousProp}}' returned a '{{value}}' value. The property: '{{prop}}' does not exist on a '{{value}}' value. - - #{cmd('{{cmd}}')} waited for the specified property '{{prop}}' to become accessible, but it never did. - - If you do not expect the property '{{prop}}' to exist, then add an assertion such as: - - cy.wrap({ foo: {{value}} }).its('foo.baz').should('not.exist') - """ - invalid_prop_name_arg: "#{cmd('{{cmd}}')} only accepts a string or a number as the {{identifier}}Name argument." - null_or_undefined_property_name: "#{cmd('{{cmd}}')} expects the {{identifier}}Name argument to have a value." - invalid_options_arg: "#{cmd('{{cmd}}')} only accepts an object as the options argument." - invalid_num_of_args: - """ + nonexistent_prop: { + message: """ + #{cmd('{{cmd}}')} errored because the property: `{{prop}}` does not exist on your subject. + + #{cmd('{{cmd}}')} waited for the specified property `{{prop}}` to exist, but it never did. + + If you do not expect the property `{{prop}}` to exist, then add an assertion such as: + + `cy.wrap({ foo: 'bar' }).its('quux').should('not.exist')` + """ + docsUrl: "https://on.cypress.io/{{cmd}}" + } + previous_prop_null_or_undefined: { + message: """ + #{cmd('{{cmd}}')} errored because the property: `{{previousProp}}` returned a `{{value}}` value. The property: `{{prop}}` does not exist on a `{{value}}` value. + + #{cmd('{{cmd}}')} waited for the specified property `{{prop}}` to become accessible, but it never did. + + If you do not expect the property `{{prop}}` to exist, then add an assertion such as: + + `cy.wrap({ foo: {{value}} }).its('foo.baz').should('not.exist')` + """ + docsUrl: "https://on.cypress.io/{{cmd}}" + } + invalid_1st_arg: { + message: "#{cmd('{{cmd}}')} only accepts a string as the first argument." + docsUrl: "https://on.cypress.io/{{cmd}}" + } + invalid_num_of_args: { + message: """ #{cmd('{{cmd}}')} does not accept additional arguments. - If you want to invoke a function with arguments, use cy.invoke(). + + If you want to invoke a function with arguments, use `.invoke()`. """ - timed_out: + docsUrl: "https://on.cypress.io/{{cmd}}" + } + invalid_options_arg: { + message: "#{cmd('{{cmd}}')} only accepts an object as the options argument." + docsUrl: "https://on.cypress.io/{{cmd}}" + } + invalid_prop_name_arg: { + message: "#{cmd('{{cmd}}')} only accepts a string or a number as the {{identifier}}Name argument." + docsUrl: "https://on.cypress.io/{{cmd}}" + } + null_or_undefined_property_name: { + message: "#{cmd('{{cmd}}')} expects the {{identifier}}Name argument to have a value." + docsUrl: "https://on.cypress.io/{{cmd}}" + } + timed_out: { + message: """ + #{cmd('{{cmd}}')} timed out after waiting `{{timeout}}ms`. + + Your callback function returned a promise which never resolved. + + The callback function was: + + {{func}} """ - #{cmd('{{cmd}}')} timed out after waiting '{{timeout}}ms'. - - Your callback function returned a promise which never resolved. - - The callback function was: - - {{func}} - """ - + docsUrl: "https://on.cypress.io/{{cmd}}" + } location: - invalid_key: "Location object does not have key: {{key}}" + invalid_key: { + message: "Location object does not have key: `{{key}}`" + docsUrl: "https://on.cypress.io/location" + } log: - invalid_argument: "Cypress.log() can only be called with an options object. Your argument was: '{{arg}}'" + invalid_argument: { + message: "`Cypress.log()` can only be called with an options object. Your argument was: `{{arg}}`" + docsUrl: "https://on.cypress.io/cypress-log" + } miscellaneous: - custom_command_interface_changed: (obj) -> - """ - Cypress.#{obj.method}(...) has been removed and replaced by: - - Cypress.Commands.add(...) + custom_command_interface_changed: (obj) -> { + message: """ + Cypress.#{obj.method}(...) has been removed and replaced by: - Instead of indicating 'parent', 'child', or 'dual' commands, you pass an options object - to describe the requirements around the previous subject. You can also enforce specific - subject types such as requiring the subject to be a DOM element. + `Cypress.Commands.add(...)` - To rewrite this custom command you'd likely write: + Instead of indicating `parent`, `child`, or `dual` commands, you pass an `options` object + to describe the requirements around the previous subject. You can also enforce specific + subject types such as requiring the subject to be a DOM element. - Cypress.Commands.add(#{obj.signature}) + To rewrite this custom command you'd likely write: - https://on.cypress.io/custom-command-interface-changed - """ - returned_value_and_commands_from_custom_command: (obj) -> - """ + `Cypress.Commands.add(#{obj.signature})` + """ + docsUrl: "https://on.cypress.io/custom-command-interface-changed" + } + returned_value_and_commands_from_custom_command: (obj) -> { + message: """ Cypress detected that you invoked one or more cy commands in a custom command but returned a different value. The custom command was: @@ -474,30 +647,30 @@ module.exports = { Because cy commands are asynchronous and are queued to be run later, it doesn't make sense to return anything else. - For convenience, you can also simply omit any return value or return 'undefined' and Cypress will not error. + For convenience, you can also simply omit any return value or return `undefined` and Cypress will not error. In previous versions of Cypress we automatically detected this and forced the cy commands to be returned. To make things less magical and clearer, we are now throwing an error. - - https://on.cypress.io/returning-value-and-commands-in-custom-command - """ - returned_value_and_commands: (ret) -> """ + docsUrl: "https://on.cypress.io/returning-value-and-commands-in-custom-command" + } + returned_value_and_commands: (obj) -> { + message: """ Cypress detected that you invoked one or more cy commands but returned a different value. The return value was: - > #{ret} + > #{obj.returned} Because cy commands are asynchronous and are queued to be run later, it doesn't make sense to return anything else. - For convenience, you can also simply omit any return value or return 'undefined' and Cypress will not error. + For convenience, you can also simply omit any return value or return `undefined` and Cypress will not error. In previous versions of Cypress we automatically detected this and forced the cy commands to be returned. To make things less magical and clearer, we are now throwing an error. - - https://on.cypress.io/returning-value-and-commands-in-test - """ - command_returned_promise_and_commands: (obj) -> """ + docsUrl: "https://on.cypress.io/returning-value-and-commands-in-test" + } + command_returned_promise_and_commands: (obj) -> { + message: """ Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise. The command that returned the promise was: @@ -513,337 +686,516 @@ module.exports = { Cypress will resolve your command with whatever the final Cypress command yields. The reason this is an error instead of a warning is because Cypress internally queues commands serially whereas Promises execute as soon as they are invoked. Attempting to reconcile this would prevent Cypress from ever resolving. - - https://on.cypress.io/returning-promise-and-commands-in-another-command - """ - mixing_promises_and_commands: (title) -> """ + docsUrl: "https://on.cypress.io/returning-promise-and-commands-in-another-command" + } + mixing_promises_and_commands: (obj) -> { + message: """ Cypress detected that you returned a promise in a test, but also invoked one or more cy commands inside of that promise. The test title was: - > #{title} + > #{obj.title} While this works in practice, it's often indicative of an anti-pattern. You almost never need to return both a promise and also invoke cy commands. Cy commands themselves are already promise like, and you can likely avoid the use of the separate Promise. - - https://on.cypress.io/returning-promise-and-commands-in-test """ + docsUrl: "https://on.cypress.io/returning-promise-and-commands-in-test" + } command_log_renamed: """ - Cypress.Log.command() has been renamed to Cypress.log() + `Cypress.Log.command()` has been renamed to `Cypress.log()` Please update your code. You should be able to safely do a find/replace. """ + dangling_commands: { + message: """ + Oops, Cypress detected something wrong with your test code. - dangling_commands: """ - Oops, Cypress detected something wrong with your test code. - - The test has finished but Cypress still has commands in its queue. - The {{numCommands}} queued commands that have not yet run are: + The test has finished but Cypress still has commands in its queue. + The {{numCommands}} queued commands that have not yet run are: - {{commands}} + {{commands}} - In every situation we've seen, this has been caused by programmer error. - Most often this indicates a race condition due to a forgotten 'return' or from commands in a previously run test bleeding into the current test. + In every situation we've seen, this has been caused by programmer error. - For a much more thorough explanation including examples please review this error here: + Most often this indicates a race condition due to a forgotten 'return' or from commands in a previously run test bleeding into the current test. - https://on.cypress.io/command-queue-ended-early - """ - invalid_command: "Could not find a command for: '{{name}}'.\n\nAvailable commands are: {{cmds}}.\n" - invalid_overwrite: "Cannot overwite command for: '{{name}}'. An existing command does not exist by that name." + For a much more thorough explanation including examples please review this error here: + """ + docsUrl: "https://on.cypress.io/command-queue-ended-early" + } + invalid_command: { + message: "Could not find a command for: `{{name}}`.\n\nAvailable commands are: {{cmds}}.\n" + docsUrl: "https://on.cypress.io/api" + } + invalid_overwrite: { + message: "Cannot overwite command for: `{{name}}`. An existing command does not exist by that name." + docsUrl: "https://on.cypress.io/api" + } invoking_child_without_parent: (obj) -> """ Oops, it looks like you are trying to call a child command before running a parent command. You wrote code that looks like this: - #{cmd(obj.cmd, obj.args)} + `#{cmd(obj.cmd, obj.args)}` A child command must be chained after a parent because it operates on a previous subject. - For example - if we were issuing the child command 'click'... + For example - if we were issuing the child command `click`... cy .get('button') // parent command must come first .click() // then child command comes second - """ - no_cy: "Cypress.cy is undefined. You may be trying to query outside of a running test. Cannot call Cypress.$()" - no_runner: "Cannot call Cypress#run without a runner instance." - outside_test: """ - Cypress cannot execute commands outside a running test. + no_cy: "`Cypress.cy` is `undefined`. You may be trying to query outside of a running test. Cannot call `Cypress.$()`" + no_runner: "Cannot call `Cypress#run` without a runner instance." + outside_test: { + message: """ + Cypress cannot execute commands outside a running test. - This usually happens when you accidentally write commands outside an 'it(...)' test. + This usually happens when you accidentally write commands outside an `it(...)` test. - If that is the case, just move these commands inside an it(...) test. + If that is the case, just move these commands inside an `it(...)` test. - Check your test file for errors. - - https://on.cypress.io/cannot-execute-commands-outside-test - """ - outside_test_with_cmd: """ - Cannot call "#{cmd('{{cmd}}')}" outside a running test. - - This usually happens when you accidentally write commands outside an it(...) test. + Check your test file for errors. + """ + docsUrl: "https://on.cypress.io/cannot-execute-commands-outside-test" + } + outside_test_with_cmd: { + message: """ + Cannot call #{cmd('{{cmd}}')} outside a running test. - If that is the case, just move these commands inside an it(...) test. + This usually happens when you accidentally write commands outside an `it(...)` test. - Check your test file for errors. + If that is the case, just move these commands inside an `it(...)` test. - https://on.cypress.io/cannot-execute-commands-outside-test - """ - private_custom_command_interface: "You cannot use the undocumented private command interface: {{method}}" + Check your test file for errors. + """ + docsUrl: "https://on.cypress.io/cannot-execute-commands-outside-test" + } + private_custom_command_interface: "You cannot use the undocumented private command interface: `{{method}}`" private_property: """ - You are accessing a private property directly on 'cy' which has been renamed. + You are accessing a private property directly on `cy` which has been renamed. This was never documented nor supported. Please go through the public function: #{cmd('state', "...")} """ - retry_timed_out: "Timed out retrying: {{error}}" + retry_timed_out: "Timed out retrying: " mocha: - async_timed_out: "Timed out after '{{ms}}ms'. The done() callback was never invoked!" - invalid_interface: "Invalid mocha interface '{{name}}'" - timed_out: "Cypress command timeout of '{{ms}}ms' exceeded." - overspecified: """ - Cypress detected that you returned a promise in a test, but also invoked a done callback. Return a promise -or- invoke a done callback, not both. + async_timed_out: "Timed out after `{{ms}}ms`. The `done()` callback was never invoked!" + invalid_interface: "Invalid mocha interface `{{name}}`" + timed_out: "Cypress command timeout of `{{ms}}ms` exceeded." + overspecified: { + message: """ + Cypress detected that you returned a promise in a test, but also invoked a done callback. Return a promise -or- invoke a done callback, not both. - Read more here: https://on.cypress.io/returning-promise-and-invoking-done-callback + Original mocha error: - #{divider(60, '-')} - - Original mocha error: - - {{error}} - """ + {{error}} + """ + docsUrl: "https://on.cypress.io/returning-promise-and-invoking-done-callback" + } navigation: - cross_origin: ({ message, originPolicy, configFile }) -> """ - Cypress detected a cross origin error happened on page load: - - > #{message} - - Before the page load, you were bound to the origin policy: + cross_origin: ({ message, originPolicy, configFile }) -> { + message: """ + Cypress detected a cross origin error happened on page load: - > #{originPolicy} + > #{message} - A cross origin error happens when your application navigates to a new URL which does not match the origin policy above. + Before the page load, you were bound to the origin policy: - A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different. + > #{originPolicy} - Cypress does not allow you to navigate to a different origin URL within a single test. + A cross origin error happens when your application navigates to a new URL which does not match the origin policy above. - You may need to restructure some of your test code to avoid this problem. + A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different. - Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false } in #{formatConfigFile(configFile)}. + Cypress does not allow you to navigate to a different origin URL within a single test. - https://on.cypress.io/cross-origin-violation + You may need to restructure some of your test code to avoid this problem. - """ + Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false } in #{formatConfigFile(configFile)}. + """ + docsUrl: "https://on.cypress.io/cross-origin-violation" + } timed_out: ({ ms, configFile }) -> """ - Timed out after waiting '#{ms}ms' for your remote page to load. + Timed out after waiting `#{ms}ms` for your remote page to load. - Your page did not fire its 'load' event within '#{ms}ms'. + Your page did not fire its `load` event within `#{ms}ms`. - You can try increasing the 'pageLoadTimeout' value in #{formatConfigFile(configFile)} to wait longer. + You can try increasing the `pageLoadTimeout` value in #{formatConfigFile(configFile)} to wait longer. - Browsers will not fire the 'load' event until all stylesheets and scripts are done downloading. + Browsers will not fire the `load` event until all stylesheets and scripts are done downloading. - When this 'load' event occurs, Cypress will continue running commands. + When this `load` event occurs, Cypress will continue running commands. """ ng: - no_global: "Angular global (window.angular) was not found in your window. You cannot use #{cmd('ng')} methods without angular." + no_global: "Angular global (`window.angular`) was not found in your window. You cannot use #{cmd('ng')} methods without angular." reload: - invalid_arguments: "#{cmd('reload')} can only accept a boolean or options as its arguments." + invalid_arguments: { + message: "#{cmd('reload')} can only accept a boolean or `options` as its arguments." + docsUrl: "https://on.cypress.io/reload" + } request: - body_circular: ({ path }) -> """ - The `body` parameter supplied to #{cmd('request')} contained a circular reference at the path "#{path.join(".")}". - - `body` can only be a string or an object with no circular references. - """ - status_code_flags_invalid: """ - #{cmd('request')} was invoked with { failOnStatusCode: false, retryOnStatusCodeFailure: true }. + body_circular: ({ path }) -> { + message: """ + The `body` parameter supplied to #{cmd('request')} contained a circular reference at the path "#{path.join(".")}". - These options are incompatible with each other. - - - To retry on non-2xx status codes, pass { failOnStatusCode: true, retryOnStatusCodeFailure: true }. - - To not retry on non-2xx status codes, pass { failOnStatusCode: true, retryOnStatusCodeFailure: true }. - - To fail on non-2xx status codes without retrying (the default behavior), pass { failOnStatusCode: true, retryOnStatusCodeFailure: false } - """ - auth_invalid: "#{cmd('request')} must be passed an object literal for the 'auth' option." - gzip_invalid: "#{cmd('request')} requires the 'gzip' option to be a boolean." - headers_invalid: "#{cmd('request')} requires the 'headers' option to be an object literal." - invalid_method: "#{cmd('request')} was called with an invalid method: '{{method}}'. Method can be: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, or any other method supported by Node's HTTP parser." - failonstatus_deprecated_warning: "The cy.request() 'failOnStatus' option has been renamed to 'failOnStatusCode'. Please update your code. This option will be removed at a later time." - form_invalid: """ - #{cmd('request')} requires the 'form' option to be a boolean. - - If you're trying to send a x-www-form-urlencoded request then pass either a string or object literal to the 'body' property. - """ - loading_failed: (obj) -> + `body` can only be a string or an object with no circular references. """ - #{cmd('request')} failed trying to load: - - #{obj.url} - - We attempted to make an http request to this URL but the request failed without a response. - - We received this error at the network level: - - > #{obj.error} - - #{divider(60, '-')} - - The request we sent was: - - #{getHttpProps([ - {key: 'method', value: obj.method}, - {key: 'URL', value: obj.url}, - ])} - - #{divider(60, '-')} - - Common situations why this would fail: - - you don't have internet access - - you forgot to run / boot your web server - - your web server isn't accessible - - you have weird network configuration settings on your computer + docsUrl: "https://on.cypress.io/request" + } + status_code_flags_invalid: { + message: """ + #{cmd('request')} was invoked with `{ failOnStatusCode: false, retryOnStatusCodeFailure: true }`. - The stack trace for this error is: - - #{obj.stack} - """ + These options are incompatible with each other. - status_invalid: (obj) -> + - To retry on non-2xx status codes, pass `{ failOnStatusCode: true, retryOnStatusCodeFailure: true }`. + - To not retry on non-2xx status codes, pass `{ failOnStatusCode: true, retryOnStatusCodeFailure: true }`. + - To fail on non-2xx status codes without retrying (the default behavior), pass `{ failOnStatusCode: true, retryOnStatusCodeFailure: false }` """ - #{cmd('request')} failed on: + docsUrl: "https://on.cypress.io/request" + } + auth_invalid: { + message: "#{cmd('request')} must be passed an object literal for the `auth` option." + docsUrl: "https://on.cypress.io/request" + } + gzip_invalid: { + message: "#{cmd('request')} requires the `gzip` option to be a boolean." + docsUrl: "https://on.cypress.io/request" + } + headers_invalid: { + message: "#{cmd('request')} requires the `headers` option to be an object literal." + docsUrl: "https://on.cypress.io/request" + } + invalid_method: { + message: "#{cmd('request')} was called with an invalid method: `{{method}}`. Method can be: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, or any other method supported by Node's HTTP parser." + docsUrl: "https://on.cypress.io/request" + } + failonstatus_deprecated_warning: { + message: "The #{cmd('request')} `failOnStatus` option has been renamed to `failOnStatusCode`. Please update your code. This option will be removed at a later time." + docsUrl: "https://on.cypress.io/request" + } + form_invalid: { + message: """ + #{cmd('request')} requires the `form` option to be a boolean. + + If you're trying to send a `x-www-form-urlencoded` request then pass either a string or object literal to the `body` property. + """ + docsUrl: "https://on.cypress.io/request" + } + loading_failed: (obj) -> { + message: """ + #{cmd('request')} failed trying to load: - #{obj.url} + #{obj.url} - The response we received from your web server was: + We attempted to make an http request to this URL but the request failed without a response. - > #{obj.status}: #{obj.statusText} + We received this error at the network level: - This was considered a failure because the status code was not '2xx' or '3xx'. + > #{obj.error} - If you do not want status codes to cause failures pass the option: 'failOnStatusCode: false' + #{divider(60, '-')} - #{divider(60, '-')} + The request we sent was: - The request we sent was: + #{getHttpProps([ + {key: 'method', value: obj.method}, + {key: 'URL', value: obj.url}, + ])} - #{getHttpProps([ - {key: 'method', value: obj.method}, - {key: 'URL', value: obj.url}, - {key: 'headers', value: obj.requestHeaders}, - {key: 'body', value: obj.requestBody} - {key: 'redirects', value: obj.redirects} - ])} + #{divider(60, '-')} - #{divider(60, '-')} + Common situations why this would fail: + - you don't have internet access + - you forgot to run / boot your web server + - your web server isn't accessible + - you have weird network configuration settings on your computer - The response we got was: + The stack trace for this error is: - #{getHttpProps([ - {key: 'status', value: obj.status + ' - ' + obj.statusText}, - {key: 'headers', value: obj.responseHeaders}, - {key: 'body', value: obj.responseBody} - ])} + #{obj.stack} + """ + docsUrl: "https://on.cypress.io/request" + } + status_invalid: (obj) -> { + message: """ + #{cmd('request')} failed on: - """ - timed_out: (obj) -> - """ - #{cmd('request')} timed out waiting #{obj.timeout}ms for a response from your server. + #{obj.url} - The request we sent was: + The response we received from your web server was: - #{getHttpProps([ - {key: 'method', value: obj.method}, - {key: 'URL', value: obj.url}, - ])} + > #{obj.status}: #{obj.statusText} - No response was received within the timeout. - """ - url_missing: "#{cmd('request')} requires a url. You did not provide a url." - url_invalid: ({configFile}) -> - "#{cmd('request')} must be provided a fully qualified url - one that begins with 'http'. By default #{cmd('request')} will use either the current window's origin or the 'baseUrl' in #{formatConfigFile(configFile)}. Neither of those values were present." - url_wrong_type: "#{cmd('request')} requires the url to be a string." + This was considered a failure because the status code was not `2xx` or `3xx`. + + If you do not want status codes to cause failures pass the option: `failOnStatusCode: false` + + #{divider(60, '-')} + + The request we sent was: + + #{getHttpProps([ + {key: 'method', value: obj.method}, + {key: 'URL', value: obj.url}, + {key: 'headers', value: obj.requestHeaders}, + {key: 'body', value: obj.requestBody} + {key: 'redirects', value: obj.redirects} + ])} + + #{divider(60, '-')} + + The response we got was: + + #{getHttpProps([ + {key: 'status', value: obj.status + ' - ' + obj.statusText}, + {key: 'headers', value: obj.responseHeaders}, + {key: 'body', value: obj.responseBody} + ])} + + """ + docsUrl: "https://on.cypress.io/request" + } + timed_out: (obj) -> { + message: """ + #{cmd('request')} timed out waiting `#{obj.timeout}ms` for a response from your server. + + The request we sent was: + + #{getHttpProps([ + {key: 'method', value: obj.method}, + {key: 'URL', value: obj.url}, + ])} + + No response was received within the timeout. + """ + docsUrl: "https://on.cypress.io/request" + } + url_missing: { + message: "#{cmd('request')} requires a `url`. You did not provide a `url`." + docsUrl: "https://on.cypress.io/request" + } + url_invalid: ({ configFile }) -> { + message: "#{cmd('request')} must be provided a fully qualified `url` - one that begins with `http`. By default #{cmd('request')} will use either the current window's origin or the `baseUrl` in #{formatConfigFile(configFile)}. Neither of those values were present." + docsUrl: "https://on.cypress.io/request" + } + url_wrong_type: { + message: "#{cmd('request')} requires the `url` to be a string." + docsUrl: "https://on.cypress.io/request" + } route: - failed_prerequisites: "#{cmd('route')} cannot be invoked before starting the #{cmd('server')}" - invalid_arguments: "#{cmd('route')} was not provided any arguments. You must provide valid arguments." - method_invalid: "#{cmd('route')} was called with an invalid method: '{{method}}'. Method can be: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, or any other method supported by Node's HTTP parser." - response_invalid: "#{cmd('route')} cannot accept an undefined or null response. It must be set to something, even an empty string will work." - url_invalid: "#{cmd('route')} was called with an invalid url. Url must be either a string or regular expression." - url_missing: "#{cmd('route')} must be called with a url. It can be a string or regular expression." - url_percentencoding_warning: ({ decodedUrl }) -> """ - A URL with percent-encoded characters was passed to cy.route(), but cy.route() expects a decoded URL. - - Did you mean to pass "#{decodedUrl}"? - """ + failed_prerequisites: { + message: "#{cmd('route')} cannot be invoked before starting the #{cmd('server')}" + docsUrl: "https://on.cypress.io/server" + } + invalid_arguments: { + message: "#{cmd('route')} was not provided any arguments. You must provide valid arguments." + docsUrl: "https://on.cypress.io/route" + } + method_invalid: { + message: "#{cmd('route')} was called with an invalid method: `{{method}}`. Method can be: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, or any other method supported by Node's HTTP parser." + docsUrl: "https://on.cypress.io/route" + } + response_invalid: { + message: "#{cmd('route')} cannot accept an `undefined` or `null` response. It must be set to something, even an empty string will work." + docsUrl: "https://on.cypress.io/route" + } + url_invalid: { + message: "#{cmd('route')} was called with an invalid `url`. `url` must be either a string or regular expression." + docsUrl: "https://on.cypress.io/route" + } + url_missing: { + message: "#{cmd('route')} must be called with a `url`. It can be a string or regular expression." + docsUrl: "https://on.cypress.io/route" + } + url_percentencoding_warning: ({ decodedUrl }) -> { + message: """ + A `url` with percent-encoded characters was passed to #{cmd('route')}, but #{cmd('route')} expects a decoded `url`. + + Did you mean to pass "#{decodedUrl}"? + """ + docsUrl: "https://on.cypress.io/route" + } scrollIntoView: - invalid_argument: "#{cmd('scrollIntoView')} can only be called with an options object. Your argument was: {{arg}}" - subject_is_window: "Cannot call #{cmd('scrollIntoView')} on Window subject." - multiple_elements: "#{cmd('scrollIntoView')} can only be used to scroll to 1 element, you tried to scroll to {{num}} elements.\n\n" - invalid_easing: "#{cmd('scrollIntoView')} must be called with a valid easing. Your easing was: {{easing}}" - invalid_duration: "#{cmd('scrollIntoView')} must be called with a valid duration. Duration may be either a number (ms) or a string representing a number (ms). Your duration was: {{duration}}" + invalid_argument: { + message: "#{cmd('scrollIntoView')} can only be called with an `options` object. Your argument was: `{{arg}}`" + docsUrl: "https://on.cypress.io/scrollintoview" + } + multiple_elements: { + message: "#{cmd('scrollIntoView')} can only be used to scroll to 1 element, you tried to scroll to {{num}} elements.\n\n" + docsUrl: "https://on.cypress.io/scrollintoview" + } + invalid_easing: { + message: "#{cmd('scrollIntoView')} must be called with a valid `easing`. Your easing was: `{{easing}}`" + docsUrl: "https://on.cypress.io/scrollintoview" + } + invalid_duration: { + message: "#{cmd('scrollIntoView')} must be called with a valid `duration`. Duration may be either a number (ms) or a string representing a number (ms). Your duration was: `{{duration}}`" + docsUrl: "https://on.cypress.io/scrollintoview" + } scrollTo: - invalid_target: "#{cmd('scrollTo')} must be called with a valid position. It can be a string, number or object. Your position was: {{x}}, {{y}}" - multiple_containers: "#{cmd('scrollTo')} can only be used to scroll one element, you tried to scroll {{num}} elements.\n\n" - invalid_easing: "#{cmd('scrollTo')} must be called with a valid easing. Your easing was: {{easing}}" - invalid_duration: "#{cmd('scrollTo')} must be called with a valid duration. Duration may be either a number (ms) or a string representing a number (ms). Your duration was: {{duration}}" - animation_failed: "#{cmd('scrollTo')} failed." + animation_failed: { + message: "#{cmd('scrollTo')} failed to scroll." + docsUrl: "https://on.cypress.io/scrollto" + } + invalid_easing: { + message: "#{cmd('scrollTo')} must be called with a valid `easing`. Your easing was: `{{easing}}`" + docsUrl: "https://on.cypress.io/scrollto" + } + invalid_duration: { + message: "#{cmd('scrollTo')} must be called with a valid `duration`. Duration may be either a number (ms) or a string representing a number (ms). Your duration was: `{{duration}}`" + docsUrl: "https://on.cypress.io/scrollto" + } + invalid_target: { + message: "#{cmd('scrollTo')} must be called with a valid `position`. It can be a string, number or object. Your position was: `{{x}}, {{y}}`" + docsUrl: "https://on.cypress.io/scrollto" + } + multiple_containers: { + message: "#{cmd('scrollTo')} can only be used to scroll 1 element, you tried to scroll {{num}} elements.\n\n" + docsUrl: "https://on.cypress.io/scrollto" + } screenshot: - invalid_arg: "{{cmd}}() must be called with an object. You passed: {{arg}}" - invalid_capture: "{{cmd}}() 'capture' option must be one of the following: 'fullPage', 'viewport', or 'runner'. You passed: {{arg}}" - invalid_boolean: "{{cmd}}() '{{option}}' option must be a boolean. You passed: {{arg}}" - invalid_blackout: "{{cmd}}() 'blackout' option must be an array of strings. You passed: {{arg}}" - invalid_clip: "{{cmd}}() 'clip' option must be an object with the keys { width, height, x, y } and number values. You passed: {{arg}}" - invalid_height: "#{cmd('screenshot')} only works with a screenshot area with a height greater than zero." - invalid_padding: "{{cmd}}() 'padding' option must be either a number or an array of numbers with a maximum length of 4. You passed: {{arg}}" - invalid_callback: "{{cmd}}() '{{callback}}' option must be a function. You passed: {{arg}}" - multiple_elements: "#{cmd('screenshot')} only works for a single element. You attempted to screenshot {{numElements}} elements." - timed_out: "#{cmd('screenshot')} timed out waiting '{{timeout}}ms' to complete." + invalid_arg: (obj) -> { + message: "#{cmd(obj.cmd)} must be called with an object. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/#{getScreenshotDocsPath(obj.cmd)}" + } + invalid_capture: (obj) -> { + message: "#{cmd(obj.cmd)} `capture` option must be one of the following: `fullPage`, `viewport`, or `runner`. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/#{getScreenshotDocsPath(obj.cmd)}" + } + invalid_boolean: (obj) -> { + message: "#{cmd(obj.cmd)} `{{option}}` option must be a boolean. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/#{getScreenshotDocsPath(obj.cmd)}" + } + invalid_blackout: (obj) -> { + message: "#{cmd(obj.cmd)} `blackout` option must be an array of strings. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/#{getScreenshotDocsPath(obj.cmd)}" + } + invalid_callback: (obj) -> { + message: "#{cmd(obj.cmd)} `{{callback}}` option must be a function. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/#{getScreenshotDocsPath(obj.cmd)}" + } + invalid_clip: (obj) -> { + message: "#{cmd(obj.cmd)} `clip` option must be an object with the keys `{ width, height, x, y }` and number values. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/#{getScreenshotDocsPath(obj.cmd)}" + } + invalid_height: (obj) -> { + message: "#{cmd('screenshot')} only works with a screenshot area with a height greater than zero." + docsUrl: "https://on.cypress.io/screenshot" + } + invalid_padding: (obj) -> { + message: "#{cmd(obj.cmd)} `padding` option must be either a number or an array of numbers with a maximum length of 4. You passed: `{{arg}}`" + docsUrl: "https://on.cypress.io/#{getScreenshotDocsPath(obj.cmd)}" + } + multiple_elements: { + message: "#{cmd('screenshot')} only works for a single element. You attempted to screenshot {{numElements}} elements." + docsUrl: "https://on.cypress.io/screenshot" + } + timed_out: { + message: "#{cmd('screenshot')} timed out waiting `{{timeout}}ms` to complete." + docsUrl: "https://on.cypress.io/screenshot" + } select: - disabled: "#{cmd('select')} failed because this element is currently disabled:\n\n{{node}}" - invalid_element: "#{cmd('select')} can only be called on a . Your subject contained {{num}} elements." - multiple_matches: "#{cmd('select')} matched more than one option by value or text: {{value}}" - no_matches: "#{cmd('select')} failed because it could not find a single