From a8796c48dc875380c495bccb5ea7db49ac432499 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Mon, 27 Apr 2020 15:25:25 +0900 Subject: [PATCH 1/3] decaffeinate: Rename location.coffee and 10 other files from .coffee to .js --- packages/driver/src/cy/commands/{location.coffee => location.js} | 0 packages/driver/src/cy/commands/{misc.coffee => misc.js} | 0 .../driver/src/cy/commands/{navigation.coffee => navigation.js} | 0 packages/driver/src/cy/commands/{popups.coffee => popups.js} | 0 packages/driver/src/cy/commands/{request.coffee => request.js} | 0 .../driver/src/cy/commands/{screenshot.coffee => screenshot.js} | 0 packages/driver/src/cy/commands/{task.coffee => task.js} | 0 .../driver/src/cy/commands/{traversals.coffee => traversals.js} | 0 packages/driver/src/cy/commands/{waiting.coffee => waiting.js} | 0 packages/driver/src/cy/commands/{window.coffee => window.js} | 0 packages/driver/src/cy/commands/{xhr.coffee => xhr.js} | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename packages/driver/src/cy/commands/{location.coffee => location.js} (100%) rename packages/driver/src/cy/commands/{misc.coffee => misc.js} (100%) rename packages/driver/src/cy/commands/{navigation.coffee => navigation.js} (100%) rename packages/driver/src/cy/commands/{popups.coffee => popups.js} (100%) rename packages/driver/src/cy/commands/{request.coffee => request.js} (100%) rename packages/driver/src/cy/commands/{screenshot.coffee => screenshot.js} (100%) rename packages/driver/src/cy/commands/{task.coffee => task.js} (100%) rename packages/driver/src/cy/commands/{traversals.coffee => traversals.js} (100%) rename packages/driver/src/cy/commands/{waiting.coffee => waiting.js} (100%) rename packages/driver/src/cy/commands/{window.coffee => window.js} (100%) rename packages/driver/src/cy/commands/{xhr.coffee => xhr.js} (100%) diff --git a/packages/driver/src/cy/commands/location.coffee b/packages/driver/src/cy/commands/location.js similarity index 100% rename from packages/driver/src/cy/commands/location.coffee rename to packages/driver/src/cy/commands/location.js diff --git a/packages/driver/src/cy/commands/misc.coffee b/packages/driver/src/cy/commands/misc.js similarity index 100% rename from packages/driver/src/cy/commands/misc.coffee rename to packages/driver/src/cy/commands/misc.js diff --git a/packages/driver/src/cy/commands/navigation.coffee b/packages/driver/src/cy/commands/navigation.js similarity index 100% rename from packages/driver/src/cy/commands/navigation.coffee rename to packages/driver/src/cy/commands/navigation.js diff --git a/packages/driver/src/cy/commands/popups.coffee b/packages/driver/src/cy/commands/popups.js similarity index 100% rename from packages/driver/src/cy/commands/popups.coffee rename to packages/driver/src/cy/commands/popups.js diff --git a/packages/driver/src/cy/commands/request.coffee b/packages/driver/src/cy/commands/request.js similarity index 100% rename from packages/driver/src/cy/commands/request.coffee rename to packages/driver/src/cy/commands/request.js diff --git a/packages/driver/src/cy/commands/screenshot.coffee b/packages/driver/src/cy/commands/screenshot.js similarity index 100% rename from packages/driver/src/cy/commands/screenshot.coffee rename to packages/driver/src/cy/commands/screenshot.js diff --git a/packages/driver/src/cy/commands/task.coffee b/packages/driver/src/cy/commands/task.js similarity index 100% rename from packages/driver/src/cy/commands/task.coffee rename to packages/driver/src/cy/commands/task.js diff --git a/packages/driver/src/cy/commands/traversals.coffee b/packages/driver/src/cy/commands/traversals.js similarity index 100% rename from packages/driver/src/cy/commands/traversals.coffee rename to packages/driver/src/cy/commands/traversals.js diff --git a/packages/driver/src/cy/commands/waiting.coffee b/packages/driver/src/cy/commands/waiting.js similarity index 100% rename from packages/driver/src/cy/commands/waiting.coffee rename to packages/driver/src/cy/commands/waiting.js diff --git a/packages/driver/src/cy/commands/window.coffee b/packages/driver/src/cy/commands/window.js similarity index 100% rename from packages/driver/src/cy/commands/window.coffee rename to packages/driver/src/cy/commands/window.js diff --git a/packages/driver/src/cy/commands/xhr.coffee b/packages/driver/src/cy/commands/xhr.js similarity index 100% rename from packages/driver/src/cy/commands/xhr.coffee rename to packages/driver/src/cy/commands/xhr.js From 8bd67a3e5c6377d8eacb8f689eb4a3def972b27f Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Mon, 27 Apr 2020 15:25:28 +0900 Subject: [PATCH 2/3] decaffeinate: Convert location.coffee and 10 other files to JS --- packages/driver/src/cy/commands/location.js | 178 +- packages/driver/src/cy/commands/misc.js | 82 +- packages/driver/src/cy/commands/navigation.js | 1532 +++++++++-------- packages/driver/src/cy/commands/popups.js | 62 +- packages/driver/src/cy/commands/request.js | 589 ++++--- packages/driver/src/cy/commands/screenshot.js | 670 +++---- packages/driver/src/cy/commands/task.js | 146 +- packages/driver/src/cy/commands/traversals.js | 142 +- packages/driver/src/cy/commands/waiting.js | 389 +++-- packages/driver/src/cy/commands/window.js | 389 +++-- packages/driver/src/cy/commands/xhr.js | 920 +++++----- 11 files changed, 2761 insertions(+), 2338 deletions(-) diff --git a/packages/driver/src/cy/commands/location.js b/packages/driver/src/cy/commands/location.js index 307d429a26b5..bdd9e2a1dc85 100644 --- a/packages/driver/src/cy/commands/location.js +++ b/packages/driver/src/cy/commands/location.js @@ -1,77 +1,101 @@ -_ = require("lodash") -Promise = require("bluebird") - -$errUtils = require("../../cypress/error_utils") -$Location = require("../../cypress/location") - -module.exports = (Commands, Cypress, cy, state, config) -> - Commands.addAll({ - url: (options = {}) -> - userOptions = options - options = _.defaults({}, userOptions, { log: true }) - - if options.log isnt false - options._log = Cypress.log({ - message: "" - }) - - getHref = => - cy.getRemoteLocation("href") - - do resolveHref = => - Promise.try(getHref).then (href) => - cy.verifyUpcomingAssertions(href, options, { - onRetry: resolveHref - }) - - hash: (options = {}) -> - userOptions = options - options = _.defaults({}, userOptions, { log: true }) - - if options.log isnt false - options._log = Cypress.log({ - message: "" - }) - - getHash = => - cy.getRemoteLocation("hash") - - do resolveHash = => - Promise.try(getHash).then (hash) => - cy.verifyUpcomingAssertions(hash, options, { - onRetry: resolveHash - }) - - location: (key, options) -> - userOptions = options - ## normalize arguments allowing key + options to be undefined - ## key can represent the options - if _.isObject(key) and _.isUndefined(userOptions) - userOptions = key - - userOptions ?= {} - - options = _.defaults({}, userOptions, { log: true }) - - getLocation = => - location = cy.getRemoteLocation() - - ret = if _.isString(key) - ## use existential here because we only want to throw - ## on null or undefined values (and not empty strings) - location[key] ? - $errUtils.throwErrByPath("location.invalid_key", { args: { key } }) - else - location - - if options.log isnt false - options._log = Cypress.log({ - message: key ? "" - }) - - do resolveLocation = => - Promise.try(getLocation).then (ret) => - cy.verifyUpcomingAssertions(ret, options, { - onRetry: resolveLocation - }) - }) +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const Promise = require("bluebird"); + +const $errUtils = require("../../cypress/error_utils"); +const $Location = require("../../cypress/location"); + +module.exports = (Commands, Cypress, cy, state, config) => Commands.addAll({ + url(options = {}) { + let resolveHref; + const userOptions = options; + options = _.defaults({}, userOptions, { log: true }); + + if (options.log !== false) { + options._log = Cypress.log({ + message: "" + }); + } + + const getHref = () => { + return cy.getRemoteLocation("href"); + }; + + return (resolveHref = () => { + return Promise.try(getHref).then(href => { + return cy.verifyUpcomingAssertions(href, options, { + onRetry: resolveHref + }); + }); + })(); + }, + + hash(options = {}) { + let resolveHash; + const userOptions = options; + options = _.defaults({}, userOptions, { log: true }); + + if (options.log !== false) { + options._log = Cypress.log({ + message: "" + }); + } + + const getHash = () => { + return cy.getRemoteLocation("hash"); + }; + + return (resolveHash = () => { + return Promise.try(getHash).then(hash => { + return cy.verifyUpcomingAssertions(hash, options, { + onRetry: resolveHash + }); + }); + })(); + }, + + location(key, options) { + let resolveLocation; + let userOptions = options; + //# normalize arguments allowing key + options to be undefined + //# key can represent the options + if (_.isObject(key) && _.isUndefined(userOptions)) { + userOptions = key; + } + + if (userOptions == null) { userOptions = {}; } + + options = _.defaults({}, userOptions, { log: true }); + + const getLocation = () => { + let ret; + const location = cy.getRemoteLocation(); + + return ret = _.isString(key) ? + //# use existential here because we only want to throw + //# on null or undefined values (and not empty strings) + location[key] != null ? location[key] : $errUtils.throwErrByPath("location.invalid_key", { args: { key } }) + : + location; + }; + + if (options.log !== false) { + options._log = Cypress.log({ + message: key != null ? key : "" + }); + } + + return (resolveLocation = () => { + return Promise.try(getLocation).then(ret => { + return cy.verifyUpcomingAssertions(ret, options, { + onRetry: resolveLocation + }); + }); + })(); + } +}); diff --git a/packages/driver/src/cy/commands/misc.js b/packages/driver/src/cy/commands/misc.js index 09429edc647a..891d0d33c140 100644 --- a/packages/driver/src/cy/commands/misc.js +++ b/packages/driver/src/cy/commands/misc.js @@ -1,45 +1,57 @@ -_ = require("lodash") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); -$dom = require("../../dom") +const $dom = require("../../dom"); -module.exports = (Commands, Cypress, cy, state, config) -> +module.exports = function(Commands, Cypress, cy, state, config) { Commands.addAll({ prevSubject: "optional" }, { - end: -> - null - }) + end() { + return null; + } + }); - Commands.addAll({ - noop: (arg) -> arg + return Commands.addAll({ + noop(arg) { return arg; }, - log: (msg, args) -> + log(msg, args) { Cypress.log({ - end: true - snapshot: true - message: [msg, args] - consoleProps: -> - { - message: msg - args: args - } - }) - - return null - - wrap: (arg, options = {}) -> - userOptions = options - options = _.defaults({}, userOptions, { log: true }) - - if options.log isnt false + end: true, + snapshot: true, + message: [msg, args], + consoleProps() { + return { + message: msg, + args + }; + } + }); + + return null; + }, + + wrap(arg, options = {}) { + let resolveWrap; + const userOptions = options; + options = _.defaults({}, userOptions, { log: true }); + + if (options.log !== false) { options._log = Cypress.log({ message: arg - }) + }); - if $dom.isElement(arg) - options._log.set({$el: arg}) + if ($dom.isElement(arg)) { + options._log.set({$el: arg}); + } + } - do resolveWrap = -> - cy.verifyUpcomingAssertions(arg, options, { - onRetry: resolveWrap - }) - .return(arg) - }) + return (resolveWrap = () => cy.verifyUpcomingAssertions(arg, options, { + onRetry: resolveWrap + }) + .return(arg))(); + } + }); +}; diff --git a/packages/driver/src/cy/commands/navigation.js b/packages/driver/src/cy/commands/navigation.js index 38124bba07a3..0fa329cb982c 100644 --- a/packages/driver/src/cy/commands/navigation.js +++ b/packages/driver/src/cy/commands/navigation.js @@ -1,853 +1,957 @@ -_ = require("lodash") -whatIsCircular = require("@cypress/what-is-circular") -moment = require("moment") -UrlParse = require("url-parse") -Promise = require("bluebird") - -$utils = require("../../cypress/utils") -$errUtils = require("../../cypress/error_utils") -$Log = require("../../cypress/log") -$Location = require("../../cypress/location") - -debug = require('debug')('cypress:driver:navigation') - -id = null -previousDomainVisited = null -hasVisitedAboutBlank = null -currentlyVisitingAboutBlank = null -knownCommandCausedInstability = null - -REQUEST_URL_OPTS = "auth failOnStatusCode retryOnNetworkFailure retryOnStatusCodeFailure method body headers" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS104: Avoid inline assignments + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const whatIsCircular = require("@cypress/what-is-circular"); +const moment = require("moment"); +const UrlParse = require("url-parse"); +const Promise = require("bluebird"); + +const $utils = require("../../cypress/utils"); +const $errUtils = require("../../cypress/error_utils"); +const $Log = require("../../cypress/log"); +const $Location = require("../../cypress/location"); + +const debug = require('debug')('cypress:driver:navigation'); + +let id = null; +let previousDomainVisited = null; +let hasVisitedAboutBlank = null; +let currentlyVisitingAboutBlank = null; +let knownCommandCausedInstability = null; + +const REQUEST_URL_OPTS = "auth failOnStatusCode retryOnNetworkFailure retryOnStatusCodeFailure method body headers" +.split(" "); + +const VISIT_OPTS = "url log onBeforeLoad onLoad timeout requestTimeout" .split(" ") +.concat(REQUEST_URL_OPTS); -VISIT_OPTS = "url log onBeforeLoad onLoad timeout requestTimeout" -.split(" ") -.concat(REQUEST_URL_OPTS) - -reset = (test = {}) -> - knownCommandCausedInstability = false +const reset = function(test = {}) { + knownCommandCausedInstability = false; - ## continuously reset this - ## before each test run! - previousDomainVisited = false + //# continuously reset this + //# before each test run! + previousDomainVisited = false; - ## make sure we reset that we haven't - ## visited about blank again - hasVisitedAboutBlank = false + //# make sure we reset that we haven't + //# visited about blank again + hasVisitedAboutBlank = false; - currentlyVisitingAboutBlank = false + currentlyVisitingAboutBlank = false; - id = test.id + return id = test.id; +}; -VALID_VISIT_METHODS = ['GET', 'POST'] +const VALID_VISIT_METHODS = ['GET', 'POST']; -isValidVisitMethod = (method) -> - _.includes(VALID_VISIT_METHODS, method) +const isValidVisitMethod = method => _.includes(VALID_VISIT_METHODS, method); -timedOutWaitingForPageLoad = (ms, log) -> - debug('timedOutWaitingForPageLoad') - $errUtils.throwErrByPath("navigation.timed_out", { +const timedOutWaitingForPageLoad = function(ms, log) { + debug('timedOutWaitingForPageLoad'); + return $errUtils.throwErrByPath("navigation.timed_out", { args: { - configFile: Cypress.config("configFile") + configFile: Cypress.config("configFile"), ms - } + }, onFail: log - }) - -bothUrlsMatchAndRemoteHasHash = (current, remote) -> - ## the remote has a hash - ## or the last char of href - ## is a hash - (remote.hash or remote.href.slice(-1) is "#") and + }); +}; - ## both must have the same origin - (current.origin is remote.origin) and +const bothUrlsMatchAndRemoteHasHash = (current, remote) => //# the remote has a hash +//# or the last char of href +//# is a hash +//# both must have the same query params +(remote.hash || (remote.href.slice(-1) === "#")) && - ## both must have the same pathname - (current.pathname is remote.pathname) and + //# both must have the same origin + (current.origin === remote.origin) && - ## both must have the same query params - (current.search is remote.search) + //# both must have the same pathname + (current.pathname === remote.pathname) && current.search === remote.search; -cannotVisitDifferentOrigin = (origin, previousUrlVisited, remoteUrl, existingUrl, log) -> - differences = [] +const cannotVisitDifferentOrigin = function(origin, previousUrlVisited, remoteUrl, existingUrl, log) { + const differences = []; - if remoteUrl.protocol isnt existingUrl.protocol - differences.push('protocol') - if remoteUrl.port isnt existingUrl.port - differences.push('port') - if remoteUrl.superDomain isnt existingUrl.superDomain - differences.push('superdomain') + if (remoteUrl.protocol !== existingUrl.protocol) { + differences.push('protocol'); + } + if (remoteUrl.port !== existingUrl.port) { + differences.push('port'); + } + if (remoteUrl.superDomain !== existingUrl.superDomain) { + differences.push('superdomain'); + } - errOpts = { - onFail: log + const errOpts = { + onFail: log, args: { - differences: differences.join(', ') - previousUrl: previousUrlVisited + differences: differences.join(', '), + previousUrl: previousUrlVisited, attemptedUrl: origin } - } + }; - $errUtils.throwErrByPath("visit.cannot_visit_different_origin", errOpts) + return $errUtils.throwErrByPath("visit.cannot_visit_different_origin", errOpts); +}; -specifyFileByRelativePath = (url, log) -> - $errUtils.throwErrByPath("visit.specify_file_by_relative_path", { - onFail: log - args: { - attemptedUrl: url - } - }) +const specifyFileByRelativePath = (url, log) => $errUtils.throwErrByPath("visit.specify_file_by_relative_path", { + onFail: log, + args: { + attemptedUrl: url + } +}); -aboutBlank = (win) -> - new Promise (resolve) -> - cy.once("window:load", resolve) +const aboutBlank = win => new Promise(function(resolve) { + cy.once("window:load", resolve); - $utils.locHref("about:blank", win) + return $utils.locHref("about:blank", win); +}); -navigationChanged = (Cypress, cy, state, source, arg) -> - ## get the current url of our remote application - url = cy.getRemoteLocation("href") - debug('navigation changed:', url) +const navigationChanged = function(Cypress, cy, state, source, arg) { + //# get the current url of our remote application + let left; + const url = cy.getRemoteLocation("href"); + debug('navigation changed:', url); - ## dont trigger for empty url's or about:blank - return if _.isEmpty(url) or url is "about:blank" + //# dont trigger for empty url's or about:blank + if (_.isEmpty(url) || (url === "about:blank")) { return; } - ## start storing the history entries - urls = state("urls") ? [] + //# start storing the history entries + const urls = (left = state("urls")) != null ? left : []; - previousUrl = _.last(urls) + const previousUrl = _.last(urls); - ## ensure our new url doesnt match whatever - ## the previous was. this prevents logging - ## additionally when the url didnt actually change - return if url is previousUrl + //# ensure our new url doesnt match whatever + //# the previous was. this prevents logging + //# additionally when the url didnt actually change + if (url === previousUrl) { return; } - ## else notify the world and log this event - Cypress.action("cy:url:changed", url) + //# else notify the world and log this event + Cypress.action("cy:url:changed", url); - urls.push(url) + urls.push(url); - state("urls", urls) + state("urls", urls); - state("url", url) + state("url", url); - ## don't output a command log for 'load' or 'before:load' events - # return if source in command - return if knownCommandCausedInstability + //# don't output a command log for 'load' or 'before:load' events + // return if source in command + if (knownCommandCausedInstability) { return; } - ## ensure our new url doesnt match whatever - ## the previous was. this prevents logging - ## additionally when the url didnt actually change - Cypress.log({ - name: "new url" - message: url - event: true - type: "parent" - end: true - snapshot: true - consoleProps: -> - obj = { + //# ensure our new url doesnt match whatever + //# the previous was. this prevents logging + //# additionally when the url didnt actually change + return Cypress.log({ + name: "new url", + message: url, + event: true, + type: "parent", + end: true, + snapshot: true, + consoleProps() { + const obj = { "New Url": url + }; + + if (source) { + obj["Url Updated By"] = source; + } + + if (arg) { + obj.Args = arg; } - if source - obj["Url Updated By"] = source - - if arg - obj.Args = arg - - return obj - }) - -formSubmitted = (Cypress, e) -> - Cypress.log({ - type: "parent" - name: "form sub" - message: "--submitting form--" - event: true - end: true - snapshot: true - consoleProps: -> { - "Originated From": e.target - "Args": e + return obj; + } + }); +}; + +const formSubmitted = (Cypress, e) => Cypress.log({ + type: "parent", + name: "form sub", + message: "--submitting form--", + event: true, + end: true, + snapshot: true, + consoleProps() { return { + "Originated From": e.target, + "Args": e + }; } +}); + +const pageLoading = function(bool, state) { + if (state("pageLoading") === bool) { return; } + + state("pageLoading", bool); + + return Cypress.action("app:page:loading", bool); +}; + +const stabilityChanged = function(Cypress, state, config, stable, event) { + debug('stabilityChanged:', stable); + if (currentlyVisitingAboutBlank) { + if (stable === false) { + //# if we're currently visiting about blank + //# and becoming unstable for the first time + //# notifiy that we're page loading + pageLoading(true, state); + return; + } else { + //# else wait until after we finish visiting + //# about blank + return; } - }) - -pageLoading = (bool, state) -> - return if state("pageLoading") is bool - - state("pageLoading", bool) - - Cypress.action("app:page:loading", bool) - -stabilityChanged = (Cypress, state, config, stable, event) -> - debug('stabilityChanged:', stable) - if currentlyVisitingAboutBlank - if stable is false - ## if we're currently visiting about blank - ## and becoming unstable for the first time - ## notifiy that we're page loading - pageLoading(true, state) - return - else - ## else wait until after we finish visiting - ## about blank - return - - ## let the world know that the app is page:loading - pageLoading(!stable, state) - - ## if we aren't becoming unstable - ## then just return now - return if stable isnt false - - ## if we purposefully just caused the page to load - ## (and thus instability) don't log this out - return if knownCommandCausedInstability - - ## bail if we dont have a runnable - ## because beforeunload can happen at any time - ## we may no longer be testing and thus dont - ## want to fire a new loading event - ## TODO - ## this may change in the future since we want - ## to add debuggability in the chrome console - ## which at that point we may keep runnable around - return if not state("runnable") - - options = {} + } + + //# let the world know that the app is page:loading + pageLoading(!stable, state); + + //# if we aren't becoming unstable + //# then just return now + if (stable !== false) { return; } + + //# if we purposefully just caused the page to load + //# (and thus instability) don't log this out + if (knownCommandCausedInstability) { return; } + + //# bail if we dont have a runnable + //# because beforeunload can happen at any time + //# we may no longer be testing and thus dont + //# want to fire a new loading event + //# TODO + //# this may change in the future since we want + //# to add debuggability in the chrome console + //# which at that point we may keep runnable around + if (!state("runnable")) { return; } + + const options = {}; _.defaults(options, { timeout: config("pageLoadTimeout") - }) + }); options._log = Cypress.log({ - type: "parent" - name: "page load" - message: "--waiting for new page to load--" - event: true - consoleProps: -> { + type: "parent", + name: "page load", + message: "--waiting for new page to load--", + event: true, + consoleProps() { return { Note: "This event initially fires when your application fires its 'beforeunload' event and completes when your application fires its 'load' event after the next page loads." - } - }) + }; } + }); - cy.clearTimeout("page load") + cy.clearTimeout("page load"); - onPageLoadErr = (err) -> - state("onPageLoadErr", null) + const onPageLoadErr = function(err) { + state("onPageLoadErr", null); - { originPolicy } = $Location.create(window.location.href) + const { originPolicy } = $Location.create(window.location.href); - try - $errUtils.throwErrByPath("navigation.cross_origin", { - onFail: options._log + try { + return $errUtils.throwErrByPath("navigation.cross_origin", { + onFail: options._log, args: { - configFile: Cypress.config("configFile") - message: err.message - originPolicy: originPolicy + configFile: Cypress.config("configFile"), + message: err.message, + originPolicy } - }) - catch err - return err + }); + } catch (error) { + err = error; + return err; + } + }; - state("onPageLoadErr", onPageLoadErr) + state("onPageLoadErr", onPageLoadErr); - loading = -> - debug('waiting for window:load') - new Promise (resolve, reject) -> - cy.once "window:load", -> - cy.state("onPageLoadErr", null) + const loading = function() { + debug('waiting for window:load'); + return new Promise((resolve, reject) => cy.once("window:load", function() { + cy.state("onPageLoadErr", null); - options._log.set("message", "--page loaded--").snapshot().end() + options._log.set("message", "--page loaded--").snapshot().end(); - resolve() + return resolve(); + })); + }; - reject = (err) -> - if r = state("reject") - r(err) + const reject = function(err) { + let r; + if (r = state("reject")) { + return r(err); + } + }; - loading() + return loading() .timeout(options.timeout, "page load") - .catch Promise.TimeoutError, -> - ## clean this up - cy.state("onPageLoadErr", null) - - try - timedOutWaitingForPageLoad(options.timeout, options._log) - catch err - reject(err) - -normalizeTimeoutOptions = (options) -> - ## there are really two timeout values - pageLoadTimeout - ## and the underlying responseTimeout. for the purposes - ## of resolving resolving the url, we only care about - ## responseTimeout - since pageLoadTimeout is a driver - ## and browser concern. therefore we normalize the options - ## object and send 'responseTimeout' as options.timeout - ## for the backend. - _ - .chain(options) - .pick(REQUEST_URL_OPTS) - .extend({ timeout: options.responseTimeout }) - .value() - -module.exports = (Commands, Cypress, cy, state, config) -> - reset() - - Cypress.on "test:before:run:async", -> - ## reset any state on the backend - Cypress.backend('reset:server:state') - - Cypress.on("test:before:run", reset) - - Cypress.on "stability:changed", (bool, event) -> - ## only send up page loading events when we're - ## not stable! - stabilityChanged(Cypress, state, config, bool, event) - - Cypress.on "navigation:changed", (source, arg) -> - navigationChanged(Cypress, cy, state, source, arg) - - Cypress.on "form:submitted", (e) -> - formSubmitted(Cypress, e) - - visitFailedByErr = (err, url, fn) -> - err.url = url - - Cypress.action("cy:visit:failed", err) - - fn() - - requestUrl = (url, options = {}) -> - Cypress.backend( - "resolve:url", - url, - normalizeTimeoutOptions(options) - ) - .then (resp = {}) -> - switch - ## if we didn't even get an OK response - ## then immediately die - when not resp.isOkStatusCode - err = new Error - err.gotResponse = true - _.extend(err, resp) - - throw err - - when not resp.isHtml - ## throw invalid contentType error - err = new Error - err.invalidContentType = true - _.extend(err, resp) - - throw err - - else - resp - - Cypress.on "window:before:load", (contentWindow) -> - ## TODO: just use a closure here - current = state("current") - - return if not current - - runnable = state("runnable") - - return if not runnable - - options = _.last(current.get("args")) - options?.onBeforeLoad?.call(runnable.ctx, contentWindow) - - Commands.addAll({ - reload: (args...) -> - throwArgsErr = => - $errUtils.throwErrByPath("reload.invalid_arguments") - - switch args.length - when 0 - forceReload = false - userOptions = {} - - when 1 - if _.isObject(args[0]) - userOptions = args[0] - else - forceReload = args[0] - - when 2 - forceReload = args[0] - userOptions = args[1] - - else - throwArgsErr() + .catch(Promise.TimeoutError, function() { + //# clean this up + cy.state("onPageLoadErr", null); + + try { + return timedOutWaitingForPageLoad(options.timeout, options._log); + } catch (err) { + return reject(err); + } + }); +}; + +const normalizeTimeoutOptions = options => //# there are really two timeout values - pageLoadTimeout +//# and the underlying responseTimeout. for the purposes +//# of resolving resolving the url, we only care about +//# responseTimeout - since pageLoadTimeout is a driver +//# and browser concern. therefore we normalize the options +//# object and send 'responseTimeout' as options.timeout +//# for the backend. +_ +.chain(options) +.pick(REQUEST_URL_OPTS) +.extend({ timeout: options.responseTimeout }) +.value(); + +module.exports = function(Commands, Cypress, cy, state, config) { + reset(); + + Cypress.on("test:before:run:async", () => //# reset any state on the backend + Cypress.backend('reset:server:state')); + + Cypress.on("test:before:run", reset); + + Cypress.on("stability:changed", (bool, event) => //# only send up page loading events when we're + //# not stable! + stabilityChanged(Cypress, state, config, bool, event)); + + Cypress.on("navigation:changed", (source, arg) => navigationChanged(Cypress, cy, state, source, arg)); + + Cypress.on("form:submitted", e => formSubmitted(Cypress, e)); + + const visitFailedByErr = function(err, url, fn) { + err.url = url; + + Cypress.action("cy:visit:failed", err); + + return fn(); + }; + + const requestUrl = (url, options = {}) => Cypress.backend( + "resolve:url", + url, + normalizeTimeoutOptions(options) + ) + .then(function(resp = {}) { + switch (false) { + //# if we didn't even get an OK response + //# then immediately die + case !!resp.isOkStatusCode: + var err = new Error; + err.gotResponse = true; + _.extend(err, resp); + + throw err; + + case !!resp.isHtml: + //# throw invalid contentType error + err = new Error; + err.invalidContentType = true; + _.extend(err, resp); + + throw err; + + default: + return resp; + } + }); + + Cypress.on("window:before:load", function(contentWindow) { + //# TODO: just use a closure here + const current = state("current"); + + if (!current) { return; } + + const runnable = state("runnable"); + + if (!runnable) { return; } + + const options = _.last(current.get("args")); + return __guard__(options != null ? options.onBeforeLoad : undefined, x => x.call(runnable.ctx, contentWindow)); + }); + + return Commands.addAll({ + reload(...args) { + let forceReload, userOptions; + const throwArgsErr = () => { + return $errUtils.throwErrByPath("reload.invalid_arguments"); + }; + + switch (args.length) { + case 0: + forceReload = false; + userOptions = {}; + break; + + case 1: + if (_.isObject(args[0])) { + userOptions = args[0]; + } else { + forceReload = args[0]; + } + break; + + case 2: + forceReload = args[0]; + userOptions = args[1]; + break; + + default: + throwArgsErr(); + } - ## clear the current timeout - cy.clearTimeout("reload") + //# clear the current timeout + cy.clearTimeout("reload"); - cleanup = null - options = _.defaults({}, userOptions, { - log: true + let cleanup = null; + const options = _.defaults({}, userOptions, { + log: true, timeout: config("pageLoadTimeout") - }) + }); - reload = -> - new Promise (resolve, reject) -> - forceReload ?= false - userOptions ?= {} + const reload = () => new Promise(function(resolve, reject) { + if (forceReload == null) { forceReload = false; } + if (userOptions == null) { userOptions = {}; } - if not _.isObject(userOptions) - throwArgsErr() + if (!_.isObject(userOptions)) { + throwArgsErr(); + } - if not _.isBoolean(forceReload) - throwArgsErr() + if (!_.isBoolean(forceReload)) { + throwArgsErr(); + } - if options.log - options._log = Cypress.log({}) + if (options.log) { + options._log = Cypress.log({}); - options._log.snapshot("before", {next: "after"}) + options._log.snapshot("before", {next: "after"}); + } - cleanup = -> - knownCommandCausedInstability = false + cleanup = function() { + knownCommandCausedInstability = false; - cy.removeListener("window:load", resolve) + return cy.removeListener("window:load", resolve); + }; - knownCommandCausedInstability = true + knownCommandCausedInstability = true; - cy.once("window:load", resolve) + cy.once("window:load", resolve); - $utils.locReload(forceReload, state("window")) + return $utils.locReload(forceReload, state("window")); + }); - reload() + return reload() .timeout(options.timeout, "reload") - .catch Promise.TimeoutError, (err) -> - timedOutWaitingForPageLoad(options.timeout, options._log) - .finally -> - cleanup?() + .catch(Promise.TimeoutError, err => timedOutWaitingForPageLoad(options.timeout, options._log)).finally(function() { + if (typeof cleanup === 'function') { + cleanup(); + } - return null + return null; + }); + }, - go: (numberOrString, options = {}) -> - userOptions = options - options = _.defaults {}, userOptions, { - log: true + go(numberOrString, options = {}) { + const userOptions = options; + options = _.defaults({}, userOptions, { + log: true, timeout: config("pageLoadTimeout") - } + }); - if options.log + if (options.log) { options._log = Cypress.log({ - }) + }); + } - win = state("window") + const win = state("window"); - goNumber = (num) -> - if num is 0 - $errUtils.throwErrByPath("go.invalid_number", { onFail: options._log }) + const goNumber = function(num) { + if (num === 0) { + $errUtils.throwErrByPath("go.invalid_number", { onFail: options._log }); + } - cleanup = null + let cleanup = null; - if options._log - options._log.snapshot("before", {next: "after"}) + if (options._log) { + options._log.snapshot("before", {next: "after"}); + } - go = -> - Promise.try -> - didUnload = false + const go = () => Promise.try(function() { + let didUnload = false; - beforeUnload = -> - didUnload = true + const beforeUnload = () => didUnload = true; - ## clear the current timeout - cy.clearTimeout() + //# clear the current timeout + cy.clearTimeout(); - cy.once("window:before:unload", beforeUnload) + cy.once("window:before:unload", beforeUnload); - didLoad = new Promise (resolve) -> - cleanup = -> - cy.removeListener("window:load", resolve) - cy.removeListener("window:before:unload", beforeUnload) + const didLoad = new Promise(function(resolve) { + cleanup = function() { + cy.removeListener("window:load", resolve); + return cy.removeListener("window:before:unload", beforeUnload); + }; - cy.once("window:load", resolve) + return cy.once("window:load", resolve); + }); - knownCommandCausedInstability = true + knownCommandCausedInstability = true; - win.history.go(num) + win.history.go(num); - retWin = -> - ## need to set the attributes of 'go' - ## consoleProps here with win + const retWin = () => //# need to set the attributes of 'go' + //# consoleProps here with win - ## make sure we resolve our go function - ## with the remove window (just like cy.visit) - state("window") + //# make sure we resolve our go function + //# with the remove window (just like cy.visit) + state("window"); - Promise - .delay(100) - .then -> - knownCommandCausedInstability = false + return Promise + .delay(100) + .then(function() { + knownCommandCausedInstability = false; - ## if we've didUnload then we know we're - ## doing a full page refresh and we need - ## to wait until - if didUnload - didLoad.then(retWin) - else - retWin() + //# if we've didUnload then we know we're + //# doing a full page refresh and we need + //# to wait until + if (didUnload) { + return didLoad.then(retWin); + } else { + return retWin(); + } + }); + }); - go() + return go() .timeout(options.timeout, "go") - .catch Promise.TimeoutError, (err) -> - timedOutWaitingForPageLoad(options.timeout, options._log) - .finally -> - cleanup?() - - return null - - goString = (str) -> - switch str - when "forward" then goNumber(1) - when "back" then goNumber(-1) - else - $errUtils.throwErrByPath("go.invalid_direction", { - onFail: options._log + .catch(Promise.TimeoutError, err => timedOutWaitingForPageLoad(options.timeout, options._log)).finally(function() { + if (typeof cleanup === 'function') { + cleanup(); + } + + return null; + }); + }; + + const goString = function(str) { + switch (str) { + case "forward": return goNumber(1); + case "back": return goNumber(-1); + default: + return $errUtils.throwErrByPath("go.invalid_direction", { + onFail: options._log, args: { str } - }) + }); + } + }; - switch - when _.isFinite(numberOrString) then goNumber(numberOrString) - when _.isString(numberOrString) then goString(numberOrString) - else - $errUtils.throwErrByPath("go.invalid_argument", { onFail: options._log }) + switch (false) { + case !_.isFinite(numberOrString): return goNumber(numberOrString); + case !_.isString(numberOrString): return goString(numberOrString); + default: + return $errUtils.throwErrByPath("go.invalid_argument", { onFail: options._log }); + } + }, - visit: (url, options = {}) -> - if options.url and url - $errUtils.throwErrByPath("visit.no_duplicate_url", { args: { optionsUrl: options.url, url: url }}) - userOptions = options + visit(url, options = {}) { + let baseUrl, message, path, qs; + if (options.url && url) { + $errUtils.throwErrByPath("visit.no_duplicate_url", { args: { optionsUrl: options.url, url }}); + } + let userOptions = options; - if userOptions.url and url - $utils.throwErrByPath("visit.no_duplicate_url", { args: { optionsUrl: userOptions.url, url: url }}) + if (userOptions.url && url) { + $utils.throwErrByPath("visit.no_duplicate_url", { args: { optionsUrl: userOptions.url, url }}); + } - if _.isObject(url) and _.isEqual(userOptions, {}) - ## options specified as only argument - userOptions = url - url = userOptions.url + if (_.isObject(url) && _.isEqual(userOptions, {})) { + //# options specified as only argument + userOptions = url; + ({ + url + } = userOptions); + } - if not _.isString(url) - $errUtils.throwErrByPath("visit.invalid_1st_arg") + if (!_.isString(url)) { + $errUtils.throwErrByPath("visit.invalid_1st_arg"); + } - consoleProps = {} + const consoleProps = {}; - if not _.isEmpty(userOptions) - consoleProps["Options"] = _.pick(userOptions, VISIT_OPTS) + if (!_.isEmpty(userOptions)) { + consoleProps["Options"] = _.pick(userOptions, VISIT_OPTS); + } options = _.defaults({}, userOptions, { - auth: null - failOnStatusCode: true - retryOnNetworkFailure: true - retryOnStatusCodeFailure: false - method: 'GET' - body: null - headers: {} - log: true - responseTimeout: config('responseTimeout') - timeout: config("pageLoadTimeout") - onBeforeLoad: -> - onLoad: -> - }) - - if !_.isUndefined(options.qs) and not _.isObject(options.qs) - $errUtils.throwErrByPath("visit.invalid_qs", { args: { qs: String(options.qs) }}) + auth: null, + failOnStatusCode: true, + retryOnNetworkFailure: true, + retryOnStatusCodeFailure: false, + method: 'GET', + body: null, + headers: {}, + log: true, + responseTimeout: config('responseTimeout'), + timeout: config("pageLoadTimeout"), + onBeforeLoad() {}, + onLoad() {} + }); + + if (!_.isUndefined(options.qs) && !_.isObject(options.qs)) { + $errUtils.throwErrByPath("visit.invalid_qs", { args: { qs: String(options.qs) }}); + } - if options.retryOnStatusCodeFailure and not options.failOnStatusCode - $errUtils.throwErrByPath("visit.status_code_flags_invalid") + if (options.retryOnStatusCodeFailure && !options.failOnStatusCode) { + $errUtils.throwErrByPath("visit.status_code_flags_invalid"); + } - if not isValidVisitMethod(options.method) - $errUtils.throwErrByPath("visit.invalid_method", { args: { method: options.method }}) + if (!isValidVisitMethod(options.method)) { + $errUtils.throwErrByPath("visit.invalid_method", { args: { method: options.method }}); + } - if not _.isObject(options.headers) - $errUtils.throwErrByPath("visit.invalid_headers") + if (!_.isObject(options.headers)) { + $errUtils.throwErrByPath("visit.invalid_headers"); + } - if _.isObject(options.body) and path = whatIsCircular(options.body) - $errUtils.throwErrByPath("visit.body_circular", { args: { path }}) + if (_.isObject(options.body) && (path = whatIsCircular(options.body))) { + $errUtils.throwErrByPath("visit.body_circular", { args: { path }}); + } - if options.log - message = url + if (options.log) { + message = url; - if options.method != 'GET' - message = "#{options.method} #{message}" + if (options.method !== 'GET') { + message = `${options.method} ${message}`; + } options._log = Cypress.log({ - message: message - consoleProps: -> consoleProps - }) - - url = $Location.normalize(url) - - if baseUrl = config("baseUrl") - url = $Location.qualifyWithBaseUrl(baseUrl, url) - - if qs = options.qs - url = $Location.mergeUrlWithParams(url, qs) - - cleanup = null - - ## clear the current timeout - cy.clearTimeout("visit") - - win = state("window") - $autIframe = state("$autIframe") - runnable = state("runnable") - - changeIframeSrc = (url, event) -> - ## when the remote iframe's load event fires - ## callback fn - new Promise (resolve) -> - ## if we're listening for hashchange - ## events then change the strategy - ## to listen to this event emitting - ## from the window and not cy - ## see issue 652 for why. - ## the hashchange events are firing too - ## fast for us. They even resolve asynchronously - ## before other application's hashchange events - ## have even fired. - if event is "hashchange" - win.addEventListener("hashchange", resolve) - else - cy.once(event, resolve) - - cleanup = -> - if event is "hashchange" - win.removeEventListener("hashchange", resolve) - else - cy.removeListener(event, resolve) - - knownCommandCausedInstability = false - - knownCommandCausedInstability = true - - $utils.iframeSrc($autIframe, url) - - onLoad = ({runOnLoadCallback, totalTime}) -> - ## reset window on load - win = state("window") - - ## the onLoad callback should only be skipped if specified - if runOnLoadCallback isnt false - options.onLoad?.call(runnable.ctx, win) - - if options._log + message, + consoleProps() { return consoleProps; } + }); + } + + url = $Location.normalize(url); + + if (baseUrl = config("baseUrl")) { + url = $Location.qualifyWithBaseUrl(baseUrl, url); + } + + if (qs = options.qs) { + url = $Location.mergeUrlWithParams(url, qs); + } + + let cleanup = null; + + //# clear the current timeout + cy.clearTimeout("visit"); + + let win = state("window"); + const $autIframe = state("$autIframe"); + const runnable = state("runnable"); + + const changeIframeSrc = (url, event) => //# when the remote iframe's load event fires + //# callback fn + new Promise(function(resolve) { + //# if we're listening for hashchange + //# events then change the strategy + //# to listen to this event emitting + //# from the window and not cy + //# see issue 652 for why. + //# the hashchange events are firing too + //# fast for us. They even resolve asynchronously + //# before other application's hashchange events + //# have even fired. + if (event === "hashchange") { + win.addEventListener("hashchange", resolve); + } else { + cy.once(event, resolve); + } + + cleanup = function() { + if (event === "hashchange") { + win.removeEventListener("hashchange", resolve); + } else { + cy.removeListener(event, resolve); + } + + return knownCommandCausedInstability = false; + }; + + knownCommandCausedInstability = true; + + return $utils.iframeSrc($autIframe, url); + }); + + const onLoad = function({runOnLoadCallback, totalTime}) { + //# reset window on load + win = state("window"); + + //# the onLoad callback should only be skipped if specified + if (runOnLoadCallback !== false) { + if (options.onLoad != null) { + options.onLoad.call(runnable.ctx, win); + } + } + + if (options._log) { options._log.set({ - url + url, totalTime - }) + }); + } - return Promise.resolve(win) + return Promise.resolve(win); + }; - go = -> - ## hold onto our existing url - existing = $utils.locExisting() + const go = function() { + //# hold onto our existing url + let a, remoteUrl; + const existing = $utils.locExisting(); - ## TODO: $Location.resolve(existing.origin, url) + //# TODO: $Location.resolve(existing.origin, url) - if $Location.isLocalFileUrl(url) - return specifyFileByRelativePath(url, options._log) + if ($Location.isLocalFileUrl(url)) { + return specifyFileByRelativePath(url, options._log); + } - ## in the case we are visiting a relative url - ## then prepend the existing origin to it - ## so we get the right remote url - if not $Location.isFullyQualifiedUrl(url) - remoteUrl = $Location.fullyQualifyUrl(url) + //# in the case we are visiting a relative url + //# then prepend the existing origin to it + //# so we get the right remote url + if (!$Location.isFullyQualifiedUrl(url)) { + remoteUrl = $Location.fullyQualifyUrl(url); + } - remote = $Location.create(remoteUrl ? url) + let remote = $Location.create(remoteUrl != null ? remoteUrl : url); - ## reset auth options if we have them - if a = remote.authObj - options.auth = a + //# reset auth options if we have them + if (a = remote.authObj) { + options.auth = a; + } - ## store the existing hash now since - ## we'll need to apply it later - existingHash = remote.hash ? "" - existingAuth = remote.auth ? "" + //# store the existing hash now since + //# we'll need to apply it later + const existingHash = remote.hash != null ? remote.hash : ""; + const existingAuth = remote.auth != null ? remote.auth : ""; - if previousDomainVisited and remote.originPolicy isnt existing.originPolicy - ## if we've already visited a new superDomain - ## then die else we'd be in a terrible endless loop - return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log) + if (previousDomainVisited && (remote.originPolicy !== existing.originPolicy)) { + //# if we've already visited a new superDomain + //# then die else we'd be in a terrible endless loop + return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log); + } - current = $Location.create(win.location.href) + const current = $Location.create(win.location.href); - ## if all that is changing is the hash then we know - ## the browser won't actually make a new http request - ## for this, and so we need to resolve onLoad immediately - ## and bypass the actual visit resolution stuff - if bothUrlsMatchAndRemoteHasHash(current, remote) - ## https://github.com/cypress-io/cypress/issues/1311 - if current.hash is remote.hash - consoleProps["Note"] = "Because this visit was to the same hash, the page did not reload and the onBeforeLoad and onLoad callbacks did not fire." + //# if all that is changing is the hash then we know + //# the browser won't actually make a new http request + //# for this, and so we need to resolve onLoad immediately + //# and bypass the actual visit resolution stuff + if (bothUrlsMatchAndRemoteHasHash(current, remote)) { + //# https://github.com/cypress-io/cypress/issues/1311 + if (current.hash === remote.hash) { + consoleProps["Note"] = "Because this visit was to the same hash, the page did not reload and the onBeforeLoad and onLoad callbacks did not fire."; - return onLoad({runOnLoadCallback: false}) + return onLoad({runOnLoadCallback: false}); + } return changeIframeSrc(remote.href, "hashchange") - .then(onLoad) - - if existingHash - ## strip out the existing hash if we have one - ## before telling our backend to resolve this url - url = url.replace(existingHash, "") - - if existingAuth - ## strip out the existing url if we have one - url = url.replace(existingAuth + "@", "") - - requestUrl(url, options) - .then (resp = {}) => - {url, originalUrl, cookies, redirects, filePath} = resp - - ## reapply the existing hash - url += existingHash - originalUrl += existingHash - - if filePath - consoleProps["File Served"] = filePath - else - if url isnt originalUrl - consoleProps["Original Url"] = originalUrl - - if options.log - message = options._log.get('message') - - if redirects and redirects.length - message = [message].concat(redirects).join(" -> ") - - options._log.set({message: message}) - - consoleProps["Resolved Url"] = url - consoleProps["Redirects"] = redirects - consoleProps["Cookies Set"] = cookies - - remote = $Location.create(url) - - ## if the origin currently matches - ## then go ahead and change the iframe's src - ## and we're good to go - # if origin is existing.origin - if remote.originPolicy is existing.originPolicy - previousDomainVisited = remote.origin - - url = $Location.fullyQualifyUrl(url) - - changeIframeSrc(url, "window:load") - .then -> - onLoad(resp) - else - ## if we've already visited a new origin - ## then die else we'd be in a terrible endless loop - if previousDomainVisited - return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log) - - ## tell our backend we're changing domains - ## TODO: add in other things we want to preserve - ## state for like scrollTop - s = { - currentId: id - tests: Cypress.getTestsState() - startTime: Cypress.getStartTime() - emissions: Cypress.getEmissions() + .then(onLoad); + } + + if (existingHash) { + //# strip out the existing hash if we have one + //# before telling our backend to resolve this url + url = url.replace(existingHash, ""); + } + + if (existingAuth) { + //# strip out the existing url if we have one + url = url.replace(existingAuth + "@", ""); + } + + return requestUrl(url, options) + .then((resp = {}) => { + let cookies, filePath, originalUrl, redirects; + ({url, originalUrl, cookies, redirects, filePath} = resp); + + //# reapply the existing hash + url += existingHash; + originalUrl += existingHash; + + if (filePath) { + consoleProps["File Served"] = filePath; + } else { + if (url !== originalUrl) { + consoleProps["Original Url"] = originalUrl; + } + } + + if (options.log) { + message = options._log.get('message'); + + if (redirects && redirects.length) { + message = [message].concat(redirects).join(" -> "); } - s.passed = Cypress.countByTestState(s.tests, "passed") - s.failed = Cypress.countByTestState(s.tests, "failed") - s.pending = Cypress.countByTestState(s.tests, "pending") - s.numLogs = $Log.countLogsByTests(s.tests) - - Cypress.action("cy:collect:run:state") - .then (a = []) -> - ## merge all the states together holla' - s = _.reduce a, (memo, obj) -> - _.extend(memo, obj) - , s - - Cypress.backend("preserve:run:state", s) - .then -> - ## and now we must change the url to be the new - ## origin but include the test that we're currently on - newUri = new UrlParse(remote.origin) + options._log.set({message}); + } + + consoleProps["Resolved Url"] = url; + consoleProps["Redirects"] = redirects; + consoleProps["Cookies Set"] = cookies; + + remote = $Location.create(url); + + //# if the origin currently matches + //# then go ahead and change the iframe's src + //# and we're good to go + // if origin is existing.origin + if (remote.originPolicy === existing.originPolicy) { + previousDomainVisited = remote.origin; + + url = $Location.fullyQualifyUrl(url); + + return changeIframeSrc(url, "window:load") + .then(() => onLoad(resp)); + } else { + //# if we've already visited a new origin + //# then die else we'd be in a terrible endless loop + if (previousDomainVisited) { + return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log); + } + + //# tell our backend we're changing domains + //# TODO: add in other things we want to preserve + //# state for like scrollTop + let s = { + currentId: id, + tests: Cypress.getTestsState(), + startTime: Cypress.getStartTime(), + emissions: Cypress.getEmissions() + }; + + s.passed = Cypress.countByTestState(s.tests, "passed"); + s.failed = Cypress.countByTestState(s.tests, "failed"); + s.pending = Cypress.countByTestState(s.tests, "pending"); + s.numLogs = $Log.countLogsByTests(s.tests); + + return Cypress.action("cy:collect:run:state") + .then(function(a = []) { + //# merge all the states together holla' + s = _.reduce(a, (memo, obj) => _.extend(memo, obj) + , s); + + return Cypress.backend("preserve:run:state", s);}).then(function() { + //# and now we must change the url to be the new + //# origin but include the test that we're currently on + const newUri = new UrlParse(remote.origin); newUri .set("pathname", existing.pathname) .set("query", existing.search) - .set("hash", existing.hash) - - ## replace is broken in electron so switching - ## to href for now - # $utils.locReplace(window, newUri.toString()) - $utils.locHref(newUri.toString(), window) - - ## we are returning a Promise which never resolves - ## because we're changing top to be a brand new URL - ## and want to block the rest of our commands - return Promise.delay(1e9) - .catch (err) -> - switch - when err.gotResponse, err.invalidContentType - visitFailedByErr err, err.originalUrl, -> - args = { - url: err.originalUrl - path: err.filePath - status: err.status - statusText: err.statusText - redirects: err.redirects + .set("hash", existing.hash); + + //# replace is broken in electron so switching + //# to href for now + // $utils.locReplace(window, newUri.toString()) + $utils.locHref(newUri.toString(), window); + + //# we are returning a Promise which never resolves + //# because we're changing top to be a brand new URL + //# and want to block the rest of our commands + return Promise.delay(1e9); + }); + } + }).catch(function(err) { + switch (false) { + case !err.gotResponse: case !err.invalidContentType: + return visitFailedByErr(err, err.originalUrl, function() { + const args = { + url: err.originalUrl, + path: err.filePath, + status: err.status, + statusText: err.statusText, + redirects: err.redirects, contentType: err.contentType - } - - msg = switch - when err.gotResponse - type = if err.filePath then "file" else "http" - - "visit.loading_#{type}_failed" - - when err.invalidContentType - "visit.loading_invalid_content_type" - - $errUtils.throwErrByPath(msg, { - onFail: options._log - args: args - }) - else - visitFailedByErr err, url, -> - $errUtils.throwErrByPath("visit.loading_network_failed", { - onFail: options._log - args: { - url: url - error: err - stack: err.stack - } - noStackTrace: true - }) - - visit = -> - ## if we've visiting for the first time during - ## a test then we want to first visit about:blank - ## so that we nuke the previous state. subsequent - ## visits will not navigate to about:blank so that - ## our history entries are intact - if not hasVisitedAboutBlank - hasVisitedAboutBlank = true - currentlyVisitingAboutBlank = true - - aboutBlank(win) - .then -> - currentlyVisitingAboutBlank = false - - go() - else - go() - - visit() + }; + + const msg = (() => { switch (false) { + case !err.gotResponse: + var type = err.filePath ? "file" : "http"; + + return `visit.loading_${type}_failed`; + + case !err.invalidContentType: + return "visit.loading_invalid_content_type"; + } })(); + + return $errUtils.throwErrByPath(msg, { + onFail: options._log, + args + }); + }); + default: + return visitFailedByErr(err, url, () => $errUtils.throwErrByPath("visit.loading_network_failed", { + onFail: options._log, + args: { + url, + error: err, + stack: err.stack + }, + noStackTrace: true + })); + } + }); + }; + + const visit = function() { + //# if we've visiting for the first time during + //# a test then we want to first visit about:blank + //# so that we nuke the previous state. subsequent + //# visits will not navigate to about:blank so that + //# our history entries are intact + if (!hasVisitedAboutBlank) { + hasVisitedAboutBlank = true; + currentlyVisitingAboutBlank = true; + + return aboutBlank(win) + .then(function() { + currentlyVisitingAboutBlank = false; + + return go(); + }); + } else { + return go(); + } + }; + + return visit() .timeout(options.timeout, "visit") - .catch Promise.TimeoutError, (err) => - timedOutWaitingForPageLoad(options.timeout, options._log) - .finally -> - cleanup?() + .catch(Promise.TimeoutError, err => { + return timedOutWaitingForPageLoad(options.timeout, options._log); + }).finally(function() { + if (typeof cleanup === 'function') { + cleanup(); + } + + return null; + }); + } + }); +}; - return null - }) +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/packages/driver/src/cy/commands/popups.js b/packages/driver/src/cy/commands/popups.js index 7119313e2223..dbaa53a6fe44 100644 --- a/packages/driver/src/cy/commands/popups.js +++ b/packages/driver/src/cy/commands/popups.js @@ -1,33 +1,35 @@ -windowAlert = (Cypress, str) -> - Cypress.log({ - type: "parent" - name: "alert" - message: str - event: true - end: true - snapshot: true - consoleProps: -> { - "Alerted": str - } - }) +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const windowAlert = (Cypress, str) => Cypress.log({ + type: "parent", + name: "alert", + message: str, + event: true, + end: true, + snapshot: true, + consoleProps() { return { + "Alerted": str + }; } +}); -windowConfirmed = (Cypress, str, ret) -> - Cypress.log({ - type: "parent" - name: "confirm" - message: str - event: true - end: true - snapshot: true - consoleProps: -> { - "Prompted": str - "Confirmed": ret - } - }) +const windowConfirmed = (Cypress, str, ret) => Cypress.log({ + type: "parent", + name: "confirm", + message: str, + event: true, + end: true, + snapshot: true, + consoleProps() { return { + "Prompted": str, + "Confirmed": ret + }; } +}); -module.exports = (Commands, Cypress, cy, state, config) -> - Cypress.on "window:alert", (str) -> - windowAlert(Cypress, str) +module.exports = function(Commands, Cypress, cy, state, config) { + Cypress.on("window:alert", str => windowAlert(Cypress, str)); - Cypress.on "window:confirmed", (str, ret) -> - windowConfirmed(Cypress, str, ret) + return Cypress.on("window:confirmed", (str, ret) => windowConfirmed(Cypress, str, ret)); +}; diff --git a/packages/driver/src/cy/commands/request.js b/packages/driver/src/cy/commands/request.js index 2e47aa026c1e..c8b6deb771ee 100644 --- a/packages/driver/src/cy/commands/request.js +++ b/packages/driver/src/cy/commands/request.js @@ -1,276 +1,319 @@ -_ = require("lodash") -whatIsCircular = require("@cypress/what-is-circular") -Promise = require("bluebird") - -$utils = require("../../cypress/utils") -$errUtils = require("../../cypress/error_utils") -$Location = require("../../cypress/location") - -isOptional = (memo, val, key) -> - if _.isNull(val) - memo.push(key) - memo - -REQUEST_DEFAULTS = { - url: "" - method: "GET" - qs: null - body: null - auth: null - headers: null - json: null - form: null - gzip: true - timeout: null - followRedirect: true - failOnStatusCode: true - retryOnNetworkFailure: true +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const whatIsCircular = require("@cypress/what-is-circular"); +const Promise = require("bluebird"); + +const $utils = require("../../cypress/utils"); +const $errUtils = require("../../cypress/error_utils"); +const $Location = require("../../cypress/location"); + +const isOptional = function(memo, val, key) { + if (_.isNull(val)) { + memo.push(key); + } + return memo; +}; + +const REQUEST_DEFAULTS = { + url: "", + method: "GET", + qs: null, + body: null, + auth: null, + headers: null, + json: null, + form: null, + gzip: true, + timeout: null, + followRedirect: true, + failOnStatusCode: true, + retryOnNetworkFailure: true, retryOnStatusCodeFailure: false -} - -REQUEST_PROPS = _.keys(REQUEST_DEFAULTS) - -OPTIONAL_OPTS = _.reduce(REQUEST_DEFAULTS, isOptional, []) - -hasFormUrlEncodedContentTypeHeader = (headers) -> - header = _.findKey(headers, _.matches("application/x-www-form-urlencoded")) - - header and _.toLower(header) is "content-type" - -isValidJsonObj = (body) -> - _.isObject(body) and not _.isFunction(body) - -whichAreOptional = (val, key) -> - val is null and key in OPTIONAL_OPTS - -needsFormSpecified = (options = {}) -> - { body, json, headers } = options - - ## json isn't true, and we have an object body and the user - ## specified that the content-type header is x-www-form-urlencoded - json isnt true and _.isObject(body) and hasFormUrlEncodedContentTypeHeader(headers) - -module.exports = (Commands, Cypress, cy, state, config) -> - # Cypress.extend - # ## set defaults for all requests? - # requestDefaults: (options = {}) -> - - Commands.addAll({ - ## allow our signature to be similar to cy.route - ## METHOD / URL / BODY - ## or object literal with all expanded options - request: (args...) -> - userOptions = o = {} - - switch - when _.isObject(args[0]) - _.extend(userOptions, args[0]) - - when args.length is 1 - o.url = args[0] - - when args.length is 2 - ## if our first arg is a valid - ## HTTP method then set method + url - if $utils.isValidHttpMethod(args[0]) - o.method = args[0] - o.url = args[1] - else - ## set url + body - o.url = args[0] - o.body = args[1] - - when args.length is 3 - o.method = args[0] - o.url = args[1] - o.body = args[2] - - options = _.defaults({}, userOptions, REQUEST_DEFAULTS, { - log: true - }) - - ## if timeout is not supplied, use the configured default - options.timeout ||= config("responseTimeout") - - options.method = options.method.toUpperCase() - - if options.retryOnStatusCodeFailure and not options.failOnStatusCode - $errUtils.throwErrByPath("request.status_code_flags_invalid") - - if _.has(options, "failOnStatus") - $errUtils.warnByPath("request.failonstatus_deprecated_warning") - options.failOnStatusCode = options.failOnStatus - - ## normalize followRedirects -> followRedirect - ## because we are nice - if _.has(options, "followRedirects") - options.followRedirect = options.followRedirects - - if not $utils.isValidHttpMethod(options.method) - $errUtils.throwErrByPath("request.invalid_method", { - args: { method: o.method } - }) - - if not options.url - $errUtils.throwErrByPath("request.url_missing") - - if not _.isString(options.url) - $errUtils.throwErrByPath("request.url_wrong_type") - - ## normalize the url by prepending it with our current origin - ## or the baseUrl - ## or just using the options.url if its FQDN - ## origin may return an empty string if we haven't visited anything yet - options.url = $Location.normalize(options.url) - - if originOrBase = config("baseUrl") or cy.getRemoteLocation("origin") - options.url = $Location.qualifyWithBaseUrl(originOrBase, options.url) - - ## Make sure the url unicode characters are properly escaped - ## https://github.com/cypress-io/cypress/issues/5274 - try - options.url = new URL(options.url).href - catch err - if !(err instanceof TypeError) ## unexpected, new URL should only throw TypeError - throw err - - # The URL object cannot be constructed because of URL failure - $errUtils.throwErrByPath("request.url_invalid", { - args: { - configFile: Cypress.config("configFile") - } - }) - - - ## if options.url isnt FQDN then we need to throw here - ## if we made a request prior to a visit then it needs - ## to be filled out - if not $Location.isFullyQualifiedUrl(options.url) - $errUtils.throwErrByPath("request.url_invalid", { - args: { - configFile: Cypress.config("configFile") - } - }) - - ## if a user has `x-www-form-urlencoded` content-type set - ## with an object body, they meant to add 'form: true' - ## so we are nice and do it for them :) - ## https://github.com/cypress-io/cypress/issues/2923 - if needsFormSpecified(options) - options.form = true - - if _.isObject(options.body) and path = whatIsCircular(options.body) - $errUtils.throwErrByPath("request.body_circular", { args: { path }}) - - ## only set json to true if form isnt true - ## and we have a valid object for body - if options.form isnt true and isValidJsonObj(options.body) - options.json = true - - options = _.omitBy(options, whichAreOptional) - - if a = options.auth - if not _.isObject(a) - $errUtils.throwErrByPath("request.auth_invalid") - - if h = options.headers - if _.isObject(h) - options.headers = h - else - $errUtils.throwErrByPath("request.headers_invalid") - - if not _.isBoolean(options.gzip) - $errUtils.throwErrByPath("request.gzip_invalid") - - if f = options.form - if not _.isBoolean(f) - $errUtils.throwErrByPath("request.form_invalid") - - ## clone the requestOpts and reduce them down - ## to the bare minimum to send to lib/request - requestOpts = _.pick(options, REQUEST_PROPS) - - if options.log - options._log = Cypress.log({ - message: "" - consoleProps: -> - resp = options.response ? {} - rr = resp.allRequestResponses ? [] - - obj = {} - - word = $utils.plural(rr.length, "Requests", "Request") - - ## if we have only a single request/response then - ## flatten this to an object, else keep as array - rr = if rr.length is 1 then rr[0] else rr - - obj[word] = rr - obj["Yielded"] = _.pick(resp, "status", "duration", "body", "headers") - - return obj - - renderProps: -> - status = switch - when r = options.response - r.status - else - indicator = "pending" - "---" - - indicator ?= if options.response?.isOkStatusCode then "successful" else "bad" - - { - message: "#{options.method} #{status} #{options.url}" - indicator: indicator - } - }) - - ## need to remove the current timeout - ## because we're handling timeouts ourselves - cy.clearTimeout("http:request") - - Cypress.backend("http:request", requestOpts) - .timeout(options.timeout) - .then (response) => - options.response = response - - ## bomb if we should fail on non okay status code - if options.failOnStatusCode and response.isOkStatusCode isnt true - $errUtils.throwErrByPath("request.status_invalid", { - onFail: options._log - args: { - method: requestOpts.method - url: requestOpts.url - requestBody: response.requestBody - requestHeaders: response.requestHeaders - status: response.status - statusText: response.statusText - responseBody: response.body - responseHeaders: response.headers - redirects: response.redirects - } - }) - - return response - .catch Promise.TimeoutError, (err) => - $errUtils.throwErrByPath("request.timed_out", { - onFail: options._log +}; + +const REQUEST_PROPS = _.keys(REQUEST_DEFAULTS); + +const OPTIONAL_OPTS = _.reduce(REQUEST_DEFAULTS, isOptional, []); + +const hasFormUrlEncodedContentTypeHeader = function(headers) { + const header = _.findKey(headers, _.matches("application/x-www-form-urlencoded")); + + return header && (_.toLower(header) === "content-type"); +}; + +const isValidJsonObj = body => _.isObject(body) && !_.isFunction(body); + +const whichAreOptional = (val, key) => (val === null) && OPTIONAL_OPTS.includes(key); + +const needsFormSpecified = function(options = {}) { + const { body, json, headers } = options; + + //# json isn't true, and we have an object body and the user + //# specified that the content-type header is x-www-form-urlencoded + return (json !== true) && _.isObject(body) && hasFormUrlEncodedContentTypeHeader(headers); +}; + +module.exports = (Commands, Cypress, cy, state, config) => // Cypress.extend +// ## set defaults for all requests? +// requestDefaults: (options = {}) -> + +Commands.addAll({ + //# allow our signature to be similar to cy.route + //# METHOD / URL / BODY + //# or object literal with all expanded options + request(...args) { + let a, f, h, o, originOrBase, path; + const userOptions = (o = {}); + + switch (false) { + case !_.isObject(args[0]): + _.extend(userOptions, args[0]); + break; + + case args.length !== 1: + o.url = args[0]; + break; + + case args.length !== 2: + //# if our first arg is a valid + //# HTTP method then set method + url + if ($utils.isValidHttpMethod(args[0])) { + o.method = args[0]; + o.url = args[1]; + } else { + //# set url + body + o.url = args[0]; + o.body = args[1]; + } + break; + + case args.length !== 3: + o.method = args[0]; + o.url = args[1]; + o.body = args[2]; + break; + } + + let options = _.defaults({}, userOptions, REQUEST_DEFAULTS, { + log: true + }); + + //# if timeout is not supplied, use the configured default + if (!options.timeout) { options.timeout = config("responseTimeout"); } + + options.method = options.method.toUpperCase(); + + if (options.retryOnStatusCodeFailure && !options.failOnStatusCode) { + $errUtils.throwErrByPath("request.status_code_flags_invalid"); + } + + if (_.has(options, "failOnStatus")) { + $errUtils.warnByPath("request.failonstatus_deprecated_warning"); + options.failOnStatusCode = options.failOnStatus; + } + + //# normalize followRedirects -> followRedirect + //# because we are nice + if (_.has(options, "followRedirects")) { + options.followRedirect = options.followRedirects; + } + + if (!$utils.isValidHttpMethod(options.method)) { + $errUtils.throwErrByPath("request.invalid_method", { + args: { method: o.method } + }); + } + + if (!options.url) { + $errUtils.throwErrByPath("request.url_missing"); + } + + if (!_.isString(options.url)) { + $errUtils.throwErrByPath("request.url_wrong_type"); + } + + //# normalize the url by prepending it with our current origin + //# or the baseUrl + //# or just using the options.url if its FQDN + //# origin may return an empty string if we haven't visited anything yet + options.url = $Location.normalize(options.url); + + if (originOrBase = config("baseUrl") || cy.getRemoteLocation("origin")) { + options.url = $Location.qualifyWithBaseUrl(originOrBase, options.url); + } + + //# Make sure the url unicode characters are properly escaped + //# https://github.com/cypress-io/cypress/issues/5274 + try { + options.url = new URL(options.url).href; + } catch (error) { + const err = error; + if (!(err instanceof TypeError)) { //# unexpected, new URL should only throw TypeError + throw err; + } + + // The URL object cannot be constructed because of URL failure + $errUtils.throwErrByPath("request.url_invalid", { + args: { + configFile: Cypress.config("configFile") + } + }); + } + + + //# if options.url isnt FQDN then we need to throw here + //# if we made a request prior to a visit then it needs + //# to be filled out + if (!$Location.isFullyQualifiedUrl(options.url)) { + $errUtils.throwErrByPath("request.url_invalid", { + args: { + configFile: Cypress.config("configFile") + } + }); + } + + //# if a user has `x-www-form-urlencoded` content-type set + //# with an object body, they meant to add 'form: true' + //# so we are nice and do it for them :) + //# https://github.com/cypress-io/cypress/issues/2923 + if (needsFormSpecified(options)) { + options.form = true; + } + + if (_.isObject(options.body) && (path = whatIsCircular(options.body))) { + $errUtils.throwErrByPath("request.body_circular", { args: { path }}); + } + + //# only set json to true if form isnt true + //# and we have a valid object for body + if ((options.form !== true) && isValidJsonObj(options.body)) { + options.json = true; + } + + options = _.omitBy(options, whichAreOptional); + + if (a = options.auth) { + if (!_.isObject(a)) { + $errUtils.throwErrByPath("request.auth_invalid"); + } + } + + if (h = options.headers) { + if (_.isObject(h)) { + options.headers = h; + } else { + $errUtils.throwErrByPath("request.headers_invalid"); + } + } + + if (!_.isBoolean(options.gzip)) { + $errUtils.throwErrByPath("request.gzip_invalid"); + } + + if (f = options.form) { + if (!_.isBoolean(f)) { + $errUtils.throwErrByPath("request.form_invalid"); + } + } + + //# clone the requestOpts and reduce them down + //# to the bare minimum to send to lib/request + const requestOpts = _.pick(options, REQUEST_PROPS); + + if (options.log) { + options._log = Cypress.log({ + message: "", + consoleProps() { + const resp = options.response != null ? options.response : {}; + let rr = resp.allRequestResponses != null ? resp.allRequestResponses : []; + + const obj = {}; + + const word = $utils.plural(rr.length, "Requests", "Request"); + + //# if we have only a single request/response then + //# flatten this to an object, else keep as array + rr = rr.length === 1 ? rr[0] : rr; + + obj[word] = rr; + obj["Yielded"] = _.pick(resp, "status", "duration", "body", "headers"); + + return obj; + }, + + renderProps() { + let indicator; + const status = (() => { let r; + switch (false) { + case !(r = options.response): + return r.status; + default: + indicator = "pending"; + return "---"; + } })(); + + if (indicator == null) { indicator = (options.response != null ? options.response.isOkStatusCode : undefined) ? "successful" : "bad"; } + + return { + message: `${options.method} ${status} ${options.url}`, + indicator + }; + } + }); + } + + //# need to remove the current timeout + //# because we're handling timeouts ourselves + cy.clearTimeout("http:request"); + + return Cypress.backend("http:request", requestOpts) + .timeout(options.timeout) + .then(response => { + options.response = response; + + //# bomb if we should fail on non okay status code + if (options.failOnStatusCode && (response.isOkStatusCode !== true)) { + $errUtils.throwErrByPath("request.status_invalid", { + onFail: options._log, args: { - url: requestOpts.url - method: requestOpts.method - timeout: options.timeout + method: requestOpts.method, + url: requestOpts.url, + requestBody: response.requestBody, + requestHeaders: response.requestHeaders, + status: response.status, + statusText: response.statusText, + responseBody: response.body, + responseHeaders: response.headers, + redirects: response.redirects } - }) - .catch { backend: true }, (err) -> - $errUtils.throwErrByPath("request.loading_failed", { - onFail: options._log - args: { - error: err.message - stack: err.stack - method: requestOpts.method - url: requestOpts.url - }, - noStackTrace: true - }) - }) + }); + } + + return response; + }).catch(Promise.TimeoutError, err => { + return $errUtils.throwErrByPath("request.timed_out", { + onFail: options._log, + args: { + url: requestOpts.url, + method: requestOpts.method, + timeout: options.timeout + } + }); + }).catch({ backend: true }, err => $errUtils.throwErrByPath("request.loading_failed", { + onFail: options._log, + args: { + error: err.message, + stack: err.stack, + method: requestOpts.method, + url: requestOpts.url + }, + noStackTrace: true + })); + } +}); diff --git a/packages/driver/src/cy/commands/screenshot.js b/packages/driver/src/cy/commands/screenshot.js index d917c4ace592..3d837495f45b 100644 --- a/packages/driver/src/cy/commands/screenshot.js +++ b/packages/driver/src/cy/commands/screenshot.js @@ -1,413 +1,457 @@ -_ = require("lodash") -$ = require("jquery") -bytes = require("bytes") -Promise = require("bluebird") - -$Screenshot = require("../../cypress/screenshot") -$dom = require("../../dom") -$errUtils = require("../../cypress/error_utils") - -getViewportHeight = (state) -> - ## TODO this doesn't seem correct - Math.min(state("viewportHeight"), window.innerHeight) - -getViewportWidth = (state) -> - Math.min(state("viewportWidth"), window.innerWidth) - -automateScreenshot = (state, options = {}) -> - { runnable, timeout } = options - - titles = [] - - ## if this a hook then push both the current test title - ## and our own hook title - if runnable.type is "hook" - if runnable.ctx and (ct = runnable.ctx.currentTest) - titles.push(ct.title, runnable.title) - else - titles.push(runnable.title) +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const $ = require("jquery"); +const bytes = require("bytes"); +const Promise = require("bluebird"); + +const $Screenshot = require("../../cypress/screenshot"); +const $dom = require("../../dom"); +const $errUtils = require("../../cypress/error_utils"); + +const getViewportHeight = state => //# TODO this doesn't seem correct +Math.min(state("viewportHeight"), window.innerHeight); + +const getViewportWidth = state => Math.min(state("viewportWidth"), window.innerWidth); + +const automateScreenshot = function(state, options = {}) { + const { runnable, timeout } = options; + + const titles = []; + + //# if this a hook then push both the current test title + //# and our own hook title + if (runnable.type === "hook") { + let ct; + if (runnable.ctx && (ct = runnable.ctx.currentTest)) { + titles.push(ct.title, runnable.title); + } + } else { + titles.push(runnable.title); + } - getParentTitle = (runnable) -> - if p = runnable.parent - if t = p.title - titles.unshift(t) + var getParentTitle = function(runnable) { + let p; + if (p = runnable.parent) { + let t; + if (t = p.title) { + titles.unshift(t); + } - getParentTitle(p) + return getParentTitle(p); + } + }; - getParentTitle(runnable) + getParentTitle(runnable); - props = _.extend({ - titles - testId: runnable.id + const props = _.extend({ + titles, + testId: runnable.id, takenPaths: state("screenshotPaths") - }, _.omit(options, "runnable", "timeout", "log", "subject")) + }, _.omit(options, "runnable", "timeout", "log", "subject")); - automate = -> - Cypress.automation("take:screenshot", props) + const automate = () => Cypress.automation("take:screenshot", props); - if not timeout - automate() - else - ## need to remove the current timeout - ## because we're handling timeouts ourselves - cy.clearTimeout("take:screenshot") + if (!timeout) { + return automate(); + } else { + //# need to remove the current timeout + //# because we're handling timeouts ourselves + cy.clearTimeout("take:screenshot"); - automate() + return automate() .timeout(timeout) - .catch (err) -> - $errUtils.throwErr(err, { onFail: options.log }) - .catch Promise.TimeoutError, (err) -> - $errUtils.throwErrByPath "screenshot.timed_out", { - onFail: options.log - args: { timeout } - } + .catch(err => $errUtils.throwErr(err, { onFail: options.log })).catch(Promise.TimeoutError, err => $errUtils.throwErrByPath("screenshot.timed_out", { + onFail: options.log, + args: { timeout } + })); + } +}; -scrollOverrides = (win, doc) -> - originalOverflow = doc.documentElement.style.overflow - originalBodyOverflowY = doc.body.style.overflowY - originalX = win.scrollX - originalY = win.scrollY +const scrollOverrides = function(win, doc) { + const originalOverflow = doc.documentElement.style.overflow; + const originalBodyOverflowY = doc.body.style.overflowY; + const originalX = win.scrollX; + const originalY = win.scrollY; - ## overflow-y: scroll can break `window.scrollTo` - if doc.body - doc.body.style.overflowY = "visible" + //# overflow-y: scroll can break `window.scrollTo` + if (doc.body) { + doc.body.style.overflowY = "visible"; + } - ## hide scrollbars - doc.documentElement.style.overflow = "hidden" + //# hide scrollbars + doc.documentElement.style.overflow = "hidden"; - return -> - doc.documentElement.style.overflow = originalOverflow - if doc.body - doc.body.style.overflowY = originalBodyOverflowY - win.scrollTo(originalX, originalY) + return function() { + doc.documentElement.style.overflow = originalOverflow; + if (doc.body) { + doc.body.style.overflowY = originalBodyOverflowY; + } + return win.scrollTo(originalX, originalY); + }; +}; -validateNumScreenshots = (numScreenshots, automationOptions) -> - if numScreenshots < 1 - $errUtils.throwErrByPath("screenshot.invalid_height", { +const validateNumScreenshots = function(numScreenshots, automationOptions) { + if (numScreenshots < 1) { + return $errUtils.throwErrByPath("screenshot.invalid_height", { log: automationOptions.log - }) - -takeScrollingScreenshots = (scrolls, win, state, automationOptions) -> - scrollAndTake = ({ y, clip, afterScroll }, index) -> - win.scrollTo(0, y) - if afterScroll - clip = afterScroll() - options = _.extend({}, automationOptions, { - current: index + 1 - total: scrolls.length - clip: clip - }) - automateScreenshot(state, options) - - Promise + }); + } +}; + +const takeScrollingScreenshots = function(scrolls, win, state, automationOptions) { + const scrollAndTake = function({ y, clip, afterScroll }, index) { + win.scrollTo(0, y); + if (afterScroll) { + clip = afterScroll(); + } + const options = _.extend({}, automationOptions, { + current: index + 1, + total: scrolls.length, + clip + }); + return automateScreenshot(state, options); + }; + + return Promise .mapSeries(scrolls, scrollAndTake) - .then(_.last) + .then(_.last); +}; -takeFullPageScreenshot = (state, automationOptions) -> - win = state("window") - doc = state("document") +const takeFullPageScreenshot = function(state, automationOptions) { + const win = state("window"); + const doc = state("document"); - resetScrollOverrides = scrollOverrides(win, doc) + const resetScrollOverrides = scrollOverrides(win, doc); - docHeight = $(doc).height() - viewportHeight = getViewportHeight(state) - numScreenshots = Math.ceil(docHeight / viewportHeight) + const docHeight = $(doc).height(); + const viewportHeight = getViewportHeight(state); + const numScreenshots = Math.ceil(docHeight / viewportHeight); - validateNumScreenshots(numScreenshots, automationOptions) + validateNumScreenshots(numScreenshots, automationOptions); - scrolls = _.map _.times(numScreenshots), (index) -> - y = viewportHeight * index - clip = if index + 1 is numScreenshots - heightLeft = docHeight - (viewportHeight * index) - { - x: automationOptions.clip.x - y: viewportHeight - heightLeft - width: automationOptions.clip.width + const scrolls = _.map(_.times(numScreenshots), function(index) { + const y = viewportHeight * index; + const clip = (() => { + if ((index + 1) === numScreenshots) { + const heightLeft = docHeight - (viewportHeight * index); + return { + x: automationOptions.clip.x, + y: viewportHeight - heightLeft, + width: automationOptions.clip.width, height: heightLeft - } - else - automationOptions.clip + }; + } else { + return automationOptions.clip; + } + })(); - { y, clip } + return { y, clip }; +}); - takeScrollingScreenshots(scrolls, win, state, automationOptions) - .finally(resetScrollOverrides) + return takeScrollingScreenshots(scrolls, win, state, automationOptions) + .finally(resetScrollOverrides); +}; -applyPaddingToElementPositioning = (elPosition, automationOptions) -> - if not automationOptions.padding - return elPosition +const applyPaddingToElementPositioning = function(elPosition, automationOptions) { + if (!automationOptions.padding) { + return elPosition; + } - [ paddingTop, paddingRight, paddingBottom, paddingLeft ] = automationOptions.padding + const [ paddingTop, paddingRight, paddingBottom, paddingLeft ] = automationOptions.padding; return { - width: elPosition.width + paddingLeft + paddingRight - height: elPosition.height + paddingTop + paddingBottom + width: elPosition.width + paddingLeft + paddingRight, + height: elPosition.height + paddingTop + paddingBottom, fromElViewport: { - top: elPosition.fromElViewport.top - paddingTop - left: elPosition.fromElViewport.left - paddingLeft + top: elPosition.fromElViewport.top - paddingTop, + left: elPosition.fromElViewport.left - paddingLeft, bottom: elPosition.fromElViewport.bottom + paddingBottom - } + }, fromElWindow: { top: elPosition.fromElWindow.top - paddingTop } - } + }; +}; -takeElementScreenshot = ($el, state, automationOptions) -> - win = state("window") - doc = state("document") +const takeElementScreenshot = function($el, state, automationOptions) { + const win = state("window"); + const doc = state("document"); - resetScrollOverrides = scrollOverrides(win, doc) + const resetScrollOverrides = scrollOverrides(win, doc); - elPosition = applyPaddingToElementPositioning( + let elPosition = applyPaddingToElementPositioning( $dom.getElementPositioning($el), automationOptions - ) - viewportHeight = getViewportHeight(state) - viewportWidth = getViewportWidth(state) - numScreenshots = Math.ceil(elPosition.height / viewportHeight) + ); + const viewportHeight = getViewportHeight(state); + const viewportWidth = getViewportWidth(state); + const numScreenshots = Math.ceil(elPosition.height / viewportHeight); - validateNumScreenshots(numScreenshots, automationOptions) + validateNumScreenshots(numScreenshots, automationOptions); - scrolls = _.map _.times(numScreenshots), (index) -> - y = elPosition.fromElWindow.top + (viewportHeight * index) + const scrolls = _.map(_.times(numScreenshots), function(index) { + const y = elPosition.fromElWindow.top + (viewportHeight * index); - afterScroll = -> + const afterScroll = function() { elPosition = applyPaddingToElementPositioning( $dom.getElementPositioning($el), automationOptions - ) - x = Math.min(viewportWidth, elPosition.fromElViewport.left) - width = Math.min(viewportWidth - x, elPosition.width) + ); + const x = Math.min(viewportWidth, elPosition.fromElViewport.left); + const width = Math.min(viewportWidth - x, elPosition.width); - if numScreenshots is 1 + if (numScreenshots === 1) { return { - x: x - y: elPosition.fromElViewport.top - width: width + x, + y: elPosition.fromElViewport.top, + width, height: elPosition.height - } + }; + } - if index + 1 is numScreenshots - overlap = (numScreenshots - 1) * viewportHeight + elPosition.fromElViewport.top - heightLeft = elPosition.fromElViewport.bottom - overlap + if ((index + 1) === numScreenshots) { + const overlap = ((numScreenshots - 1) * viewportHeight) + elPosition.fromElViewport.top; + const heightLeft = elPosition.fromElViewport.bottom - overlap; return { - x: x - y: overlap - width: width + x, + y: overlap, + width, height: heightLeft - } + }; + } return { - x: x - y: Math.max(0, elPosition.fromElViewport.top) - width: width - ## TODO: try simplifying to just 'viewportHeight' + x, + y: Math.max(0, elPosition.fromElViewport.top), + width, + //# TODO: try simplifying to just 'viewportHeight' height: Math.min(viewportHeight, elPosition.fromElViewport.top + elPosition.height) - } - - { y, afterScroll } - - takeScrollingScreenshots(scrolls, win, state, automationOptions) - .finally(resetScrollOverrides) - -## "app only" means we're hiding the runner UI -isAppOnly = ({ capture }) -> - capture is "viewport" or capture is "fullPage" - -getShouldScale = ({ capture, scale }) -> - if isAppOnly({ capture }) then scale else true - -getBlackout = ({ capture, blackout }) -> - if isAppOnly({ capture }) then blackout else [] - -takeScreenshot = (Cypress, state, screenshotConfig, options = {}) -> - { - capture - padding - clip - disableTimersAndAnimations - onBeforeScreenshot + }; + }; + + return { y, afterScroll }; +}); + + return takeScrollingScreenshots(scrolls, win, state, automationOptions) + .finally(resetScrollOverrides); +}; + +//# "app only" means we're hiding the runner UI +const isAppOnly = ({ capture }) => (capture === "viewport") || (capture === "fullPage"); + +const getShouldScale = function({ capture, scale }) { + if (isAppOnly({ capture })) { return scale; } else { return true; } +}; + +const getBlackout = function({ capture, blackout }) { + if (isAppOnly({ capture })) { return blackout; } else { return []; } +}; + +const takeScreenshot = function(Cypress, state, screenshotConfig, options = {}) { + const { + capture, + padding, + clip, + disableTimersAndAnimations, + onBeforeScreenshot, onAfterScreenshot - } = screenshotConfig + } = screenshotConfig; - { subject, runnable, name } = options + const { subject, runnable, name } = options; - startTime = new Date() + const startTime = new Date(); - send = (event, props, resolve) -> - Cypress.action("cy:#{event}", props, resolve) + const send = (event, props, resolve) => Cypress.action(`cy:${event}`, props, resolve); - sendAsync = (event, props) -> - new Promise (resolve) -> - send(event, props, resolve) + const sendAsync = (event, props) => new Promise(resolve => send(event, props, resolve)); - getOptions = (isOpen) -> - { - id: runnable.id - isOpen: isOpen - appOnly: isAppOnly(screenshotConfig) - scale: getShouldScale(screenshotConfig) - waitForCommandSynchronization: not isAppOnly(screenshotConfig) - disableTimersAndAnimations: disableTimersAndAnimations - blackout: getBlackout(screenshotConfig) - } + const getOptions = isOpen => ({ + id: runnable.id, + isOpen, + appOnly: isAppOnly(screenshotConfig), + scale: getShouldScale(screenshotConfig), + waitForCommandSynchronization: !isAppOnly(screenshotConfig), + disableTimersAndAnimations, + blackout: getBlackout(screenshotConfig) + }); - before = -> - if disableTimersAndAnimations - cy.pauseTimers(true) + const before = function() { + if (disableTimersAndAnimations) { + cy.pauseTimers(true); + } - sendAsync("before:screenshot", getOptions(true)) + return sendAsync("before:screenshot", getOptions(true)); + }; - after = -> - send("after:screenshot", getOptions(false)) + const after = function() { + send("after:screenshot", getOptions(false)); - if disableTimersAndAnimations - cy.pauseTimers(false) + if (disableTimersAndAnimations) { + return cy.pauseTimers(false); + } + }; - automationOptions = _.extend({}, options, { - capture + const automationOptions = _.extend({}, options, { + capture, clip: { - x: 0 - y: 0 - width: getViewportWidth(state) + x: 0, + y: 0, + width: getViewportWidth(state), height: getViewportHeight(state) - } - padding - userClip: clip + }, + padding, + userClip: clip, viewport: { - width: window.innerWidth + width: window.innerWidth, height: window.innerHeight - } - scaled: getShouldScale(screenshotConfig) - blackout: getBlackout(screenshotConfig) + }, + scaled: getShouldScale(screenshotConfig), + blackout: getBlackout(screenshotConfig), startTime: startTime.toISOString() - }) + }); - ## use the subject as $el or yield the wrapped documentElement - $el = if $dom.isElement(subject) + //# use the subject as $el or yield the wrapped documentElement + const $el = $dom.isElement(subject) ? subject - else - $dom.wrap(state("document").documentElement) - - before() - .then -> - onBeforeScreenshot and onBeforeScreenshot.call(state("ctx"), $el) - - $Screenshot.onBeforeScreenshot($el) - - switch - when $dom.isElement(subject) - takeElementScreenshot($el, state, automationOptions) - when capture is "fullPage" - takeFullPageScreenshot(state, automationOptions) - else - automateScreenshot(state, automationOptions) - .then (props) -> - onAfterScreenshot and onAfterScreenshot.call(state("ctx"), $el, props) - - $Screenshot.onAfterScreenshot($el, props) - - return props - .finally(after) - -module.exports = (Commands, Cypress, cy, state, config) -> - - ## failure screenshot when not interactive - Cypress.on "runnable:after:run:async", (test, runnable) -> - screenshotConfig = $Screenshot.getConfig() - - return if not test.err or not screenshotConfig.screenshotOnRunFailure or config("isInteractive") or test.err.isPending - - ## if a screenshot has not been taken (by cy.screenshot()) in the test - ## that failed, we can bypass UI-changing and pixel-checking (simple: true) - ## otheriwse, we need to do all the standard checks - ## to make sure the UI is in the right place (simple: false) - screenshotConfig.capture = "runner" - takeScreenshot(Cypress, state, screenshotConfig, { - runnable - simple: !state("screenshotTaken") - testFailure: true + : + $dom.wrap(state("document").documentElement); + + return before() + .then(function() { + onBeforeScreenshot && onBeforeScreenshot.call(state("ctx"), $el); + + $Screenshot.onBeforeScreenshot($el); + + switch (false) { + case !$dom.isElement(subject): + return takeElementScreenshot($el, state, automationOptions); + case capture !== "fullPage": + return takeFullPageScreenshot(state, automationOptions); + default: + return automateScreenshot(state, automationOptions); + }}).then(function(props) { + onAfterScreenshot && onAfterScreenshot.call(state("ctx"), $el, props); + + $Screenshot.onAfterScreenshot($el, props); + + return props;}).finally(after); +}; + +module.exports = function(Commands, Cypress, cy, state, config) { + + //# failure screenshot when not interactive + Cypress.on("runnable:after:run:async", function(test, runnable) { + const screenshotConfig = $Screenshot.getConfig(); + + if (!test.err || !screenshotConfig.screenshotOnRunFailure || config("isInteractive") || test.err.isPending) { return; } + + //# if a screenshot has not been taken (by cy.screenshot()) in the test + //# that failed, we can bypass UI-changing and pixel-checking (simple: true) + //# otheriwse, we need to do all the standard checks + //# to make sure the UI is in the right place (simple: false) + screenshotConfig.capture = "runner"; + return takeScreenshot(Cypress, state, screenshotConfig, { + runnable, + simple: !state("screenshotTaken"), + testFailure: true, timeout: config("responseTimeout") - }) + }); + }); - Commands.addAll({ prevSubject: ["optional", "element", "window", "document"] }, { - screenshot: (subject, name, options = {}) -> - userOptions = options + return Commands.addAll({ prevSubject: ["optional", "element", "window", "document"] }, { + screenshot(subject, name, options = {}) { + let userOptions = options; - if _.isObject(name) - userOptions = name - name = null + if (_.isObject(name)) { + userOptions = name; + name = null; + } - withinSubject = state("withinSubject") - if withinSubject and $dom.isElement(withinSubject) - subject = withinSubject + const withinSubject = state("withinSubject"); + if (withinSubject && $dom.isElement(withinSubject)) { + subject = withinSubject; + } - ## TODO: handle hook titles - runnable = state("runnable") + //# TODO: handle hook titles + const runnable = state("runnable"); - options = _.defaults {}, userOptions, { - log: true + options = _.defaults({}, userOptions, { + log: true, timeout: config("responseTimeout") - } + }); - isWin = $dom.isWindow(subject) + const isWin = $dom.isWindow(subject); - screenshotConfig = _.pick(options, "capture", "scale", "disableTimersAndAnimations", "blackout", "waitForCommandSynchronization", "padding", "clip", "onBeforeScreenshot", "onAfterScreenshot") - screenshotConfig = $Screenshot.validate(screenshotConfig, "screenshot", options._log) - screenshotConfig = _.extend($Screenshot.getConfig(), screenshotConfig) + let screenshotConfig = _.pick(options, "capture", "scale", "disableTimersAndAnimations", "blackout", "waitForCommandSynchronization", "padding", "clip", "onBeforeScreenshot", "onAfterScreenshot"); + screenshotConfig = $Screenshot.validate(screenshotConfig, "screenshot", options._log); + screenshotConfig = _.extend($Screenshot.getConfig(), screenshotConfig); - ## set this regardless of options.log b/c its used by the - ## yielded value below - consoleProps = _.omit(screenshotConfig, "scale", "screenshotOnRunFailure") + //# set this regardless of options.log b/c its used by the + //# yielded value below + let consoleProps = _.omit(screenshotConfig, "scale", "screenshotOnRunFailure"); consoleProps = _.extend(consoleProps, { - scaled: getShouldScale(screenshotConfig) + scaled: getShouldScale(screenshotConfig), blackout: getBlackout(screenshotConfig) - }) + }); - if name - consoleProps.name = name + if (name) { + consoleProps.name = name; + } - if options.log + if (options.log) { options._log = Cypress.log({ - message: name - consoleProps: -> - consoleProps - }) + message: name, + consoleProps() { + return consoleProps; + } + }); + } - if not isWin and subject and subject.length > 1 + if (!isWin && subject && (subject.length > 1)) { $errUtils.throwErrByPath("screenshot.multiple_elements", { - log: options._log + log: options._log, args: { numElements: subject.length } - }) + }); + } - if $dom.isElement(subject) - screenshotConfig.capture = "viewport" + if ($dom.isElement(subject)) { + screenshotConfig.capture = "viewport"; + } - state("screenshotTaken", true) + state("screenshotTaken", true); - takeScreenshot(Cypress, state, screenshotConfig, { - name - subject - runnable - log: options._log + return takeScreenshot(Cypress, state, screenshotConfig, { + name, + subject, + runnable, + log: options._log, timeout: options.timeout }) - .then (props) -> - { duration, path, size } = props - { width, height } = props.dimensions + .then(function(props) { + const { duration, path, size } = props; + const { width, height } = props.dimensions; - takenPaths = state("screenshotPaths") or [] - state("screenshotPaths", takenPaths.concat([path])) + const takenPaths = state("screenshotPaths") || []; + state("screenshotPaths", takenPaths.concat([path])); _.extend(consoleProps, props, { - size: bytes(size, { unitSeparator: " " }) - duration: "#{duration}ms" - dimensions: "#{width}px x #{height}px" - }) + size: bytes(size, { unitSeparator: " " }), + duration: `${duration}ms`, + dimensions: `${width}px x ${height}px` + }); - if subject - consoleProps.subject = subject + if (subject) { + consoleProps.subject = subject; + } - return subject - }) + return subject; + }); + } + }); +}; diff --git a/packages/driver/src/cy/commands/task.js b/packages/driver/src/cy/commands/task.js index 25c2030e95f9..03746bc1087a 100644 --- a/packages/driver/src/cy/commands/task.js +++ b/packages/driver/src/cy/commands/task.js @@ -1,81 +1,91 @@ -_ = require("lodash") -Promise = require("bluebird") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const Promise = require("bluebird"); -$utils = require("../../cypress/utils") -$errUtils = require("../../cypress/error_utils") +const $utils = require("../../cypress/utils"); +const $errUtils = require("../../cypress/error_utils"); -module.exports = (Commands, Cypress, cy, state, config) -> - Commands.addAll({ - task: (task, arg, options = {}) -> - userOptions = options - options = _.defaults({}, userOptions, { - log: true - timeout: Cypress.config("taskTimeout") - }) +module.exports = (Commands, Cypress, cy, state, config) => Commands.addAll({ + task(task, arg, options = {}) { + let consoleOutput, message; + const userOptions = options; + options = _.defaults({}, userOptions, { + log: true, + timeout: Cypress.config("taskTimeout") + }); - if options.log - consoleOutput = { - task: task - arg: arg - } + if (options.log) { + consoleOutput = { + task, + arg + }; - message = task - if arg - message += ", #{$utils.stringify(arg)}" + message = task; + if (arg) { + message += `, ${$utils.stringify(arg)}`; + } - options._log = Cypress.log({ - message: message - consoleProps: -> - consoleOutput - }) + options._log = Cypress.log({ + message, + consoleProps() { + return consoleOutput; + } + }); + } - if not task or not _.isString(task) - $errUtils.throwErrByPath("task.invalid_argument", { - onFail: options._log, - args: { task: task ? "" } - }) + if (!task || !_.isString(task)) { + $errUtils.throwErrByPath("task.invalid_argument", { + onFail: options._log, + args: { task: task != null ? task : "" } + }); + } - ## need to remove the current timeout - ## because we're handling timeouts ourselves - cy.clearTimeout() + //# need to remove the current timeout + //# because we're handling timeouts ourselves + cy.clearTimeout(); - Cypress.backend("task", { - task: task - arg: arg - timeout: options.timeout - }) - .timeout(options.timeout) - .then (result) -> - if options._log - _.extend(consoleOutput, { Yielded: result }) - return result + return Cypress.backend("task", { + task, + arg, + timeout: options.timeout + }) + .timeout(options.timeout) + .then(function(result) { + if (options._log) { + _.extend(consoleOutput, { Yielded: result }); + } + return result;}).catch(Promise.TimeoutError, () => $errUtils.throwErrByPath("task.timed_out", { + onFail: options._log, + args: { task, timeout: options.timeout } + })) - .catch Promise.TimeoutError, -> - $errUtils.throwErrByPath "task.timed_out", { - onFail: options._log - args: { task, timeout: options.timeout } - } + .catch({ timedOut: true }, error => $errUtils.throwErrByPath("task.server_timed_out", { + onFail: options._log, + args: { task, timeout: options.timeout, error: error.message } + })) - .catch { timedOut: true }, (error) -> - $errUtils.throwErrByPath "task.server_timed_out", { - onFail: options._log - args: { task, timeout: options.timeout, error: error.message } - } - - .catch (error) -> - ## re-throw if timedOut error from above - throw error if error.name is "CypressError" + .catch(function(error) { + //# re-throw if timedOut error from above + if (error.name === "CypressError") { throw error; } - $errUtils.normalizeErrorStack(error) + $errUtils.normalizeErrorStack(error); - if error?.isKnownError - $errUtils.throwErrByPath("task.known_error", { - onFail: options._log - args: { task, error: error.message } - }) + if (error != null ? error.isKnownError : undefined) { + $errUtils.throwErrByPath("task.known_error", { + onFail: options._log, + args: { task, error: error.message } + }); + } - $errUtils.throwErrByPath("task.failed", { - onFail: options._log - args: { task, error: error?.stack or error?.message or error } - }) - }) + return $errUtils.throwErrByPath("task.failed", { + onFail: options._log, + args: { task, error: (error != null ? error.stack : undefined) || (error != null ? error.message : undefined) || error } + }); + }); + } +}); diff --git a/packages/driver/src/cy/commands/traversals.js b/packages/driver/src/cy/commands/traversals.js index 82c500fa4cec..c4de7ca332ff 100644 --- a/packages/driver/src/cy/commands/traversals.js +++ b/packages/driver/src/cy/commands/traversals.js @@ -1,64 +1,80 @@ -_ = require("lodash") - -$dom = require("../../dom") - -traversals = "find filter not children eq closest first last next nextAll nextUntil parent parents parentsUntil prev prevAll prevUntil siblings".split(" ") - -module.exports = (Commands, Cypress, cy, state, config) -> - _.each traversals, (traversal) -> - Commands.add traversal, { prevSubject: "element" }, (subject, arg1, arg2, options) -> - if _.isObject(arg1) and not _.isFunction(arg1) - options = arg1 - - if _.isObject(arg2) and not _.isFunction(arg2) - options = arg2 - - userOptions = if options then options else {} - - options = _.defaults({}, userOptions, {log: true}) - - getSelector = -> - args = _.chain([arg1, arg2]).reject(_.isFunction).reject(_.isObject).value() - args = _.without(args, null, undefined) - args.join(", ") - - consoleProps = { - Selector: getSelector() - "Applied To": $dom.getElements(subject) +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); + +const $dom = require("../../dom"); + +const traversals = "find filter not children eq closest first last next nextAll nextUntil parent parents parentsUntil prev prevAll prevUntil siblings".split(" "); + +module.exports = (Commands, Cypress, cy, state, config) => _.each(traversals, traversal => Commands.add(traversal, { prevSubject: "element" }, function(subject, arg1, arg2, options) { + let getElements; + if (_.isObject(arg1) && !_.isFunction(arg1)) { + options = arg1; + } + + if (_.isObject(arg2) && !_.isFunction(arg2)) { + options = arg2; + } + + const userOptions = options ? options : {}; + + options = _.defaults({}, userOptions, {log: true}); + + const getSelector = function() { + let args = _.chain([arg1, arg2]).reject(_.isFunction).reject(_.isObject).value(); + args = _.without(args, null, undefined); + return args.join(", "); + }; + + const consoleProps = { + Selector: getSelector(), + "Applied To": $dom.getElements(subject) + }; + + if (options.log !== false) { + options._log = Cypress.log({ + message: getSelector(), + consoleProps() { return consoleProps; } + }); + } + + const setEl = function($el) { + if (options.log === false) { return; } + + consoleProps.Yielded = $dom.getElements($el); + consoleProps.Elements = $el != null ? $el.length : undefined; + + return options._log.set({$el}); + }; + + return (getElements = function() { + //# catch sizzle errors here + let $el; + try { + $el = subject[traversal].call(subject, arg1, arg2); + + //# normalize the selector since jQuery won't have it + //# or completely borks it + $el.selector = getSelector(); + } catch (e) { + e.onFail = () => options._log.error(e); + throw e; + } + + setEl($el); + + return cy.verifyUpcomingAssertions($el, options, { + onRetry: getElements, + onFail(err) { + if (err.type === "existence") { + const node = $dom.stringify(subject, "short"); + return err.message += ` Queried from element: ${node}`; + } } - - if options.log isnt false - options._log = Cypress.log({ - message: getSelector() - consoleProps: -> consoleProps - }) - - setEl = ($el) -> - return if options.log is false - - consoleProps.Yielded = $dom.getElements($el) - consoleProps.Elements = $el?.length - - options._log.set({$el: $el}) - - do getElements = -> - ## catch sizzle errors here - try - $el = subject[traversal].call(subject, arg1, arg2) - - ## normalize the selector since jQuery won't have it - ## or completely borks it - $el.selector = getSelector() - catch e - e.onFail = -> options._log.error(e) - throw e - - setEl($el) - - cy.verifyUpcomingAssertions($el, options, { - onRetry: getElements - onFail: (err) -> - if err.type is "existence" - node = $dom.stringify(subject, "short") - err.message += " Queried from element: #{node}" - }) + }); + })(); +})); diff --git a/packages/driver/src/cy/commands/waiting.js b/packages/driver/src/cy/commands/waiting.js index 29d72156d398..2567ac4df34e 100644 --- a/packages/driver/src/cy/commands/waiting.js +++ b/packages/driver/src/cy/commands/waiting.js @@ -1,207 +1,242 @@ -_ = require("lodash") -Promise = require("bluebird") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const Promise = require("bluebird"); -$errUtils = require("../../cypress/error_utils") +const $errUtils = require("../../cypress/error_utils"); -getNumRequests = (state, alias) => - requests = state("aliasRequests") ? {} - index = requests[alias] ?= 0 +const getNumRequests = (state, alias) => { + let left; + const requests = (left = state("aliasRequests")) != null ? left : {}; + const index = requests[alias] != null ? requests[alias] : (requests[alias] = 0); - requests[alias] += 1 + requests[alias] += 1; - state("aliasRequests", requests) + state("aliasRequests", requests); - [index, _.ordinalize(requests[alias])] + return [index, _.ordinalize(requests[alias])]; +}; -throwErr = (arg) -> - $errUtils.throwErrByPath("wait.invalid_1st_arg", {args: {arg}}) +const throwErr = arg => $errUtils.throwErrByPath("wait.invalid_1st_arg", {args: {arg}}); -module.exports = (Commands, Cypress, cy, state, config) -> - waitFunction = -> - $errUtils.throwErrByPath("wait.fn_deprecated") +module.exports = function(Commands, Cypress, cy, state, config) { + const waitFunction = () => $errUtils.throwErrByPath("wait.fn_deprecated"); - userOptions = null + let userOptions = null; - waitNumber = (subject, ms, options) -> - ## increase the timeout by the delta - cy.timeout(ms, true, "wait") + const waitNumber = function(subject, ms, options) { + //# increase the timeout by the delta + cy.timeout(ms, true, "wait"); - if options.log isnt false + if (options.log !== false) { options._log = Cypress.log({ - consoleProps: -> { - "Waited For": "#{ms}ms before continuing" + consoleProps() { return { + "Waited For": `${ms}ms before continuing`, "Yielded": subject - } - }) + }; } + }); + } - Promise + return Promise .delay(ms, "wait") - .return(subject) - - waitString = (subject, str, options) -> - if options.log isnt false - log = options._log = Cypress.log({ - type: "parent" - aliasType: "route" + .return(subject); + }; + + const waitString = function(subject, str, options) { + let log; + if (options.log !== false) { + log = (options._log = Cypress.log({ + type: "parent", + aliasType: "route", options: userOptions - }) + })); + } - checkForXhr = (alias, type, index, num, options) -> - options.type = type + var checkForXhr = function(alias, type, index, num, options) { + options.type = type; - ## append .type to the alias - xhr = cy.getIndexedXhrByAlias(alias + "." + type, index) + //# append .type to the alias + const xhr = cy.getIndexedXhrByAlias(alias + "." + type, index); - ## return our xhr object - return Promise.resolve(xhr) if xhr + //# return our xhr object + if (xhr) { return Promise.resolve(xhr); } options.error = $errUtils.errMsgByPath("wait.timed_out", { - timeout: options.timeout - alias - num + timeout: options.timeout, + alias, + num, type - }) - - args = arguments - - cy.retry -> - checkForXhr.apply(window, args) - , options - - waitForXhr = (str, options) -> - ## we always want to strip everything after the last '.' - ## since we support alias property 'request' - if _.indexOf(str, ".") == -1 || - str.slice(1) in _.keys(cy.state("aliases")) - [str, str2] = [str, null] - else - # potentially request, response or index - allParts = _.split(str, '.') - [str, str2] = [_.join(_.dropRight(allParts, 1), '.'), _.last(allParts)] - - if not aliasObj = cy.getAlias(str, "wait", log) - cy.aliasNotFoundFor(str, "wait", log) - - ## if this alias is for a route then poll - ## until we find the response xhr object - ## by its alias - {alias, command} = aliasObj - - str = _.compact([alias, str2]).join(".") - - type = cy.getXhrTypeByAlias(str) - - [ index, num ] = getNumRequests(state, alias) - - ## if we have a command then continue to - ## build up an array of referencesAlias - ## because wait can reference an array of aliases - if log - referencesAlias = log.get("referencesAlias") ? [] - aliases = [].concat(referencesAlias) - - if str + }); + + const args = arguments; + + return cy.retry(() => checkForXhr.apply(window, args) + , options); + }; + + const waitForXhr = function(str, options) { + //# we always want to strip everything after the last '.' + //# since we support alias property 'request' + let aliasObj, needle, str2; + if ((_.indexOf(str, ".") === -1) || + (needle = str.slice(1), _.keys(cy.state("aliases")).includes(needle))) { + [str, str2] = [str, null]; + } else { + // potentially request, response or index + const allParts = _.split(str, '.'); + [str, str2] = [_.join(_.dropRight(allParts, 1), '.'), _.last(allParts)]; + } + + if (!(aliasObj = cy.getAlias(str, "wait", log))) { + cy.aliasNotFoundFor(str, "wait", log); + } + + //# if this alias is for a route then poll + //# until we find the response xhr object + //# by its alias + const {alias, command} = aliasObj; + + str = _.compact([alias, str2]).join("."); + + const type = cy.getXhrTypeByAlias(str); + + const [ index, num ] = getNumRequests(state, alias); + + //# if we have a command then continue to + //# build up an array of referencesAlias + //# because wait can reference an array of aliases + if (log) { + let left; + const referencesAlias = (left = log.get("referencesAlias")) != null ? left : []; + const aliases = [].concat(referencesAlias); + + if (str) { aliases.push({ - name: str + name: str, cardinal: index + 1, ordinal: num - }) + }); + } - log.set "referencesAlias", aliases + log.set("referencesAlias", aliases); + } - if command.get("name") isnt "route" + if (command.get("name") !== "route") { $errUtils.throwErrByPath("wait.invalid_alias", { - onFail: options._log + onFail: options._log, args: { alias } - }) - - ## create shallow copy of each options object - ## but slice out the error since we may set - ## the error related to a previous xhr - timeout = options.timeout - requestTimeout = options.requestTimeout ? timeout - responseTimeout = options.responseTimeout ? timeout - - waitForRequest = -> - options = _.omit(options, "_runnableTimeout") - options.timeout = requestTimeout ? Cypress.config("requestTimeout") - checkForXhr(alias, "request", index, num, options) - - waitForResponse = -> - options = _.omit(options, "_runnableTimeout") - options.timeout = responseTimeout ? Cypress.config("responseTimeout") - checkForXhr(alias, "response", index, num, options) - - ## if we were only waiting for the request - ## then resolve immediately do not wait for response - if type is "request" - waitForRequest() - else - waitForRequest().then(waitForResponse) - - Promise - .map [].concat(str), (str) -> - ## we may get back an xhr value instead - ## of a promise, so we have to wrap this - ## in another promise :-( - waitForXhr(str, _.omit(options, "error")) - .then (responses) -> - ## if we only asked to wait for one alias - ## then return that, else return the array of xhr responses - ret = if responses.length is 1 then responses[0] else responses - - if log - log.set "consoleProps", -> { - "Waited For": (_.map(log.get("referencesAlias"), 'name') || []).join(", ") + }); + } + + //# create shallow copy of each options object + //# but slice out the error since we may set + //# the error related to a previous xhr + const { + timeout + } = options; + const requestTimeout = options.requestTimeout != null ? options.requestTimeout : timeout; + const responseTimeout = options.responseTimeout != null ? options.responseTimeout : timeout; + + const waitForRequest = function() { + options = _.omit(options, "_runnableTimeout"); + options.timeout = requestTimeout != null ? requestTimeout : Cypress.config("requestTimeout"); + return checkForXhr(alias, "request", index, num, options); + }; + + const waitForResponse = function() { + options = _.omit(options, "_runnableTimeout"); + options.timeout = responseTimeout != null ? responseTimeout : Cypress.config("responseTimeout"); + return checkForXhr(alias, "response", index, num, options); + }; + + //# if we were only waiting for the request + //# then resolve immediately do not wait for response + if (type === "request") { + return waitForRequest(); + } else { + return waitForRequest().then(waitForResponse); + } + }; + + return Promise + .map([].concat(str), str => //# we may get back an xhr value instead + //# of a promise, so we have to wrap this + //# in another promise :-( + waitForXhr(str, _.omit(options, "error"))).then(function(responses) { + //# if we only asked to wait for one alias + //# then return that, else return the array of xhr responses + const ret = responses.length === 1 ? responses[0] : responses; + + if (log) { + log.set("consoleProps", () => ({ + "Waited For": (_.map(log.get("referencesAlias"), 'name') || []).join(", "), "Yielded": ret + })); + + log.snapshot().end(); + } + + return ret; + }); + }; + + return Commands.addAll({ prevSubject: "optional" }, { + wait(subject, msOrFnOrAlias, options = {}) { + userOptions = options; + + //# check to ensure options is an object + //# if its a string the user most likely is trying + //# to wait on multiple aliases and forget to make this + //# an array + if (_.isString(userOptions)) { + $errUtils.throwErrByPath("wait.invalid_arguments"); + } + + options = _.defaults({}, userOptions, { log: true }); + const args = [subject, msOrFnOrAlias, options]; + + try { + switch (false) { + case !_.isFinite(msOrFnOrAlias): + return waitNumber.apply(window, args); + case !_.isFunction(msOrFnOrAlias): + return waitFunction(); + case !_.isString(msOrFnOrAlias): + return waitString.apply(window, args); + case !_.isArray(msOrFnOrAlias) || !!_.isEmpty(msOrFnOrAlias): + return waitString.apply(window, args); + default: + //# figure out why this error failed + var arg = (() => { switch (false) { + case !_.isNaN(msOrFnOrAlias): return "NaN"; + case msOrFnOrAlias !== Infinity: return "Infinity"; + case !_.isSymbol(msOrFnOrAlias): return msOrFnOrAlias.toString(); + default: + try { + return JSON.stringify(msOrFnOrAlias); + } catch (error) { + return "an invalid argument"; + } + } })(); + + return throwErr(arg); } - - log.snapshot().end() - - return ret - - Commands.addAll({ prevSubject: "optional" }, { - wait: (subject, msOrFnOrAlias, options = {}) -> - userOptions = options - - ## check to ensure options is an object - ## if its a string the user most likely is trying - ## to wait on multiple aliases and forget to make this - ## an array - if _.isString(userOptions) - $errUtils.throwErrByPath("wait.invalid_arguments") - - options = _.defaults({}, userOptions, { log: true }) - args = [subject, msOrFnOrAlias, options] - - try - switch - when _.isFinite(msOrFnOrAlias) - waitNumber.apply(window, args) - when _.isFunction(msOrFnOrAlias) - waitFunction() - when _.isString(msOrFnOrAlias) - waitString.apply(window, args) - when _.isArray(msOrFnOrAlias) and not _.isEmpty(msOrFnOrAlias) - waitString.apply(window, args) - else - ## figure out why this error failed - arg = switch - when _.isNaN(msOrFnOrAlias) then "NaN" - when msOrFnOrAlias is Infinity then "Infinity" - when _.isSymbol(msOrFnOrAlias) then msOrFnOrAlias.toString() - else - try - JSON.stringify(msOrFnOrAlias) - catch - "an invalid argument" - - throwErr(arg) - catch err - if err.name is "CypressError" - throw err - else - ## whatever was passed in could not be parsed - ## by our switch case - throwErr("an invalid argument") - }) + } catch (err) { + if (err.name === "CypressError") { + throw err; + } else { + //# whatever was passed in could not be parsed + //# by our switch case + return throwErr("an invalid argument"); + } + } + } + }); +}; diff --git a/packages/driver/src/cy/commands/window.js b/packages/driver/src/cy/commands/window.js index bbeed504979e..fdec9729c8d4 100644 --- a/packages/driver/src/cy/commands/window.js +++ b/packages/driver/src/cy/commands/window.js @@ -1,227 +1,270 @@ -_ = require("lodash") -Promise = require("bluebird") - -$errUtils = require("../../cypress/error_utils") - -viewports = { - "macbook-15" : "1440x900" - "macbook-13" : "1280x800" - "macbook-11" : "1366x768" - "ipad-2" : "768x1024" - "ipad-mini" : "768x1024" - "iphone-xr" : "414x896" - "iphone-x" : "375x812" - "iphone-6+" : "414x736" - "iphone-6" : "375x667" - "iphone-5" : "320x568" - "iphone-4" : "320x480" - "iphone-3" : "320x480" - "samsung-s10" : "360x760" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const Promise = require("bluebird"); + +const $errUtils = require("../../cypress/error_utils"); + +const viewports = { + "macbook-15" : "1440x900", + "macbook-13" : "1280x800", + "macbook-11" : "1366x768", + "ipad-2" : "768x1024", + "ipad-mini" : "768x1024", + "iphone-xr" : "414x896", + "iphone-x" : "375x812", + "iphone-6+" : "414x736", + "iphone-6" : "375x667", + "iphone-5" : "320x568", + "iphone-4" : "320x480", + "iphone-3" : "320x480", + "samsung-s10" : "360x760", "samsung-note9" : "414x846" -} +}; -validOrientations = ["landscape", "portrait"] +const validOrientations = ["landscape", "portrait"]; -## NOTE: this is outside the function because its 'global' state to the -## cypress application and not local to the specific run. the last -## viewport set is always the 'current' viewport as opposed to the -## config. there was a bug where re-running tests without a hard -## refresh would cause viewport to hang -currentViewport = null +//# NOTE: this is outside the function because its 'global' state to the +//# cypress application and not local to the specific run. the last +//# viewport set is always the 'current' viewport as opposed to the +//# config. there was a bug where re-running tests without a hard +//# refresh would cause viewport to hang +let currentViewport = null; -module.exports = (Commands, Cypress, cy, state, config) -> - defaultViewport = _.pick(config(), "viewportWidth", "viewportHeight") +module.exports = function(Commands, Cypress, cy, state, config) { + const defaultViewport = _.pick(config(), "viewportWidth", "viewportHeight"); - ## currentViewport could already be set due to previous runs - currentViewport ?= defaultViewport + //# currentViewport could already be set due to previous runs + if (currentViewport == null) { currentViewport = defaultViewport; } - Cypress.on "test:before:run:async", -> - ## if we have viewportDefaults it means - ## something has changed the default and we - ## need to restore prior to running the next test - ## after which we simply null and wait for the - ## next viewport change - setViewportAndSynchronize(defaultViewport.viewportWidth, defaultViewport.viewportHeight) + Cypress.on("test:before:run:async", () => //# if we have viewportDefaults it means + //# something has changed the default and we + //# need to restore prior to running the next test + //# after which we simply null and wait for the + //# next viewport change + setViewportAndSynchronize(defaultViewport.viewportWidth, defaultViewport.viewportHeight)); - setViewportAndSynchronize = (width, height) -> - viewport = {viewportWidth: width, viewportHeight: height} + var setViewportAndSynchronize = function(width, height) { + const viewport = {viewportWidth: width, viewportHeight: height}; - ## store viewport on the state for logs - state(viewport) + //# store viewport on the state for logs + state(viewport); - new Promise (resolve) -> - if currentViewport.viewportWidth is width and currentViewport.viewportHeight is height - ## noop if viewport won't change - return resolve(currentViewport) + return new Promise(function(resolve) { + if ((currentViewport.viewportWidth === width) && (currentViewport.viewportHeight === height)) { + //# noop if viewport won't change + return resolve(currentViewport); + } currentViewport = { - viewportWidth: width + viewportWidth: width, viewportHeight: height - } + }; - ## force our UI to change to the viewport and wait for it - ## to be updated - Cypress.action "cy:viewport:changed", viewport, -> - resolve(viewport) + //# force our UI to change to the viewport and wait for it + //# to be updated + return Cypress.action("cy:viewport:changed", viewport, () => resolve(viewport)); + }); + }; - Commands.addAll({ - title: (options = {}) -> - userOptions = options - options = _.defaults({}, userOptions, {log: true}) + return Commands.addAll({ + title(options = {}) { + let resolveTitle; + const userOptions = options; + options = _.defaults({}, userOptions, {log: true}); - if options.log + if (options.log) { options._log = Cypress.log({ - }) + }); + } - do resolveTitle = => - doc = state("document") + return (resolveTitle = () => { + const doc = state("document"); - title = (doc and doc.title) or "" + const title = (doc && doc.title) || ""; - cy.verifyUpcomingAssertions(title, options, { + return cy.verifyUpcomingAssertions(title, options, { onRetry: resolveTitle - }) + }); + })(); + }, - window: (options = {}) -> - userOptions = options - options = _.defaults({}, userOptions, {log: true}) + window(options = {}) { + let verifyAssertions; + const userOptions = options; + options = _.defaults({}, userOptions, {log: true}); - if options.log + if (options.log) { options._log = Cypress.log({ - }) + }); + } - getWindow = => - window = state("window") - $errUtils.throwErrByPath("window.iframe_undefined", { onFail: options._log }) if not window + const getWindow = () => { + const window = state("window"); + if (!window) { $errUtils.throwErrByPath("window.iframe_undefined", { onFail: options._log }); } - return window + return window; + }; - ## wrap retrying into its own - ## separate function - retryWindow = => - Promise + //# wrap retrying into its own + //# separate function + var retryWindow = () => { + return Promise .try(getWindow) - .catch (err) => - options.error = err - cy.retry(retryWindow, options) - - do verifyAssertions = => - Promise.try(retryWindow).then (win) => - cy.verifyUpcomingAssertions(win, options, { + .catch(err => { + options.error = err; + return cy.retry(retryWindow, options); + }); + }; + + return (verifyAssertions = () => { + return Promise.try(retryWindow).then(win => { + return cy.verifyUpcomingAssertions(win, options, { onRetry: verifyAssertions - }) + }); + }); + })(); + }, - document: (options = {}) -> - userOptions = options - options = _.defaults({}, userOptions, {log: true}) + document(options = {}) { + let verifyAssertions; + const userOptions = options; + options = _.defaults({}, userOptions, {log: true}); - if options.log + if (options.log) { options._log = Cypress.log({ - }) + }); + } - getDocument = => - win = state("window") - ## TODO: add failing test around logging twice - $errUtils.throwErrByPath("window.iframe_doc_undefined") if not win?.document + const getDocument = () => { + const win = state("window"); + //# TODO: add failing test around logging twice + if (!(win != null ? win.document : undefined)) { $errUtils.throwErrByPath("window.iframe_doc_undefined"); } - return win.document + return win.document; + }; - ## wrap retrying into its own - ## separate function - retryDocument = => - Promise + //# wrap retrying into its own + //# separate function + var retryDocument = () => { + return Promise .try(getDocument) - .catch (err) => - options.error = err - cy.retry(retryDocument, options) - - do verifyAssertions = => - Promise.try(retryDocument).then (doc) => - cy.verifyUpcomingAssertions(doc, options, { + .catch(err => { + options.error = err; + return cy.retry(retryDocument, options); + }); + }; + + return (verifyAssertions = () => { + return Promise.try(retryDocument).then(doc => { + return cy.verifyUpcomingAssertions(doc, options, { onRetry: verifyAssertions - }) + }); + }); + })(); + }, - viewport: (presetOrWidth, heightOrOrientation, options = {}) -> - userOptions = options + viewport(presetOrWidth, heightOrOrientation, options = {}) { + let height, width; + const userOptions = options; - if _.isObject(heightOrOrientation) - options = heightOrOrientation + if (_.isObject(heightOrOrientation)) { + options = heightOrOrientation; + } - options = _.defaults({}, userOptions, { log: true }) + options = _.defaults({}, userOptions, { log: true }); - if options.log + if (options.log) { options._log = Cypress.log({ - consoleProps: -> - obj = {} - obj.Preset = preset if preset - obj.Width = width - obj.Height = height - obj - }) - - throwErrBadArgs = => - $errUtils.throwErrByPath "viewport.bad_args", { onFail: options._log } - - widthAndHeightAreValidNumbers = (width, height) -> - _.every [width, height], (val) -> - _.isNumber(val) and _.isFinite(val) - - widthAndHeightAreWithinBounds = (width, height) -> - _.every [width, height], (val) -> - val >= 0 - - switch - when _.isString(presetOrWidth) and _.isBlank(presetOrWidth) - $errUtils.throwErrByPath "viewport.empty_string", { onFail: options._log } - - when _.isString(presetOrWidth) - getPresetDimensions = (preset) => - try - _.map(viewports[presetOrWidth].split("x"), Number) - catch e - presets = _.keys(viewports).join(", ") - $errUtils.throwErrByPath "viewport.missing_preset", { - onFail: options._log - args: { preset, presets } - } + consoleProps() { + const obj = {}; + if (preset) { obj.Preset = preset; } + obj.Width = width; + obj.Height = height; + return obj; + } + }); + } + + const throwErrBadArgs = () => { + return $errUtils.throwErrByPath("viewport.bad_args", { onFail: options._log }); + }; + + const widthAndHeightAreValidNumbers = (width, height) => _.every([width, height], val => _.isNumber(val) && _.isFinite(val)); - orientationIsValidAndLandscape = (orientation) => - if orientation not in validOrientations - all = validOrientations.join("` or `") - $errUtils.throwErrByPath "viewport.invalid_orientation", { - onFail: options._log + const widthAndHeightAreWithinBounds = (width, height) => _.every([width, height], val => val >= 0); + + switch (false) { + case !_.isString(presetOrWidth) || !_.isBlank(presetOrWidth): + $errUtils.throwErrByPath("viewport.empty_string", { onFail: options._log }); + break; + + case !_.isString(presetOrWidth): + var getPresetDimensions = preset => { + try { + return _.map(viewports[presetOrWidth].split("x"), Number); + } catch (e) { + const presets = _.keys(viewports).join(", "); + return $errUtils.throwErrByPath("viewport.missing_preset", { + onFail: options._log, + args: { preset, presets } + }); + } + }; + + var orientationIsValidAndLandscape = orientation => { + if (!validOrientations.includes(orientation)) { + const all = validOrientations.join("` or `"); + $errUtils.throwErrByPath("viewport.invalid_orientation", { + onFail: options._log, args: { all, orientation } - } + }); + } - orientation is "landscape" + return orientation === "landscape"; + }; - preset = presetOrWidth - orientation = heightOrOrientation + var preset = presetOrWidth; + var orientation = heightOrOrientation; - ## get preset, split by x, convert to a number - dimensions = getPresetDimensions(preset) + //# get preset, split by x, convert to a number + var dimensions = getPresetDimensions(preset); - if _.isString(orientation) - if orientationIsValidAndLandscape(orientation) - dimensions.reverse() + if (_.isString(orientation)) { + if (orientationIsValidAndLandscape(orientation)) { + dimensions.reverse(); + } + } - [width, height] = dimensions + [width, height] = dimensions; + break; - when widthAndHeightAreValidNumbers(presetOrWidth, heightOrOrientation) - width = presetOrWidth - height = heightOrOrientation + case !widthAndHeightAreValidNumbers(presetOrWidth, heightOrOrientation): + width = presetOrWidth; + height = heightOrOrientation; - if not widthAndHeightAreWithinBounds(width, height) - $errUtils.throwErrByPath "viewport.dimensions_out_of_range", { onFail: options._log } + if (!widthAndHeightAreWithinBounds(width, height)) { + $errUtils.throwErrByPath("viewport.dimensions_out_of_range", { onFail: options._log }); + } + break; - else - throwErrBadArgs() + default: + throwErrBadArgs(); + } - setViewportAndSynchronize(width, height) - .then (viewport) -> - if options._log - options._log.set(viewport) + return setViewportAndSynchronize(width, height) + .then(function(viewport) { + if (options._log) { + options._log.set(viewport); + } - return null + return null; + }); + } - }) + }); +}; diff --git a/packages/driver/src/cy/commands/xhr.js b/packages/driver/src/cy/commands/xhr.js index 14e3be667593..81f1ebf819ac 100644 --- a/packages/driver/src/cy/commands/xhr.js +++ b/packages/driver/src/cy/commands/xhr.js @@ -1,454 +1,544 @@ -_ = require("lodash") -Promise = require("bluebird") - -$utils = require("../../cypress/utils") -$errUtils = require("../../cypress/error_utils") -$Server = require("../../cypress/server") -$Location = require("../../cypress/location") - -server = null - -tryDecodeUri = (uri) -> - try - return decodeURI(uri) - catch - return uri +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const Promise = require("bluebird"); + +const $utils = require("../../cypress/utils"); +const $errUtils = require("../../cypress/error_utils"); +const $Server = require("../../cypress/server"); +const $Location = require("../../cypress/location"); + +let server = null; + +const tryDecodeUri = function(uri) { + try { + return decodeURI(uri); + } catch (error) { + return uri; + } +}; -getServer = -> - server ? unavailableErr() +const getServer = () => server != null ? server : unavailableErr(); -cancelPendingXhrs = -> - if server - server.cancelPendingXhrs() +const cancelPendingXhrs = function() { + if (server) { + server.cancelPendingXhrs(); + } - return null + return null; +}; -reset = -> - if server - server.restore() +const reset = function() { + if (server) { + server.restore(); + } - server = null + return server = null; +}; -isUrlLikeArgs = (url, response) -> - (not _.isObject(url) and not _.isObject(response)) or - (_.isRegExp(url) or _.isString(url)) +const isUrlLikeArgs = (url, response) => (!_.isObject(url) && !_.isObject(response)) || + (_.isRegExp(url) || _.isString(url)); -getUrl = (options) -> - options.originalUrl or options.url +const getUrl = options => options.originalUrl || options.url; -unavailableErr = -> - $errUtils.throwErrByPath("server.unavailable") +var unavailableErr = () => $errUtils.throwErrByPath("server.unavailable"); -getDisplayName = (route) -> - if route and route.response? then "xhr stub" else "xhr" +const getDisplayName = function(route) { + if (route && (route.response != null)) { return "xhr stub"; } else { return "xhr"; } +}; -stripOrigin = (url) -> - location = $Location.create(url) - url.replace(location.origin, "") +const stripOrigin = function(url) { + const location = $Location.create(url); + return url.replace(location.origin, ""); +}; -getXhrServer = (state) -> - state("server") ? unavailableErr() +const getXhrServer = function(state) { + let left; + return (left = state("server")) != null ? left : unavailableErr(); +}; -setRequest = (state, xhr, alias) -> - requests = state("requests") ? [] +const setRequest = function(state, xhr, alias) { + let left; + const requests = (left = state("requests")) != null ? left : []; requests.push({ - xhr: xhr - alias: alias - }) + xhr, + alias + }); - state("requests", requests) + return state("requests", requests); +}; -setResponse = (state, xhr) -> - obj = _.find(state("requests"), { xhr }) +const setResponse = function(state, xhr) { + let left; + const obj = _.find(state("requests"), { xhr }); - ## if we've been reset between tests and an xhr - ## leaked through, then we may not be able to associate - ## this response correctly - return if not obj + //# if we've been reset between tests and an xhr + //# leaked through, then we may not be able to associate + //# this response correctly + if (!obj) { return; } - index = state("requests").indexOf(obj) + const index = state("requests").indexOf(obj); - responses = state("responses") ? [] + const responses = (left = state("responses")) != null ? left : []; - ## set the response in the same index as the request - ## so we can later wait on this specific index'd response - ## else its not deterministic + //# set the response in the same index as the request + //# so we can later wait on this specific index'd response + //# else its not deterministic responses[index] = { - xhr: xhr - alias: obj?.alias - } + xhr, + alias: (obj != null ? obj.alias : undefined) + }; - state("responses", responses) + return state("responses", responses); +}; -startXhrServer = (cy, state, config) -> - logs = {} +const startXhrServer = function(cy, state, config) { + const logs = {}; server = $Server.create({ - xhrUrl: config("xhrUrl") - stripOrigin: stripOrigin - - ## shouldnt these stubs be called routes? - ## rename everything related to stubs => routes - onSend: (xhr, stack, route) => - alias = route?.alias - - setRequest(state, xhr, alias) - - if rl = route and route.log - numResponses = rl.get("numResponses") - rl.set "numResponses", numResponses + 1 - - logs[xhr.id] = log = Cypress.log({ - message: "" - name: "xhr" - displayName: getDisplayName(route) - alias: alias - aliasType: "route" - type: "parent" - event: true - consoleProps: => - consoleObj = { - Alias: alias - Method: xhr.method - URL: xhr.url - "Matched URL": route?.url - Status: xhr.statusMessage - Duration: xhr.duration - "Stubbed": if route and route.response? then "Yes" else "No" - Request: xhr.request - Response: xhr.response + xhrUrl: config("xhrUrl"), + stripOrigin, + + //# shouldnt these stubs be called routes? + //# rename everything related to stubs => routes + onSend: (xhr, stack, route) => { + let log, rl; + const alias = route != null ? route.alias : undefined; + + setRequest(state, xhr, alias); + + if (rl = route && route.log) { + const numResponses = rl.get("numResponses"); + rl.set("numResponses", numResponses + 1); + } + + logs[xhr.id] = (log = Cypress.log({ + message: "", + name: "xhr", + displayName: getDisplayName(route), + alias, + aliasType: "route", + type: "parent", + event: true, + consoleProps: () => { + const consoleObj = { + Alias: alias, + Method: xhr.method, + URL: xhr.url, + "Matched URL": (route != null ? route.url : undefined), + Status: xhr.statusMessage, + Duration: xhr.duration, + "Stubbed": route && (route.response != null) ? "Yes" : "No", + Request: xhr.request, + Response: xhr.response, XHR: xhr._getXhr() + }; + + if (route && route.is404) { + consoleObj.Note = "This request did not match any of your routes. It was automatically sent back '404'. Setting cy.server({force404: false}) will turn off this behavior."; } - if route and route.is404 - consoleObj.Note = "This request did not match any of your routes. It was automatically sent back '404'. Setting cy.server({force404: false}) will turn off this behavior." - - consoleObj.groups = -> - [ - { - name: "Initiator" - items: [stack] - label: false - } - ] - - consoleObj - renderProps: -> - status = switch - when xhr.aborted - indicator = "aborted" - "(aborted)" - when xhr.canceled - indicator = "aborted" - "(canceled)" - when xhr.status > 0 - xhr.status - else - indicator = "pending" - "---" - - indicator ?= if /^2/.test(status) then "successful" else "bad" + consoleObj.groups = () => [ + { + name: "Initiator", + items: [stack], + label: false + } + ]; + + return consoleObj; + }, + renderProps() { + let indicator; + const status = (() => { switch (false) { + case !xhr.aborted: + indicator = "aborted"; + return "(aborted)"; + case !xhr.canceled: + indicator = "aborted"; + return "(canceled)"; + case xhr.status <= 0: + return xhr.status; + default: + indicator = "pending"; + return "---"; + } })(); + + if (indicator == null) { indicator = /^2/.test(status) ? "successful" : "bad"; } return { indicator, - message: "#{xhr.method} #{status} #{stripOrigin(xhr.url)}" - } - }) - - log.snapshot("request") - - onLoad: (xhr) => - setResponse(state, xhr) - - if log = logs[xhr.id] - log.snapshot("response").end() - - onNetworkError: (xhr) -> - err = $errUtils.cypressErrByPath("xhr.network_error") - - if log = logs[xhr.id] - log.snapshot("failed").error(err) - - onFixtureError: (xhr, err) -> - err = $errUtils.cypressErr(err) - - @onError(xhr, err) - - onError: (xhr, err) -> - err.onFail = -> - - if log = logs[xhr.id] - log.snapshot("error").error(err) - - ## re-throw the error since this came from AUT code, and needs to - ## cause an 'uncaught:exception' event. This error will be caught in - ## top.onerror with stack as 5th argument. - throw err - - onXhrAbort: (xhr, stack) => - setResponse(state, xhr) - - err = new Error $errUtils.errMsgByPath("xhr.aborted") - err.name = "AbortError" - err.stack = stack - - if log = logs[xhr.id] - log.snapshot("aborted").error(err) - - onXhrCancel: (xhr) -> - setResponse(state, xhr) - - if log = logs[xhr.id] - log.snapshot("canceled").set({ + message: `${xhr.method} ${status} ${stripOrigin(xhr.url)}` + }; + } + })); + + return log.snapshot("request"); + }, + + onLoad: xhr => { + let log; + setResponse(state, xhr); + + if (log = logs[xhr.id]) { + return log.snapshot("response").end(); + } + }, + + onNetworkError(xhr) { + let log; + const err = $errUtils.cypressErrByPath("xhr.network_error"); + + if (log = logs[xhr.id]) { + return log.snapshot("failed").error(err); + } + }, + + onFixtureError(xhr, err) { + err = $errUtils.cypressErr(err); + + return this.onError(xhr, err); + }, + + onError(xhr, err) { + let log; + err.onFail = function() {}; + + if (log = logs[xhr.id]) { + log.snapshot("error").error(err); + } + + //# re-throw the error since this came from AUT code, and needs to + //# cause an 'uncaught:exception' event. This error will be caught in + //# top.onerror with stack as 5th argument. + throw err; + }, + + onXhrAbort: (xhr, stack) => { + let log; + setResponse(state, xhr); + + const err = new Error($errUtils.errMsgByPath("xhr.aborted")); + err.name = "AbortError"; + err.stack = stack; + + if (log = logs[xhr.id]) { + return log.snapshot("aborted").error(err); + } + }, + + onXhrCancel(xhr) { + let log; + setResponse(state, xhr); + + if (log = logs[xhr.id]) { + return log.snapshot("canceled").set({ ended: true, state: "failed" - }) - - onAnyAbort: (route, xhr) => - if route and _.isFunction(route.onAbort) - route.onAbort.call(cy, xhr) - - onAnyRequest: (route, xhr) => - if route and _.isFunction(route.onRequest) - route.onRequest.call(cy, xhr) - - onAnyResponse: (route, xhr) => - if route and _.isFunction(route.onResponse) - route.onResponse.call(cy, xhr) - }) - - win = state("window") - - server.bindTo(win) - - state("server", server) - - return server - -defaults = { - method: undefined - status: undefined - delay: undefined - headers: undefined ## response headers - response: undefined - autoRespond: undefined - waitOnResponses: undefined - onAbort: undefined - onRequest: undefined ## need to rebind these to 'cy' context + }); + } + }, + + onAnyAbort: (route, xhr) => { + if (route && _.isFunction(route.onAbort)) { + return route.onAbort.call(cy, xhr); + } + }, + + onAnyRequest: (route, xhr) => { + if (route && _.isFunction(route.onRequest)) { + return route.onRequest.call(cy, xhr); + } + }, + + onAnyResponse: (route, xhr) => { + if (route && _.isFunction(route.onResponse)) { + return route.onResponse.call(cy, xhr); + } + } + }); + + const win = state("window"); + + server.bindTo(win); + + state("server", server); + + return server; +}; + +const defaults = { + method: undefined, + status: undefined, + delay: undefined, + headers: undefined, //# response headers + response: undefined, + autoRespond: undefined, + waitOnResponses: undefined, + onAbort: undefined, + onRequest: undefined, //# need to rebind these to 'cy' context onResponse: undefined -} - -module.exports = (Commands, Cypress, cy, state, config) -> - reset() - - ## if our page is going away due to - ## a form submit / anchor click then - ## we need to cancel all pending - ## XHR's so the command log displays - ## correctly - Cypress.on("window:unload", cancelPendingXhrs) - - Cypress.on "test:before:run", -> - ## reset the existing server - reset() - - ## create the server before each test run - ## its possible for this to fail if the - ## last test we ran ended with an invalid - ## window such as if the last test ended - ## with a cross origin window - try - server = startXhrServer(cy, state, config) - catch err - ## in this case, just don't bind to the server - server = null - - return null - - Cypress.on "window:before:load", (contentWindow) -> - if server - ## dynamically bind the server to whatever is currently running - server.bindTo(contentWindow) - else - ## if we don't have a server such as the case when - ## the last window was cross origin, try to bind - ## to it now - server = startXhrServer(cy, state, config) - - Commands.addAll({ - server: (options) -> - userOptions = options - - if arguments.length is 0 - userOptions = {} - - if not _.isObject(userOptions) - $errUtils.throwErrByPath("server.invalid_argument") +}; + +module.exports = function(Commands, Cypress, cy, state, config) { + reset(); + + //# if our page is going away due to + //# a form submit / anchor click then + //# we need to cancel all pending + //# XHR's so the command log displays + //# correctly + Cypress.on("window:unload", cancelPendingXhrs); + + Cypress.on("test:before:run", function() { + //# reset the existing server + reset(); + + //# create the server before each test run + //# its possible for this to fail if the + //# last test we ran ended with an invalid + //# window such as if the last test ended + //# with a cross origin window + try { + server = startXhrServer(cy, state, config); + } catch (err) { + //# in this case, just don't bind to the server + server = null; + } + + return null; + }); + + Cypress.on("window:before:load", function(contentWindow) { + if (server) { + //# dynamically bind the server to whatever is currently running + return server.bindTo(contentWindow); + } else { + //# if we don't have a server such as the case when + //# the last window was cross origin, try to bind + //# to it now + return server = startXhrServer(cy, state, config); + } + }); + + return Commands.addAll({ + server(options) { + let userOptions = options; + + if (arguments.length === 0) { + userOptions = {}; + } + + if (!_.isObject(userOptions)) { + $errUtils.throwErrByPath("server.invalid_argument"); + } options = _.defaults({}, userOptions, { - enable: true ## set enable to false to turn off stubbing - }) - - ## if we disable the server later make sure - ## we cannot add cy.routes to it - state("serverIsStubbed", options.enable) - - getXhrServer(state).set(options) - - route: (args...) -> - ## TODO: - ## if we return a function which returns a promise - ## then we should be handling potential timeout issues - ## just like cy.then does - - ## method / url / response / options - ## url / response / options - ## options - - ## by default assume we have a specified - ## response from the user - hasResponse = true - - if not state("serverIsStubbed") - $errUtils.throwErrByPath("route.failed_prerequisites") - - ## get the default options currently set - ## on our server - options = o = getXhrServer(state).getOptions() - - ## enable the entire routing definition to be a function - parseArgs = (args...) -> - switch - when _.isObject(args[0]) and not _.isRegExp(args[0]) - ## we dont have a specified response - if not _.has(args[0], "response") - hasResponse = false - - options = o = _.extend {}, options, args[0] - - when args.length is 0 - $errUtils.throwErrByPath "route.invalid_arguments" - - when args.length is 1 - o.url = args[0] - - hasResponse = false - - when args.length is 2 - ## if our url actually matches an http method - ## then we know the user doesn't want to stub this route - if _.isString(args[0]) and $utils.isValidHttpMethod(args[0]) - o.method = args[0] - o.url = args[1] - - hasResponse = false - else - o.url = args[0] - o.response = args[1] - - when args.length is 3 - if $utils.isValidHttpMethod(args[0]) or isUrlLikeArgs(args[1], args[2]) - o.method = args[0] - o.url = args[1] - o.response = args[2] - else - o.url = args[0] - o.response = args[1] - - _.extend o, args[2] - - when args.length is 4 - o.method = args[0] - o.url = args[1] - o.response = args[2] - - _.extend o, args[3] - - if _.isString(o.method) - o.method = o.method.toUpperCase() - - _.defaults(options, defaults) - - if not options.url - $errUtils.throwErrByPath "route.url_missing" - - if not (_.isString(options.url) or _.isRegExp(options.url)) - $errUtils.throwErrByPath "route.url_invalid" - - if not $utils.isValidHttpMethod(options.method) - $errUtils.throwErrByPath "route.method_invalid", { + enable: true //# set enable to false to turn off stubbing + }); + + //# if we disable the server later make sure + //# we cannot add cy.routes to it + state("serverIsStubbed", options.enable); + + return getXhrServer(state).set(options); + }, + + route(...args) { + //# TODO: + //# if we return a function which returns a promise + //# then we should be handling potential timeout issues + //# just like cy.then does + + //# method / url / response / options + //# url / response / options + //# options + + //# by default assume we have a specified + //# response from the user + let o; + let hasResponse = true; + + if (!state("serverIsStubbed")) { + $errUtils.throwErrByPath("route.failed_prerequisites"); + } + + //# get the default options currently set + //# on our server + let options = (o = getXhrServer(state).getOptions()); + + //# enable the entire routing definition to be a function + const parseArgs = function(...args) { + let alias; + switch (false) { + case !_.isObject(args[0]) || !!_.isRegExp(args[0]): + //# we dont have a specified response + if (!_.has(args[0], "response")) { + hasResponse = false; + } + + options = (o = _.extend({}, options, args[0])); + break; + + case args.length !== 0: + $errUtils.throwErrByPath("route.invalid_arguments"); + break; + + case args.length !== 1: + o.url = args[0]; + + hasResponse = false; + break; + + case args.length !== 2: + //# if our url actually matches an http method + //# then we know the user doesn't want to stub this route + if (_.isString(args[0]) && $utils.isValidHttpMethod(args[0])) { + o.method = args[0]; + o.url = args[1]; + + hasResponse = false; + } else { + o.url = args[0]; + o.response = args[1]; + } + break; + + case args.length !== 3: + if ($utils.isValidHttpMethod(args[0]) || isUrlLikeArgs(args[1], args[2])) { + o.method = args[0]; + o.url = args[1]; + o.response = args[2]; + } else { + o.url = args[0]; + o.response = args[1]; + + _.extend(o, args[2]); + } + break; + + case args.length !== 4: + o.method = args[0]; + o.url = args[1]; + o.response = args[2]; + + _.extend(o, args[3]); + break; + } + + if (_.isString(o.method)) { + o.method = o.method.toUpperCase(); + } + + _.defaults(options, defaults); + + if (!options.url) { + $errUtils.throwErrByPath("route.url_missing"); + } + + if (!(_.isString(options.url) || _.isRegExp(options.url))) { + $errUtils.throwErrByPath("route.url_invalid"); + } + + if (!$utils.isValidHttpMethod(options.method)) { + $errUtils.throwErrByPath("route.method_invalid", { args: { method: o.method } - } - - if hasResponse and not options.response? - $errUtils.throwErrByPath "route.response_invalid" - - ## convert to wildcard regex - if options.url is "*" - options.originalUrl = "*" - options.url = /.*/ - - ## look ahead to see if this - ## command (route) has an alias? - if alias = cy.getNextAlias() - options.alias = alias - - if _.isFunction(o.response) - getResponse = => - o.response.call(state("runnable").ctx, options) - - ## allow route to return a promise - Promise.try(getResponse) - .then (resp) -> - options.response = resp - - route() - else - route() - - route = -> - ## if our response is a string and - ## a reference to an alias - if _.isString(o.response) and aliasObj = cy.getAlias(o.response, "route") - ## reset the route's response to be the - ## aliases subject - options.response = aliasObj.subject - - url = getUrl(options) - - urlString = url.toString() - - ## https://github.com/cypress-io/cypress/issues/2372 - if (decodedUrl = tryDecodeUri(urlString)) and urlString != decodedUrl - $errUtils.warnByPath("route.url_percentencoding_warning", { args: { decodedUrl }}) + }); + } + + if (hasResponse && (options.response == null)) { + $errUtils.throwErrByPath("route.response_invalid"); + } + + //# convert to wildcard regex + if (options.url === "*") { + options.originalUrl = "*"; + options.url = /.*/; + } + + //# look ahead to see if this + //# command (route) has an alias? + if (alias = cy.getNextAlias()) { + options.alias = alias; + } + + if (_.isFunction(o.response)) { + const getResponse = () => { + return o.response.call(state("runnable").ctx, options); + }; + + //# allow route to return a promise + return Promise.try(getResponse) + .then(function(resp) { + options.response = resp; + + return route(); + }); + } else { + return route(); + } + }; + + var route = function() { + //# if our response is a string and + //# a reference to an alias + let aliasObj, decodedUrl; + if (_.isString(o.response) && (aliasObj = cy.getAlias(o.response, "route"))) { + //# reset the route's response to be the + //# aliases subject + options.response = aliasObj.subject; + } + + const url = getUrl(options); + + const urlString = url.toString(); + + //# https://github.com/cypress-io/cypress/issues/2372 + if ((decodedUrl = tryDecodeUri(urlString)) && (urlString !== decodedUrl)) { + $errUtils.warnByPath("route.url_percentencoding_warning", { args: { decodedUrl }}); + } options.log = Cypress.log({ - name: "route" - instrument: "route" - method: options.method - url: getUrl(options) - status: options.status - response: options.response - alias: options.alias - isStubbed: options.response? - numResponses: 0 - consoleProps: -> - Method: options.method - URL: url - Status: options.status - Response: options.response - Alias: options.alias - }) - - return getXhrServer(state).route(options) - - if _.isFunction(args[0]) - getArgs = => - args[0].call(state("runnable").ctx) - - Promise.try(getArgs) - .then(parseArgs) - else - parseArgs(args...) - }) + name: "route", + instrument: "route", + method: options.method, + url: getUrl(options), + status: options.status, + response: options.response, + alias: options.alias, + isStubbed: (options.response != null), + numResponses: 0, + consoleProps() { + return { + Method: options.method, + URL: url, + Status: options.status, + Response: options.response, + Alias: options.alias + }; + } + }); + + return getXhrServer(state).route(options); + }; + + if (_.isFunction(args[0])) { + const getArgs = () => { + return args[0].call(state("runnable").ctx); + }; + + return Promise.try(getArgs) + .then(parseArgs); + } else { + return parseArgs(...args); + } + } + }); +}; From 145bea07cd7cd649c4b0137d2e70088bc87e4907 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Mon, 27 Apr 2020 15:25:58 +0900 Subject: [PATCH 3/3] decaffeinate: Run post-processing cleanups on location.coffee and 10 other files --- packages/driver/src/cy/commands/location.js | 201 ++- packages/driver/src/cy/commands/misc.js | 81 +- packages/driver/src/cy/commands/navigation.js | 1389 +++++++++-------- packages/driver/src/cy/commands/popups.js | 71 +- packages/driver/src/cy/commands/request.js | 545 ++++--- packages/driver/src/cy/commands/screenshot.js | 566 +++---- packages/driver/src/cy/commands/task.js | 156 +- packages/driver/src/cy/commands/traversals.js | 165 +- packages/driver/src/cy/commands/waiting.js | 375 ++--- packages/driver/src/cy/commands/window.js | 425 ++--- packages/driver/src/cy/commands/xhr.js | 715 +++++---- 11 files changed, 2432 insertions(+), 2257 deletions(-) diff --git a/packages/driver/src/cy/commands/location.js b/packages/driver/src/cy/commands/location.js index bdd9e2a1dc85..da792b2ab0b7 100644 --- a/packages/driver/src/cy/commands/location.js +++ b/packages/driver/src/cy/commands/location.js @@ -1,101 +1,100 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); -const Promise = require("bluebird"); - -const $errUtils = require("../../cypress/error_utils"); -const $Location = require("../../cypress/location"); - -module.exports = (Commands, Cypress, cy, state, config) => Commands.addAll({ - url(options = {}) { - let resolveHref; - const userOptions = options; - options = _.defaults({}, userOptions, { log: true }); - - if (options.log !== false) { - options._log = Cypress.log({ - message: "" - }); - } - - const getHref = () => { - return cy.getRemoteLocation("href"); - }; - - return (resolveHref = () => { - return Promise.try(getHref).then(href => { - return cy.verifyUpcomingAssertions(href, options, { - onRetry: resolveHref - }); - }); - })(); - }, - - hash(options = {}) { - let resolveHash; - const userOptions = options; - options = _.defaults({}, userOptions, { log: true }); - - if (options.log !== false) { - options._log = Cypress.log({ - message: "" - }); - } - - const getHash = () => { - return cy.getRemoteLocation("hash"); - }; - - return (resolveHash = () => { - return Promise.try(getHash).then(hash => { - return cy.verifyUpcomingAssertions(hash, options, { - onRetry: resolveHash - }); - }); - })(); - }, - - location(key, options) { - let resolveLocation; - let userOptions = options; - //# normalize arguments allowing key + options to be undefined - //# key can represent the options - if (_.isObject(key) && _.isUndefined(userOptions)) { - userOptions = key; - } - - if (userOptions == null) { userOptions = {}; } - - options = _.defaults({}, userOptions, { log: true }); - - const getLocation = () => { - let ret; - const location = cy.getRemoteLocation(); - - return ret = _.isString(key) ? - //# use existential here because we only want to throw - //# on null or undefined values (and not empty strings) - location[key] != null ? location[key] : $errUtils.throwErrByPath("location.invalid_key", { args: { key } }) - : - location; - }; - - if (options.log !== false) { - options._log = Cypress.log({ - message: key != null ? key : "" - }); - } - - return (resolveLocation = () => { - return Promise.try(getLocation).then(ret => { - return cy.verifyUpcomingAssertions(ret, options, { - onRetry: resolveLocation - }); - }); - })(); - } -}); +const _ = require('lodash') +const Promise = require('bluebird') + +const $errUtils = require('../../cypress/error_utils') + +module.exports = (Commands, Cypress, cy) => { + Commands.addAll({ + url (options = {}) { + const userOptions = options + + options = _.defaults({}, userOptions, { log: true }) + + if (options.log !== false) { + options._log = Cypress.log({ + message: '', + }) + } + + const getHref = () => { + return cy.getRemoteLocation('href') + } + + const resolveHref = () => { + return Promise.try(getHref).then((href) => { + return cy.verifyUpcomingAssertions(href, options, { + onRetry: resolveHref, + }) + }) + } + + return resolveHref() + }, + + hash (options = {}) { + const userOptions = options + + options = _.defaults({}, userOptions, { log: true }) + + if (options.log !== false) { + options._log = Cypress.log({ + message: '', + }) + } + + const getHash = () => { + return cy.getRemoteLocation('hash') + } + + const resolveHash = () => { + return Promise.try(getHash).then((hash) => { + return cy.verifyUpcomingAssertions(hash, options, { + onRetry: resolveHash, + }) + }) + } + + return resolveHash() + }, + + location (key, options) { + let userOptions = options + + // normalize arguments allowing key + options to be undefined + // key can represent the options + if (_.isObject(key) && _.isUndefined(userOptions)) { + userOptions = key + } + + userOptions = userOptions || {} + + options = _.defaults({}, userOptions, { log: true }) + + const getLocation = () => { + const location = cy.getRemoteLocation() + + return _.isString(key) + // use existential here because we only want to throw + // on null or undefined values (and not empty strings) + ? location[key] ?? $errUtils.throwErrByPath('location.invalid_key', { args: { key } }) + : location + } + + if (options.log !== false) { + options._log = Cypress.log({ + message: key != null ? key : '', + }) + } + + const resolveLocation = () => { + return Promise.try(getLocation).then((ret) => { + return cy.verifyUpcomingAssertions(ret, options, { + onRetry: resolveLocation, + }) + }) + } + + return resolveLocation() + }, + }) +} diff --git a/packages/driver/src/cy/commands/misc.js b/packages/driver/src/cy/commands/misc.js index 891d0d33c140..0d08708c5a43 100644 --- a/packages/driver/src/cy/commands/misc.js +++ b/packages/driver/src/cy/commands/misc.js @@ -1,57 +1,58 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); - -const $dom = require("../../dom"); - -module.exports = function(Commands, Cypress, cy, state, config) { - Commands.addAll({ prevSubject: "optional" }, { - end() { - return null; - } - }); - - return Commands.addAll({ - noop(arg) { return arg; }, - - log(msg, args) { +const _ = require('lodash') + +const $dom = require('../../dom') + +module.exports = (Commands, Cypress, cy) => { + Commands.addAll({ prevSubject: 'optional' }, { + end () { + return null + }, + }) + + Commands.addAll({ + noop (arg) { + return arg + }, + + log (msg, args) { Cypress.log({ end: true, snapshot: true, message: [msg, args], - consoleProps() { + consoleProps () { return { message: msg, - args - }; - } - }); + args, + } + }, + }) - return null; + return null }, - wrap(arg, options = {}) { - let resolveWrap; - const userOptions = options; - options = _.defaults({}, userOptions, { log: true }); + wrap (arg, options = {}) { + const userOptions = options + + options = _.defaults({}, userOptions, { log: true }) if (options.log !== false) { options._log = Cypress.log({ - message: arg - }); + message: arg, + }) if ($dom.isElement(arg)) { - options._log.set({$el: arg}); + options._log.set({ $el: arg }) } } - return (resolveWrap = () => cy.verifyUpcomingAssertions(arg, options, { - onRetry: resolveWrap - }) - .return(arg))(); - } - }); -}; + const resolveWrap = () => { + return cy.verifyUpcomingAssertions(arg, options, { + onRetry: resolveWrap, + }) + .return(arg) + } + + return resolveWrap() + }, + }) +} diff --git a/packages/driver/src/cy/commands/navigation.js b/packages/driver/src/cy/commands/navigation.js index 0fa329cb982c..80b53a4eebd7 100644 --- a/packages/driver/src/cy/commands/navigation.js +++ b/packages/driver/src/cy/commands/navigation.js @@ -1,92 +1,89 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS103: Rewrite code to no longer use __guard__ - * DS104: Avoid inline assignments - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); -const whatIsCircular = require("@cypress/what-is-circular"); -const moment = require("moment"); -const UrlParse = require("url-parse"); -const Promise = require("bluebird"); - -const $utils = require("../../cypress/utils"); -const $errUtils = require("../../cypress/error_utils"); -const $Log = require("../../cypress/log"); -const $Location = require("../../cypress/location"); - -const debug = require('debug')('cypress:driver:navigation'); - -let id = null; -let previousDomainVisited = null; -let hasVisitedAboutBlank = null; -let currentlyVisitingAboutBlank = null; -let knownCommandCausedInstability = null; - -const REQUEST_URL_OPTS = "auth failOnStatusCode retryOnNetworkFailure retryOnStatusCodeFailure method body headers" -.split(" "); - -const VISIT_OPTS = "url log onBeforeLoad onLoad timeout requestTimeout" -.split(" ") -.concat(REQUEST_URL_OPTS); - -const reset = function(test = {}) { - knownCommandCausedInstability = false; - - //# continuously reset this - //# before each test run! - previousDomainVisited = false; - - //# make sure we reset that we haven't - //# visited about blank again - hasVisitedAboutBlank = false; - - currentlyVisitingAboutBlank = false; - - return id = test.id; -}; - -const VALID_VISIT_METHODS = ['GET', 'POST']; - -const isValidVisitMethod = method => _.includes(VALID_VISIT_METHODS, method); - -const timedOutWaitingForPageLoad = function(ms, log) { - debug('timedOutWaitingForPageLoad'); - return $errUtils.throwErrByPath("navigation.timed_out", { - args: { - configFile: Cypress.config("configFile"), - ms - }, - onFail: log - }); -}; +/* global cy, Cypress */ +const _ = require('lodash') +const whatIsCircular = require('@cypress/what-is-circular') +const UrlParse = require('url-parse') +const Promise = require('bluebird') + +const $utils = require('../../cypress/utils') +const $errUtils = require('../../cypress/error_utils') +const $Log = require('../../cypress/log') +const $Location = require('../../cypress/location') + +const debug = require('debug')('cypress:driver:navigation') + +let id = null +let previousDomainVisited = null +let hasVisitedAboutBlank = null +let currentlyVisitingAboutBlank = null +let knownCommandCausedInstability = null + +const REQUEST_URL_OPTS = 'auth failOnStatusCode retryOnNetworkFailure retryOnStatusCodeFailure method body headers' +.split(' ') + +const VISIT_OPTS = 'url log onBeforeLoad onLoad timeout requestTimeout' +.split(' ') +.concat(REQUEST_URL_OPTS) + +const reset = (test = {}) => { + knownCommandCausedInstability = false -const bothUrlsMatchAndRemoteHasHash = (current, remote) => //# the remote has a hash -//# or the last char of href -//# is a hash -//# both must have the same query params -(remote.hash || (remote.href.slice(-1) === "#")) && + // continuously reset this + // before each test run! + previousDomainVisited = false - //# both must have the same origin - (current.origin === remote.origin) && + // make sure we reset that we haven't + // visited about blank again + hasVisitedAboutBlank = false - //# both must have the same pathname - (current.pathname === remote.pathname) && current.search === remote.search; + currentlyVisitingAboutBlank = false -const cannotVisitDifferentOrigin = function(origin, previousUrlVisited, remoteUrl, existingUrl, log) { - const differences = []; + id = test.id +} + +const VALID_VISIT_METHODS = ['GET', 'POST'] + +const isValidVisitMethod = (method) => { + return _.includes(VALID_VISIT_METHODS, method) +} + +const timedOutWaitingForPageLoad = (ms, log) => { + debug('timedOutWaitingForPageLoad') + + $errUtils.throwErrByPath('navigation.timed_out', { + args: { + configFile: Cypress.config('configFile'), + ms, + }, + onFail: log, + }) +} + +const bothUrlsMatchAndRemoteHasHash = (current, remote) => { + // the remote has a hash + // or the last char of href + // is a hash + return (remote.hash || remote.href.slice(-1) === '#') && + // both must have the same origin + current.origin === remote.origin && + // both must have the same pathname + current.pathname === remote.pathname && + // both must have the same query params + current.search === remote.search +} + +const cannotVisitDifferentOrigin = (origin, previousUrlVisited, remoteUrl, existingUrl, log) => { + const differences = [] if (remoteUrl.protocol !== existingUrl.protocol) { - differences.push('protocol'); + differences.push('protocol') } + if (remoteUrl.port !== existingUrl.port) { - differences.push('port'); + differences.push('port') } + if (remoteUrl.superDomain !== existingUrl.superDomain) { - differences.push('superdomain'); + differences.push('superdomain') } const errOpts = { @@ -94,506 +91,568 @@ const cannotVisitDifferentOrigin = function(origin, previousUrlVisited, remoteUr args: { differences: differences.join(', '), previousUrl: previousUrlVisited, - attemptedUrl: origin - } - }; + attemptedUrl: origin, + }, + } - return $errUtils.throwErrByPath("visit.cannot_visit_different_origin", errOpts); -}; + $errUtils.throwErrByPath('visit.cannot_visit_different_origin', errOpts) +} -const specifyFileByRelativePath = (url, log) => $errUtils.throwErrByPath("visit.specify_file_by_relative_path", { - onFail: log, - args: { - attemptedUrl: url - } -}); +const specifyFileByRelativePath = (url, log) => { + $errUtils.throwErrByPath('visit.specify_file_by_relative_path', { + onFail: log, + args: { + attemptedUrl: url, + }, + }) +} -const aboutBlank = win => new Promise(function(resolve) { - cy.once("window:load", resolve); +const aboutBlank = (win) => { + return new Promise((resolve) => { + cy.once('window:load', resolve) - return $utils.locHref("about:blank", win); -}); + return $utils.locHref('about:blank', win) + }) +} -const navigationChanged = function(Cypress, cy, state, source, arg) { - //# get the current url of our remote application - let left; - const url = cy.getRemoteLocation("href"); - debug('navigation changed:', url); +const navigationChanged = (Cypress, cy, state, source, arg) => { + // get the current url of our remote application + const url = cy.getRemoteLocation('href') - //# dont trigger for empty url's or about:blank - if (_.isEmpty(url) || (url === "about:blank")) { return; } + debug('navigation changed:', url) - //# start storing the history entries - const urls = (left = state("urls")) != null ? left : []; + // dont trigger for empty url's or about:blank + if (_.isEmpty(url) || (url === 'about:blank')) { + return + } + + // start storing the history entries + const urls = state('urls') || [] - const previousUrl = _.last(urls); + const previousUrl = _.last(urls) - //# ensure our new url doesnt match whatever - //# the previous was. this prevents logging - //# additionally when the url didnt actually change - if (url === previousUrl) { return; } + // ensure our new url doesnt match whatever + // the previous was. this prevents logging + // additionally when the url didnt actually change + if (url === previousUrl) { + return + } - //# else notify the world and log this event - Cypress.action("cy:url:changed", url); + // else notify the world and log this event + Cypress.action('cy:url:changed', url) - urls.push(url); + urls.push(url) - state("urls", urls); + state('urls', urls) - state("url", url); + state('url', url) - //# don't output a command log for 'load' or 'before:load' events + // don't output a command log for 'load' or 'before:load' events // return if source in command - if (knownCommandCausedInstability) { return; } + if (knownCommandCausedInstability) { + return + } - //# ensure our new url doesnt match whatever - //# the previous was. this prevents logging - //# additionally when the url didnt actually change - return Cypress.log({ - name: "new url", + // ensure our new url doesnt match whatever + // the previous was. this prevents logging + // additionally when the url didnt actually change + Cypress.log({ + name: 'new url', message: url, event: true, - type: "parent", + type: 'parent', end: true, snapshot: true, - consoleProps() { + consoleProps () { const obj = { - "New Url": url - }; + 'New Url': url, + } if (source) { - obj["Url Updated By"] = source; + obj['Url Updated By'] = source } if (arg) { - obj.Args = arg; + obj.Args = arg } - return obj; - } - }); -}; - -const formSubmitted = (Cypress, e) => Cypress.log({ - type: "parent", - name: "form sub", - message: "--submitting form--", - event: true, - end: true, - snapshot: true, - consoleProps() { return { - "Originated From": e.target, - "Args": e - }; } -}); - -const pageLoading = function(bool, state) { - if (state("pageLoading") === bool) { return; } - - state("pageLoading", bool); - - return Cypress.action("app:page:loading", bool); -}; - -const stabilityChanged = function(Cypress, state, config, stable, event) { - debug('stabilityChanged:', stable); + return obj + }, + }) +} + +const formSubmitted = (Cypress, e) => { + Cypress.log({ + type: 'parent', + name: 'form sub', + message: '--submitting form--', + event: true, + end: true, + snapshot: true, + consoleProps () { + return { + 'Originated From': e.target, + 'Args': e, + } + }, + }) +} + +const pageLoading = (bool, state) => { + if (state('pageLoading') === bool) { + return + } + + state('pageLoading', bool) + + Cypress.action('app:page:loading', bool) +} + +const stabilityChanged = (Cypress, state, config, stable) => { + debug('stabilityChanged:', stable) if (currentlyVisitingAboutBlank) { if (stable === false) { - //# if we're currently visiting about blank - //# and becoming unstable for the first time - //# notifiy that we're page loading - pageLoading(true, state); - return; - } else { - //# else wait until after we finish visiting - //# about blank - return; + // if we're currently visiting about blank + // and becoming unstable for the first time + // notifiy that we're page loading + pageLoading(true, state) + + return } + + // else wait until after we finish visiting + // about blank + return } - //# let the world know that the app is page:loading - pageLoading(!stable, state); + // let the world know that the app is page:loading + pageLoading(!stable, state) - //# if we aren't becoming unstable - //# then just return now - if (stable !== false) { return; } + // if we aren't becoming unstable + // then just return now + if (stable !== false) { + return + } - //# if we purposefully just caused the page to load - //# (and thus instability) don't log this out - if (knownCommandCausedInstability) { return; } + // if we purposefully just caused the page to load + // (and thus instability) don't log this out + if (knownCommandCausedInstability) { + return + } - //# bail if we dont have a runnable - //# because beforeunload can happen at any time - //# we may no longer be testing and thus dont - //# want to fire a new loading event - //# TODO - //# this may change in the future since we want - //# to add debuggability in the chrome console - //# which at that point we may keep runnable around - if (!state("runnable")) { return; } + // bail if we dont have a runnable + // because beforeunload can happen at any time + // we may no longer be testing and thus dont + // want to fire a new loading event + // TODO + // this may change in the future since we want + // to add debuggability in the chrome console + // which at that point we may keep runnable around + if (!state('runnable')) { + return + } - const options = {}; + const options = {} _.defaults(options, { - timeout: config("pageLoadTimeout") - }); + timeout: config('pageLoadTimeout'), + }) options._log = Cypress.log({ - type: "parent", - name: "page load", - message: "--waiting for new page to load--", + type: 'parent', + name: 'page load', + message: '--waiting for new page to load--', event: true, - consoleProps() { return { - Note: "This event initially fires when your application fires its 'beforeunload' event and completes when your application fires its 'load' event after the next page loads." - }; } - }); + consoleProps () { + return { + Note: 'This event initially fires when your application fires its \'beforeunload\' event and completes when your application fires its \'load\' event after the next page loads.', + } + }, + }) - cy.clearTimeout("page load"); + cy.clearTimeout('page load') - const onPageLoadErr = function(err) { - state("onPageLoadErr", null); + const onPageLoadErr = (err) => { + state('onPageLoadErr', null) - const { originPolicy } = $Location.create(window.location.href); + const { originPolicy } = $Location.create(window.location.href) try { - return $errUtils.throwErrByPath("navigation.cross_origin", { + $errUtils.throwErrByPath('navigation.cross_origin', { onFail: options._log, args: { - configFile: Cypress.config("configFile"), + configFile: Cypress.config('configFile'), message: err.message, - originPolicy - } - }); + originPolicy, + }, + }) } catch (error) { - err = error; - return err; + err = error + + return err } - }; + } - state("onPageLoadErr", onPageLoadErr); + state('onPageLoadErr', onPageLoadErr) - const loading = function() { - debug('waiting for window:load'); - return new Promise((resolve, reject) => cy.once("window:load", function() { - cy.state("onPageLoadErr", null); + const loading = () => { + debug('waiting for window:load') - options._log.set("message", "--page loaded--").snapshot().end(); + return new Promise((resolve) => { + return cy.once('window:load', () => { + cy.state('onPageLoadErr', null) - return resolve(); - })); - }; + options._log.set('message', '--page loaded--').snapshot().end() - const reject = function(err) { - let r; - if (r = state("reject")) { - return r(err); + return resolve() + }) + }) + } + + const reject = (err) => { + const r = state('reject') + + if (r) { + return r(err) } - }; + } return loading() - .timeout(options.timeout, "page load") - .catch(Promise.TimeoutError, function() { - //# clean this up - cy.state("onPageLoadErr", null); + .timeout(options.timeout, 'page load') + .catch(Promise.TimeoutError, () => { + // clean this up + cy.state('onPageLoadErr', null) try { - return timedOutWaitingForPageLoad(options.timeout, options._log); + return timedOutWaitingForPageLoad(options.timeout, options._log) } catch (err) { - return reject(err); + return reject(err) } - }); -}; - -const normalizeTimeoutOptions = options => //# there are really two timeout values - pageLoadTimeout -//# and the underlying responseTimeout. for the purposes -//# of resolving resolving the url, we only care about -//# responseTimeout - since pageLoadTimeout is a driver -//# and browser concern. therefore we normalize the options -//# object and send 'responseTimeout' as options.timeout -//# for the backend. -_ -.chain(options) -.pick(REQUEST_URL_OPTS) -.extend({ timeout: options.responseTimeout }) -.value(); - -module.exports = function(Commands, Cypress, cy, state, config) { - reset(); - - Cypress.on("test:before:run:async", () => //# reset any state on the backend - Cypress.backend('reset:server:state')); - - Cypress.on("test:before:run", reset); - - Cypress.on("stability:changed", (bool, event) => //# only send up page loading events when we're - //# not stable! - stabilityChanged(Cypress, state, config, bool, event)); - - Cypress.on("navigation:changed", (source, arg) => navigationChanged(Cypress, cy, state, source, arg)); - - Cypress.on("form:submitted", e => formSubmitted(Cypress, e)); - - const visitFailedByErr = function(err, url, fn) { - err.url = url; - - Cypress.action("cy:visit:failed", err); - - return fn(); - }; - - const requestUrl = (url, options = {}) => Cypress.backend( - "resolve:url", - url, - normalizeTimeoutOptions(options) - ) - .then(function(resp = {}) { - switch (false) { - //# if we didn't even get an OK response - //# then immediately die - case !!resp.isOkStatusCode: - var err = new Error; - err.gotResponse = true; - _.extend(err, resp); - - throw err; - - case !!resp.isHtml: - //# throw invalid contentType error - err = new Error; - err.invalidContentType = true; - _.extend(err, resp); - - throw err; - - default: - return resp; - } - }); + }) +} + +// there are really two timeout values - pageLoadTimeout +// and the underlying responseTimeout. for the purposes +// of resolving resolving the url, we only care about +// responseTimeout - since pageLoadTimeout is a driver +// and browser concern. therefore we normalize the options +// object and send 'responseTimeout' as options.timeout +// for the backend. +const normalizeTimeoutOptions = (options) => { + return _ + .chain(options) + .pick(REQUEST_URL_OPTS) + .extend({ timeout: options.responseTimeout }) + .value() +} + +module.exports = (Commands, Cypress, cy, state, config) => { + reset() + + Cypress.on('test:before:run:async', () => { + // reset any state on the backend + Cypress.backend('reset:server:state') + }) + + Cypress.on('test:before:run', reset) + + Cypress.on('stability:changed', (bool, event) => { + // only send up page loading events when we're + // not stable! + stabilityChanged(Cypress, state, config, bool, event) + }) + + Cypress.on('navigation:changed', (source, arg) => { + navigationChanged(Cypress, cy, state, source, arg) + }) + + Cypress.on('form:submitted', (e) => { + formSubmitted(Cypress, e) + }) + + const visitFailedByErr = (err, url, fn) => { + err.url = url + + Cypress.action('cy:visit:failed', err) + + return fn() + } - Cypress.on("window:before:load", function(contentWindow) { - //# TODO: just use a closure here - const current = state("current"); + const requestUrl = (url, options = {}) => { + return Cypress.backend( + 'resolve:url', + url, + normalizeTimeoutOptions(options), + ) + .then((resp = {}) => { + if (!resp.isOkStatusCode) { + // if we didn't even get an OK response + // then immediately die + const err = new Error + + err.gotResponse = true + _.extend(err, resp) + + throw err + } - if (!current) { return; } + if (!resp.isHtml) { + // throw invalid contentType error + const err = new Error - const runnable = state("runnable"); + err.invalidContentType = true + _.extend(err, resp) - if (!runnable) { return; } + throw err + } - const options = _.last(current.get("args")); - return __guard__(options != null ? options.onBeforeLoad : undefined, x => x.call(runnable.ctx, contentWindow)); - }); + return resp + }) + } + + Cypress.on('window:before:load', (contentWindow) => { + // TODO: just use a closure here + const current = state('current') - return Commands.addAll({ - reload(...args) { - let forceReload, userOptions; + if (!current) { + return + } + + const runnable = state('runnable') + + if (!runnable) { + return + } + + const options = _.last(current.get('args')) + + return options?.onBeforeLoad?.call(runnable.ctx, contentWindow) + }) + + Commands.addAll({ + reload (...args) { + let forceReload + let userOptions const throwArgsErr = () => { - return $errUtils.throwErrByPath("reload.invalid_arguments"); - }; + $errUtils.throwErrByPath('reload.invalid_arguments') + } switch (args.length) { case 0: - forceReload = false; - userOptions = {}; - break; + forceReload = false + userOptions = {} + break case 1: if (_.isObject(args[0])) { - userOptions = args[0]; + userOptions = args[0] } else { - forceReload = args[0]; + forceReload = args[0] } - break; + + break case 2: - forceReload = args[0]; - userOptions = args[1]; - break; + forceReload = args[0] + userOptions = args[1] + break default: - throwArgsErr(); + throwArgsErr() } - //# clear the current timeout - cy.clearTimeout("reload"); + // clear the current timeout + cy.clearTimeout('reload') - let cleanup = null; + let cleanup = null const options = _.defaults({}, userOptions, { log: true, - timeout: config("pageLoadTimeout") - }); + timeout: config('pageLoadTimeout'), + }) - const reload = () => new Promise(function(resolve, reject) { - if (forceReload == null) { forceReload = false; } - if (userOptions == null) { userOptions = {}; } + const reload = () => { + return new Promise((resolve) => { + forceReload = forceReload || false + userOptions = userOptions || {} - if (!_.isObject(userOptions)) { - throwArgsErr(); - } + if (!_.isObject(userOptions)) { + throwArgsErr() + } - if (!_.isBoolean(forceReload)) { - throwArgsErr(); - } + if (!_.isBoolean(forceReload)) { + throwArgsErr() + } - if (options.log) { - options._log = Cypress.log({}); + if (options.log) { + options._log = Cypress.log({}) - options._log.snapshot("before", {next: "after"}); - } + options._log.snapshot('before', { next: 'after' }) + } - cleanup = function() { - knownCommandCausedInstability = false; + cleanup = () => { + knownCommandCausedInstability = false - return cy.removeListener("window:load", resolve); - }; + return cy.removeListener('window:load', resolve) + } - knownCommandCausedInstability = true; + knownCommandCausedInstability = true - cy.once("window:load", resolve); + cy.once('window:load', resolve) - return $utils.locReload(forceReload, state("window")); - }); + return $utils.locReload(forceReload, state('window')) + }) + } return reload() - .timeout(options.timeout, "reload") - .catch(Promise.TimeoutError, err => timedOutWaitingForPageLoad(options.timeout, options._log)).finally(function() { + .timeout(options.timeout, 'reload') + .catch(Promise.TimeoutError, () => { + return timedOutWaitingForPageLoad(options.timeout, options._log) + }) + .finally(() => { if (typeof cleanup === 'function') { - cleanup(); + cleanup() } - return null; - }); + return null + }) }, - go(numberOrString, options = {}) { - const userOptions = options; + go (numberOrString, options = {}) { + const userOptions = options + options = _.defaults({}, userOptions, { log: true, - timeout: config("pageLoadTimeout") - }); + timeout: config('pageLoadTimeout'), + }) if (options.log) { - options._log = Cypress.log({ - }); + options._log = Cypress.log({}) } - const win = state("window"); + const win = state('window') - const goNumber = function(num) { + const goNumber = (num) => { if (num === 0) { - $errUtils.throwErrByPath("go.invalid_number", { onFail: options._log }); + $errUtils.throwErrByPath('go.invalid_number', { onFail: options._log }) } - let cleanup = null; + let cleanup = null if (options._log) { - options._log.snapshot("before", {next: "after"}); + options._log.snapshot('before', { next: 'after' }) } - const go = () => Promise.try(function() { - let didUnload = false; + const go = () => { + return Promise.try(() => { + let didUnload = false + + const beforeUnload = () => { + didUnload = true + } - const beforeUnload = () => didUnload = true; + // clear the current timeout + cy.clearTimeout() - //# clear the current timeout - cy.clearTimeout(); + cy.once('window:before:unload', beforeUnload) - cy.once("window:before:unload", beforeUnload); + const didLoad = new Promise((resolve) => { + cleanup = function () { + cy.removeListener('window:load', resolve) - const didLoad = new Promise(function(resolve) { - cleanup = function() { - cy.removeListener("window:load", resolve); - return cy.removeListener("window:before:unload", beforeUnload); - }; + return cy.removeListener('window:before:unload', beforeUnload) + } - return cy.once("window:load", resolve); - }); + return cy.once('window:load', resolve) + }) - knownCommandCausedInstability = true; + knownCommandCausedInstability = true - win.history.go(num); + win.history.go(num) - const retWin = () => //# need to set the attributes of 'go' - //# consoleProps here with win + // need to set the attributes of 'go' + // consoleProps here with win + // make sure we resolve our go function + // with the remove window (just like cy.visit) + const retWin = () => state('window') - //# make sure we resolve our go function - //# with the remove window (just like cy.visit) - state("window"); + return Promise + .delay(100) + .then(() => { + knownCommandCausedInstability = false - return Promise - .delay(100) - .then(function() { - knownCommandCausedInstability = false; + // if we've didUnload then we know we're + // doing a full page refresh and we need + // to wait until + if (didUnload) { + return didLoad.then(retWin) + } - //# if we've didUnload then we know we're - //# doing a full page refresh and we need - //# to wait until - if (didUnload) { - return didLoad.then(retWin); - } else { - return retWin(); - } - }); - }); + return retWin() + }) + }) + } return go() - .timeout(options.timeout, "go") - .catch(Promise.TimeoutError, err => timedOutWaitingForPageLoad(options.timeout, options._log)).finally(function() { + .timeout(options.timeout, 'go') + .catch(Promise.TimeoutError, () => { + return timedOutWaitingForPageLoad(options.timeout, options._log) + }).finally(() => { if (typeof cleanup === 'function') { - cleanup(); + cleanup() } - return null; - }); - }; + return null + }) + } - const goString = function(str) { + const goString = (str) => { switch (str) { - case "forward": return goNumber(1); - case "back": return goNumber(-1); + case 'forward': return goNumber(1) + case 'back': return goNumber(-1) default: - return $errUtils.throwErrByPath("go.invalid_direction", { + $errUtils.throwErrByPath('go.invalid_direction', { onFail: options._log, - args: { str } - }); + args: { str }, + }) } - }; + } - switch (false) { - case !_.isFinite(numberOrString): return goNumber(numberOrString); - case !_.isString(numberOrString): return goString(numberOrString); - default: - return $errUtils.throwErrByPath("go.invalid_argument", { onFail: options._log }); + if (_.isFinite(numberOrString)) { + return goNumber(numberOrString) } + + if (_.isString(numberOrString)) { + return goString(numberOrString) + } + + $errUtils.throwErrByPath('go.invalid_argument', { onFail: options._log }) }, - visit(url, options = {}) { - let baseUrl, message, path, qs; + visit (url, options = {}) { if (options.url && url) { - $errUtils.throwErrByPath("visit.no_duplicate_url", { args: { optionsUrl: options.url, url }}); + $errUtils.throwErrByPath('visit.no_duplicate_url', { args: { optionsUrl: options.url, url } }) } - let userOptions = options; + + let userOptions = options if (userOptions.url && url) { - $utils.throwErrByPath("visit.no_duplicate_url", { args: { optionsUrl: userOptions.url, url }}); + $utils.throwErrByPath('visit.no_duplicate_url', { args: { optionsUrl: userOptions.url, url } }) } if (_.isObject(url) && _.isEqual(userOptions, {})) { - //# options specified as only argument - userOptions = url; - ({ - url - } = userOptions); + // options specified as only argument + userOptions = url + url = userOptions.url } if (!_.isString(url)) { - $errUtils.throwErrByPath("visit.invalid_1st_arg"); + $errUtils.throwErrByPath('visit.invalid_1st_arg') } - const consoleProps = {}; + const consoleProps = {} if (!_.isEmpty(userOptions)) { - consoleProps["Options"] = _.pick(userOptions, VISIT_OPTS); + consoleProps['Options'] = _.pick(userOptions, VISIT_OPTS) } options = _.defaults({}, userOptions, { @@ -606,352 +665,370 @@ module.exports = function(Commands, Cypress, cy, state, config) { headers: {}, log: true, responseTimeout: config('responseTimeout'), - timeout: config("pageLoadTimeout"), - onBeforeLoad() {}, - onLoad() {} - }); + timeout: config('pageLoadTimeout'), + onBeforeLoad () {}, + onLoad () {}, + }) if (!_.isUndefined(options.qs) && !_.isObject(options.qs)) { - $errUtils.throwErrByPath("visit.invalid_qs", { args: { qs: String(options.qs) }}); + $errUtils.throwErrByPath('visit.invalid_qs', { args: { qs: String(options.qs) } }) } if (options.retryOnStatusCodeFailure && !options.failOnStatusCode) { - $errUtils.throwErrByPath("visit.status_code_flags_invalid"); + $errUtils.throwErrByPath('visit.status_code_flags_invalid') } if (!isValidVisitMethod(options.method)) { - $errUtils.throwErrByPath("visit.invalid_method", { args: { method: options.method }}); + $errUtils.throwErrByPath('visit.invalid_method', { args: { method: options.method } }) } if (!_.isObject(options.headers)) { - $errUtils.throwErrByPath("visit.invalid_headers"); + $errUtils.throwErrByPath('visit.invalid_headers') } - if (_.isObject(options.body) && (path = whatIsCircular(options.body))) { - $errUtils.throwErrByPath("visit.body_circular", { args: { path }}); + const path = whatIsCircular(options.body) + + if (_.isObject(options.body) && path) { + $errUtils.throwErrByPath('visit.body_circular', { args: { path } }) } if (options.log) { - message = url; + let message = url if (options.method !== 'GET') { - message = `${options.method} ${message}`; + message = `${options.method} ${message}` } options._log = Cypress.log({ message, - consoleProps() { return consoleProps; } - }); + consoleProps () { + return consoleProps + }, + }) } - url = $Location.normalize(url); + url = $Location.normalize(url) - if (baseUrl = config("baseUrl")) { - url = $Location.qualifyWithBaseUrl(baseUrl, url); - } + const baseUrl = config('baseUrl') - if (qs = options.qs) { - url = $Location.mergeUrlWithParams(url, qs); + if (baseUrl) { + url = $Location.qualifyWithBaseUrl(baseUrl, url) } - let cleanup = null; - - //# clear the current timeout - cy.clearTimeout("visit"); - - let win = state("window"); - const $autIframe = state("$autIframe"); - const runnable = state("runnable"); + const qs = options.qs - const changeIframeSrc = (url, event) => //# when the remote iframe's load event fires - //# callback fn - new Promise(function(resolve) { - //# if we're listening for hashchange - //# events then change the strategy - //# to listen to this event emitting - //# from the window and not cy - //# see issue 652 for why. - //# the hashchange events are firing too - //# fast for us. They even resolve asynchronously - //# before other application's hashchange events - //# have even fired. - if (event === "hashchange") { - win.addEventListener("hashchange", resolve); - } else { - cy.once(event, resolve); - } + if (qs) { + url = $Location.mergeUrlWithParams(url, qs) + } - cleanup = function() { - if (event === "hashchange") { - win.removeEventListener("hashchange", resolve); + let cleanup = null + + // clear the current timeout + cy.clearTimeout('visit') + + let win = state('window') + const $autIframe = state('$autIframe') + const runnable = state('runnable') + + const changeIframeSrc = (url, event) => { + // when the remote iframe's load event fires + // callback fn + return new Promise((resolve) => { + // if we're listening for hashchange + // events then change the strategy + // to listen to this event emitting + // from the window and not cy + // see issue 652 for why. + // the hashchange events are firing too + // fast for us. They even resolve asynchronously + // before other application's hashchange events + // have even fired. + if (event === 'hashchange') { + win.addEventListener('hashchange', resolve) } else { - cy.removeListener(event, resolve); + cy.once(event, resolve) } - return knownCommandCausedInstability = false; - }; + cleanup = () => { + if (event === 'hashchange') { + win.removeEventListener('hashchange', resolve) + } else { + cy.removeListener(event, resolve) + } - knownCommandCausedInstability = true; + knownCommandCausedInstability = false + } + + knownCommandCausedInstability = true - return $utils.iframeSrc($autIframe, url); - }); + return $utils.iframeSrc($autIframe, url) + }) + } - const onLoad = function({runOnLoadCallback, totalTime}) { - //# reset window on load - win = state("window"); + const onLoad = ({ runOnLoadCallback, totalTime }) => { + // reset window on load + win = state('window') - //# the onLoad callback should only be skipped if specified + // the onLoad callback should only be skipped if specified if (runOnLoadCallback !== false) { if (options.onLoad != null) { - options.onLoad.call(runnable.ctx, win); + options.onLoad.call(runnable.ctx, win) } } if (options._log) { options._log.set({ url, - totalTime - }); + totalTime, + }) } - return Promise.resolve(win); - }; + return Promise.resolve(win) + } - const go = function() { - //# hold onto our existing url - let a, remoteUrl; - const existing = $utils.locExisting(); + const go = () => { + // hold onto our existing url + const existing = $utils.locExisting() - //# TODO: $Location.resolve(existing.origin, url) + // TODO: $Location.resolve(existing.origin, url) if ($Location.isLocalFileUrl(url)) { - return specifyFileByRelativePath(url, options._log); + return specifyFileByRelativePath(url, options._log) } - //# in the case we are visiting a relative url - //# then prepend the existing origin to it - //# so we get the right remote url + let remoteUrl + + // in the case we are visiting a relative url + // then prepend the existing origin to it + // so we get the right remote url if (!$Location.isFullyQualifiedUrl(url)) { - remoteUrl = $Location.fullyQualifyUrl(url); + remoteUrl = $Location.fullyQualifyUrl(url) } - let remote = $Location.create(remoteUrl != null ? remoteUrl : url); + let remote = $Location.create(remoteUrl || url) + + // reset auth options if we have them + const a = remote.authObj - //# reset auth options if we have them - if (a = remote.authObj) { - options.auth = a; + if (a) { + options.auth = a } - //# store the existing hash now since - //# we'll need to apply it later - const existingHash = remote.hash != null ? remote.hash : ""; - const existingAuth = remote.auth != null ? remote.auth : ""; + // store the existing hash now since + // we'll need to apply it later + const existingHash = remote.hash || '' + const existingAuth = remote.auth || '' if (previousDomainVisited && (remote.originPolicy !== existing.originPolicy)) { - //# if we've already visited a new superDomain - //# then die else we'd be in a terrible endless loop - return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log); + // if we've already visited a new superDomain + // then die else we'd be in a terrible endless loop + return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log) } - const current = $Location.create(win.location.href); + const current = $Location.create(win.location.href) - //# if all that is changing is the hash then we know - //# the browser won't actually make a new http request - //# for this, and so we need to resolve onLoad immediately - //# and bypass the actual visit resolution stuff + // if all that is changing is the hash then we know + // the browser won't actually make a new http request + // for this, and so we need to resolve onLoad immediately + // and bypass the actual visit resolution stuff if (bothUrlsMatchAndRemoteHasHash(current, remote)) { - //# https://github.com/cypress-io/cypress/issues/1311 + // https://github.com/cypress-io/cypress/issues/1311 if (current.hash === remote.hash) { - consoleProps["Note"] = "Because this visit was to the same hash, the page did not reload and the onBeforeLoad and onLoad callbacks did not fire."; + consoleProps['Note'] = 'Because this visit was to the same hash, the page did not reload and the onBeforeLoad and onLoad callbacks did not fire.' - return onLoad({runOnLoadCallback: false}); + return onLoad({ runOnLoadCallback: false }) } - return changeIframeSrc(remote.href, "hashchange") - .then(onLoad); + return changeIframeSrc(remote.href, 'hashchange') + .then(onLoad) } if (existingHash) { - //# strip out the existing hash if we have one - //# before telling our backend to resolve this url - url = url.replace(existingHash, ""); + // strip out the existing hash if we have one + // before telling our backend to resolve this url + url = url.replace(existingHash, '') } if (existingAuth) { - //# strip out the existing url if we have one - url = url.replace(existingAuth + "@", ""); + // strip out the existing url if we have one + url = url.replace(`${existingAuth}@`, '') } return requestUrl(url, options) .then((resp = {}) => { - let cookies, filePath, originalUrl, redirects; - ({url, originalUrl, cookies, redirects, filePath} = resp); + let { url, originalUrl, cookies, redirects, filePath } = resp - //# reapply the existing hash - url += existingHash; - originalUrl += existingHash; + // reapply the existing hash + url += existingHash + originalUrl += existingHash if (filePath) { - consoleProps["File Served"] = filePath; + consoleProps['File Served'] = filePath } else { if (url !== originalUrl) { - consoleProps["Original Url"] = originalUrl; + consoleProps['Original Url'] = originalUrl } } if (options.log) { - message = options._log.get('message'); + let message = options._log.get('message') if (redirects && redirects.length) { - message = [message].concat(redirects).join(" -> "); + message = [message].concat(redirects).join(' -> ') } - options._log.set({message}); + options._log.set({ message }) } - consoleProps["Resolved Url"] = url; - consoleProps["Redirects"] = redirects; - consoleProps["Cookies Set"] = cookies; + consoleProps['Resolved Url'] = url + consoleProps['Redirects'] = redirects + consoleProps['Cookies Set'] = cookies - remote = $Location.create(url); + remote = $Location.create(url) - //# if the origin currently matches - //# then go ahead and change the iframe's src - //# and we're good to go + // if the origin currently matches + // then go ahead and change the iframe's src + // and we're good to go // if origin is existing.origin if (remote.originPolicy === existing.originPolicy) { - previousDomainVisited = remote.origin; + previousDomainVisited = remote.origin - url = $Location.fullyQualifyUrl(url); + url = $Location.fullyQualifyUrl(url) - return changeIframeSrc(url, "window:load") - .then(() => onLoad(resp)); - } else { - //# if we've already visited a new origin - //# then die else we'd be in a terrible endless loop - if (previousDomainVisited) { - return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log); - } + return changeIframeSrc(url, 'window:load') + .then(() => { + return onLoad(resp) + }) + } + + // if we've already visited a new origin + // then die else we'd be in a terrible endless loop + if (previousDomainVisited) { + return cannotVisitDifferentOrigin(remote.origin, previousDomainVisited, remote, existing, options._log) + } - //# tell our backend we're changing domains - //# TODO: add in other things we want to preserve - //# state for like scrollTop - let s = { - currentId: id, - tests: Cypress.getTestsState(), - startTime: Cypress.getStartTime(), - emissions: Cypress.getEmissions() - }; - - s.passed = Cypress.countByTestState(s.tests, "passed"); - s.failed = Cypress.countByTestState(s.tests, "failed"); - s.pending = Cypress.countByTestState(s.tests, "pending"); - s.numLogs = $Log.countLogsByTests(s.tests); - - return Cypress.action("cy:collect:run:state") - .then(function(a = []) { - //# merge all the states together holla' - s = _.reduce(a, (memo, obj) => _.extend(memo, obj) - , s); - - return Cypress.backend("preserve:run:state", s);}).then(function() { - //# and now we must change the url to be the new - //# origin but include the test that we're currently on - const newUri = new UrlParse(remote.origin); - newUri - .set("pathname", existing.pathname) - .set("query", existing.search) - .set("hash", existing.hash); - - //# replace is broken in electron so switching - //# to href for now - // $utils.locReplace(window, newUri.toString()) - $utils.locHref(newUri.toString(), window); - - //# we are returning a Promise which never resolves - //# because we're changing top to be a brand new URL - //# and want to block the rest of our commands - return Promise.delay(1e9); - }); + // tell our backend we're changing domains + // TODO: add in other things we want to preserve + // state for like scrollTop + let s = { + currentId: id, + tests: Cypress.getTestsState(), + startTime: Cypress.getStartTime(), + emissions: Cypress.getEmissions(), } - }).catch(function(err) { - switch (false) { - case !err.gotResponse: case !err.invalidContentType: - return visitFailedByErr(err, err.originalUrl, function() { - const args = { - url: err.originalUrl, - path: err.filePath, - status: err.status, - statusText: err.statusText, - redirects: err.redirects, - contentType: err.contentType - }; - - const msg = (() => { switch (false) { - case !err.gotResponse: - var type = err.filePath ? "file" : "http"; - - return `visit.loading_${type}_failed`; - - case !err.invalidContentType: - return "visit.loading_invalid_content_type"; - } })(); - - return $errUtils.throwErrByPath(msg, { - onFail: options._log, - args - }); - }); - default: - return visitFailedByErr(err, url, () => $errUtils.throwErrByPath("visit.loading_network_failed", { + + s.passed = Cypress.countByTestState(s.tests, 'passed') + s.failed = Cypress.countByTestState(s.tests, 'failed') + s.pending = Cypress.countByTestState(s.tests, 'pending') + s.numLogs = $Log.countLogsByTests(s.tests) + + return Cypress.action('cy:collect:run:state') + .then((a = []) => { + // merge all the states together holla' + s = _.reduce(a, (memo, obj) => { + return _.extend(memo, obj) + }, s) + + return Cypress.backend('preserve:run:state', s) + }) + .then(() => { + // and now we must change the url to be the new + // origin but include the test that we're currently on + const newUri = new UrlParse(remote.origin) + + newUri + .set('pathname', existing.pathname) + .set('query', existing.search) + .set('hash', existing.hash) + + // replace is broken in electron so switching + // to href for now + // $utils.locReplace(window, newUri.toString()) + $utils.locHref(newUri.toString(), window) + + // we are returning a Promise which never resolves + // because we're changing top to be a brand new URL + // and want to block the rest of our commands + return Promise.delay(1e9) + }) + }) + .catch((err) => { + if (err.gotResponse || err.invalidContentType) { + visitFailedByErr(err, err.originalUrl, () => { + const args = { + url: err.originalUrl, + path: err.filePath, + status: err.status, + statusText: err.statusText, + redirects: err.redirects, + contentType: err.contentType, + } + + let msg = '' + + if (err.gotResponse) { + const type = err.filePath ? 'file' : 'http' + + msg = `visit.loading_${type}_failed` + } + + if (err.invalidContentType) { + msg = 'visit.loading_invalid_content_type' + } + + $errUtils.throwErrByPath(msg, { onFail: options._log, - args: { - url, - error: err, - stack: err.stack - }, - noStackTrace: true - })); + args, + }) + }) } - }); - }; - - const visit = function() { - //# if we've visiting for the first time during - //# a test then we want to first visit about:blank - //# so that we nuke the previous state. subsequent - //# visits will not navigate to about:blank so that - //# our history entries are intact + + visitFailedByErr(err, url, () => { + $errUtils.throwErrByPath('visit.loading_network_failed', { + onFail: options._log, + args: { + url, + error: err, + stack: err.stack, + }, + noStackTrace: true, + }) + }) + }) + } + + const visit = () => { + // if we've visiting for the first time during + // a test then we want to first visit about:blank + // so that we nuke the previous state. subsequent + // visits will not navigate to about:blank so that + // our history entries are intact if (!hasVisitedAboutBlank) { - hasVisitedAboutBlank = true; - currentlyVisitingAboutBlank = true; + hasVisitedAboutBlank = true + currentlyVisitingAboutBlank = true return aboutBlank(win) - .then(function() { - currentlyVisitingAboutBlank = false; + .then(() => { + currentlyVisitingAboutBlank = false - return go(); - }); - } else { - return go(); + return go() + }) } - }; + + return go() + } return visit() - .timeout(options.timeout, "visit") - .catch(Promise.TimeoutError, err => { - return timedOutWaitingForPageLoad(options.timeout, options._log); - }).finally(function() { + .timeout(options.timeout, 'visit') + .catch(Promise.TimeoutError, () => { + return timedOutWaitingForPageLoad(options.timeout, options._log) + }).finally(() => { if (typeof cleanup === 'function') { - cleanup(); + cleanup() } - return null; - }); - } - }); -}; - -function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return null + }) + }, + }) +} diff --git a/packages/driver/src/cy/commands/popups.js b/packages/driver/src/cy/commands/popups.js index dbaa53a6fe44..a405965e6f81 100644 --- a/packages/driver/src/cy/commands/popups.js +++ b/packages/driver/src/cy/commands/popups.js @@ -1,35 +1,42 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const windowAlert = (Cypress, str) => Cypress.log({ - type: "parent", - name: "alert", - message: str, - event: true, - end: true, - snapshot: true, - consoleProps() { return { - "Alerted": str - }; } -}); +const windowAlert = (Cypress, str) => { + Cypress.log({ + type: 'parent', + name: 'alert', + message: str, + event: true, + end: true, + snapshot: true, + consoleProps () { + return { + 'Alerted': str, + } + }, + }) +} -const windowConfirmed = (Cypress, str, ret) => Cypress.log({ - type: "parent", - name: "confirm", - message: str, - event: true, - end: true, - snapshot: true, - consoleProps() { return { - "Prompted": str, - "Confirmed": ret - }; } -}); +const windowConfirmed = (Cypress, str, ret) => { + Cypress.log({ + type: 'parent', + name: 'confirm', + message: str, + event: true, + end: true, + snapshot: true, + consoleProps () { + return { + 'Prompted': str, + 'Confirmed': ret, + } + }, + }) +} -module.exports = function(Commands, Cypress, cy, state, config) { - Cypress.on("window:alert", str => windowAlert(Cypress, str)); +module.exports = (Commands, Cypress) => { + Cypress.on('window:alert', (str) => { + windowAlert(Cypress, str) + }) - return Cypress.on("window:confirmed", (str, ret) => windowConfirmed(Cypress, str, ret)); -}; + Cypress.on('window:confirmed', (str, ret) => { + windowConfirmed(Cypress, str, ret) + }) +} diff --git a/packages/driver/src/cy/commands/request.js b/packages/driver/src/cy/commands/request.js index c8b6deb771ee..a368b0aad3c1 100644 --- a/packages/driver/src/cy/commands/request.js +++ b/packages/driver/src/cy/commands/request.js @@ -1,28 +1,22 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); -const whatIsCircular = require("@cypress/what-is-circular"); -const Promise = require("bluebird"); - -const $utils = require("../../cypress/utils"); -const $errUtils = require("../../cypress/error_utils"); -const $Location = require("../../cypress/location"); - -const isOptional = function(memo, val, key) { +const _ = require('lodash') +const whatIsCircular = require('@cypress/what-is-circular') +const Promise = require('bluebird') + +const $utils = require('../../cypress/utils') +const $errUtils = require('../../cypress/error_utils') +const $Location = require('../../cypress/location') + +const isOptional = (memo, val, key) => { if (_.isNull(val)) { - memo.push(key); + memo.push(key) } - return memo; -}; + + return memo +} const REQUEST_DEFAULTS = { - url: "", - method: "GET", + url: '', + method: 'GET', qs: null, body: null, auth: null, @@ -34,286 +28,291 @@ const REQUEST_DEFAULTS = { followRedirect: true, failOnStatusCode: true, retryOnNetworkFailure: true, - retryOnStatusCodeFailure: false -}; + retryOnStatusCodeFailure: false, +} + +const REQUEST_PROPS = _.keys(REQUEST_DEFAULTS) + +const OPTIONAL_OPTS = _.reduce(REQUEST_DEFAULTS, isOptional, []) + +const hasFormUrlEncodedContentTypeHeader = (headers) => { + const header = _.findKey(headers, _.matches('application/x-www-form-urlencoded')) + + return header && (_.toLower(header) === 'content-type') +} + +const isValidJsonObj = (body) => { + return _.isObject(body) && !_.isFunction(body) +} + +const whichAreOptional = (val, key) => { + return (val === null) && OPTIONAL_OPTS.includes(key) +} + +const needsFormSpecified = (options = {}) => { + const { body, json, headers } = options + + // json isn't true, and we have an object body and the user + // specified that the content-type header is x-www-form-urlencoded + return (json !== true) && _.isObject(body) && hasFormUrlEncodedContentTypeHeader(headers) +} + +module.exports = (Commands, Cypress, cy, state, config) => { + Commands.addAll({ + // allow our signature to be similar to cy.route + // METHOD / URL / BODY + // or object literal with all expanded options + request (...args) { + const o = {} + const userOptions = o + + if (_.isObject(args[0])) { + _.extend(userOptions, args[0]) + } else if (args.length === 1) { + o.url = args[0] + } else if (args.length === 2) { + // if our first arg is a valid + // HTTP method then set method + url + if ($utils.isValidHttpMethod(args[0])) { + o.method = args[0] + o.url = args[1] + } else { + // set url + body + o.url = args[0] + o.body = args[1] + } + } else if (args.length === 3) { + o.method = args[0] + o.url = args[1] + o.body = args[2] + } -const REQUEST_PROPS = _.keys(REQUEST_DEFAULTS); + let options = _.defaults({}, userOptions, REQUEST_DEFAULTS, { + log: true, + }) -const OPTIONAL_OPTS = _.reduce(REQUEST_DEFAULTS, isOptional, []); + // if timeout is not supplied, use the configured default + if (!options.timeout) { + options.timeout = config('responseTimeout') + } -const hasFormUrlEncodedContentTypeHeader = function(headers) { - const header = _.findKey(headers, _.matches("application/x-www-form-urlencoded")); + options.method = options.method.toUpperCase() - return header && (_.toLower(header) === "content-type"); -}; + if (options.retryOnStatusCodeFailure && !options.failOnStatusCode) { + $errUtils.throwErrByPath('request.status_code_flags_invalid') + } -const isValidJsonObj = body => _.isObject(body) && !_.isFunction(body); + if (_.has(options, 'failOnStatus')) { + $errUtils.warnByPath('request.failonstatus_deprecated_warning') + options.failOnStatusCode = options.failOnStatus + } -const whichAreOptional = (val, key) => (val === null) && OPTIONAL_OPTS.includes(key); + // normalize followRedirects -> followRedirect + // because we are nice + if (_.has(options, 'followRedirects')) { + options.followRedirect = options.followRedirects + } -const needsFormSpecified = function(options = {}) { - const { body, json, headers } = options; + if (!$utils.isValidHttpMethod(options.method)) { + $errUtils.throwErrByPath('request.invalid_method', { + args: { method: o.method }, + }) + } - //# json isn't true, and we have an object body and the user - //# specified that the content-type header is x-www-form-urlencoded - return (json !== true) && _.isObject(body) && hasFormUrlEncodedContentTypeHeader(headers); -}; + if (!options.url) { + $errUtils.throwErrByPath('request.url_missing') + } -module.exports = (Commands, Cypress, cy, state, config) => // Cypress.extend -// ## set defaults for all requests? -// requestDefaults: (options = {}) -> + if (!_.isString(options.url)) { + $errUtils.throwErrByPath('request.url_wrong_type') + } -Commands.addAll({ - //# allow our signature to be similar to cy.route - //# METHOD / URL / BODY - //# or object literal with all expanded options - request(...args) { - let a, f, h, o, originOrBase, path; - const userOptions = (o = {}); + // normalize the url by prepending it with our current origin + // or the baseUrl + // or just using the options.url if its FQDN + // origin may return an empty string if we haven't visited anything yet + options.url = $Location.normalize(options.url) - switch (false) { - case !_.isObject(args[0]): - _.extend(userOptions, args[0]); - break; + const originOrBase = config('baseUrl') || cy.getRemoteLocation('origin') - case args.length !== 1: - o.url = args[0]; - break; + if (originOrBase) { + options.url = $Location.qualifyWithBaseUrl(originOrBase, options.url) + } - case args.length !== 2: - //# if our first arg is a valid - //# HTTP method then set method + url - if ($utils.isValidHttpMethod(args[0])) { - o.method = args[0]; - o.url = args[1]; - } else { - //# set url + body - o.url = args[0]; - o.body = args[1]; + // Make sure the url unicode characters are properly escaped + // https://github.com/cypress-io/cypress/issues/5274 + try { + options.url = new URL(options.url).href + } catch (error) { + const err = error + + if (!(err instanceof TypeError)) { // unexpected, new URL should only throw TypeError + throw err } - break; - - case args.length !== 3: - o.method = args[0]; - o.url = args[1]; - o.body = args[2]; - break; - } - - let options = _.defaults({}, userOptions, REQUEST_DEFAULTS, { - log: true - }); - - //# if timeout is not supplied, use the configured default - if (!options.timeout) { options.timeout = config("responseTimeout"); } - - options.method = options.method.toUpperCase(); - - if (options.retryOnStatusCodeFailure && !options.failOnStatusCode) { - $errUtils.throwErrByPath("request.status_code_flags_invalid"); - } - - if (_.has(options, "failOnStatus")) { - $errUtils.warnByPath("request.failonstatus_deprecated_warning"); - options.failOnStatusCode = options.failOnStatus; - } - - //# normalize followRedirects -> followRedirect - //# because we are nice - if (_.has(options, "followRedirects")) { - options.followRedirect = options.followRedirects; - } - - if (!$utils.isValidHttpMethod(options.method)) { - $errUtils.throwErrByPath("request.invalid_method", { - args: { method: o.method } - }); - } - - if (!options.url) { - $errUtils.throwErrByPath("request.url_missing"); - } - - if (!_.isString(options.url)) { - $errUtils.throwErrByPath("request.url_wrong_type"); - } - - //# normalize the url by prepending it with our current origin - //# or the baseUrl - //# or just using the options.url if its FQDN - //# origin may return an empty string if we haven't visited anything yet - options.url = $Location.normalize(options.url); - - if (originOrBase = config("baseUrl") || cy.getRemoteLocation("origin")) { - options.url = $Location.qualifyWithBaseUrl(originOrBase, options.url); - } - - //# Make sure the url unicode characters are properly escaped - //# https://github.com/cypress-io/cypress/issues/5274 - try { - options.url = new URL(options.url).href; - } catch (error) { - const err = error; - if (!(err instanceof TypeError)) { //# unexpected, new URL should only throw TypeError - throw err; + + // The URL object cannot be constructed because of URL failure + $errUtils.throwErrByPath('request.url_invalid', { + args: { + configFile: Cypress.config('configFile'), + }, + }) } - // The URL object cannot be constructed because of URL failure - $errUtils.throwErrByPath("request.url_invalid", { - args: { - configFile: Cypress.config("configFile") - } - }); - } + // if options.url isnt FQDN then we need to throw here + // if we made a request prior to a visit then it needs + // to be filled out + if (!$Location.isFullyQualifiedUrl(options.url)) { + $errUtils.throwErrByPath('request.url_invalid', { + args: { + configFile: Cypress.config('configFile'), + }, + }) + } + // if a user has `x-www-form-urlencoded` content-type set + // with an object body, they meant to add 'form: true' + // so we are nice and do it for them :) + // https://github.com/cypress-io/cypress/issues/2923 + if (needsFormSpecified(options)) { + options.form = true + } - //# if options.url isnt FQDN then we need to throw here - //# if we made a request prior to a visit then it needs - //# to be filled out - if (!$Location.isFullyQualifiedUrl(options.url)) { - $errUtils.throwErrByPath("request.url_invalid", { - args: { - configFile: Cypress.config("configFile") - } - }); - } - - //# if a user has `x-www-form-urlencoded` content-type set - //# with an object body, they meant to add 'form: true' - //# so we are nice and do it for them :) - //# https://github.com/cypress-io/cypress/issues/2923 - if (needsFormSpecified(options)) { - options.form = true; - } - - if (_.isObject(options.body) && (path = whatIsCircular(options.body))) { - $errUtils.throwErrByPath("request.body_circular", { args: { path }}); - } - - //# only set json to true if form isnt true - //# and we have a valid object for body - if ((options.form !== true) && isValidJsonObj(options.body)) { - options.json = true; - } - - options = _.omitBy(options, whichAreOptional); - - if (a = options.auth) { - if (!_.isObject(a)) { - $errUtils.throwErrByPath("request.auth_invalid"); + const path = whatIsCircular(options.body) + + if (_.isObject(options.body) && path) { + $errUtils.throwErrByPath('request.body_circular', { args: { path } }) } - } - if (h = options.headers) { - if (_.isObject(h)) { - options.headers = h; - } else { - $errUtils.throwErrByPath("request.headers_invalid"); + // only set json to true if form isnt true + // and we have a valid object for body + if ((options.form !== true) && isValidJsonObj(options.body)) { + options.json = true } - } - if (!_.isBoolean(options.gzip)) { - $errUtils.throwErrByPath("request.gzip_invalid"); - } + options = _.omitBy(options, whichAreOptional) - if (f = options.form) { - if (!_.isBoolean(f)) { - $errUtils.throwErrByPath("request.form_invalid"); + const { auth, headers, form } = options + + if (auth) { + if (!_.isObject(auth)) { + $errUtils.throwErrByPath('request.auth_invalid') + } } - } - - //# clone the requestOpts and reduce them down - //# to the bare minimum to send to lib/request - const requestOpts = _.pick(options, REQUEST_PROPS); - - if (options.log) { - options._log = Cypress.log({ - message: "", - consoleProps() { - const resp = options.response != null ? options.response : {}; - let rr = resp.allRequestResponses != null ? resp.allRequestResponses : []; - - const obj = {}; - - const word = $utils.plural(rr.length, "Requests", "Request"); - - //# if we have only a single request/response then - //# flatten this to an object, else keep as array - rr = rr.length === 1 ? rr[0] : rr; - - obj[word] = rr; - obj["Yielded"] = _.pick(resp, "status", "duration", "body", "headers"); - - return obj; - }, - - renderProps() { - let indicator; - const status = (() => { let r; - switch (false) { - case !(r = options.response): - return r.status; - default: - indicator = "pending"; - return "---"; - } })(); - - if (indicator == null) { indicator = (options.response != null ? options.response.isOkStatusCode : undefined) ? "successful" : "bad"; } - - return { - message: `${options.method} ${status} ${options.url}`, - indicator - }; + + if (headers) { + if (!_.isObject(headers)) { + $errUtils.throwErrByPath('request.headers_invalid') } - }); - } + } - //# need to remove the current timeout - //# because we're handling timeouts ourselves - cy.clearTimeout("http:request"); + if (!_.isBoolean(options.gzip)) { + $errUtils.throwErrByPath('request.gzip_invalid') + } - return Cypress.backend("http:request", requestOpts) - .timeout(options.timeout) - .then(response => { - options.response = response; + if (form) { + if (!_.isBoolean(form)) { + $errUtils.throwErrByPath('request.form_invalid') + } + } - //# bomb if we should fail on non okay status code - if (options.failOnStatusCode && (response.isOkStatusCode !== true)) { - $errUtils.throwErrByPath("request.status_invalid", { - onFail: options._log, - args: { - method: requestOpts.method, - url: requestOpts.url, - requestBody: response.requestBody, - requestHeaders: response.requestHeaders, - status: response.status, - statusText: response.statusText, - responseBody: response.body, - responseHeaders: response.headers, - redirects: response.redirects - } - }); + // clone the requestOpts and reduce them down + // to the bare minimum to send to lib/request + const requestOpts = _.pick(options, REQUEST_PROPS) + + if (options.log) { + options._log = Cypress.log({ + message: '', + consoleProps () { + const resp = options.response || {} + let rr = resp.allRequestResponses || [] + + const obj = {} + + const word = $utils.plural(rr.length, 'Requests', 'Request') + + // if we have only a single request/response then + // flatten this to an object, else keep as array + rr = rr.length === 1 ? rr[0] : rr + + obj[word] = rr + obj['Yielded'] = _.pick(resp, 'status', 'duration', 'body', 'headers') + + return obj + }, + + renderProps () { + let indicator + let status + const r = options.response + + if (r) { + status = r.status + } else { + indicator = 'pending' + status = '---' + } + + if (!indicator) { + indicator = options.response?.isOkStatusCode ? 'successful' : 'bad' + } + + return { + message: `${options.method} ${status} ${options.url}`, + indicator, + } + }, + }) } - return response; - }).catch(Promise.TimeoutError, err => { - return $errUtils.throwErrByPath("request.timed_out", { - onFail: options._log, - args: { - url: requestOpts.url, - method: requestOpts.method, - timeout: options.timeout + // need to remove the current timeout + // because we're handling timeouts ourselves + cy.clearTimeout('http:request') + + return Cypress.backend('http:request', requestOpts) + .timeout(options.timeout) + .then((response) => { + options.response = response + + // bomb if we should fail on non okay status code + if (options.failOnStatusCode && (response.isOkStatusCode !== true)) { + $errUtils.throwErrByPath('request.status_invalid', { + onFail: options._log, + args: { + method: requestOpts.method, + url: requestOpts.url, + requestBody: response.requestBody, + requestHeaders: response.requestHeaders, + status: response.status, + statusText: response.statusText, + responseBody: response.body, + responseHeaders: response.headers, + redirects: response.redirects, + }, + }) } - }); - }).catch({ backend: true }, err => $errUtils.throwErrByPath("request.loading_failed", { - onFail: options._log, - args: { - error: err.message, - stack: err.stack, - method: requestOpts.method, - url: requestOpts.url - }, - noStackTrace: true - })); - } -}); + + return response + }).catch(Promise.TimeoutError, () => { + $errUtils.throwErrByPath('request.timed_out', { + onFail: options._log, + args: { + url: requestOpts.url, + method: requestOpts.method, + timeout: options.timeout, + }, + }) + }).catch({ backend: true }, (err) => { + $errUtils.throwErrByPath('request.loading_failed', { + onFail: options._log, + args: { + error: err.message, + stack: err.stack, + method: requestOpts.method, + url: requestOpts.url, + }, + noStackTrace: true, + }) + }) + }, + }) +} diff --git a/packages/driver/src/cy/commands/screenshot.js b/packages/driver/src/cy/commands/screenshot.js index 3d837495f45b..43da03ae66f1 100644 --- a/packages/driver/src/cy/commands/screenshot.js +++ b/packages/driver/src/cy/commands/screenshot.js @@ -1,168 +1,181 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); -const $ = require("jquery"); -const bytes = require("bytes"); -const Promise = require("bluebird"); - -const $Screenshot = require("../../cypress/screenshot"); -const $dom = require("../../dom"); -const $errUtils = require("../../cypress/error_utils"); - -const getViewportHeight = state => //# TODO this doesn't seem correct -Math.min(state("viewportHeight"), window.innerHeight); - -const getViewportWidth = state => Math.min(state("viewportWidth"), window.innerWidth); - -const automateScreenshot = function(state, options = {}) { - const { runnable, timeout } = options; - - const titles = []; - - //# if this a hook then push both the current test title - //# and our own hook title - if (runnable.type === "hook") { - let ct; - if (runnable.ctx && (ct = runnable.ctx.currentTest)) { - titles.push(ct.title, runnable.title); +/* global Cypress, cy */ +const _ = require('lodash') +const $ = require('jquery') +const bytes = require('bytes') +const Promise = require('bluebird') + +const $Screenshot = require('../../cypress/screenshot') +const $dom = require('../../dom') +const $errUtils = require('../../cypress/error_utils') + +const getViewportHeight = (state) => { + // TODO this doesn't seem correct + return Math.min(state('viewportHeight'), window.innerHeight) +} + +const getViewportWidth = (state) => { + return Math.min(state('viewportWidth'), window.innerWidth) +} + +const automateScreenshot = (state, options = {}) => { + const { runnable, timeout } = options + + const titles = [] + + // if this a hook then push both the current test title + // and our own hook title + if (runnable.type === 'hook') { + let ct = runnable.ctx.currentTest + + if (runnable.ctx && ct) { + titles.push(ct.title, runnable.title) } } else { - titles.push(runnable.title); + titles.push(runnable.title) } - var getParentTitle = function(runnable) { - let p; - if (p = runnable.parent) { - let t; - if (t = p.title) { - titles.unshift(t); + const getParentTitle = (runnable) => { + const p = runnable.parent + + if (p) { + const t = p.title + + if (t) { + titles.unshift(t) } - return getParentTitle(p); + return getParentTitle(p) } - }; + } - getParentTitle(runnable); + getParentTitle(runnable) const props = _.extend({ titles, testId: runnable.id, - takenPaths: state("screenshotPaths") - }, _.omit(options, "runnable", "timeout", "log", "subject")); + takenPaths: state('screenshotPaths'), + }, _.omit(options, 'runnable', 'timeout', 'log', 'subject')) - const automate = () => Cypress.automation("take:screenshot", props); + const automate = () => { + return Cypress.automation('take:screenshot', props) + } if (!timeout) { - return automate(); - } else { - //# need to remove the current timeout - //# because we're handling timeouts ourselves - cy.clearTimeout("take:screenshot"); - return automate() - .timeout(timeout) - .catch(err => $errUtils.throwErr(err, { onFail: options.log })).catch(Promise.TimeoutError, err => $errUtils.throwErrByPath("screenshot.timed_out", { - onFail: options.log, - args: { timeout } - })); } -}; - -const scrollOverrides = function(win, doc) { - const originalOverflow = doc.documentElement.style.overflow; - const originalBodyOverflowY = doc.body.style.overflowY; - const originalX = win.scrollX; - const originalY = win.scrollY; - //# overflow-y: scroll can break `window.scrollTo` + // need to remove the current timeout + // because we're handling timeouts ourselves + cy.clearTimeout('take:screenshot') + + return automate() + .timeout(timeout) + .catch((err) => { + return $errUtils.throwErr(err, { onFail: options.log }) + }) + .catch(Promise.TimeoutError, () => { + return $errUtils.throwErrByPath('screenshot.timed_out', { + onFail: options.log, + args: { timeout }, + }) + }) +} + +const scrollOverrides = (win, doc) => { + const originalOverflow = doc.documentElement.style.overflow + const originalBodyOverflowY = doc.body.style.overflowY + const originalX = win.scrollX + const originalY = win.scrollY + + // overflow-y: scroll can break `window.scrollTo` if (doc.body) { - doc.body.style.overflowY = "visible"; + doc.body.style.overflowY = 'visible' } - //# hide scrollbars - doc.documentElement.style.overflow = "hidden"; + // hide scrollbars + doc.documentElement.style.overflow = 'hidden' - return function() { - doc.documentElement.style.overflow = originalOverflow; + return () => { + doc.documentElement.style.overflow = originalOverflow if (doc.body) { - doc.body.style.overflowY = originalBodyOverflowY; + doc.body.style.overflowY = originalBodyOverflowY } - return win.scrollTo(originalX, originalY); - }; -}; -const validateNumScreenshots = function(numScreenshots, automationOptions) { + return win.scrollTo(originalX, originalY) + } +} + +const validateNumScreenshots = (numScreenshots, automationOptions) => { if (numScreenshots < 1) { - return $errUtils.throwErrByPath("screenshot.invalid_height", { - log: automationOptions.log - }); + $errUtils.throwErrByPath('screenshot.invalid_height', { + log: automationOptions.log, + }) } -}; +} -const takeScrollingScreenshots = function(scrolls, win, state, automationOptions) { - const scrollAndTake = function({ y, clip, afterScroll }, index) { - win.scrollTo(0, y); +const takeScrollingScreenshots = (scrolls, win, state, automationOptions) => { + const scrollAndTake = ({ y, clip, afterScroll }, index) => { + win.scrollTo(0, y) if (afterScroll) { - clip = afterScroll(); + clip = afterScroll() } + const options = _.extend({}, automationOptions, { current: index + 1, total: scrolls.length, - clip - }); - return automateScreenshot(state, options); - }; + clip, + }) + + return automateScreenshot(state, options) + } return Promise .mapSeries(scrolls, scrollAndTake) - .then(_.last); -}; + .then(_.last) +} -const takeFullPageScreenshot = function(state, automationOptions) { - const win = state("window"); - const doc = state("document"); +const takeFullPageScreenshot = (state, automationOptions) => { + const win = state('window') + const doc = state('document') - const resetScrollOverrides = scrollOverrides(win, doc); + const resetScrollOverrides = scrollOverrides(win, doc) - const docHeight = $(doc).height(); - const viewportHeight = getViewportHeight(state); - const numScreenshots = Math.ceil(docHeight / viewportHeight); + const docHeight = $(doc).height() + const viewportHeight = getViewportHeight(state) + const numScreenshots = Math.ceil(docHeight / viewportHeight) - validateNumScreenshots(numScreenshots, automationOptions); + validateNumScreenshots(numScreenshots, automationOptions) - const scrolls = _.map(_.times(numScreenshots), function(index) { - const y = viewportHeight * index; - const clip = (() => { - if ((index + 1) === numScreenshots) { - const heightLeft = docHeight - (viewportHeight * index); - return { + const scrolls = _.map(_.times(numScreenshots), (index) => { + const y = viewportHeight * index + let clip + + if ((index + 1) === numScreenshots) { + const heightLeft = docHeight - (viewportHeight * index) + + clip = { x: automationOptions.clip.x, y: viewportHeight - heightLeft, width: automationOptions.clip.width, - height: heightLeft - }; + height: heightLeft, + } } else { - return automationOptions.clip; + clip = automationOptions.clip } - })(); - return { y, clip }; -}); + return { y, clip } + }) return takeScrollingScreenshots(scrolls, win, state, automationOptions) - .finally(resetScrollOverrides); -}; + .finally(resetScrollOverrides) +} -const applyPaddingToElementPositioning = function(elPosition, automationOptions) { +const applyPaddingToElementPositioning = (elPosition, automationOptions) => { if (!automationOptions.padding) { - return elPosition; + return elPosition } - const [ paddingTop, paddingRight, paddingBottom, paddingLeft ] = automationOptions.padding; + const [paddingTop, paddingRight, paddingBottom, paddingLeft] = automationOptions.padding return { width: elPosition.width + paddingLeft + paddingRight, @@ -170,132 +183,143 @@ const applyPaddingToElementPositioning = function(elPosition, automationOptions) fromElViewport: { top: elPosition.fromElViewport.top - paddingTop, left: elPosition.fromElViewport.left - paddingLeft, - bottom: elPosition.fromElViewport.bottom + paddingBottom + bottom: elPosition.fromElViewport.bottom + paddingBottom, }, fromElWindow: { - top: elPosition.fromElWindow.top - paddingTop - } - }; -}; + top: elPosition.fromElWindow.top - paddingTop, + }, + } +} -const takeElementScreenshot = function($el, state, automationOptions) { - const win = state("window"); - const doc = state("document"); +const takeElementScreenshot = ($el, state, automationOptions) => { + const win = state('window') + const doc = state('document') - const resetScrollOverrides = scrollOverrides(win, doc); + const resetScrollOverrides = scrollOverrides(win, doc) let elPosition = applyPaddingToElementPositioning( $dom.getElementPositioning($el), - automationOptions - ); - const viewportHeight = getViewportHeight(state); - const viewportWidth = getViewportWidth(state); - const numScreenshots = Math.ceil(elPosition.height / viewportHeight); + automationOptions, + ) + const viewportHeight = getViewportHeight(state) + const viewportWidth = getViewportWidth(state) + const numScreenshots = Math.ceil(elPosition.height / viewportHeight) - validateNumScreenshots(numScreenshots, automationOptions); + validateNumScreenshots(numScreenshots, automationOptions) - const scrolls = _.map(_.times(numScreenshots), function(index) { - const y = elPosition.fromElWindow.top + (viewportHeight * index); + const scrolls = _.map(_.times(numScreenshots), (index) => { + const y = elPosition.fromElWindow.top + (viewportHeight * index) - const afterScroll = function() { + const afterScroll = () => { elPosition = applyPaddingToElementPositioning( $dom.getElementPositioning($el), - automationOptions - ); - const x = Math.min(viewportWidth, elPosition.fromElViewport.left); - const width = Math.min(viewportWidth - x, elPosition.width); + automationOptions, + ) + + const x = Math.min(viewportWidth, elPosition.fromElViewport.left) + const width = Math.min(viewportWidth - x, elPosition.width) if (numScreenshots === 1) { return { x, y: elPosition.fromElViewport.top, width, - height: elPosition.height - }; + height: elPosition.height, + } } if ((index + 1) === numScreenshots) { - const overlap = ((numScreenshots - 1) * viewportHeight) + elPosition.fromElViewport.top; - const heightLeft = elPosition.fromElViewport.bottom - overlap; + const overlap = ((numScreenshots - 1) * viewportHeight) + elPosition.fromElViewport.top + const heightLeft = elPosition.fromElViewport.bottom - overlap return { x, y: overlap, width, - height: heightLeft - }; + height: heightLeft, + } } return { x, y: Math.max(0, elPosition.fromElViewport.top), width, - //# TODO: try simplifying to just 'viewportHeight' - height: Math.min(viewportHeight, elPosition.fromElViewport.top + elPosition.height) - }; - }; + // TODO: try simplifying to just 'viewportHeight' + height: Math.min(viewportHeight, elPosition.fromElViewport.top + elPosition.height), + } + } - return { y, afterScroll }; -}); + return { y, afterScroll } + }) return takeScrollingScreenshots(scrolls, win, state, automationOptions) - .finally(resetScrollOverrides); -}; + .finally(resetScrollOverrides) +} -//# "app only" means we're hiding the runner UI -const isAppOnly = ({ capture }) => (capture === "viewport") || (capture === "fullPage"); +// "app only" means we're hiding the runner UI +const isAppOnly = ({ capture }) => { + return (capture === 'viewport') || (capture === 'fullPage') +} -const getShouldScale = function({ capture, scale }) { - if (isAppOnly({ capture })) { return scale; } else { return true; } -}; +const getShouldScale = ({ capture, scale }) => { + return isAppOnly({ capture }) ? scale : true +} -const getBlackout = function({ capture, blackout }) { - if (isAppOnly({ capture })) { return blackout; } else { return []; } -}; +const getBlackout = ({ capture, blackout }) => { + return isAppOnly({ capture }) ? blackout : [] +} -const takeScreenshot = function(Cypress, state, screenshotConfig, options = {}) { +const takeScreenshot = (Cypress, state, screenshotConfig, options = {}) => { const { capture, padding, clip, disableTimersAndAnimations, onBeforeScreenshot, - onAfterScreenshot - } = screenshotConfig; + onAfterScreenshot, + } = screenshotConfig - const { subject, runnable, name } = options; + const { subject, runnable } = options - const startTime = new Date(); + const startTime = new Date() - const send = (event, props, resolve) => Cypress.action(`cy:${event}`, props, resolve); + const send = (event, props, resolve) => { + Cypress.action(`cy:${event}`, props, resolve) + } - const sendAsync = (event, props) => new Promise(resolve => send(event, props, resolve)); + const sendAsync = (event, props) => { + return new Promise((resolve) => { + return send(event, props, resolve) + }) + } - const getOptions = isOpen => ({ - id: runnable.id, - isOpen, - appOnly: isAppOnly(screenshotConfig), - scale: getShouldScale(screenshotConfig), - waitForCommandSynchronization: !isAppOnly(screenshotConfig), - disableTimersAndAnimations, - blackout: getBlackout(screenshotConfig) - }); + const getOptions = (isOpen) => { + return { + id: runnable.id, + isOpen, + appOnly: isAppOnly(screenshotConfig), + scale: getShouldScale(screenshotConfig), + waitForCommandSynchronization: !isAppOnly(screenshotConfig), + disableTimersAndAnimations, + blackout: getBlackout(screenshotConfig), + } + } - const before = function() { + const before = () => { if (disableTimersAndAnimations) { - cy.pauseTimers(true); + cy.pauseTimers(true) } - return sendAsync("before:screenshot", getOptions(true)); - }; + return sendAsync('before:screenshot', getOptions(true)) + } - const after = function() { - send("after:screenshot", getOptions(false)); + const after = () => { + send('after:screenshot', getOptions(false)) if (disableTimersAndAnimations) { - return cy.pauseTimers(false); + return cy.pauseTimers(false) } - }; + } const automationOptions = _.extend({}, options, { capture, @@ -303,155 +327,169 @@ const takeScreenshot = function(Cypress, state, screenshotConfig, options = {}) x: 0, y: 0, width: getViewportWidth(state), - height: getViewportHeight(state) + height: getViewportHeight(state), }, padding, userClip: clip, viewport: { width: window.innerWidth, - height: window.innerHeight + height: window.innerHeight, }, scaled: getShouldScale(screenshotConfig), blackout: getBlackout(screenshotConfig), - startTime: startTime.toISOString() - }); + startTime: startTime.toISOString(), + }) - //# use the subject as $el or yield the wrapped documentElement - const $el = $dom.isElement(subject) ? - subject - : - $dom.wrap(state("document").documentElement); + // use the subject as $el or yield the wrapped documentElement + const $el = $dom.isElement(subject) + ? subject + : $dom.wrap(state('document').documentElement) return before() - .then(function() { - onBeforeScreenshot && onBeforeScreenshot.call(state("ctx"), $el); + .then(() => { + if (onBeforeScreenshot) { + onBeforeScreenshot.call(state('ctx'), $el) + } + + $Screenshot.onBeforeScreenshot($el) - $Screenshot.onBeforeScreenshot($el); + if ($dom.isElement(subject)) { + return takeElementScreenshot($el, state, automationOptions) + } - switch (false) { - case !$dom.isElement(subject): - return takeElementScreenshot($el, state, automationOptions); - case capture !== "fullPage": - return takeFullPageScreenshot(state, automationOptions); - default: - return automateScreenshot(state, automationOptions); - }}).then(function(props) { - onAfterScreenshot && onAfterScreenshot.call(state("ctx"), $el, props); + if (capture === 'fullPage') { + return takeFullPageScreenshot(state, automationOptions) + } - $Screenshot.onAfterScreenshot($el, props); + return automateScreenshot(state, automationOptions) + }) + .then((props) => { + if (onAfterScreenshot) { + onAfterScreenshot.call(state('ctx'), $el, props) + } - return props;}).finally(after); -}; + $Screenshot.onAfterScreenshot($el, props) -module.exports = function(Commands, Cypress, cy, state, config) { + return props + }) + .finally(after) +} - //# failure screenshot when not interactive - Cypress.on("runnable:after:run:async", function(test, runnable) { - const screenshotConfig = $Screenshot.getConfig(); +module.exports = function (Commands, Cypress, cy, state, config) { + // failure screenshot when not interactive + Cypress.on('runnable:after:run:async', (test, runnable) => { + const screenshotConfig = $Screenshot.getConfig() - if (!test.err || !screenshotConfig.screenshotOnRunFailure || config("isInteractive") || test.err.isPending) { return; } + if (!test.err || !screenshotConfig.screenshotOnRunFailure || config('isInteractive') || test.err.isPending) { + return + } + + // if a screenshot has not been taken (by cy.screenshot()) in the test + // that failed, we can bypass UI-changing and pixel-checking (simple: true) + // otheriwse, we need to do all the standard checks + // to make sure the UI is in the right place (simple: false) + screenshotConfig.capture = 'runner' - //# if a screenshot has not been taken (by cy.screenshot()) in the test - //# that failed, we can bypass UI-changing and pixel-checking (simple: true) - //# otheriwse, we need to do all the standard checks - //# to make sure the UI is in the right place (simple: false) - screenshotConfig.capture = "runner"; return takeScreenshot(Cypress, state, screenshotConfig, { runnable, - simple: !state("screenshotTaken"), + simple: !state('screenshotTaken'), testFailure: true, - timeout: config("responseTimeout") - }); - }); + timeout: config('responseTimeout'), + }) + }) - return Commands.addAll({ prevSubject: ["optional", "element", "window", "document"] }, { - screenshot(subject, name, options = {}) { - let userOptions = options; + Commands.addAll({ prevSubject: ['optional', 'element', 'window', 'document'] }, { + screenshot (subject, name, options = {}) { + let userOptions = options if (_.isObject(name)) { - userOptions = name; - name = null; + userOptions = name + name = null } - const withinSubject = state("withinSubject"); + const withinSubject = state('withinSubject') + if (withinSubject && $dom.isElement(withinSubject)) { - subject = withinSubject; + subject = withinSubject } - //# TODO: handle hook titles - const runnable = state("runnable"); + // TODO: handle hook titles + const runnable = state('runnable') options = _.defaults({}, userOptions, { log: true, - timeout: config("responseTimeout") - }); + timeout: config('responseTimeout'), + }) - const isWin = $dom.isWindow(subject); + const isWin = $dom.isWindow(subject) - let screenshotConfig = _.pick(options, "capture", "scale", "disableTimersAndAnimations", "blackout", "waitForCommandSynchronization", "padding", "clip", "onBeforeScreenshot", "onAfterScreenshot"); - screenshotConfig = $Screenshot.validate(screenshotConfig, "screenshot", options._log); - screenshotConfig = _.extend($Screenshot.getConfig(), screenshotConfig); + let screenshotConfig = _.pick(options, 'capture', 'scale', 'disableTimersAndAnimations', 'blackout', 'waitForCommandSynchronization', 'padding', 'clip', 'onBeforeScreenshot', 'onAfterScreenshot') + + screenshotConfig = $Screenshot.validate(screenshotConfig, 'screenshot', options._log) + screenshotConfig = _.extend($Screenshot.getConfig(), screenshotConfig) + + // set this regardless of options.log b/c its used by the + // yielded value below + let consoleProps = _.omit(screenshotConfig, 'scale', 'screenshotOnRunFailure') - //# set this regardless of options.log b/c its used by the - //# yielded value below - let consoleProps = _.omit(screenshotConfig, "scale", "screenshotOnRunFailure"); consoleProps = _.extend(consoleProps, { scaled: getShouldScale(screenshotConfig), - blackout: getBlackout(screenshotConfig) - }); + blackout: getBlackout(screenshotConfig), + }) if (name) { - consoleProps.name = name; + consoleProps.name = name } if (options.log) { options._log = Cypress.log({ message: name, - consoleProps() { - return consoleProps; - } - }); + consoleProps () { + return consoleProps + }, + }) } - if (!isWin && subject && (subject.length > 1)) { - $errUtils.throwErrByPath("screenshot.multiple_elements", { + if (!isWin && subject && subject.length > 1) { + $errUtils.throwErrByPath('screenshot.multiple_elements', { log: options._log, - args: { numElements: subject.length } - }); + args: { numElements: subject.length }, + }) } if ($dom.isElement(subject)) { - screenshotConfig.capture = "viewport"; + screenshotConfig.capture = 'viewport' } - state("screenshotTaken", true); + state('screenshotTaken', true) return takeScreenshot(Cypress, state, screenshotConfig, { name, subject, runnable, log: options._log, - timeout: options.timeout + timeout: options.timeout, }) - .then(function(props) { - const { duration, path, size } = props; - const { width, height } = props.dimensions; + .then((props) => { + const { duration, path, size } = props + const { width, height } = props.dimensions + + const takenPaths = state('screenshotPaths') || [] - const takenPaths = state("screenshotPaths") || []; - state("screenshotPaths", takenPaths.concat([path])); + state('screenshotPaths', takenPaths.concat([path])) _.extend(consoleProps, props, { - size: bytes(size, { unitSeparator: " " }), + size: bytes(size, { unitSeparator: ' ' }), duration: `${duration}ms`, - dimensions: `${width}px x ${height}px` - }); + dimensions: `${width}px x ${height}px`, + }) if (subject) { - consoleProps.subject = subject; + consoleProps.subject = subject } - return subject; - }); - } - }); -}; + return subject + }) + }, + }) +} diff --git a/packages/driver/src/cy/commands/task.js b/packages/driver/src/cy/commands/task.js index 03746bc1087a..f8ab36fa5fbc 100644 --- a/packages/driver/src/cy/commands/task.js +++ b/packages/driver/src/cy/commands/task.js @@ -1,91 +1,97 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); -const Promise = require("bluebird"); +const _ = require('lodash') +const Promise = require('bluebird') -const $utils = require("../../cypress/utils"); -const $errUtils = require("../../cypress/error_utils"); +const $utils = require('../../cypress/utils') +const $errUtils = require('../../cypress/error_utils') -module.exports = (Commands, Cypress, cy, state, config) => Commands.addAll({ - task(task, arg, options = {}) { - let consoleOutput, message; - const userOptions = options; - options = _.defaults({}, userOptions, { - log: true, - timeout: Cypress.config("taskTimeout") - }); +module.exports = (Commands, Cypress, cy) => { + Commands.addAll({ + task (task, arg, options = {}) { + const userOptions = options - if (options.log) { - consoleOutput = { - task, - arg - }; + options = _.defaults({}, userOptions, { + log: true, + timeout: Cypress.config('taskTimeout'), + }) - message = task; - if (arg) { - message += `, ${$utils.stringify(arg)}`; - } + let consoleOutput - options._log = Cypress.log({ - message, - consoleProps() { - return consoleOutput; + if (options.log) { + consoleOutput = { + task, + arg, } - }); - } - if (!task || !_.isString(task)) { - $errUtils.throwErrByPath("task.invalid_argument", { - onFail: options._log, - args: { task: task != null ? task : "" } - }); - } + let message = task - //# need to remove the current timeout - //# because we're handling timeouts ourselves - cy.clearTimeout(); + if (arg) { + message += `, ${$utils.stringify(arg)}` + } - return Cypress.backend("task", { - task, - arg, - timeout: options.timeout - }) - .timeout(options.timeout) - .then(function(result) { - if (options._log) { - _.extend(consoleOutput, { Yielded: result }); + options._log = Cypress.log({ + message, + consoleProps () { + return consoleOutput + }, + }) } - return result;}).catch(Promise.TimeoutError, () => $errUtils.throwErrByPath("task.timed_out", { - onFail: options._log, - args: { task, timeout: options.timeout } - })) - .catch({ timedOut: true }, error => $errUtils.throwErrByPath("task.server_timed_out", { - onFail: options._log, - args: { task, timeout: options.timeout, error: error.message } - })) + if (!task || !_.isString(task)) { + $errUtils.throwErrByPath('task.invalid_argument', { + onFail: options._log, + args: { task: task || '' }, + }) + } - .catch(function(error) { - //# re-throw if timedOut error from above - if (error.name === "CypressError") { throw error; } + // need to remove the current timeout + // because we're handling timeouts ourselves + cy.clearTimeout() - $errUtils.normalizeErrorStack(error); + return Cypress.backend('task', { + task, + arg, + timeout: options.timeout, + }) + .timeout(options.timeout) + .then((result) => { + if (options._log) { + _.extend(consoleOutput, { Yielded: result }) + } - if (error != null ? error.isKnownError : undefined) { - $errUtils.throwErrByPath("task.known_error", { + return result + }) + .catch(Promise.TimeoutError, () => { + $errUtils.throwErrByPath('task.timed_out', { onFail: options._log, - args: { task, error: error.message } - }); - } + args: { task, timeout: options.timeout }, + }) + }) + .catch({ timedOut: true }, (error) => { + $errUtils.throwErrByPath('task.server_timed_out', { + onFail: options._log, + args: { task, timeout: options.timeout, error: error.message }, + }) + }) + .catch((error) => { + // re-throw if timedOut error from above + if (error.name === 'CypressError') { + throw error + } + + $errUtils.normalizeErrorStack(error) - return $errUtils.throwErrByPath("task.failed", { - onFail: options._log, - args: { task, error: (error != null ? error.stack : undefined) || (error != null ? error.message : undefined) || error } - }); - }); - } -}); + if (error?.isKnownError) { + $errUtils.throwErrByPath('task.known_error', { + onFail: options._log, + args: { task, error: error.message }, + }) + } + + $errUtils.throwErrByPath('task.failed', { + onFail: options._log, + args: { task, error: error?.stack || error?.message || error }, + }) + }) + }, + }) +} diff --git a/packages/driver/src/cy/commands/traversals.js b/packages/driver/src/cy/commands/traversals.js index c4de7ca332ff..67af56b12c52 100644 --- a/packages/driver/src/cy/commands/traversals.js +++ b/packages/driver/src/cy/commands/traversals.js @@ -1,80 +1,89 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); - -const $dom = require("../../dom"); - -const traversals = "find filter not children eq closest first last next nextAll nextUntil parent parents parentsUntil prev prevAll prevUntil siblings".split(" "); - -module.exports = (Commands, Cypress, cy, state, config) => _.each(traversals, traversal => Commands.add(traversal, { prevSubject: "element" }, function(subject, arg1, arg2, options) { - let getElements; - if (_.isObject(arg1) && !_.isFunction(arg1)) { - options = arg1; - } - - if (_.isObject(arg2) && !_.isFunction(arg2)) { - options = arg2; - } - - const userOptions = options ? options : {}; - - options = _.defaults({}, userOptions, {log: true}); - - const getSelector = function() { - let args = _.chain([arg1, arg2]).reject(_.isFunction).reject(_.isObject).value(); - args = _.without(args, null, undefined); - return args.join(", "); - }; - - const consoleProps = { - Selector: getSelector(), - "Applied To": $dom.getElements(subject) - }; - - if (options.log !== false) { - options._log = Cypress.log({ - message: getSelector(), - consoleProps() { return consoleProps; } - }); - } - - const setEl = function($el) { - if (options.log === false) { return; } - - consoleProps.Yielded = $dom.getElements($el); - consoleProps.Elements = $el != null ? $el.length : undefined; - - return options._log.set({$el}); - }; - - return (getElements = function() { - //# catch sizzle errors here - let $el; - try { - $el = subject[traversal].call(subject, arg1, arg2); - - //# normalize the selector since jQuery won't have it - //# or completely borks it - $el.selector = getSelector(); - } catch (e) { - e.onFail = () => options._log.error(e); - throw e; - } - - setEl($el); - - return cy.verifyUpcomingAssertions($el, options, { - onRetry: getElements, - onFail(err) { - if (err.type === "existence") { - const node = $dom.stringify(subject, "short"); - return err.message += ` Queried from element: ${node}`; +const _ = require('lodash') + +const $dom = require('../../dom') + +const traversals = 'find filter not children eq closest first last next nextAll nextUntil parent parents parentsUntil prev prevAll prevUntil siblings'.split(' ') + +module.exports = (Commands, Cypress, cy) => { + _.each(traversals, (traversal) => { + Commands.add(traversal, { prevSubject: 'element' }, (subject, arg1, arg2, options) => { + if (_.isObject(arg1) && !_.isFunction(arg1)) { + options = arg1 + } + + if (_.isObject(arg2) && !_.isFunction(arg2)) { + options = arg2 + } + + const userOptions = options || {} + + options = _.defaults({}, userOptions, { log: true }) + + const getSelector = () => { + let args = _.chain([arg1, arg2]).reject(_.isFunction).reject(_.isObject).value() + + args = _.without(args, null, undefined) + + return args.join(', ') + } + + const consoleProps = { + Selector: getSelector(), + 'Applied To': $dom.getElements(subject), + } + + if (options.log !== false) { + options._log = Cypress.log({ + message: getSelector(), + consoleProps () { + return consoleProps + }, + }) + } + + const setEl = ($el) => { + if (options.log === false) { + return + } + + consoleProps.Yielded = $dom.getElements($el) + consoleProps.Elements = $el?.length + + return options._log.set({ $el }) + } + + const getElements = () => { + let $el + + try { + $el = subject[traversal].call(subject, arg1, arg2) + + // normalize the selector since jQuery won't have it + // or completely borks it + $el.selector = getSelector() + } catch (e) { + e.onFail = () => { + return options._log.error(e) + } + + throw e } + + setEl($el) + + return cy.verifyUpcomingAssertions($el, options, { + onRetry: getElements, + onFail (err) { + if (err.type === 'existence') { + const node = $dom.stringify(subject, 'short') + + err.message += ` Queried from element: ${node}` + } + }, + }) } - }); - })(); -})); + + return getElements() + }) + }) +} diff --git a/packages/driver/src/cy/commands/waiting.js b/packages/driver/src/cy/commands/waiting.js index 2567ac4df34e..4b42383083eb 100644 --- a/packages/driver/src/cy/commands/waiting.js +++ b/packages/driver/src/cy/commands/waiting.js @@ -1,242 +1,267 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS204: Change includes calls to have a more natural evaluation order - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); -const Promise = require("bluebird"); - -const $errUtils = require("../../cypress/error_utils"); +const _ = require('lodash') +const Promise = require('bluebird') + +const $errUtils = require('../../cypress/error_utils') const getNumRequests = (state, alias) => { - let left; - const requests = (left = state("aliasRequests")) != null ? left : {}; - const index = requests[alias] != null ? requests[alias] : (requests[alias] = 0); + const requests = state('aliasRequests') || {} + + requests[alias] = requests[alias] || 0 + + const index = requests[alias] - requests[alias] += 1; + requests[alias] += 1 - state("aliasRequests", requests); + state('aliasRequests', requests) - return [index, _.ordinalize(requests[alias])]; -}; + return [index, _.ordinalize(requests[alias])] +} -const throwErr = arg => $errUtils.throwErrByPath("wait.invalid_1st_arg", {args: {arg}}); +const throwErr = (arg) => { + $errUtils.throwErrByPath('wait.invalid_1st_arg', { args: { arg } }) +} -module.exports = function(Commands, Cypress, cy, state, config) { - const waitFunction = () => $errUtils.throwErrByPath("wait.fn_deprecated"); +module.exports = (Commands, Cypress, cy, state) => { + const waitFunction = () => { + $errUtils.throwErrByPath('wait.fn_deprecated') + } - let userOptions = null; + let userOptions = null - const waitNumber = function(subject, ms, options) { - //# increase the timeout by the delta - cy.timeout(ms, true, "wait"); + const waitNumber = (subject, ms, options) => { + // increase the timeout by the delta + cy.timeout(ms, true, 'wait') if (options.log !== false) { options._log = Cypress.log({ - consoleProps() { return { - "Waited For": `${ms}ms before continuing`, - "Yielded": subject - }; } - }); + consoleProps () { + return { + 'Waited For': `${ms}ms before continuing`, + 'Yielded': subject, + } + }, + }) } return Promise - .delay(ms, "wait") - .return(subject); - }; + .delay(ms, 'wait') + .return(subject) + } + + const waitString = (subject, str, options) => { + let log - const waitString = function(subject, str, options) { - let log; if (options.log !== false) { - log = (options._log = Cypress.log({ - type: "parent", - aliasType: "route", - options: userOptions - })); + log = options._log = Cypress.log({ + type: 'parent', + aliasType: 'route', + options: userOptions, + }) } - var checkForXhr = function(alias, type, index, num, options) { - options.type = type; + const checkForXhr = function (alias, type, index, num, options) { + options.type = type - //# append .type to the alias - const xhr = cy.getIndexedXhrByAlias(alias + "." + type, index); + // append .type to the alias + const xhr = cy.getIndexedXhrByAlias(`${alias}.${type}`, index) - //# return our xhr object - if (xhr) { return Promise.resolve(xhr); } + // return our xhr object + if (xhr) { + return Promise.resolve(xhr) + } - options.error = $errUtils.errMsgByPath("wait.timed_out", { + options.error = $errUtils.errMsgByPath('wait.timed_out', { timeout: options.timeout, alias, num, - type - }); - - const args = arguments; - - return cy.retry(() => checkForXhr.apply(window, args) - , options); - }; - - const waitForXhr = function(str, options) { - //# we always want to strip everything after the last '.' - //# since we support alias property 'request' - let aliasObj, needle, str2; - if ((_.indexOf(str, ".") === -1) || - (needle = str.slice(1), _.keys(cy.state("aliases")).includes(needle))) { - [str, str2] = [str, null]; + type, + }) + + const args = [alias, type, index, num, options] + + return cy.retry(() => { + return checkForXhr.apply(window, args) + }, options) + } + + const waitForXhr = function (str, options) { + let str2 + + // we always want to strip everything after the last '.' + // since we support alias property 'request' + if ((_.indexOf(str, '.') === -1) || + _.keys(cy.state('aliases')).includes(str.slice(1))) { + str2 = null } else { // potentially request, response or index - const allParts = _.split(str, '.'); - [str, str2] = [_.join(_.dropRight(allParts, 1), '.'), _.last(allParts)]; + const allParts = _.split(str, '.') + + str = _.join(_.dropRight(allParts, 1), '.') + str2 = _.last(allParts) } - if (!(aliasObj = cy.getAlias(str, "wait", log))) { - cy.aliasNotFoundFor(str, "wait", log); + const aliasObj = cy.getAlias(str, 'wait', log) + + if (!aliasObj) { + cy.aliasNotFoundFor(str, 'wait', log) } - //# if this alias is for a route then poll - //# until we find the response xhr object - //# by its alias - const {alias, command} = aliasObj; + // if this alias is for a route then poll + // until we find the response xhr object + // by its alias + const { alias, command } = aliasObj - str = _.compact([alias, str2]).join("."); + str = _.compact([alias, str2]).join('.') - const type = cy.getXhrTypeByAlias(str); + const type = cy.getXhrTypeByAlias(str) - const [ index, num ] = getNumRequests(state, alias); + const [index, num] = getNumRequests(state, alias) - //# if we have a command then continue to - //# build up an array of referencesAlias - //# because wait can reference an array of aliases + // if we have a command then continue to + // build up an array of referencesAlias + // because wait can reference an array of aliases if (log) { - let left; - const referencesAlias = (left = log.get("referencesAlias")) != null ? left : []; - const aliases = [].concat(referencesAlias); + const referencesAlias = log.get('referencesAlias') || [] + const aliases = [].concat(referencesAlias) if (str) { aliases.push({ name: str, cardinal: index + 1, - ordinal: num - }); + ordinal: num, + }) } - log.set("referencesAlias", aliases); + log.set('referencesAlias', aliases) } - if (command.get("name") !== "route") { - $errUtils.throwErrByPath("wait.invalid_alias", { + if (command.get('name') !== 'route') { + $errUtils.throwErrByPath('wait.invalid_alias', { onFail: options._log, - args: { alias } - }); + args: { alias }, + }) } - //# create shallow copy of each options object - //# but slice out the error since we may set - //# the error related to a previous xhr - const { - timeout - } = options; - const requestTimeout = options.requestTimeout != null ? options.requestTimeout : timeout; - const responseTimeout = options.responseTimeout != null ? options.responseTimeout : timeout; - - const waitForRequest = function() { - options = _.omit(options, "_runnableTimeout"); - options.timeout = requestTimeout != null ? requestTimeout : Cypress.config("requestTimeout"); - return checkForXhr(alias, "request", index, num, options); - }; - - const waitForResponse = function() { - options = _.omit(options, "_runnableTimeout"); - options.timeout = responseTimeout != null ? responseTimeout : Cypress.config("responseTimeout"); - return checkForXhr(alias, "response", index, num, options); - }; - - //# if we were only waiting for the request - //# then resolve immediately do not wait for response - if (type === "request") { - return waitForRequest(); - } else { - return waitForRequest().then(waitForResponse); + // create shallow copy of each options object + // but slice out the error since we may set + // the error related to a previous xhr + const { timeout } = options + const requestTimeout = options.requestTimeout || timeout + const responseTimeout = options.responseTimeout || timeout + + const waitForRequest = () => { + options = _.omit(options, '_runnableTimeout') + options.timeout = requestTimeout || Cypress.config('requestTimeout') + + return checkForXhr(alias, 'request', index, num, options) + } + + const waitForResponse = () => { + options = _.omit(options, '_runnableTimeout') + options.timeout = responseTimeout || Cypress.config('responseTimeout') + + return checkForXhr(alias, 'response', index, num, options) + } + + // if we were only waiting for the request + // then resolve immediately do not wait for response + if (type === 'request') { + return waitForRequest() } - }; + + return waitForRequest().then(waitForResponse) + } return Promise - .map([].concat(str), str => //# we may get back an xhr value instead - //# of a promise, so we have to wrap this - //# in another promise :-( - waitForXhr(str, _.omit(options, "error"))).then(function(responses) { - //# if we only asked to wait for one alias - //# then return that, else return the array of xhr responses - const ret = responses.length === 1 ? responses[0] : responses; + .map([].concat(str), (str) => { + // we may get back an xhr value instead + // of a promise, so we have to wrap this + // in another promise :-( + return waitForXhr(str, _.omit(options, 'error')) + }) + .then((responses) => { + // if we only asked to wait for one alias + // then return that, else return the array of xhr responses + const ret = responses.length === 1 ? responses[0] : responses if (log) { - log.set("consoleProps", () => ({ - "Waited For": (_.map(log.get("referencesAlias"), 'name') || []).join(", "), - "Yielded": ret - })); - - log.snapshot().end(); + log.set('consoleProps', () => { + return { + 'Waited For': (_.map(log.get('referencesAlias'), 'name') || []).join(', '), + 'Yielded': ret, + } + }) + + log.snapshot().end() } - return ret; - }); - }; + return ret + }) + } - return Commands.addAll({ prevSubject: "optional" }, { - wait(subject, msOrFnOrAlias, options = {}) { - userOptions = options; + Commands.addAll({ prevSubject: 'optional' }, { + wait (subject, msOrFnOrAlias, options = {}) { + userOptions = options - //# check to ensure options is an object - //# if its a string the user most likely is trying - //# to wait on multiple aliases and forget to make this - //# an array + // check to ensure options is an object + // if its a string the user most likely is trying + // to wait on multiple aliases and forget to make this + // an array if (_.isString(userOptions)) { - $errUtils.throwErrByPath("wait.invalid_arguments"); + $errUtils.throwErrByPath('wait.invalid_arguments') } - options = _.defaults({}, userOptions, { log: true }); - const args = [subject, msOrFnOrAlias, options]; + options = _.defaults({}, userOptions, { log: true }) + const args = [subject, msOrFnOrAlias, options] try { - switch (false) { - case !_.isFinite(msOrFnOrAlias): - return waitNumber.apply(window, args); - case !_.isFunction(msOrFnOrAlias): - return waitFunction(); - case !_.isString(msOrFnOrAlias): - return waitString.apply(window, args); - case !_.isArray(msOrFnOrAlias) || !!_.isEmpty(msOrFnOrAlias): - return waitString.apply(window, args); - default: - //# figure out why this error failed - var arg = (() => { switch (false) { - case !_.isNaN(msOrFnOrAlias): return "NaN"; - case msOrFnOrAlias !== Infinity: return "Infinity"; - case !_.isSymbol(msOrFnOrAlias): return msOrFnOrAlias.toString(); - default: - try { - return JSON.stringify(msOrFnOrAlias); - } catch (error) { - return "an invalid argument"; - } - } })(); - - return throwErr(arg); + if (_.isFinite(msOrFnOrAlias)) { + return waitNumber.apply(window, args) + } + + if (_.isFunction(msOrFnOrAlias)) { + return waitFunction() + } + + if (_.isString(msOrFnOrAlias)) { + return waitString.apply(window, args) + } + + if (_.isArray(msOrFnOrAlias) && !_.isEmpty(msOrFnOrAlias)) { + return waitString.apply(window, args) + } + + // figure out why this error failed + if (_.isNaN(msOrFnOrAlias)) { + throwErr('NaN') + } + + if (msOrFnOrAlias === Infinity) { + throwErr('Infinity') + } + + if (_.isSymbol(msOrFnOrAlias)) { + throwErr(msOrFnOrAlias.toString()) } + + let arg + + try { + arg = JSON.stringify(msOrFnOrAlias) + } catch (error) { + arg = 'an invalid argument' + } + + return throwErr(arg) } catch (err) { - if (err.name === "CypressError") { - throw err; + if (err.name === 'CypressError') { + throw err } else { - //# whatever was passed in could not be parsed - //# by our switch case - return throwErr("an invalid argument"); + // whatever was passed in could not be parsed + // by our switch case + return throwErr('an invalid argument') } } - } - }); -}; + }, + }) +} diff --git a/packages/driver/src/cy/commands/window.js b/packages/driver/src/cy/commands/window.js index fdec9729c8d4..b889b31ddee3 100644 --- a/packages/driver/src/cy/commands/window.js +++ b/packages/driver/src/cy/commands/window.js @@ -1,270 +1,295 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); -const Promise = require("bluebird"); +const _ = require('lodash') +const Promise = require('bluebird') -const $errUtils = require("../../cypress/error_utils"); +const $errUtils = require('../../cypress/error_utils') const viewports = { - "macbook-15" : "1440x900", - "macbook-13" : "1280x800", - "macbook-11" : "1366x768", - "ipad-2" : "768x1024", - "ipad-mini" : "768x1024", - "iphone-xr" : "414x896", - "iphone-x" : "375x812", - "iphone-6+" : "414x736", - "iphone-6" : "375x667", - "iphone-5" : "320x568", - "iphone-4" : "320x480", - "iphone-3" : "320x480", - "samsung-s10" : "360x760", - "samsung-note9" : "414x846" -}; - -const validOrientations = ["landscape", "portrait"]; - -//# NOTE: this is outside the function because its 'global' state to the -//# cypress application and not local to the specific run. the last -//# viewport set is always the 'current' viewport as opposed to the -//# config. there was a bug where re-running tests without a hard -//# refresh would cause viewport to hang -let currentViewport = null; - -module.exports = function(Commands, Cypress, cy, state, config) { - const defaultViewport = _.pick(config(), "viewportWidth", "viewportHeight"); - - //# currentViewport could already be set due to previous runs - if (currentViewport == null) { currentViewport = defaultViewport; } - - Cypress.on("test:before:run:async", () => //# if we have viewportDefaults it means - //# something has changed the default and we - //# need to restore prior to running the next test - //# after which we simply null and wait for the - //# next viewport change - setViewportAndSynchronize(defaultViewport.viewportWidth, defaultViewport.viewportHeight)); - - var setViewportAndSynchronize = function(width, height) { - const viewport = {viewportWidth: width, viewportHeight: height}; - - //# store viewport on the state for logs - state(viewport); - - return new Promise(function(resolve) { - if ((currentViewport.viewportWidth === width) && (currentViewport.viewportHeight === height)) { - //# noop if viewport won't change - return resolve(currentViewport); + 'macbook-15': '1440x900', + 'macbook-13': '1280x800', + 'macbook-11': '1366x768', + 'ipad-2': '768x1024', + 'ipad-mini': '768x1024', + 'iphone-xr': '414x896', + 'iphone-x': '375x812', + 'iphone-6+': '414x736', + 'iphone-6': '375x667', + 'iphone-5': '320x568', + 'iphone-4': '320x480', + 'iphone-3': '320x480', + 'samsung-s10': '360x760', + 'samsung-note9': '414x846', +} + +const validOrientations = ['landscape', 'portrait'] + +// NOTE: this is outside the function because its 'global' state to the +// cypress application and not local to the specific run. the last +// viewport set is always the 'current' viewport as opposed to the +// config. there was a bug where re-running tests without a hard +// refresh would cause viewport to hang +let currentViewport = null + +module.exports = (Commands, Cypress, cy, state, config) => { + const defaultViewport = _.pick(config(), 'viewportWidth', 'viewportHeight') + + // currentViewport could already be set due to previous runs + currentViewport = currentViewport || defaultViewport + + Cypress.on('test:before:run:async', () => { + // if we have viewportDefaults it means + // something has changed the default and we + // need to restore prior to running the next test + // after which we simply null and wait for the + // next viewport change + setViewportAndSynchronize(defaultViewport.viewportWidth, defaultViewport.viewportHeight) + }) + + const setViewportAndSynchronize = (width, height) => { + const viewport = { viewportWidth: width, viewportHeight: height } + + // store viewport on the state for logs + state(viewport) + + return new Promise((resolve) => { + if (currentViewport.viewportWidth === width && currentViewport.viewportHeight === height) { + // noop if viewport won't change + return resolve(currentViewport) } currentViewport = { viewportWidth: width, - viewportHeight: height - }; + viewportHeight: height, + } + + // force our UI to change to the viewport and wait for it + // to be updated + return Cypress.action('cy:viewport:changed', viewport, () => { + return resolve(viewport) + }) + }) + } - //# force our UI to change to the viewport and wait for it - //# to be updated - return Cypress.action("cy:viewport:changed", viewport, () => resolve(viewport)); - }); - }; + Commands.addAll({ + title (options = {}) { + const userOptions = options - return Commands.addAll({ - title(options = {}) { - let resolveTitle; - const userOptions = options; - options = _.defaults({}, userOptions, {log: true}); + options = _.defaults({}, userOptions, { log: true }) if (options.log) { options._log = Cypress.log({ - }); + }) } - return (resolveTitle = () => { - const doc = state("document"); + const resolveTitle = () => { + const doc = state('document') - const title = (doc && doc.title) || ""; + const title = (doc && doc.title) || '' return cy.verifyUpcomingAssertions(title, options, { - onRetry: resolveTitle - }); - })(); + onRetry: resolveTitle, + }) + } + + return resolveTitle() }, - window(options = {}) { - let verifyAssertions; - const userOptions = options; - options = _.defaults({}, userOptions, {log: true}); + window (options = {}) { + const userOptions = options + + options = _.defaults({}, userOptions, { log: true }) if (options.log) { - options._log = Cypress.log({ - }); + options._log = Cypress.log({}) } const getWindow = () => { - const window = state("window"); - if (!window) { $errUtils.throwErrByPath("window.iframe_undefined", { onFail: options._log }); } + const window = state('window') - return window; - }; + if (!window) { + $errUtils.throwErrByPath('window.iframe_undefined', { onFail: options._log }) + } + + return window + } - //# wrap retrying into its own - //# separate function - var retryWindow = () => { + // wrap retrying into its own + // separate function + const retryWindow = () => { return Promise .try(getWindow) - .catch(err => { - options.error = err; - return cy.retry(retryWindow, options); - }); - }; - - return (verifyAssertions = () => { - return Promise.try(retryWindow).then(win => { + .catch((err) => { + options.error = err + + return cy.retry(retryWindow, options) + }) + } + + const verifyAssertions = () => { + return Promise.try(retryWindow).then((win) => { return cy.verifyUpcomingAssertions(win, options, { - onRetry: verifyAssertions - }); - }); - })(); + onRetry: verifyAssertions, + }) + }) + } + + return verifyAssertions() }, - document(options = {}) { - let verifyAssertions; - const userOptions = options; - options = _.defaults({}, userOptions, {log: true}); + document (options = {}) { + const userOptions = options + + options = _.defaults({}, userOptions, { log: true }) if (options.log) { - options._log = Cypress.log({ - }); + options._log = Cypress.log({}) } const getDocument = () => { - const win = state("window"); - //# TODO: add failing test around logging twice - if (!(win != null ? win.document : undefined)) { $errUtils.throwErrByPath("window.iframe_doc_undefined"); } + const win = state('window') - return win.document; - }; + // TODO: add failing test around logging twice + if (!win?.document) { + $errUtils.throwErrByPath('window.iframe_doc_undefined') + } - //# wrap retrying into its own - //# separate function - var retryDocument = () => { + return win.document + } + + // wrap retrying into its own + // separate function + const retryDocument = () => { return Promise .try(getDocument) - .catch(err => { - options.error = err; - return cy.retry(retryDocument, options); - }); - }; - - return (verifyAssertions = () => { - return Promise.try(retryDocument).then(doc => { + .catch((err) => { + options.error = err + + return cy.retry(retryDocument, options) + }) + } + + const verifyAssertions = () => { + return Promise.try(retryDocument).then((doc) => { return cy.verifyUpcomingAssertions(doc, options, { - onRetry: verifyAssertions - }); - }); - })(); + onRetry: verifyAssertions, + }) + }) + } + + return verifyAssertions() }, - viewport(presetOrWidth, heightOrOrientation, options = {}) { - let height, width; - const userOptions = options; + viewport (presetOrWidth, heightOrOrientation, options = {}) { + const userOptions = options if (_.isObject(heightOrOrientation)) { - options = heightOrOrientation; + options = heightOrOrientation } - options = _.defaults({}, userOptions, { log: true }); + options = _.defaults({}, userOptions, { log: true }) + + let height + let width if (options.log) { + // The type of presetOrWidth is either string or number + // When preset => string + // When width => number + const isPreset = typeof presetOrWidth === 'string' + options._log = Cypress.log({ - consoleProps() { - const obj = {}; - if (preset) { obj.Preset = preset; } - obj.Width = width; - obj.Height = height; - return obj; - } - }); + consoleProps () { + const obj = {} + + if (isPreset) { + obj.Preset = presetOrWidth + } + + obj.Width = width + obj.Height = height + + return obj + }, + }) } const throwErrBadArgs = () => { - return $errUtils.throwErrByPath("viewport.bad_args", { onFail: options._log }); - }; - - const widthAndHeightAreValidNumbers = (width, height) => _.every([width, height], val => _.isNumber(val) && _.isFinite(val)); - - const widthAndHeightAreWithinBounds = (width, height) => _.every([width, height], val => val >= 0); - - switch (false) { - case !_.isString(presetOrWidth) || !_.isBlank(presetOrWidth): - $errUtils.throwErrByPath("viewport.empty_string", { onFail: options._log }); - break; - - case !_.isString(presetOrWidth): - var getPresetDimensions = preset => { - try { - return _.map(viewports[presetOrWidth].split("x"), Number); - } catch (e) { - const presets = _.keys(viewports).join(", "); - return $errUtils.throwErrByPath("viewport.missing_preset", { - onFail: options._log, - args: { preset, presets } - }); - } - }; - - var orientationIsValidAndLandscape = orientation => { - if (!validOrientations.includes(orientation)) { - const all = validOrientations.join("` or `"); - $errUtils.throwErrByPath("viewport.invalid_orientation", { - onFail: options._log, - args: { all, orientation } - }); - } + return $errUtils.throwErrByPath('viewport.bad_args', { onFail: options._log }) + } - return orientation === "landscape"; - }; + const widthAndHeightAreValidNumbers = (width, height) => { + return _.every([width, height], (val) => { + return _.isNumber(val) && _.isFinite(val) + }) + } - var preset = presetOrWidth; - var orientation = heightOrOrientation; + const widthAndHeightAreWithinBounds = (width, height) => { + return _.every([width, height], (val) => { + return val >= 0 + }) + } + + if (_.isString(presetOrWidth) && _.isBlank(presetOrWidth)) { + $errUtils.throwErrByPath('viewport.empty_string', { onFail: options._log }) + } else if (_.isString(presetOrWidth)) { + const getPresetDimensions = (preset) => { + try { + return _.map(viewports[presetOrWidth].split('x'), Number) + } catch (e) { + const presets = _.keys(viewports).join(', ') + + return $errUtils.throwErrByPath('viewport.missing_preset', { + onFail: options._log, + args: { preset, presets }, + }) + } + } - //# get preset, split by x, convert to a number - var dimensions = getPresetDimensions(preset); + const orientationIsValidAndLandscape = (orientation) => { + if (!validOrientations.includes(orientation)) { + const all = validOrientations.join('` or `') - if (_.isString(orientation)) { - if (orientationIsValidAndLandscape(orientation)) { - dimensions.reverse(); - } + $errUtils.throwErrByPath('viewport.invalid_orientation', { + onFail: options._log, + args: { all, orientation }, + }) } - [width, height] = dimensions; - break; + return orientation === 'landscape' + } - case !widthAndHeightAreValidNumbers(presetOrWidth, heightOrOrientation): - width = presetOrWidth; - height = heightOrOrientation; + const preset = presetOrWidth + const orientation = heightOrOrientation - if (!widthAndHeightAreWithinBounds(width, height)) { - $errUtils.throwErrByPath("viewport.dimensions_out_of_range", { onFail: options._log }); + // get preset, split by x, convert to a number + const dimensions = getPresetDimensions(preset) + + if (_.isString(orientation)) { + if (orientationIsValidAndLandscape(orientation)) { + dimensions.reverse() } - break; + } + + [width, height] = dimensions + } else if (widthAndHeightAreValidNumbers(presetOrWidth, heightOrOrientation)) { + width = presetOrWidth + height = heightOrOrientation - default: - throwErrBadArgs(); + if (!widthAndHeightAreWithinBounds(width, height)) { + $errUtils.throwErrByPath('viewport.dimensions_out_of_range', { onFail: options._log }) + } + } else { + throwErrBadArgs() } return setViewportAndSynchronize(width, height) - .then(function(viewport) { + .then((viewport) => { if (options._log) { - options._log.set(viewport); + options._log.set(viewport) } - return null; - }); - } + return null + }) + }, - }); -}; + }) +} diff --git a/packages/driver/src/cy/commands/xhr.js b/packages/driver/src/cy/commands/xhr.js index 81f1ebf819ac..579e1cab9cf1 100644 --- a/packages/driver/src/cy/commands/xhr.js +++ b/packages/driver/src/cy/commands/xhr.js @@ -1,544 +1,533 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require("lodash"); -const Promise = require("bluebird"); - -const $utils = require("../../cypress/utils"); -const $errUtils = require("../../cypress/error_utils"); -const $Server = require("../../cypress/server"); -const $Location = require("../../cypress/location"); - -let server = null; - -const tryDecodeUri = function(uri) { +/* globals Cypress */ +const _ = require('lodash') +const Promise = require('bluebird') + +const $utils = require('../../cypress/utils') +const $errUtils = require('../../cypress/error_utils') +const $Server = require('../../cypress/server') +const $Location = require('../../cypress/location') + +let server = null + +const tryDecodeUri = (uri) => { try { - return decodeURI(uri); + return decodeURI(uri) } catch (error) { - return uri; + return uri } -}; +} -const getServer = () => server != null ? server : unavailableErr(); +const cancelPendingXhrs = () => server ? server.cancelPendingXhrs() : null -const cancelPendingXhrs = function() { +const reset = function () { if (server) { - server.cancelPendingXhrs(); + server.restore() } - return null; -}; + server = null +} -const reset = function() { - if (server) { - server.restore(); - } - - return server = null; -}; +const isUrlLikeArgs = (url, response) => { + return (!_.isObject(url) && !_.isObject(response)) || + (_.isRegExp(url) || _.isString(url)) +} -const isUrlLikeArgs = (url, response) => (!_.isObject(url) && !_.isObject(response)) || - (_.isRegExp(url) || _.isString(url)); +const getUrl = (options) => { + return options.originalUrl || options.url +} -const getUrl = options => options.originalUrl || options.url; +const unavailableErr = () => { + return $errUtils.throwErrByPath('server.unavailable') +} -var unavailableErr = () => $errUtils.throwErrByPath("server.unavailable"); +const getDisplayName = (route) => route && route.response ? 'xhr stub' : 'xhr' -const getDisplayName = function(route) { - if (route && (route.response != null)) { return "xhr stub"; } else { return "xhr"; } -}; +const stripOrigin = (url) => { + const location = $Location.create(url) -const stripOrigin = function(url) { - const location = $Location.create(url); - return url.replace(location.origin, ""); -}; + return url.replace(location.origin, '') +} -const getXhrServer = function(state) { - let left; - return (left = state("server")) != null ? left : unavailableErr(); -}; +const getXhrServer = (state) => state('server') || unavailableErr() -const setRequest = function(state, xhr, alias) { - let left; - const requests = (left = state("requests")) != null ? left : []; +const setRequest = (state, xhr, alias) => { + const requests = state('requests') || [] requests.push({ xhr, - alias - }); + alias, + }) - return state("requests", requests); -}; + return state('requests', requests) +} -const setResponse = function(state, xhr) { - let left; - const obj = _.find(state("requests"), { xhr }); +const setResponse = (state, xhr) => { + const obj = _.find(state('requests'), { xhr }) - //# if we've been reset between tests and an xhr - //# leaked through, then we may not be able to associate - //# this response correctly - if (!obj) { return; } + // if we've been reset between tests and an xhr + // leaked through, then we may not be able to associate + // this response correctly + if (!obj) { + return + } - const index = state("requests").indexOf(obj); + const index = state('requests').indexOf(obj) - const responses = (left = state("responses")) != null ? left : []; + const responses = state('responses') || [] - //# set the response in the same index as the request - //# so we can later wait on this specific index'd response - //# else its not deterministic + // set the response in the same index as the request + // so we can later wait on this specific index'd response + // else its not deterministic responses[index] = { xhr, - alias: (obj != null ? obj.alias : undefined) - }; + alias: obj?.alias, + } - return state("responses", responses); -}; + return state('responses', responses) +} -const startXhrServer = function(cy, state, config) { - const logs = {}; +const startXhrServer = (cy, state, config) => { + const logs = {} server = $Server.create({ - xhrUrl: config("xhrUrl"), + xhrUrl: config('xhrUrl'), stripOrigin, - //# shouldnt these stubs be called routes? - //# rename everything related to stubs => routes + // shouldnt these stubs be called routes? + // rename everything related to stubs => routes onSend: (xhr, stack, route) => { - let log, rl; - const alias = route != null ? route.alias : undefined; + const alias = route?.alias + + setRequest(state, xhr, alias) - setRequest(state, xhr, alias); + const rl = route && route.log - if (rl = route && route.log) { - const numResponses = rl.get("numResponses"); - rl.set("numResponses", numResponses + 1); + if (rl) { + const numResponses = rl.get('numResponses') + + rl.set('numResponses', numResponses + 1) } - logs[xhr.id] = (log = Cypress.log({ - message: "", - name: "xhr", + const log = logs[xhr.id] = Cypress.log({ + message: '', + name: 'xhr', displayName: getDisplayName(route), alias, - aliasType: "route", - type: "parent", - event: true, + aliasType: 'route', + type: 'parent', + event: true, consoleProps: () => { const consoleObj = { - Alias: alias, - Method: xhr.method, - URL: xhr.url, - "Matched URL": (route != null ? route.url : undefined), - Status: xhr.statusMessage, - Duration: xhr.duration, - "Stubbed": route && (route.response != null) ? "Yes" : "No", - Request: xhr.request, - Response: xhr.response, - XHR: xhr._getXhr() - }; + Alias: alias, + Method: xhr.method, + URL: xhr.url, + 'Matched URL': route?.url, + Status: xhr.statusMessage, + Duration: xhr.duration, + 'Stubbed': route && route.response != null ? 'Yes' : 'No', + Request: xhr.request, + Response: xhr.response, + XHR: xhr._getXhr(), + } if (route && route.is404) { - consoleObj.Note = "This request did not match any of your routes. It was automatically sent back '404'. Setting cy.server({force404: false}) will turn off this behavior."; + consoleObj.Note = 'This request did not match any of your routes. It was automatically sent back \'404\'. Setting cy.server({force404: false}) will turn off this behavior.' } - consoleObj.groups = () => [ - { - name: "Initiator", - items: [stack], - label: false - } - ]; + consoleObj.groups = () => { + return [ + { + name: 'Initiator', + items: [stack], + label: false, + }, + ] + } - return consoleObj; + return consoleObj }, - renderProps() { - let indicator; - const status = (() => { switch (false) { - case !xhr.aborted: - indicator = "aborted"; - return "(aborted)"; - case !xhr.canceled: - indicator = "aborted"; - return "(canceled)"; - case xhr.status <= 0: - return xhr.status; - default: - indicator = "pending"; - return "---"; - } })(); - - if (indicator == null) { indicator = /^2/.test(status) ? "successful" : "bad"; } + renderProps () { + let indicator + let status + + if (xhr.aborted) { + indicator = 'aborted' + status = '(aborted)' + } else if (xhr.canceled) { + indicator = 'aborted' + status = '(canceled)' + } else if (xhr.status > 0) { + status = xhr.status + } else { + indicator = 'pending' + status = '---' + } + + if (!indicator) { + indicator = /^2/.test(status) ? 'successful' : 'bad' + } return { indicator, - message: `${xhr.method} ${status} ${stripOrigin(xhr.url)}` - }; - } - })); + message: `${xhr.method} ${status} ${stripOrigin(xhr.url)}`, + } + }, + }) - return log.snapshot("request"); + return log.snapshot('request') }, - onLoad: xhr => { - let log; - setResponse(state, xhr); + onLoad: (xhr) => { + setResponse(state, xhr) + + const log = logs[xhr.id] - if (log = logs[xhr.id]) { - return log.snapshot("response").end(); + if (log) { + return log.snapshot('response').end() } }, - onNetworkError(xhr) { - let log; - const err = $errUtils.cypressErrByPath("xhr.network_error"); + onNetworkError (xhr) { + const err = $errUtils.cypressErrByPath('xhr.network_error') + + const log = logs[xhr.id] - if (log = logs[xhr.id]) { - return log.snapshot("failed").error(err); + if (log) { + return log.snapshot('failed').error(err) } }, - onFixtureError(xhr, err) { - err = $errUtils.cypressErr(err); + onFixtureError (xhr, err) { + err = $errUtils.cypressErr(err) - return this.onError(xhr, err); + return this.onError(xhr, err) }, - onError(xhr, err) { - let log; - err.onFail = function() {}; + onError (xhr, err) { + err.onFail = function () {} - if (log = logs[xhr.id]) { - log.snapshot("error").error(err); + const log = logs[xhr.id] + + if (log) { + log.snapshot('error').error(err) } - //# re-throw the error since this came from AUT code, and needs to - //# cause an 'uncaught:exception' event. This error will be caught in - //# top.onerror with stack as 5th argument. - throw err; + // re-throw the error since this came from AUT code, and needs to + // cause an 'uncaught:exception' event. This error will be caught in + // top.onerror with stack as 5th argument. + throw err }, onXhrAbort: (xhr, stack) => { - let log; - setResponse(state, xhr); + setResponse(state, xhr) + + const err = new Error($errUtils.errMsgByPath('xhr.aborted')) + + err.name = 'AbortError' + err.stack = stack - const err = new Error($errUtils.errMsgByPath("xhr.aborted")); - err.name = "AbortError"; - err.stack = stack; + const log = logs[xhr.id] - if (log = logs[xhr.id]) { - return log.snapshot("aborted").error(err); + if (log) { + return log.snapshot('aborted').error(err) } }, - onXhrCancel(xhr) { - let log; - setResponse(state, xhr); + onXhrCancel (xhr) { + setResponse(state, xhr) - if (log = logs[xhr.id]) { - return log.snapshot("canceled").set({ + const log = logs[xhr.id] + + if (log) { + return log.snapshot('canceled').set({ ended: true, - state: "failed" - }); + state: 'failed', + }) } }, onAnyAbort: (route, xhr) => { if (route && _.isFunction(route.onAbort)) { - return route.onAbort.call(cy, xhr); + return route.onAbort.call(cy, xhr) } }, onAnyRequest: (route, xhr) => { if (route && _.isFunction(route.onRequest)) { - return route.onRequest.call(cy, xhr); + return route.onRequest.call(cy, xhr) } }, onAnyResponse: (route, xhr) => { if (route && _.isFunction(route.onResponse)) { - return route.onResponse.call(cy, xhr); + return route.onResponse.call(cy, xhr) } - } - }); + }, + }) - const win = state("window"); + const win = state('window') - server.bindTo(win); + server.bindTo(win) - state("server", server); + state('server', server) - return server; -}; + return server +} const defaults = { method: undefined, status: undefined, delay: undefined, - headers: undefined, //# response headers + headers: undefined, // response headers response: undefined, autoRespond: undefined, waitOnResponses: undefined, onAbort: undefined, - onRequest: undefined, //# need to rebind these to 'cy' context - onResponse: undefined -}; - -module.exports = function(Commands, Cypress, cy, state, config) { - reset(); - - //# if our page is going away due to - //# a form submit / anchor click then - //# we need to cancel all pending - //# XHR's so the command log displays - //# correctly - Cypress.on("window:unload", cancelPendingXhrs); - - Cypress.on("test:before:run", function() { - //# reset the existing server - reset(); - - //# create the server before each test run - //# its possible for this to fail if the - //# last test we ran ended with an invalid - //# window such as if the last test ended - //# with a cross origin window + onRequest: undefined, // need to rebind these to 'cy' context + onResponse: undefined, +} + +module.exports = (Commands, Cypress, cy, state, config) => { + reset() + + // if our page is going away due to + // a form submit / anchor click then + // we need to cancel all pending + // XHR's so the command log displays + // correctly + Cypress.on('window:unload', cancelPendingXhrs) + + Cypress.on('test:before:run', () => { + // reset the existing server + reset() + + // create the server before each test run + // its possible for this to fail if the + // last test we ran ended with an invalid + // window such as if the last test ended + // with a cross origin window try { - server = startXhrServer(cy, state, config); + server = startXhrServer(cy, state, config) } catch (err) { - //# in this case, just don't bind to the server - server = null; + // in this case, just don't bind to the server + server = null } - return null; - }); + return null + }) - Cypress.on("window:before:load", function(contentWindow) { + Cypress.on('window:before:load', (contentWindow) => { if (server) { - //# dynamically bind the server to whatever is currently running - return server.bindTo(contentWindow); - } else { - //# if we don't have a server such as the case when - //# the last window was cross origin, try to bind - //# to it now - return server = startXhrServer(cy, state, config); + // dynamically bind the server to whatever is currently running + return server.bindTo(contentWindow) } - }); + + server = startXhrServer(cy, state, config) + }) return Commands.addAll({ - server(options) { - let userOptions = options; + server (options) { + let userOptions = options if (arguments.length === 0) { - userOptions = {}; + userOptions = {} } if (!_.isObject(userOptions)) { - $errUtils.throwErrByPath("server.invalid_argument"); + $errUtils.throwErrByPath('server.invalid_argument') } options = _.defaults({}, userOptions, { - enable: true //# set enable to false to turn off stubbing - }); + enable: true, // set enable to false to turn off stubbing + }) - //# if we disable the server later make sure - //# we cannot add cy.routes to it - state("serverIsStubbed", options.enable); + // if we disable the server later make sure + // we cannot add cy.routes to it + state('serverIsStubbed', options.enable) - return getXhrServer(state).set(options); + return getXhrServer(state).set(options) }, - route(...args) { - //# TODO: - //# if we return a function which returns a promise - //# then we should be handling potential timeout issues - //# just like cy.then does + route (...args) { + // TODO: + // if we return a function which returns a promise + // then we should be handling potential timeout issues + // just like cy.then does - //# method / url / response / options - //# url / response / options - //# options + // method / url / response / options + // url / response / options + // options - //# by default assume we have a specified - //# response from the user - let o; - let hasResponse = true; + // by default assume we have a specified + // response from the user + let hasResponse = true - if (!state("serverIsStubbed")) { - $errUtils.throwErrByPath("route.failed_prerequisites"); + if (!state('serverIsStubbed')) { + $errUtils.throwErrByPath('route.failed_prerequisites') } - //# get the default options currently set - //# on our server - let options = (o = getXhrServer(state).getOptions()); - - //# enable the entire routing definition to be a function - const parseArgs = function(...args) { - let alias; - switch (false) { - case !_.isObject(args[0]) || !!_.isRegExp(args[0]): - //# we dont have a specified response - if (!_.has(args[0], "response")) { - hasResponse = false; - } - - options = (o = _.extend({}, options, args[0])); - break; - - case args.length !== 0: - $errUtils.throwErrByPath("route.invalid_arguments"); - break; - - case args.length !== 1: - o.url = args[0]; - - hasResponse = false; - break; - - case args.length !== 2: - //# if our url actually matches an http method - //# then we know the user doesn't want to stub this route - if (_.isString(args[0]) && $utils.isValidHttpMethod(args[0])) { - o.method = args[0]; - o.url = args[1]; - - hasResponse = false; - } else { - o.url = args[0]; - o.response = args[1]; - } - break; - - case args.length !== 3: - if ($utils.isValidHttpMethod(args[0]) || isUrlLikeArgs(args[1], args[2])) { - o.method = args[0]; - o.url = args[1]; - o.response = args[2]; - } else { - o.url = args[0]; - o.response = args[1]; - - _.extend(o, args[2]); - } - break; + // get the default options currently set + // on our server + let o = getXhrServer(state).getOptions() + let options = o + + // enable the entire routing definition to be a function + const parseArgs = (...args) => { + if (_.isObject(args[0]) && !_.isRegExp(args[0])) { + // we dont have a specified response + if (!_.has(args[0], 'response')) { + hasResponse = false + } - case args.length !== 4: - o.method = args[0]; - o.url = args[1]; - o.response = args[2]; + options = (o = _.extend({}, options, args[0])) + } else if (args.length === 0) { + $errUtils.throwErrByPath('route.invalid_arguments') + } else if (args.length === 1) { + o.url = args[0] + + hasResponse = false + } else if (args.length === 2) { + // if our url actually matches an http method + // then we know the user doesn't want to stub this route + if (_.isString(args[0]) && $utils.isValidHttpMethod(args[0])) { + o.method = args[0] + o.url = args[1] + + hasResponse = false + } else { + o.url = args[0] + o.response = args[1] + } + } else if (args.length === 3) { + if ($utils.isValidHttpMethod(args[0]) || isUrlLikeArgs(args[1], args[2])) { + o.method = args[0] + o.url = args[1] + o.response = args[2] + } else { + o.url = args[0] + o.response = args[1] + + _.extend(o, args[2]) + } + } else if (args.length === 4) { + o.method = args[0] + o.url = args[1] + o.response = args[2] - _.extend(o, args[3]); - break; + _.extend(o, args[3]) } if (_.isString(o.method)) { - o.method = o.method.toUpperCase(); + o.method = o.method.toUpperCase() } - _.defaults(options, defaults); + _.defaults(options, defaults) if (!options.url) { - $errUtils.throwErrByPath("route.url_missing"); + $errUtils.throwErrByPath('route.url_missing') } - if (!(_.isString(options.url) || _.isRegExp(options.url))) { - $errUtils.throwErrByPath("route.url_invalid"); + if (!_.isString(options.url) && !_.isRegExp(options.url)) { + $errUtils.throwErrByPath('route.url_invalid') } if (!$utils.isValidHttpMethod(options.method)) { - $errUtils.throwErrByPath("route.method_invalid", { - args: { method: o.method } - }); + $errUtils.throwErrByPath('route.method_invalid', { + args: { method: o.method }, + }) } if (hasResponse && (options.response == null)) { - $errUtils.throwErrByPath("route.response_invalid"); + $errUtils.throwErrByPath('route.response_invalid') } - //# convert to wildcard regex - if (options.url === "*") { - options.originalUrl = "*"; - options.url = /.*/; + // convert to wildcard regex + if (options.url === '*') { + options.originalUrl = '*' + options.url = /.*/ } - //# look ahead to see if this - //# command (route) has an alias? - if (alias = cy.getNextAlias()) { - options.alias = alias; + // look ahead to see if this + // command (route) has an alias? + const alias = cy.getNextAlias() + + if (alias) { + options.alias = alias } if (_.isFunction(o.response)) { const getResponse = () => { - return o.response.call(state("runnable").ctx, options); - }; + return o.response.call(state('runnable').ctx, options) + } - //# allow route to return a promise + // allow route to return a promise return Promise.try(getResponse) - .then(function(resp) { - options.response = resp; + .then((resp) => { + options.response = resp - return route(); - }); - } else { - return route(); + return route() + }) } - }; - - var route = function() { - //# if our response is a string and - //# a reference to an alias - let aliasObj, decodedUrl; - if (_.isString(o.response) && (aliasObj = cy.getAlias(o.response, "route"))) { - //# reset the route's response to be the - //# aliases subject - options.response = aliasObj.subject; + + return route() + } + + const route = () => { + const aliasObj = cy.getAlias(o.response, 'route') + + // if our response is a string and + // a reference to an alias + if (_.isString(o.response) && aliasObj) { + // reset the route's response to be the + // aliases subject + options.response = aliasObj.subject } - const url = getUrl(options); + const url = getUrl(options) + + const urlString = url.toString() - const urlString = url.toString(); + const decodedUrl = tryDecodeUri(urlString) - //# https://github.com/cypress-io/cypress/issues/2372 - if ((decodedUrl = tryDecodeUri(urlString)) && (urlString !== decodedUrl)) { - $errUtils.warnByPath("route.url_percentencoding_warning", { args: { decodedUrl }}); + // https://github.com/cypress-io/cypress/issues/2372 + if (decodedUrl && urlString !== decodedUrl) { + $errUtils.warnByPath('route.url_percentencoding_warning', { args: { decodedUrl } }) } options.log = Cypress.log({ - name: "route", - instrument: "route", - method: options.method, - url: getUrl(options), - status: options.status, + name: 'route', + instrument: 'route', + method: options.method, + url: getUrl(options), + status: options.status, response: options.response, - alias: options.alias, + alias: options.alias, isStubbed: (options.response != null), numResponses: 0, - consoleProps() { + consoleProps () { return { - Method: options.method, - URL: url, - Status: options.status, + Method: options.method, + URL: url, + Status: options.status, Response: options.response, - Alias: options.alias - }; - } - }); + Alias: options.alias, + } + }, + }) - return getXhrServer(state).route(options); - }; + return getXhrServer(state).route(options) + } if (_.isFunction(args[0])) { const getArgs = () => { - return args[0].call(state("runnable").ctx); - }; + return args[0].call(state('runnable').ctx) + } return Promise.try(getArgs) - .then(parseArgs); - } else { - return parseArgs(...args); + .then(parseArgs) } - } - }); -}; + + return parseArgs(...args) + }, + }) +}