mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-07 23:40:21 -05:00
Merge pull request #7216 from sainthkh/decaff-driver-3
This commit is contained in:
@@ -1,116 +0,0 @@
|
||||
_ = require("lodash")
|
||||
|
||||
$errUtils = require("../cypress/error_utils")
|
||||
|
||||
aliasRe = /^@.+/
|
||||
aliasDisplayRe = /^([@]+)/
|
||||
requestXhrRe = /\.request$/
|
||||
|
||||
blacklist = ["test", "runnable", "timeout", "slow", "skip", "inspect"]
|
||||
|
||||
aliasDisplayName = (name) ->
|
||||
name.replace(aliasDisplayRe, "")
|
||||
|
||||
getXhrTypeByAlias = (alias) ->
|
||||
if requestXhrRe.test(alias) then "request" else "response"
|
||||
|
||||
validateAlias = (alias) ->
|
||||
if not _.isString(alias)
|
||||
$errUtils.throwErrByPath "as.invalid_type"
|
||||
|
||||
if aliasDisplayRe.test(alias)
|
||||
$errUtils.throwErrByPath "as.invalid_first_token", {
|
||||
args: {
|
||||
alias,
|
||||
suggestedName: alias.replace(aliasDisplayRe, '')
|
||||
}
|
||||
}
|
||||
|
||||
if _.isBlank(alias)
|
||||
$errUtils.throwErrByPath "as.empty_string"
|
||||
|
||||
if alias in blacklist
|
||||
$errUtils.throwErrByPath "as.reserved_word", { args: { alias } }
|
||||
|
||||
create = (state) ->
|
||||
addAlias = (ctx, aliasObj) ->
|
||||
{ alias, subject } = aliasObj
|
||||
|
||||
aliases = state("aliases") ? {}
|
||||
aliases[alias] = aliasObj
|
||||
state("aliases", aliases)
|
||||
|
||||
remoteSubject = cy.getRemotejQueryInstance(subject)
|
||||
|
||||
## assign the subject to our runnable ctx
|
||||
ctx[alias] = remoteSubject ? subject
|
||||
|
||||
getNextAlias = ->
|
||||
next = state("current").get("next")
|
||||
|
||||
if next and next.get("name") is "as"
|
||||
next.get("args")[0]
|
||||
|
||||
getAlias = (name, cmd, log) ->
|
||||
aliases = state("aliases") ? {}
|
||||
|
||||
## bail if the name doesnt reference an alias
|
||||
return if not aliasRe.test(name)
|
||||
|
||||
## slice off the '@'
|
||||
if not alias = aliases[name.slice(1)]
|
||||
aliasNotFoundFor(name, cmd, log)
|
||||
|
||||
return alias
|
||||
|
||||
getAvailableAliases = ->
|
||||
return [] if not aliases = state("aliases")
|
||||
|
||||
_.keys(aliases)
|
||||
|
||||
aliasNotFoundFor = (name, cmd, log) ->
|
||||
availableAliases = getAvailableAliases()
|
||||
|
||||
## throw a very specific error if our alias isnt in the right
|
||||
## format, but its word is found in the availableAliases
|
||||
if (not aliasRe.test(name)) and (name in availableAliases)
|
||||
displayName = aliasDisplayName(name)
|
||||
$errUtils.throwErrByPath "alias.invalid", {
|
||||
onFail: log
|
||||
args: { name, displayName }
|
||||
}
|
||||
|
||||
cmd ?= log and log.get("name") or state("current").get("name")
|
||||
displayName = aliasDisplayName(name)
|
||||
|
||||
errPath = if availableAliases.length
|
||||
"alias.not_registered_with_available"
|
||||
else
|
||||
"alias.not_registered_without_available"
|
||||
|
||||
$errUtils.throwErrByPath errPath, {
|
||||
onFail: log
|
||||
args: { cmd, displayName, availableAliases: availableAliases.join(", ") }
|
||||
}
|
||||
|
||||
return {
|
||||
getAlias
|
||||
|
||||
addAlias
|
||||
|
||||
## these are public because its expected other commands
|
||||
## know about them and are expected to call them
|
||||
getNextAlias
|
||||
|
||||
validateAlias
|
||||
|
||||
aliasNotFoundFor
|
||||
|
||||
getXhrTypeByAlias
|
||||
|
||||
getAvailableAliases
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/* globals cy */
|
||||
const _ = require('lodash')
|
||||
|
||||
const $errUtils = require('../cypress/error_utils')
|
||||
|
||||
const aliasRe = /^@.+/
|
||||
const aliasDisplayRe = /^([@]+)/
|
||||
const requestXhrRe = /\.request$/
|
||||
|
||||
const blacklist = ['test', 'runnable', 'timeout', 'slow', 'skip', 'inspect']
|
||||
|
||||
const aliasDisplayName = (name) => {
|
||||
return name.replace(aliasDisplayRe, '')
|
||||
}
|
||||
|
||||
const getXhrTypeByAlias = (alias) => {
|
||||
if (requestXhrRe.test(alias)) {
|
||||
return 'request'
|
||||
}
|
||||
|
||||
return 'response'
|
||||
}
|
||||
|
||||
const validateAlias = (alias) => {
|
||||
if (!_.isString(alias)) {
|
||||
$errUtils.throwErrByPath('as.invalid_type')
|
||||
}
|
||||
|
||||
if (aliasDisplayRe.test(alias)) {
|
||||
$errUtils.throwErrByPath('as.invalid_first_token', {
|
||||
args: {
|
||||
alias,
|
||||
suggestedName: alias.replace(aliasDisplayRe, ''),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (_.isBlank(alias)) {
|
||||
$errUtils.throwErrByPath('as.empty_string')
|
||||
}
|
||||
|
||||
if (blacklist.includes(alias)) {
|
||||
return $errUtils.throwErrByPath('as.reserved_word', { args: { alias } })
|
||||
}
|
||||
}
|
||||
|
||||
const create = (state) => {
|
||||
const addAlias = (ctx, aliasObj) => {
|
||||
const { alias, subject } = aliasObj
|
||||
|
||||
const aliases = state('aliases') || {}
|
||||
|
||||
aliases[alias] = aliasObj
|
||||
state('aliases', aliases)
|
||||
|
||||
const remoteSubject = cy.getRemotejQueryInstance(subject)
|
||||
|
||||
ctx[alias] = remoteSubject ?? subject
|
||||
}
|
||||
|
||||
const getNextAlias = () => {
|
||||
const next = state('current').get('next')
|
||||
|
||||
if (next && (next.get('name') === 'as')) {
|
||||
return next.get('args')[0]
|
||||
}
|
||||
}
|
||||
|
||||
const getAlias = (name, cmd, log) => {
|
||||
const aliases = state('aliases') || {}
|
||||
|
||||
// bail if the name doesnt reference an alias
|
||||
if (!aliasRe.test(name)) {
|
||||
return
|
||||
}
|
||||
|
||||
const alias = aliases[name.slice(1)]
|
||||
|
||||
// slice off the '@'
|
||||
if (!alias) {
|
||||
aliasNotFoundFor(name, cmd, log)
|
||||
}
|
||||
|
||||
return alias
|
||||
}
|
||||
|
||||
const getAvailableAliases = () => {
|
||||
const aliases = state('aliases')
|
||||
|
||||
if (!aliases) {
|
||||
return []
|
||||
}
|
||||
|
||||
return _.keys(aliases)
|
||||
}
|
||||
|
||||
const aliasNotFoundFor = (name, cmd, log) => {
|
||||
let displayName
|
||||
const availableAliases = getAvailableAliases()
|
||||
|
||||
// throw a very specific error if our alias isnt in the right
|
||||
// format, but its word is found in the availableAliases
|
||||
if (!aliasRe.test(name) && availableAliases.includes(name)) {
|
||||
displayName = aliasDisplayName(name)
|
||||
$errUtils.throwErrByPath('alias.invalid', {
|
||||
onFail: log,
|
||||
args: { name, displayName },
|
||||
})
|
||||
}
|
||||
|
||||
cmd = cmd ?? ((log && log.get('name')) || state('current').get('name'))
|
||||
displayName = aliasDisplayName(name)
|
||||
|
||||
const errPath = availableAliases.length
|
||||
? 'alias.not_registered_with_available'
|
||||
: 'alias.not_registered_without_available'
|
||||
|
||||
return $errUtils.throwErrByPath(errPath, {
|
||||
onFail: log,
|
||||
args: { cmd, displayName, availableAliases: availableAliases.join(', ') },
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAlias,
|
||||
|
||||
addAlias,
|
||||
|
||||
// these are public because its expected other commands
|
||||
// know about them and are expected to call them
|
||||
getNextAlias,
|
||||
|
||||
validateAlias,
|
||||
|
||||
aliasNotFoundFor,
|
||||
|
||||
getXhrTypeByAlias,
|
||||
|
||||
getAvailableAliases,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
_ = require("lodash")
|
||||
$dom = require("../dom")
|
||||
$utils = require("../cypress/utils")
|
||||
$errUtils = require("../cypress/error_utils")
|
||||
$elements = require("../dom/elements")
|
||||
|
||||
VALID_POSITIONS = "topLeft top topRight left center right bottomLeft bottom bottomRight".split(" ")
|
||||
|
||||
## TODO: in 4.0 we should accept a new validation type called 'elements'
|
||||
## which accepts an array of elements (and they all have to be elements!!)
|
||||
## this would fix the TODO below, and also ensure that commands understand
|
||||
## they may need to work with both element arrays, or specific items
|
||||
## such as a single element, a single document, or single window
|
||||
|
||||
returnFalse = -> return false
|
||||
|
||||
create = (state, expect) ->
|
||||
## TODO: we should probably normalize all subjects
|
||||
## into an array and loop through each and verify
|
||||
## each element in the array is valid. as it stands
|
||||
## we only validate the first
|
||||
validateType = (subject, type, cmd) ->
|
||||
name = cmd.get("name")
|
||||
prev = cmd.get("prev")
|
||||
|
||||
switch type
|
||||
when "element"
|
||||
## if this is an element then ensure its currently attached
|
||||
## to its document context
|
||||
if $dom.isElement(subject)
|
||||
ensureAttached(subject, name)
|
||||
|
||||
## always ensure this is an element
|
||||
ensureElement(subject, name)
|
||||
|
||||
when "document"
|
||||
ensureDocument(subject, name)
|
||||
|
||||
when "window"
|
||||
ensureWindow(subject, name)
|
||||
|
||||
ensureSubjectByType = (subject, type, name) ->
|
||||
current = state("current")
|
||||
|
||||
types = [].concat(type)
|
||||
|
||||
## if we have an optional subject and nothing's
|
||||
## here then just return cuz we good to go
|
||||
if ("optional" in types) and _.isUndefined(subject)
|
||||
return
|
||||
|
||||
## okay we either have a subject and either way
|
||||
## slice out optional so we can verify against
|
||||
## the various types
|
||||
types = _.without(types, "optional")
|
||||
|
||||
## if we have no types then bail
|
||||
return if types.length is 0
|
||||
|
||||
errors = []
|
||||
|
||||
for type in types
|
||||
try
|
||||
validateType(subject, type, current)
|
||||
catch err
|
||||
errors.push(err)
|
||||
|
||||
## every validation failed and we had more than one validation
|
||||
if (errors.length is types.length)
|
||||
err = errors[0]
|
||||
|
||||
if types.length > 1
|
||||
## append a nice error message telling the user this
|
||||
errProps = $errUtils.appendErrMsg(err, "All #{types.length} subject validations failed on this subject.")
|
||||
|
||||
$errUtils.mergeErrProps(err, errProps)
|
||||
|
||||
throw err
|
||||
|
||||
ensureRunnable = (name) ->
|
||||
if not state("runnable")
|
||||
$errUtils.throwErrByPath("miscellaneous.outside_test_with_cmd", {
|
||||
args: {
|
||||
cmd: name
|
||||
}
|
||||
})
|
||||
|
||||
ensureElementIsNotAnimating = ($el, coords = [], threshold) ->
|
||||
lastTwo = coords.slice(-2)
|
||||
|
||||
## bail if we dont yet have two points
|
||||
if lastTwo.length isnt 2
|
||||
$errUtils.throwErrByPath("dom.animation_check_failed")
|
||||
|
||||
[point1, point2] = lastTwo
|
||||
|
||||
## verify that there is not a distance
|
||||
## greater than a default of '5' between
|
||||
## the points
|
||||
if $utils.getDistanceBetween(point1, point2) > threshold
|
||||
cmd = state("current").get("name")
|
||||
node = $dom.stringify($el)
|
||||
$errUtils.throwErrByPath("dom.animating", {
|
||||
args: { cmd, node }
|
||||
})
|
||||
|
||||
ensureNotDisabled = (subject, onFail) ->
|
||||
cmd = state("current").get("name")
|
||||
|
||||
if subject.prop("disabled")
|
||||
node = $dom.stringify(subject)
|
||||
|
||||
$errUtils.throwErrByPath("dom.disabled", {
|
||||
onFail
|
||||
args: { cmd, node }
|
||||
})
|
||||
|
||||
ensureNotReadonly = (subject, onFail) ->
|
||||
cmd = state("current").get("name")
|
||||
|
||||
# readonly can only be applied to input/textarea
|
||||
# not on checkboxes, radios, etc..
|
||||
if $dom.isTextLike(subject.get(0)) and subject.prop("readonly")
|
||||
node = $dom.stringify(subject)
|
||||
|
||||
$errUtils.throwErrByPath("dom.readonly", {
|
||||
onFail
|
||||
args: { cmd, node }
|
||||
})
|
||||
|
||||
ensureVisibility = (subject, onFail) ->
|
||||
cmd = state("current").get("name")
|
||||
|
||||
# We overwrite the filter(":visible") in jquery
|
||||
# packages/driver/src/config/jquery.coffee#L51
|
||||
# So that this effectively calls our logic
|
||||
# for $dom.isVisible aka !$dom.isHidden
|
||||
if not (subject.length is subject.filter(":visible").length)
|
||||
reason = $dom.getReasonIsHidden(subject)
|
||||
node = $dom.stringify(subject)
|
||||
$errUtils.throwErrByPath("dom.not_visible", {
|
||||
onFail
|
||||
args: { cmd, node, reason }
|
||||
})
|
||||
|
||||
ensureAttached = (subject, name, onFail) ->
|
||||
if $dom.isDetached(subject)
|
||||
cmd = name ? state("current").get("name")
|
||||
prev = state("current").get("prev").get("name")
|
||||
node = $dom.stringify(subject)
|
||||
|
||||
$errUtils.throwErrByPath("subject.not_attached", {
|
||||
onFail
|
||||
args: { cmd, prev, node }
|
||||
})
|
||||
|
||||
ensureElement = (subject, name, onFail) ->
|
||||
if not $dom.isElement(subject)
|
||||
prev = state("current").get("prev")
|
||||
|
||||
$errUtils.throwErrByPath("subject.not_element", {
|
||||
onFail
|
||||
args: {
|
||||
name
|
||||
subject: $utils.stringifyActual(subject)
|
||||
previous: prev.get("name")
|
||||
}
|
||||
})
|
||||
|
||||
ensureWindow = (subject, name, log) ->
|
||||
if not $dom.isWindow(subject)
|
||||
prev = state("current").get("prev")
|
||||
|
||||
$errUtils.throwErrByPath("subject.not_window_or_document", {
|
||||
args: {
|
||||
name
|
||||
type: "window"
|
||||
subject: $utils.stringifyActual(subject)
|
||||
previous: prev.get("name")
|
||||
}
|
||||
})
|
||||
|
||||
ensureDocument = (subject, name, log) ->
|
||||
if not $dom.isDocument(subject)
|
||||
prev = state("current").get("prev")
|
||||
|
||||
$errUtils.throwErrByPath("subject.not_window_or_document", {
|
||||
args: {
|
||||
name
|
||||
type: "document"
|
||||
subject: $utils.stringifyActual(subject)
|
||||
previous: prev.get("name")
|
||||
}
|
||||
})
|
||||
|
||||
ensureExistence = (subject) ->
|
||||
returnFalse = ->
|
||||
cleanup()
|
||||
|
||||
return false
|
||||
|
||||
cleanup = ->
|
||||
state("onBeforeLog", null)
|
||||
|
||||
## prevent any additional logs this is an implicit assertion
|
||||
state("onBeforeLog", returnFalse)
|
||||
|
||||
## verify the $el exists and use our default error messages
|
||||
## TODO: always unbind if our expectation failed
|
||||
try
|
||||
expect(subject).to.exist
|
||||
catch err
|
||||
cleanup()
|
||||
|
||||
throw err
|
||||
|
||||
ensureElExistence = ($el) ->
|
||||
## dont throw if this isnt even a DOM object
|
||||
# return if not $dom.isJquery($el)
|
||||
|
||||
## ensure that we either had some assertions
|
||||
## or that the element existed
|
||||
return if $el and $el.length
|
||||
|
||||
## TODO: REFACTOR THIS TO CALL THE CHAI-OVERRIDES DIRECTLY
|
||||
## OR GO THROUGH I18N
|
||||
|
||||
ensureExistence($el)
|
||||
|
||||
ensureElDoesNotHaveCSS = ($el, cssProperty, cssValue, onFail) ->
|
||||
cmd = state("current").get("name")
|
||||
el = $el[0]
|
||||
win = $dom.getWindowByElement(el)
|
||||
value = win.getComputedStyle(el)[cssProperty]
|
||||
if value is cssValue
|
||||
elInherited = $elements.findParent el, (el, prevEl) ->
|
||||
if win.getComputedStyle(el)[cssProperty] isnt cssValue
|
||||
return prevEl
|
||||
|
||||
element = $dom.stringify(el)
|
||||
elementInherited = (el isnt elInherited) && $dom.stringify(elInherited)
|
||||
|
||||
consoleProps = {
|
||||
'But it has CSS': "#{cssProperty}: #{cssValue}"
|
||||
}
|
||||
|
||||
if elementInherited then _.extend(consoleProps, {
|
||||
'Inherited From': elInherited
|
||||
})
|
||||
|
||||
$errUtils.throwErrByPath("dom.pointer_events_none", {
|
||||
onFail
|
||||
args: {
|
||||
cmd
|
||||
element
|
||||
elementInherited
|
||||
}
|
||||
errProps: {
|
||||
consoleProps
|
||||
}
|
||||
})
|
||||
|
||||
ensureDescendents = ($el1, $el2, onFail) ->
|
||||
cmd = state("current").get("name")
|
||||
|
||||
if not $dom.isDescendent($el1, $el2)
|
||||
if $el2
|
||||
element1 = $dom.stringify($el1)
|
||||
element2 = $dom.stringify($el2)
|
||||
$errUtils.throwErrByPath("dom.covered", {
|
||||
onFail
|
||||
args: { cmd, element1, element2 }
|
||||
errProps: {
|
||||
consoleProps: {
|
||||
"But its Covered By": $dom.getElements($el2)
|
||||
}
|
||||
}
|
||||
})
|
||||
else
|
||||
node = $dom.stringify($el1)
|
||||
$errUtils.throwErrByPath("dom.center_hidden", {
|
||||
onFail
|
||||
args: { cmd, node }
|
||||
errProps: {
|
||||
consoleProps: {
|
||||
"But its Covered By": $dom.getElements($el2)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ensureValidPosition = (position, log) ->
|
||||
## make sure its valid first!
|
||||
if position in VALID_POSITIONS
|
||||
return true
|
||||
|
||||
$errUtils.throwErrByPath("dom.invalid_position_argument", {
|
||||
onFail: log
|
||||
args: {
|
||||
position,
|
||||
validPositions: VALID_POSITIONS.join(', ')
|
||||
}
|
||||
})
|
||||
|
||||
ensureScrollability = ($el, cmd) ->
|
||||
return true if $dom.isScrollable($el)
|
||||
|
||||
## prep args to throw in error since we can't scroll
|
||||
cmd ?= state("current").get("name")
|
||||
node = $dom.stringify($el)
|
||||
|
||||
$errUtils.throwErrByPath("dom.not_scrollable", {
|
||||
args: { cmd, node }
|
||||
})
|
||||
|
||||
return {
|
||||
ensureSubjectByType
|
||||
|
||||
ensureElement
|
||||
|
||||
ensureAttached
|
||||
|
||||
ensureRunnable
|
||||
|
||||
ensureWindow
|
||||
|
||||
ensureDocument
|
||||
|
||||
ensureElDoesNotHaveCSS
|
||||
|
||||
ensureElementIsNotAnimating
|
||||
|
||||
ensureNotDisabled
|
||||
|
||||
ensureVisibility
|
||||
|
||||
ensureExistence
|
||||
|
||||
ensureElExistence
|
||||
|
||||
ensureDescendents
|
||||
|
||||
ensureValidPosition
|
||||
|
||||
ensureScrollability
|
||||
|
||||
ensureNotReadonly
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
const _ = require('lodash')
|
||||
const $dom = require('../dom')
|
||||
const $utils = require('../cypress/utils')
|
||||
const $errUtils = require('../cypress/error_utils')
|
||||
const $elements = require('../dom/elements')
|
||||
|
||||
const VALID_POSITIONS = 'topLeft top topRight left center right bottomLeft bottom bottomRight'.split(' ')
|
||||
|
||||
// TODO: in 4.0 we should accept a new validation type called 'elements'
|
||||
// which accepts an array of elements (and they all have to be elements!!)
|
||||
// this would fix the TODO below, and also ensure that commands understand
|
||||
// they may need to work with both element arrays, or specific items
|
||||
// such as a single element, a single document, or single window
|
||||
|
||||
let returnFalse = () => {
|
||||
return false
|
||||
}
|
||||
|
||||
const create = (state, expect) => {
|
||||
// TODO: we should probably normalize all subjects
|
||||
// into an array and loop through each and verify
|
||||
// each element in the array is valid. as it stands
|
||||
// we only validate the first
|
||||
const validateType = (subject, type, cmd) => {
|
||||
const name = cmd.get('name')
|
||||
|
||||
switch (type) {
|
||||
case 'element':
|
||||
// if this is an element then ensure its currently attached
|
||||
// to its document context
|
||||
if ($dom.isElement(subject)) {
|
||||
ensureAttached(subject, name)
|
||||
}
|
||||
|
||||
// always ensure this is an element
|
||||
return ensureElement(subject, name)
|
||||
|
||||
case 'document':
|
||||
return ensureDocument(subject, name)
|
||||
|
||||
case 'window':
|
||||
return ensureWindow(subject, name)
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const ensureSubjectByType = (subject, type) => {
|
||||
const current = state('current')
|
||||
|
||||
let types = [].concat(type)
|
||||
|
||||
// if we have an optional subject and nothing's
|
||||
// here then just return cuz we good to go
|
||||
if (types.includes('optional') && _.isUndefined(subject)) {
|
||||
return
|
||||
}
|
||||
|
||||
// okay we either have a subject and either way
|
||||
// slice out optional so we can verify against
|
||||
// the various types
|
||||
types = _.without(types, 'optional')
|
||||
|
||||
// if we have no types then bail
|
||||
if (types.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let err
|
||||
const errors = []
|
||||
|
||||
for (type of types) {
|
||||
try {
|
||||
validateType(subject, type, current)
|
||||
} catch (error) {
|
||||
err = error
|
||||
errors.push(err)
|
||||
}
|
||||
}
|
||||
|
||||
// every validation failed and we had more than one validation
|
||||
if (errors.length === types.length) {
|
||||
err = errors[0]
|
||||
|
||||
if (types.length > 1) {
|
||||
// append a nice error message telling the user this
|
||||
const errProps = $errUtils.appendErrMsg(err, `All ${types.length} subject validations failed on this subject.`)
|
||||
|
||||
$errUtils.mergeErrProps(err, errProps)
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const ensureRunnable = (name) => {
|
||||
if (!state('runnable')) {
|
||||
return $errUtils.throwErrByPath('miscellaneous.outside_test_with_cmd', {
|
||||
args: {
|
||||
cmd: name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureElementIsNotAnimating = ($el, coords = [], threshold) => {
|
||||
const lastTwo = coords.slice(-2)
|
||||
|
||||
// bail if we dont yet have two points
|
||||
if (lastTwo.length !== 2) {
|
||||
$errUtils.throwErrByPath('dom.animation_check_failed')
|
||||
}
|
||||
|
||||
const [point1, point2] = lastTwo
|
||||
|
||||
// verify that there is not a distance
|
||||
// greater than a default of '5' between
|
||||
// the points
|
||||
if ($utils.getDistanceBetween(point1, point2) > threshold) {
|
||||
const cmd = state('current').get('name')
|
||||
const node = $dom.stringify($el)
|
||||
|
||||
return $errUtils.throwErrByPath('dom.animating', {
|
||||
args: { cmd, node },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureNotDisabled = (subject, onFail) => {
|
||||
const cmd = state('current').get('name')
|
||||
|
||||
if (subject.prop('disabled')) {
|
||||
const node = $dom.stringify(subject)
|
||||
|
||||
return $errUtils.throwErrByPath('dom.disabled', {
|
||||
onFail,
|
||||
args: { cmd, node },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureNotReadonly = (subject, onFail) => {
|
||||
const cmd = state('current').get('name')
|
||||
|
||||
// readonly can only be applied to input/textarea
|
||||
// not on checkboxes, radios, etc..
|
||||
if ($dom.isTextLike(subject.get(0)) && subject.prop('readonly')) {
|
||||
const node = $dom.stringify(subject)
|
||||
|
||||
return $errUtils.throwErrByPath('dom.readonly', {
|
||||
onFail,
|
||||
args: { cmd, node },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureVisibility = (subject, onFail) => {
|
||||
const cmd = state('current').get('name')
|
||||
|
||||
// We overwrite the filter(":visible") in jquery
|
||||
// packages/driver/src/config/jquery.coffee#L51
|
||||
// So that this effectively calls our logic
|
||||
// for $dom.isVisible aka !$dom.isHidden
|
||||
if (subject.length !== subject.filter(':visible').length) {
|
||||
const reason = $dom.getReasonIsHidden(subject)
|
||||
const node = $dom.stringify(subject)
|
||||
|
||||
return $errUtils.throwErrByPath('dom.not_visible', {
|
||||
onFail,
|
||||
args: { cmd, node, reason },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureAttached = (subject, name, onFail) => {
|
||||
if ($dom.isDetached(subject)) {
|
||||
const cmd = name ?? state('current').get('name')
|
||||
const prev = state('current').get('prev').get('name')
|
||||
const node = $dom.stringify(subject)
|
||||
|
||||
return $errUtils.throwErrByPath('subject.not_attached', {
|
||||
onFail,
|
||||
args: { cmd, prev, node },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureElement = (subject, name, onFail) => {
|
||||
if (!$dom.isElement(subject)) {
|
||||
const prev = state('current').get('prev')
|
||||
|
||||
return $errUtils.throwErrByPath('subject.not_element', {
|
||||
onFail,
|
||||
args: {
|
||||
name,
|
||||
subject: $utils.stringifyActual(subject),
|
||||
previous: prev.get('name'),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureWindow = (subject, name) => {
|
||||
if (!$dom.isWindow(subject)) {
|
||||
const prev = state('current').get('prev')
|
||||
|
||||
return $errUtils.throwErrByPath('subject.not_window_or_document', {
|
||||
args: {
|
||||
name,
|
||||
type: 'window',
|
||||
subject: $utils.stringifyActual(subject),
|
||||
previous: prev.get('name'),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureDocument = (subject, name) => {
|
||||
if (!$dom.isDocument(subject)) {
|
||||
const prev = state('current').get('prev')
|
||||
|
||||
return $errUtils.throwErrByPath('subject.not_window_or_document', {
|
||||
args: {
|
||||
name,
|
||||
type: 'document',
|
||||
subject: $utils.stringifyActual(subject),
|
||||
previous: prev.get('name'),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureExistence = (subject) => {
|
||||
returnFalse = () => {
|
||||
cleanup()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
return state('onBeforeLog', null)
|
||||
}
|
||||
|
||||
// prevent any additional logs this is an implicit assertion
|
||||
state('onBeforeLog', returnFalse)
|
||||
|
||||
// verify the $el exists and use our default error messages
|
||||
// TODO: always unbind if our expectation failed
|
||||
try {
|
||||
expect(subject).to.exist
|
||||
} catch (err) {
|
||||
cleanup()
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const ensureElExistence = ($el) => {
|
||||
// dont throw if this isnt even a DOM object
|
||||
// return if not $dom.isJquery($el)
|
||||
|
||||
// ensure that we either had some assertions
|
||||
// or that the element existed
|
||||
if ($el && $el.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: REFACTOR THIS TO CALL THE CHAI-OVERRIDES DIRECTLY
|
||||
// OR GO THROUGH I18N
|
||||
|
||||
return ensureExistence($el)
|
||||
}
|
||||
|
||||
const ensureElDoesNotHaveCSS = ($el, cssProperty, cssValue, onFail) => {
|
||||
const cmd = state('current').get('name')
|
||||
const el = $el[0]
|
||||
const win = $dom.getWindowByElement(el)
|
||||
const value = win.getComputedStyle(el)[cssProperty]
|
||||
|
||||
if (value === cssValue) {
|
||||
const elInherited = $elements.findParent(el, (el, prevEl) => {
|
||||
if (win.getComputedStyle(el)[cssProperty] !== cssValue) {
|
||||
return prevEl
|
||||
}
|
||||
})
|
||||
|
||||
const element = $dom.stringify(el)
|
||||
const elementInherited = (el !== elInherited) && $dom.stringify(elInherited)
|
||||
|
||||
const consoleProps = {
|
||||
'But it has CSS': `${cssProperty}: ${cssValue}`,
|
||||
}
|
||||
|
||||
if (elementInherited) {
|
||||
_.extend(consoleProps, {
|
||||
'Inherited From': elInherited,
|
||||
})
|
||||
}
|
||||
|
||||
return $errUtils.throwErrByPath('dom.pointer_events_none', {
|
||||
onFail,
|
||||
args: {
|
||||
cmd,
|
||||
element,
|
||||
elementInherited,
|
||||
},
|
||||
errProps: {
|
||||
consoleProps,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureDescendents = ($el1, $el2, onFail) => {
|
||||
const cmd = state('current').get('name')
|
||||
|
||||
if (!$dom.isDescendent($el1, $el2)) {
|
||||
if ($el2) {
|
||||
const element1 = $dom.stringify($el1)
|
||||
const element2 = $dom.stringify($el2)
|
||||
|
||||
return $errUtils.throwErrByPath('dom.covered', {
|
||||
onFail,
|
||||
args: { cmd, element1, element2 },
|
||||
errProps: {
|
||||
consoleProps: {
|
||||
'But its Covered By': $dom.getElements($el2),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const node = $dom.stringify($el1)
|
||||
|
||||
return $errUtils.throwErrByPath('dom.center_hidden', {
|
||||
onFail,
|
||||
args: { cmd, node },
|
||||
errProps: {
|
||||
consoleProps: {
|
||||
'But its Covered By': $dom.getElements($el2),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ensureValidPosition = (position, log) => {
|
||||
// make sure its valid first!
|
||||
if (VALID_POSITIONS.includes(position)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return $errUtils.throwErrByPath('dom.invalid_position_argument', {
|
||||
onFail: log,
|
||||
args: {
|
||||
position,
|
||||
validPositions: VALID_POSITIONS.join(', '),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const ensureScrollability = ($el, cmd) => {
|
||||
if ($dom.isScrollable($el)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// prep args to throw in error since we can't scroll
|
||||
cmd = cmd ?? state('current').get('name')
|
||||
|
||||
const node = $dom.stringify($el)
|
||||
|
||||
return $errUtils.throwErrByPath('dom.not_scrollable', {
|
||||
args: { cmd, node },
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ensureSubjectByType,
|
||||
|
||||
ensureElement,
|
||||
|
||||
ensureAttached,
|
||||
|
||||
ensureRunnable,
|
||||
|
||||
ensureWindow,
|
||||
|
||||
ensureDocument,
|
||||
|
||||
ensureElDoesNotHaveCSS,
|
||||
|
||||
ensureElementIsNotAnimating,
|
||||
|
||||
ensureNotDisabled,
|
||||
|
||||
ensureVisibility,
|
||||
|
||||
ensureExistence,
|
||||
|
||||
ensureElExistence,
|
||||
|
||||
ensureDescendents,
|
||||
|
||||
ensureValidPosition,
|
||||
|
||||
ensureScrollability,
|
||||
|
||||
ensureNotReadonly,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
_ = require("lodash")
|
||||
$dom = require("../dom")
|
||||
$errUtils = require("../cypress/error_utils")
|
||||
$errorMessages = require('../cypress/error_messages')
|
||||
|
||||
crossOriginScriptRe = /^script error/i
|
||||
|
||||
create = (state, config, log) ->
|
||||
commandErr = (err) ->
|
||||
current = state("current")
|
||||
|
||||
log({
|
||||
end: true
|
||||
snapshot: true
|
||||
error: err
|
||||
consoleProps: ->
|
||||
obj = {}
|
||||
## if type isnt parent then we know its dual or child
|
||||
## and we can add Applied To if there is a prev command
|
||||
## and it is a parent
|
||||
if current.get("type") isnt "parent" and prev = current.get("prev")
|
||||
ret = if $dom.isElement(prev.get("subject"))
|
||||
$dom.getElements(prev.get("subject"))
|
||||
else
|
||||
prev.get("subject")
|
||||
|
||||
obj["Applied To"] = ret
|
||||
obj
|
||||
})
|
||||
|
||||
createUncaughtException = (type, args) ->
|
||||
[msg, source, lineno, colno, err] = args
|
||||
|
||||
current = state("current")
|
||||
|
||||
## reset the msg on a cross origin script error
|
||||
## since no details are accessible
|
||||
if crossOriginScriptRe.test(msg)
|
||||
msg = $errUtils.errMsgByPath("uncaught.cross_origin_script")
|
||||
|
||||
createErrFromMsg = ->
|
||||
new Error($errUtils.errMsgByPath("uncaught.error", {
|
||||
msg, source, lineno
|
||||
}))
|
||||
|
||||
## if we have the 5th argument it means we're in a super
|
||||
## modern browser making this super simple to work with.
|
||||
err ?= createErrFromMsg()
|
||||
|
||||
uncaughtErrLookup = switch type
|
||||
when "app" then "uncaught.fromApp"
|
||||
when "spec" then "uncaught.fromSpec"
|
||||
|
||||
uncaughtErrObj = $errUtils.errObjByPath($errorMessages, uncaughtErrLookup)
|
||||
|
||||
uncaughtErrProps = $errUtils.modifyErrMsg(err, uncaughtErrObj.message, (msg1, msg2) ->
|
||||
return "#{msg1}\n\n#{msg2}"
|
||||
)
|
||||
_.defaults(uncaughtErrProps, uncaughtErrObj)
|
||||
|
||||
uncaughtErr = $errUtils.mergeErrProps(err, uncaughtErrProps)
|
||||
|
||||
$errUtils.modifyErrName(err, "Uncaught #{err.name}")
|
||||
|
||||
uncaughtErr.onFail = ->
|
||||
if l = current and current.getLastLog()
|
||||
l.error(uncaughtErr)
|
||||
|
||||
## normalize error message for firefox
|
||||
$errUtils.normalizeErrorStack(uncaughtErr)
|
||||
|
||||
return uncaughtErr
|
||||
|
||||
commandRunningFailed = (err) ->
|
||||
## allow for our own custom onFail function
|
||||
if err.onFail
|
||||
err.onFail(err)
|
||||
|
||||
## clean up this onFail callback
|
||||
## after its been called
|
||||
delete err.onFail
|
||||
else
|
||||
commandErr(err)
|
||||
|
||||
return {
|
||||
## submit a generic command error
|
||||
commandErr
|
||||
|
||||
commandRunningFailed
|
||||
|
||||
createUncaughtException
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
const _ = require('lodash')
|
||||
const $dom = require('../dom')
|
||||
const $errUtils = require('../cypress/error_utils')
|
||||
const $errorMessages = require('../cypress/error_messages')
|
||||
|
||||
const crossOriginScriptRe = /^script error/i
|
||||
|
||||
const create = (state, config, log) => {
|
||||
const commandErr = (err) => {
|
||||
const current = state('current')
|
||||
|
||||
return log({
|
||||
end: true,
|
||||
snapshot: true,
|
||||
error: err,
|
||||
consoleProps () {
|
||||
const obj = {}
|
||||
const prev = current.get('prev')
|
||||
|
||||
// if type isnt parent then we know its dual or child
|
||||
// and we can add Applied To if there is a prev command
|
||||
// and it is a parent
|
||||
if (current.get('type') !== 'parent' && prev) {
|
||||
const ret = $dom.isElement(prev.get('subject')) ?
|
||||
$dom.getElements(prev.get('subject'))
|
||||
:
|
||||
prev.get('subject')
|
||||
|
||||
obj['Applied To'] = ret
|
||||
|
||||
return obj
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createUncaughtException = (type, args) => {
|
||||
// @ts-ignore
|
||||
let [msg, source, lineno, colno, err] = args // eslint-disable-line no-unused-vars
|
||||
|
||||
const current = state('current')
|
||||
|
||||
// reset the msg on a cross origin script error
|
||||
// since no details are accessible
|
||||
if (crossOriginScriptRe.test(msg)) {
|
||||
msg = $errUtils.errMsgByPath('uncaught.cross_origin_script')
|
||||
}
|
||||
|
||||
const createErrFromMsg = () => {
|
||||
return new Error($errUtils.errMsgByPath('uncaught.error', {
|
||||
msg, source, lineno,
|
||||
}))
|
||||
}
|
||||
|
||||
// if we have the 5th argument it means we're in a super
|
||||
// modern browser making this super simple to work with.
|
||||
err = err ?? createErrFromMsg()
|
||||
|
||||
let uncaughtErrLookup = ''
|
||||
|
||||
if (type === 'app') {
|
||||
uncaughtErrLookup = 'uncaught.fromApp'
|
||||
} else if (type === 'spec') {
|
||||
uncaughtErrLookup = 'uncaught.fromSpec'
|
||||
}
|
||||
|
||||
const uncaughtErrObj = $errUtils.errObjByPath($errorMessages, uncaughtErrLookup)
|
||||
|
||||
const uncaughtErrProps = $errUtils.modifyErrMsg(err, uncaughtErrObj.message, (msg1, msg2) => {
|
||||
return `${msg1}\n\n${msg2}`
|
||||
})
|
||||
|
||||
_.defaults(uncaughtErrProps, uncaughtErrObj)
|
||||
|
||||
const uncaughtErr = $errUtils.mergeErrProps(err, uncaughtErrProps)
|
||||
|
||||
$errUtils.modifyErrName(err, `Uncaught ${err.name}`)
|
||||
|
||||
uncaughtErr.onFail = () => {
|
||||
const l = current && current.getLastLog()
|
||||
|
||||
if (l) {
|
||||
return l.error(uncaughtErr)
|
||||
}
|
||||
}
|
||||
|
||||
// normalize error message for firefox
|
||||
$errUtils.normalizeErrorStack(uncaughtErr)
|
||||
|
||||
return uncaughtErr
|
||||
}
|
||||
|
||||
const commandRunningFailed = (err) => {
|
||||
// allow for our own custom onFail function
|
||||
if (err.onFail) {
|
||||
err.onFail(err)
|
||||
|
||||
// clean up this onFail callback
|
||||
// after its been called
|
||||
delete err.onFail
|
||||
} else {
|
||||
commandErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// submit a generic command error
|
||||
commandErr,
|
||||
|
||||
commandRunningFailed,
|
||||
|
||||
createUncaughtException,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
$dom = require("../dom")
|
||||
$window = require("../dom/window")
|
||||
$elements = require("../dom/elements")
|
||||
$actionability = require("./actionability")
|
||||
|
||||
create = (state) ->
|
||||
|
||||
documentHasFocus = () ->
|
||||
## hardcode document has focus as true
|
||||
## since the test should assume the window
|
||||
## is in focus the entire time
|
||||
return true
|
||||
|
||||
fireBlur = (el) ->
|
||||
win = $window.getWindowByElement(el)
|
||||
|
||||
hasBlurred = false
|
||||
|
||||
## we need to bind to the blur event here
|
||||
## because some browsers will not ever fire
|
||||
## the blur event if the window itself is not
|
||||
## currently blured
|
||||
cleanup = ->
|
||||
$elements.callNativeMethod(el, "removeEventListener", "blur", onBlur)
|
||||
|
||||
onBlur = ->
|
||||
hasBlurred = true
|
||||
|
||||
## for simplicity we allow change events
|
||||
## to be triggered by a manual blur
|
||||
$actionability.dispatchPrimedChangeEvents(state)
|
||||
|
||||
$elements.callNativeMethod(el, "addEventListener", "blur", onBlur)
|
||||
|
||||
$elements.callNativeMethod(el, "blur")
|
||||
|
||||
cleanup()
|
||||
|
||||
## body will never emit focus events
|
||||
## so we avoid simulating this
|
||||
if $elements.isBody(el)
|
||||
return
|
||||
|
||||
## fallback if our focus event never fires
|
||||
## to simulate the focus + focusin
|
||||
if not hasBlurred
|
||||
simulateBlurEvent(el, win)
|
||||
|
||||
simulateBlurEvent = (el, win) ->
|
||||
## todo handle relatedTarget's per the spec
|
||||
focusoutEvt = new FocusEvent "focusout", {
|
||||
bubbles: true
|
||||
cancelable: false
|
||||
view: win
|
||||
relatedTarget: null
|
||||
}
|
||||
|
||||
blurEvt = new FocusEvent "blur", {
|
||||
bubble: false
|
||||
cancelable: false
|
||||
view: win
|
||||
relatedTarget: null
|
||||
}
|
||||
|
||||
el.dispatchEvent(blurEvt)
|
||||
el.dispatchEvent(focusoutEvt)
|
||||
|
||||
fireFocus = (el) ->
|
||||
## body will never emit focus events (unless it's contenteditable)
|
||||
## so we avoid simulating this
|
||||
if $elements.isBody(el) && !$elements.isContentEditable(el)
|
||||
return
|
||||
|
||||
## if we are focusing a different element
|
||||
## dispatch any primed change events
|
||||
## we have to do this because our blur
|
||||
## method might not get triggered if
|
||||
## our window is in focus since the
|
||||
## browser may fire blur events naturally
|
||||
$actionability.dispatchPrimedChangeEvents(state)
|
||||
|
||||
win = $window.getWindowByElement(el)
|
||||
|
||||
## store the current focused element
|
||||
## since when we call .focus() it will change
|
||||
$focused = getFocused()
|
||||
|
||||
hasFocused = false
|
||||
|
||||
## we need to bind to the focus event here
|
||||
## because some browsers will not ever fire
|
||||
## the focus event if the window itself is not
|
||||
## currently focused
|
||||
cleanup = ->
|
||||
$elements.callNativeMethod(el, "removeEventListener", "focus", onFocus)
|
||||
|
||||
onFocus = ->
|
||||
hasFocused = true
|
||||
|
||||
$elements.callNativeMethod(el, "addEventListener", "focus", onFocus)
|
||||
|
||||
$elements.callNativeMethod(el, "focus")
|
||||
|
||||
cleanup()
|
||||
|
||||
## fallback if our focus event never fires
|
||||
## to simulate the focus + focusin
|
||||
if not hasFocused
|
||||
|
||||
## only blur if we have a focused element AND its not
|
||||
## currently ourselves!
|
||||
if $focused and $focused.get(0) isnt el
|
||||
## additionally make sure that this isnt
|
||||
## the window, since that does not steal focus
|
||||
## or actually change the activeElement
|
||||
if not $window.isWindow(el)
|
||||
fireBlur($focused.get(0))
|
||||
|
||||
simulateFocusEvent(el, win)
|
||||
|
||||
simulateFocusEvent = (el, win) ->
|
||||
## todo handle relatedTarget's per the spec
|
||||
focusinEvt = new FocusEvent "focusin", {
|
||||
bubbles: true
|
||||
view: win
|
||||
relatedTarget: null
|
||||
}
|
||||
|
||||
focusEvt = new FocusEvent "focus", {
|
||||
view: win
|
||||
relatedTarget: null
|
||||
}
|
||||
|
||||
## not fired in the correct order per w3c spec
|
||||
## because chrome chooses to fire focus before focusin
|
||||
## and since we have a simulation fallback we end up
|
||||
## doing it how chrome does it
|
||||
## http://www.w3.org/TR/DOM-Level-3-Events/#h-events-focusevent-event-order
|
||||
el.dispatchEvent(focusEvt)
|
||||
el.dispatchEvent(focusinEvt)
|
||||
|
||||
interceptFocus = (el, contentWindow, focusOption) ->
|
||||
## normally programmatic focus calls cause "primed" focus/blur
|
||||
## events if the window is not in focus
|
||||
## so we fire fake events to act as if the window
|
||||
## is always in focus
|
||||
$focused = getFocused()
|
||||
|
||||
if (!$focused || $focused[0] isnt el) && $elements.isW3CFocusable(el)
|
||||
fireFocus(el)
|
||||
return
|
||||
|
||||
$elements.callNativeMethod(el, 'focus')
|
||||
return
|
||||
|
||||
interceptBlur = (el) ->
|
||||
## normally programmatic blur calls cause "primed" focus/blur
|
||||
## events if the window is not in focus
|
||||
## so we fire fake events to act as if the window
|
||||
## is always in focus.
|
||||
$focused = getFocused()
|
||||
|
||||
if $focused && $focused[0] is el
|
||||
fireBlur(el)
|
||||
return
|
||||
|
||||
$elements.callNativeMethod(el, 'blur')
|
||||
return
|
||||
|
||||
needsFocus = ($elToFocus, $previouslyFocusedEl) ->
|
||||
$focused = getFocused()
|
||||
|
||||
## if we dont have a focused element
|
||||
## we know we want to fire a focus event
|
||||
return true if not $focused
|
||||
|
||||
## if we didnt have a previously focused el
|
||||
## then always return true
|
||||
return true if not $previouslyFocusedEl
|
||||
|
||||
## if we are attemping to focus a differnet element
|
||||
## than the one we currently have, we know we want
|
||||
## to fire a focus event
|
||||
return true if $focused.get(0) isnt $elToFocus.get(0)
|
||||
|
||||
## if our focused element isnt the same as the previously
|
||||
## focused el then what probably happened is our mouse
|
||||
## down event caused our element to receive focuse
|
||||
## without the browser sending the focus event
|
||||
## which happens when the window isnt in focus
|
||||
return true if $previouslyFocusedEl.get(0) isnt $focused.get(0)
|
||||
|
||||
return false
|
||||
|
||||
getFocused = ->
|
||||
{ activeElement } = state("document")
|
||||
|
||||
if $dom.isFocused(activeElement)
|
||||
return $dom.wrap(activeElement)
|
||||
|
||||
return null
|
||||
|
||||
return {
|
||||
fireBlur
|
||||
|
||||
fireFocus
|
||||
|
||||
needsFocus
|
||||
|
||||
getFocused
|
||||
|
||||
interceptFocus
|
||||
|
||||
interceptBlur,
|
||||
|
||||
documentHasFocus,
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
const $dom = require('../dom')
|
||||
const $window = require('../dom/window')
|
||||
const $elements = require('../dom/elements')
|
||||
const $actionability = require('./actionability')
|
||||
|
||||
const create = (state) => {
|
||||
const documentHasFocus = () => {
|
||||
// hardcode document has focus as true
|
||||
// since the test should assume the window
|
||||
// is in focus the entire time
|
||||
return true
|
||||
}
|
||||
|
||||
const fireBlur = (el) => {
|
||||
const win = $window.getWindowByElement(el)
|
||||
|
||||
let hasBlurred = false
|
||||
|
||||
// we need to bind to the blur event here
|
||||
// because some browsers will not ever fire
|
||||
// the blur event if the window itself is not
|
||||
// currently blured
|
||||
const cleanup = () => {
|
||||
return $elements.callNativeMethod(el, 'removeEventListener', 'blur', onBlur)
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
return hasBlurred = true
|
||||
}
|
||||
|
||||
// for simplicity we allow change events
|
||||
// to be triggered by a manual blur
|
||||
$actionability.dispatchPrimedChangeEvents(state)
|
||||
|
||||
$elements.callNativeMethod(el, 'addEventListener', 'blur', onBlur)
|
||||
|
||||
$elements.callNativeMethod(el, 'blur')
|
||||
|
||||
cleanup()
|
||||
|
||||
// body will never emit focus events
|
||||
// so we avoid simulating this
|
||||
if ($elements.isBody(el)) {
|
||||
return
|
||||
}
|
||||
|
||||
// fallback if our focus event never fires
|
||||
// to simulate the focus + focusin
|
||||
if (!hasBlurred) {
|
||||
return simulateBlurEvent(el, win)
|
||||
}
|
||||
}
|
||||
|
||||
const simulateBlurEvent = (el, win) => {
|
||||
// todo handle relatedTarget's per the spec
|
||||
const focusoutEvt = new FocusEvent('focusout', {
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
view: win,
|
||||
relatedTarget: null,
|
||||
})
|
||||
|
||||
const blurEvt = new FocusEvent('blur', {
|
||||
bubble: false,
|
||||
cancelable: false,
|
||||
view: win,
|
||||
relatedTarget: null,
|
||||
})
|
||||
|
||||
el.dispatchEvent(blurEvt)
|
||||
|
||||
return el.dispatchEvent(focusoutEvt)
|
||||
}
|
||||
|
||||
const fireFocus = (el) => {
|
||||
// body will never emit focus events (unless it's contenteditable)
|
||||
// so we avoid simulating this
|
||||
if ($elements.isBody(el) && !$elements.isContentEditable(el)) {
|
||||
return
|
||||
}
|
||||
|
||||
// if we are focusing a different element
|
||||
// dispatch any primed change events
|
||||
// we have to do this because our blur
|
||||
// method might not get triggered if
|
||||
// our window is in focus since the
|
||||
// browser may fire blur events naturally
|
||||
$actionability.dispatchPrimedChangeEvents(state)
|
||||
|
||||
const win = $window.getWindowByElement(el)
|
||||
|
||||
// store the current focused element
|
||||
// since when we call .focus() it will change
|
||||
const $focused = getFocused()
|
||||
|
||||
let hasFocused = false
|
||||
|
||||
// we need to bind to the focus event here
|
||||
// because some browsers will not ever fire
|
||||
// the focus event if the window itself is not
|
||||
// currently focused
|
||||
const cleanup = () => {
|
||||
return $elements.callNativeMethod(el, 'removeEventListener', 'focus', onFocus)
|
||||
}
|
||||
|
||||
const onFocus = () => {
|
||||
return hasFocused = true
|
||||
}
|
||||
|
||||
$elements.callNativeMethod(el, 'addEventListener', 'focus', onFocus)
|
||||
|
||||
$elements.callNativeMethod(el, 'focus')
|
||||
|
||||
cleanup()
|
||||
|
||||
// fallback if our focus event never fires
|
||||
// to simulate the focus + focusin
|
||||
if (!hasFocused) {
|
||||
// only blur if we have a focused element AND its not
|
||||
// currently ourselves!
|
||||
if ($focused && $focused.get(0) !== el) {
|
||||
// additionally make sure that this isnt
|
||||
// the window, since that does not steal focus
|
||||
// or actually change the activeElement
|
||||
if (!$window.isWindow(el)) {
|
||||
fireBlur($focused.get(0))
|
||||
}
|
||||
}
|
||||
|
||||
return simulateFocusEvent(el, win)
|
||||
}
|
||||
}
|
||||
|
||||
const simulateFocusEvent = (el, win) => {
|
||||
// todo handle relatedTarget's per the spec
|
||||
const focusinEvt = new FocusEvent('focusin', {
|
||||
bubbles: true,
|
||||
view: win,
|
||||
relatedTarget: null,
|
||||
})
|
||||
|
||||
const focusEvt = new FocusEvent('focus', {
|
||||
view: win,
|
||||
relatedTarget: null,
|
||||
})
|
||||
|
||||
// not fired in the correct order per w3c spec
|
||||
// because chrome chooses to fire focus before focusin
|
||||
// and since we have a simulation fallback we end up
|
||||
// doing it how chrome does it
|
||||
// http://www.w3.org/TR/DOM-Level-3-Events/#h-events-focusevent-event-order
|
||||
el.dispatchEvent(focusEvt)
|
||||
|
||||
return el.dispatchEvent(focusinEvt)
|
||||
}
|
||||
|
||||
const interceptFocus = (el) => {
|
||||
// normally programmatic focus calls cause "primed" focus/blur
|
||||
// events if the window is not in focus
|
||||
// so we fire fake events to act as if the window
|
||||
// is always in focus
|
||||
const $focused = getFocused()
|
||||
|
||||
if ((!$focused || $focused[0] !== el) && $elements.isW3CFocusable(el)) {
|
||||
fireFocus(el)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
$elements.callNativeMethod(el, 'focus')
|
||||
}
|
||||
|
||||
const interceptBlur = (el) => {
|
||||
// normally programmatic blur calls cause "primed" focus/blur
|
||||
// events if the window is not in focus
|
||||
// so we fire fake events to act as if the window
|
||||
// is always in focus.
|
||||
const $focused = getFocused()
|
||||
|
||||
if ($focused && $focused[0] === el) {
|
||||
fireBlur(el)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
$elements.callNativeMethod(el, 'blur')
|
||||
}
|
||||
|
||||
const needsFocus = ($elToFocus, $previouslyFocusedEl) => {
|
||||
const $focused = getFocused()
|
||||
|
||||
// if we dont have a focused element
|
||||
// we know we want to fire a focus event
|
||||
if (!$focused) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if we didnt have a previously focused el
|
||||
// then always return true
|
||||
if (!$previouslyFocusedEl) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if we are attemping to focus a differnet element
|
||||
// than the one we currently have, we know we want
|
||||
// to fire a focus event
|
||||
if ($focused.get(0) !== $elToFocus.get(0)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if our focused element isnt the same as the previously
|
||||
// focused el then what probably happened is our mouse
|
||||
// down event caused our element to receive focuse
|
||||
// without the browser sending the focus event
|
||||
// which happens when the window isnt in focus
|
||||
if ($previouslyFocusedEl.get(0) !== $focused.get(0)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const getFocused = () => {
|
||||
const { activeElement } = state('document')
|
||||
|
||||
if ($dom.isFocused(activeElement)) {
|
||||
return $dom.wrap(activeElement)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
fireBlur,
|
||||
|
||||
fireFocus,
|
||||
|
||||
needsFocus,
|
||||
|
||||
getFocused,
|
||||
|
||||
interceptFocus,
|
||||
|
||||
interceptBlur,
|
||||
|
||||
documentHasFocus,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
$ = require("jquery")
|
||||
_ = require("lodash")
|
||||
|
||||
HISTORY_ATTRS = "pushState replaceState".split(" ")
|
||||
|
||||
events = []
|
||||
listenersAdded = null
|
||||
|
||||
removeAllListeners = ->
|
||||
listenersAdded = false
|
||||
|
||||
for event in events
|
||||
[win, event, cb] = event
|
||||
|
||||
win.removeEventListener(event, cb)
|
||||
|
||||
## reset all the events
|
||||
events = []
|
||||
|
||||
return null
|
||||
|
||||
addListener = (win, event, cb) ->
|
||||
events.push([win, event, cb])
|
||||
|
||||
win.addEventListener(event, cb)
|
||||
|
||||
eventHasReturnValue = (e) ->
|
||||
val = e.returnValue
|
||||
|
||||
## return false if val is an empty string
|
||||
## of if its undinefed
|
||||
return false if val is "" or _.isUndefined(val)
|
||||
|
||||
## else return true
|
||||
return true
|
||||
|
||||
module.exports = {
|
||||
bindTo: (contentWindow, callbacks = {}) ->
|
||||
return if listenersAdded
|
||||
|
||||
removeAllListeners()
|
||||
|
||||
listenersAdded = true
|
||||
|
||||
## set onerror global handler
|
||||
contentWindow.onerror = callbacks.onError
|
||||
|
||||
addListener contentWindow, "beforeunload", (e) ->
|
||||
## bail if we've canceled this event (from another source)
|
||||
## or we've set a returnValue on the original event
|
||||
return if e.defaultPrevented or eventHasReturnValue(e)
|
||||
|
||||
callbacks.onBeforeUnload(e)
|
||||
|
||||
addListener contentWindow, "unload", (e) ->
|
||||
## when we unload we need to remove all of the event listeners
|
||||
removeAllListeners()
|
||||
|
||||
## else we know to proceed onwards!
|
||||
callbacks.onUnload(e)
|
||||
|
||||
addListener contentWindow, "hashchange", (e) ->
|
||||
callbacks.onNavigation("hashchange", e)
|
||||
|
||||
for attr in HISTORY_ATTRS
|
||||
do (attr) ->
|
||||
return if not (orig = contentWindow.history?[attr])
|
||||
|
||||
contentWindow.history[attr] = (args...) ->
|
||||
orig.apply(@, args)
|
||||
|
||||
callbacks.onNavigation(attr, args)
|
||||
|
||||
addListener contentWindow, "submit", (e) ->
|
||||
## if we've prevented the default submit action
|
||||
## without stopping propagation, we will still
|
||||
## receive this event even though the form
|
||||
## did not submit
|
||||
return if e.defaultPrevented
|
||||
|
||||
## else we know to proceed onwards!
|
||||
callbacks.onSubmit(e)
|
||||
|
||||
contentWindow.alert = callbacks.onAlert
|
||||
contentWindow.confirm = callbacks.onConfirm
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
const _ = require('lodash')
|
||||
|
||||
const HISTORY_ATTRS = 'pushState replaceState'.split(' ')
|
||||
|
||||
let events = []
|
||||
let listenersAdded = null
|
||||
|
||||
const removeAllListeners = () => {
|
||||
listenersAdded = false
|
||||
|
||||
for (let e of events) {
|
||||
const [win, event, cb] = e
|
||||
|
||||
win.removeEventListener(event, cb)
|
||||
}
|
||||
|
||||
// reset all the events
|
||||
events = []
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const addListener = (win, event, cb) => {
|
||||
events.push([win, event, cb])
|
||||
|
||||
win.addEventListener(event, cb)
|
||||
}
|
||||
|
||||
const eventHasReturnValue = (e) => {
|
||||
const val = e.returnValue
|
||||
|
||||
// return false if val is an empty string
|
||||
// of if its undinefed
|
||||
if (val === '' || _.isUndefined(val)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// else return true
|
||||
return true
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bindTo (contentWindow, callbacks = {}) {
|
||||
if (listenersAdded) {
|
||||
return
|
||||
}
|
||||
|
||||
removeAllListeners()
|
||||
|
||||
listenersAdded = true
|
||||
|
||||
// set onerror global handler
|
||||
contentWindow.onerror = callbacks.onError
|
||||
|
||||
addListener(contentWindow, 'beforeunload', (e) => {
|
||||
// bail if we've canceled this event (from another source)
|
||||
// or we've set a returnValue on the original event
|
||||
if (e.defaultPrevented || eventHasReturnValue(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
callbacks.onBeforeUnload(e)
|
||||
})
|
||||
|
||||
addListener(contentWindow, 'unload', (e) => {
|
||||
// when we unload we need to remove all of the event listeners
|
||||
removeAllListeners()
|
||||
|
||||
// else we know to proceed onwards!
|
||||
callbacks.onUnload(e)
|
||||
})
|
||||
|
||||
addListener(contentWindow, 'hashchange', (e) => {
|
||||
callbacks.onNavigation('hashchange', e)
|
||||
})
|
||||
|
||||
for (let attr of HISTORY_ATTRS) {
|
||||
const orig = contentWindow.history?.[attr]
|
||||
|
||||
if (!orig) {
|
||||
continue
|
||||
}
|
||||
|
||||
contentWindow.history[attr] = function (...args) {
|
||||
orig.apply(this, args)
|
||||
|
||||
return callbacks.onNavigation(attr, args)
|
||||
}
|
||||
}
|
||||
|
||||
addListener(contentWindow, 'submit', (e) => {
|
||||
// if we've prevented the default submit action
|
||||
// without stopping propagation, we will still
|
||||
// receive this event even though the form
|
||||
// did not submit
|
||||
if (e.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
// else we know to proceed onwards!
|
||||
return callbacks.onSubmit(e)
|
||||
})
|
||||
|
||||
contentWindow.alert = callbacks.onAlert
|
||||
contentWindow.confirm = callbacks.onConfirm
|
||||
},
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
$Location = require("../cypress/location")
|
||||
$utils = require("../cypress/utils")
|
||||
|
||||
create = (state) ->
|
||||
return {
|
||||
getRemoteLocation: (key, win) ->
|
||||
try
|
||||
remoteUrl = $utils.locToString(win ? state("window"))
|
||||
location = $Location.create(remoteUrl)
|
||||
|
||||
if key
|
||||
location[key]
|
||||
else
|
||||
location
|
||||
catch e
|
||||
""
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const $Location = require('../cypress/location')
|
||||
const $utils = require('../cypress/utils')
|
||||
|
||||
const create = (state) => {
|
||||
return {
|
||||
getRemoteLocation (key, win) {
|
||||
try {
|
||||
const remoteUrl = $utils.locToString(win ?? state('window'))
|
||||
const location = $Location.create(remoteUrl)
|
||||
|
||||
if (key) {
|
||||
return location[key]
|
||||
}
|
||||
|
||||
return location
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
_ = require("lodash")
|
||||
Promise = require("bluebird")
|
||||
debug = require('debug')('cypress:driver:retries')
|
||||
|
||||
$utils = require("../cypress/utils")
|
||||
$errUtils = require("../cypress/error_utils")
|
||||
|
||||
create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) ->
|
||||
return {
|
||||
retry: (fn, options, log) ->
|
||||
## remove the runnables timeout because we are now in retry
|
||||
## mode and should be handling timing out ourselves and dont
|
||||
## want to accidentally time out via mocha
|
||||
if not options._runnableTimeout
|
||||
runnableTimeout = options.timeout ? timeout()
|
||||
clearTimeout()
|
||||
|
||||
current = state("current")
|
||||
|
||||
## use the log if passed in, else fallback to options._log
|
||||
## else fall back to just grabbing the last log per our current command
|
||||
log ?= options._log ? current?.getLastLog()
|
||||
|
||||
_.defaults(options, {
|
||||
_runnable: state("runnable")
|
||||
_runnableTimeout: runnableTimeout
|
||||
_interval: 16
|
||||
_retries: 0
|
||||
_start: new Date
|
||||
_name: current?.get("name")
|
||||
})
|
||||
|
||||
{ error } = options
|
||||
|
||||
## TODO: remove this once the codeframe PR is in since that
|
||||
## correctly handles not rewrapping errors so that stack
|
||||
## traces are correctly displayed
|
||||
if debug.enabled and error and not $errUtils.CypressErrorRe.test(error.name)
|
||||
debug('retrying due to caught error...')
|
||||
console.error(error)
|
||||
|
||||
interval = options.interval ? options._interval
|
||||
|
||||
## we calculate the total time we've been retrying
|
||||
## so we dont exceed the runnables timeout
|
||||
options.total = total = (new Date - options._start)
|
||||
|
||||
## increment retries
|
||||
options._retries += 1
|
||||
|
||||
## if our total exceeds the timeout OR the total + the interval
|
||||
## exceed the runnables timeout, then bail
|
||||
if total + interval >= options._runnableTimeout
|
||||
## snapshot the DOM since we are bailing
|
||||
## so the user can see the state we're in
|
||||
## when we fail
|
||||
log.snapshot() if log
|
||||
|
||||
if assertions = options.assertions
|
||||
finishAssertions(assertions)
|
||||
|
||||
{ error, onFail } = options
|
||||
|
||||
prependMsg = $errUtils.errMsgByPath("miscellaneous.retry_timed_out")
|
||||
|
||||
retryErrProps = $errUtils.modifyErrMsg(error, prependMsg, (msg1, msg2) ->
|
||||
return "#{msg2}#{msg1}"
|
||||
)
|
||||
|
||||
retryErr = $errUtils.mergeErrProps(error, retryErrProps)
|
||||
|
||||
$errUtils.throwErr(retryErr, {
|
||||
onFail: onFail or log
|
||||
})
|
||||
|
||||
runnableHasChanged = ->
|
||||
## if we've changed runnables don't retry!
|
||||
options._runnable isnt state("runnable")
|
||||
|
||||
ended = ->
|
||||
## we should NOT retry if
|
||||
## 1. our promise has been canceled
|
||||
## 2. or we have an error
|
||||
## 3. or if the runnables has changed
|
||||
|
||||
## although bluebird SHOULD cancel these retries
|
||||
## since they're all connected - apparently they
|
||||
## are not and the retry code is happening between
|
||||
## runnables which is bad likely due to the issue below
|
||||
##
|
||||
## bug in bluebird with not propagating cancelations
|
||||
## fast enough in a series of promises
|
||||
## https://github.com/petkaantonov/bluebird/issues/1424
|
||||
state("canceled") or state("error") or runnableHasChanged()
|
||||
|
||||
Promise
|
||||
.delay(interval)
|
||||
.then ->
|
||||
return if ended()
|
||||
|
||||
Cypress.action("cy:command:retry", options)
|
||||
|
||||
return if ended()
|
||||
|
||||
## if we are unstable then remove
|
||||
## the start since we need to retry
|
||||
## fresh once we become stable again!
|
||||
if state("isStable") is false
|
||||
options._start = undefined
|
||||
|
||||
## invoke the passed in retry fn
|
||||
## once we reach stability
|
||||
whenStable(fn)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
const _ = require('lodash')
|
||||
const Promise = require('bluebird')
|
||||
const debug = require('debug')('cypress:driver:retries')
|
||||
|
||||
const $errUtils = require('../cypress/error_utils')
|
||||
|
||||
const create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) => {
|
||||
return {
|
||||
retry (fn, options, log) {
|
||||
// remove the runnables timeout because we are now in retry
|
||||
// mode and should be handling timing out ourselves and dont
|
||||
// want to accidentally time out via mocha
|
||||
let runnableTimeout
|
||||
|
||||
if (!options._runnableTimeout) {
|
||||
runnableTimeout = options.timeout ?? timeout()
|
||||
clearTimeout()
|
||||
}
|
||||
|
||||
const current = state('current')
|
||||
|
||||
// use the log if passed in, else fallback to options._log
|
||||
// else fall back to just grabbing the last log per our current command
|
||||
if (!log) {
|
||||
log = options._log ?? current?.getLastLog()
|
||||
}
|
||||
|
||||
_.defaults(options, {
|
||||
_runnable: state('runnable'),
|
||||
_runnableTimeout: runnableTimeout,
|
||||
_interval: 16,
|
||||
_retries: 0,
|
||||
_start: new Date,
|
||||
_name: current?.get('name'),
|
||||
})
|
||||
|
||||
let { error } = options
|
||||
|
||||
// TODO: remove this once the codeframe PR is in since that
|
||||
// correctly handles not rewrapping errors so that stack
|
||||
// traces are correctly displayed
|
||||
if (debug.enabled && error && !$errUtils.CypressErrorRe.test(error.name)) {
|
||||
debug('retrying due to caught error...')
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
const interval = options.interval ?? options._interval
|
||||
|
||||
// we calculate the total time we've been retrying
|
||||
// so we dont exceed the runnables timeout
|
||||
const total = new Date - options._start
|
||||
|
||||
options.total = total
|
||||
|
||||
// increment retries
|
||||
options._retries += 1
|
||||
|
||||
// if our total exceeds the timeout OR the total + the interval
|
||||
// exceed the runnables timeout, then bail
|
||||
if ((total + interval) >= options._runnableTimeout) {
|
||||
// snapshot the DOM since we are bailing
|
||||
// so the user can see the state we're in
|
||||
// when we fail
|
||||
if (log) {
|
||||
log.snapshot()
|
||||
}
|
||||
|
||||
const assertions = options.assertions
|
||||
|
||||
if (assertions) {
|
||||
finishAssertions(assertions)
|
||||
}
|
||||
|
||||
let onFail
|
||||
|
||||
({ error, onFail } = options)
|
||||
|
||||
const prependMsg = $errUtils.errMsgByPath('miscellaneous.retry_timed_out')
|
||||
|
||||
const retryErrProps = $errUtils.modifyErrMsg(error, prependMsg, (msg1, msg2) => {
|
||||
return `${msg2}${msg1}`
|
||||
})
|
||||
|
||||
const retryErr = $errUtils.mergeErrProps(error, retryErrProps)
|
||||
|
||||
$errUtils.throwErr(retryErr, {
|
||||
onFail: onFail || log,
|
||||
})
|
||||
}
|
||||
|
||||
const runnableHasChanged = () => {
|
||||
// if we've changed runnables don't retry!
|
||||
return options._runnable !== state('runnable')
|
||||
}
|
||||
|
||||
const ended = () => {
|
||||
// we should NOT retry if
|
||||
// 1. our promise has been canceled
|
||||
// 2. or we have an error
|
||||
// 3. or if the runnables has changed
|
||||
|
||||
// although bluebird SHOULD cancel these retries
|
||||
// since they're all connected - apparently they
|
||||
// are not and the retry code is happening between
|
||||
// runnables which is bad likely due to the issue below
|
||||
//
|
||||
// bug in bluebird with not propagating cancelations
|
||||
// fast enough in a series of promises
|
||||
// https://github.com/petkaantonov/bluebird/issues/1424
|
||||
return state('canceled') || state('error') || runnableHasChanged()
|
||||
}
|
||||
|
||||
return Promise
|
||||
.delay(interval)
|
||||
.then(() => {
|
||||
if (ended()) {
|
||||
return
|
||||
}
|
||||
|
||||
Cypress.action('cy:command:retry', options)
|
||||
|
||||
if (ended()) {
|
||||
return
|
||||
}
|
||||
|
||||
// if we are unstable then remove
|
||||
// the start since we need to retry
|
||||
// fresh once we become stable again!
|
||||
if (state('isStable') === false) {
|
||||
options._start = undefined
|
||||
}
|
||||
|
||||
// invoke the passed in retry fn
|
||||
// once we reach stability
|
||||
return whenStable(fn)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
_ = require("lodash")
|
||||
$ = require("jquery")
|
||||
|
||||
$dom = require('../dom')
|
||||
$SnapshotsCss = require("./snapshots_css")
|
||||
|
||||
HIGHLIGHT_ATTR = "data-cypress-el"
|
||||
|
||||
create = ($$, state) ->
|
||||
snapshotsCss = $SnapshotsCss.create($$, state)
|
||||
snapshotsMap = new WeakMap()
|
||||
|
||||
getHtmlAttrs = (htmlEl) ->
|
||||
tmpHtmlEl = document.createElement("html")
|
||||
|
||||
_.transform htmlEl?.attributes, (memo, attr) ->
|
||||
return if not attr.specified
|
||||
|
||||
try
|
||||
## if we can successfully set the attributethen set it on memo
|
||||
## because it's possible the attribute is completely invalid
|
||||
tmpHtmlEl.setAttribute(attr.name, attr.value)
|
||||
memo[attr.name] = attr.value
|
||||
, {}
|
||||
|
||||
replaceIframes = (body) ->
|
||||
## remove iframes because we don't want extra requests made, JS run, etc
|
||||
## when restoring a snapshot
|
||||
## replace them so the lack of them doesn't cause layout issues
|
||||
## use <iframe>s as the placeholders because iframes are inline, replaced
|
||||
## elements (https://developer.mozilla.org/en-US/docs/Web/CSS/Replaced_element)
|
||||
## so it's hard to simulate their box model
|
||||
## attach class names and inline styles, so that CSS styles are applied
|
||||
## as they would be on the user's page, but override some
|
||||
## styles so it looks like a placeholder
|
||||
|
||||
## need to only replace the iframes in the cloned body, so grab those
|
||||
$iframes = body.find("iframe")
|
||||
## but query from the actual document, since the cloned body
|
||||
## iframes don't have proper styles applied
|
||||
|
||||
$$("iframe").each (idx, iframe) =>
|
||||
$iframe = $(iframe)
|
||||
|
||||
remove = ->
|
||||
$iframes.eq(idx).remove()
|
||||
|
||||
## if we don't have access to window
|
||||
## then just remove this $iframe...
|
||||
try
|
||||
if not $iframe.prop("contentWindow")
|
||||
return remove()
|
||||
catch e
|
||||
return remove()
|
||||
|
||||
props = {
|
||||
id: iframe.id
|
||||
class: iframe.className
|
||||
style: iframe.style.cssText
|
||||
}
|
||||
|
||||
dimensions = (fn) ->
|
||||
## jquery may throw here if we accidentally
|
||||
## pass an old iframe reference where the
|
||||
## document + window properties are unavailable
|
||||
try
|
||||
$iframe[fn]()
|
||||
catch e
|
||||
0
|
||||
|
||||
$placeholder = $("<iframe />", props).css({
|
||||
background: "#f8f8f8"
|
||||
border: "solid 1px #a3a3a3"
|
||||
boxSizing: "border-box"
|
||||
padding: "20px"
|
||||
width: dimensions("outerWidth")
|
||||
height: dimensions("outerHeight")
|
||||
})
|
||||
|
||||
$iframes.eq(idx).replaceWith($placeholder)
|
||||
contents = """
|
||||
<style>
|
||||
p { color: #888; font-family: sans-serif; line-height: 1.5; }
|
||||
</style>
|
||||
<p><iframe> placeholder for #{iframe.src}</p>
|
||||
"""
|
||||
$placeholder[0].src = "data:text/html;base64,#{window.btoa(contents)}"
|
||||
|
||||
getStyles = (snapshot) ->
|
||||
styleIds = snapshotsMap.get(snapshot)
|
||||
|
||||
return {} if not styleIds
|
||||
|
||||
return {
|
||||
headStyles: snapshotsCss.getStylesByIds(styleIds.headStyleIds)
|
||||
bodyStyles: snapshotsCss.getStylesByIds(styleIds.bodyStyleIds)
|
||||
}
|
||||
|
||||
detachDom = (iframeContents) ->
|
||||
{ headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds()
|
||||
htmlAttrs = getHtmlAttrs(iframeContents.find('html')[0])
|
||||
$body = iframeContents.find('body')
|
||||
|
||||
$body.find('script,link[rel="stylesheet"],style').remove()
|
||||
|
||||
snapshot = {
|
||||
name: "final state"
|
||||
htmlAttrs
|
||||
body: $body.detach()
|
||||
}
|
||||
|
||||
snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds })
|
||||
|
||||
return snapshot
|
||||
|
||||
createSnapshot = (name, $elToHighlight) ->
|
||||
## create a unique selector for this el
|
||||
## but only IF the subject is truly an element. For example
|
||||
## we might be wrapping a primitive like "$([1, 2]).first()"
|
||||
## which arrives here as number 1
|
||||
## jQuery v2 allowed to silently try setting 1[HIGHLIGHT_ATTR] doing nothing
|
||||
## jQuery v3 runs in strict mode and throws an error if you attempt to set a property
|
||||
|
||||
## TODO: in firefox sometimes this throws a cross-origin access error
|
||||
try
|
||||
isJqueryElement = $dom.isElement($elToHighlight) and $dom.isJquery($elToHighlight)
|
||||
|
||||
if isJqueryElement
|
||||
$elToHighlight.attr(HIGHLIGHT_ATTR, true)
|
||||
|
||||
## TODO: throw error here if cy is undefined!
|
||||
|
||||
$body = $$("body").clone()
|
||||
|
||||
## for the head and body, get an array of all CSS,
|
||||
## whether it's links or style tags
|
||||
## if it's same-origin, it will get the actual styles as a string
|
||||
## it it's cross-domain, it will get a reference to the link's href
|
||||
{ headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds()
|
||||
|
||||
## replaces iframes with placeholders
|
||||
replaceIframes($body)
|
||||
|
||||
## remove tags we don't want in body
|
||||
$body.find("script,link[rel='stylesheet'],style").remove()
|
||||
|
||||
## here we need to figure out if we're in a remote manual environment
|
||||
## if so we need to stringify the DOM:
|
||||
## 1. grab all inputs / textareas / options and set their value on the element
|
||||
## 2. convert DOM to string: body.prop("outerHTML")
|
||||
## 3. send this string via websocket to our server
|
||||
## 4. server rebroadcasts this to our client and its stored as a property
|
||||
|
||||
## its also possible for us to store the DOM string completely on the server
|
||||
## without ever sending it back to the browser (until its requests).
|
||||
## we could just store it in memory and wipe it out intelligently.
|
||||
## this would also prevent having to store the DOM structure on the client,
|
||||
## which would reduce memory, and some CPU operations
|
||||
|
||||
## now remove it after we clone
|
||||
if isJqueryElement
|
||||
$elToHighlight.removeAttr(HIGHLIGHT_ATTR)
|
||||
|
||||
## preserve attributes on the <html> tag
|
||||
htmlAttrs = getHtmlAttrs($$("html")[0])
|
||||
|
||||
snapshot = {
|
||||
name
|
||||
htmlAttrs
|
||||
body: $body
|
||||
}
|
||||
|
||||
snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds })
|
||||
|
||||
return snapshot
|
||||
|
||||
catch e
|
||||
null
|
||||
|
||||
return {
|
||||
createSnapshot
|
||||
|
||||
detachDom
|
||||
|
||||
getStyles
|
||||
|
||||
onCssModified: snapshotsCss.onCssModified
|
||||
|
||||
onBeforeWindowLoad: snapshotsCss.onBeforeWindowLoad
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HIGHLIGHT_ATTR
|
||||
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
const _ = require('lodash')
|
||||
const $ = require('jquery')
|
||||
|
||||
const $dom = require('../dom')
|
||||
const $SnapshotsCss = require('./snapshots_css')
|
||||
|
||||
const HIGHLIGHT_ATTR = 'data-cypress-el'
|
||||
|
||||
const create = ($$, state) => {
|
||||
const snapshotsCss = $SnapshotsCss.create($$, state)
|
||||
const snapshotsMap = new WeakMap()
|
||||
|
||||
const getHtmlAttrs = function (htmlEl) {
|
||||
const tmpHtmlEl = document.createElement('html')
|
||||
|
||||
return _.transform(htmlEl?.attributes, (memo, attr) => {
|
||||
if (!attr.specified) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// if we can successfully set the attributethen set it on memo
|
||||
// because it's possible the attribute is completely invalid
|
||||
tmpHtmlEl.setAttribute(attr.name, attr.value)
|
||||
memo[attr.name] = attr.value
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
}, {})
|
||||
}
|
||||
|
||||
const replaceIframes = (body) => {
|
||||
// remove iframes because we don't want extra requests made, JS run, etc
|
||||
// when restoring a snapshot
|
||||
// replace them so the lack of them doesn't cause layout issues
|
||||
// use <iframe>s as the placeholders because iframes are inline, replaced
|
||||
// elements (https://developer.mozilla.org/en-US/docs/Web/CSS/Replaced_element)
|
||||
// so it's hard to simulate their box model
|
||||
// attach class names and inline styles, so that CSS styles are applied
|
||||
// as they would be on the user's page, but override some
|
||||
// styles so it looks like a placeholder
|
||||
|
||||
// need to only replace the iframes in the cloned body, so grab those
|
||||
const $iframes = body.find('iframe')
|
||||
// but query from the actual document, since the cloned body
|
||||
// iframes don't have proper styles applied
|
||||
|
||||
return $$('iframe').each((idx, iframe) => {
|
||||
const $iframe = $(iframe)
|
||||
|
||||
const remove = () => {
|
||||
return $iframes.eq(idx).remove()
|
||||
}
|
||||
|
||||
// if we don't have access to window
|
||||
// then just remove this $iframe...
|
||||
try {
|
||||
if (!$iframe.prop('contentWindow')) {
|
||||
return remove()
|
||||
}
|
||||
} catch (error) {
|
||||
return remove()
|
||||
}
|
||||
|
||||
const props = {
|
||||
id: iframe.id,
|
||||
class: iframe.className,
|
||||
style: iframe.style.cssText,
|
||||
}
|
||||
|
||||
const dimensions = (fn) => {
|
||||
// jquery may throw here if we accidentally
|
||||
// pass an old iframe reference where the
|
||||
// document + window properties are unavailable
|
||||
try {
|
||||
return $iframe[fn]()
|
||||
} catch (e) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
const $placeholder = $('<iframe />', props).css({
|
||||
background: '#f8f8f8',
|
||||
border: 'solid 1px #a3a3a3',
|
||||
boxSizing: 'border-box',
|
||||
padding: '20px',
|
||||
width: dimensions('outerWidth'),
|
||||
height: dimensions('outerHeight'),
|
||||
})
|
||||
|
||||
$iframes.eq(idx).replaceWith($placeholder)
|
||||
const contents = `\
|
||||
<style>
|
||||
p { color: #888; font-family: sans-serif; line-height: 1.5; }
|
||||
</style>
|
||||
<p><iframe> placeholder for ${iframe.src}</p>\
|
||||
`
|
||||
|
||||
$placeholder[0].src = `data:text/html;base64,${window.btoa(contents)}`
|
||||
})
|
||||
}
|
||||
|
||||
const getStyles = (snapshot) => {
|
||||
const styleIds = snapshotsMap.get(snapshot)
|
||||
|
||||
if (!styleIds) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
headStyles: snapshotsCss.getStylesByIds(styleIds.headStyleIds),
|
||||
bodyStyles: snapshotsCss.getStylesByIds(styleIds.bodyStyleIds),
|
||||
}
|
||||
}
|
||||
|
||||
const detachDom = (iframeContents) => {
|
||||
const { headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds()
|
||||
const htmlAttrs = getHtmlAttrs(iframeContents.find('html')[0])
|
||||
const $body = iframeContents.find('body')
|
||||
|
||||
$body.find('script,link[rel="stylesheet"],style').remove()
|
||||
|
||||
const snapshot = {
|
||||
name: 'final state',
|
||||
htmlAttrs,
|
||||
body: $body.detach(),
|
||||
}
|
||||
|
||||
snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds })
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
const createSnapshot = (name, $elToHighlight) => {
|
||||
// create a unique selector for this el
|
||||
// but only IF the subject is truly an element. For example
|
||||
// we might be wrapping a primitive like "$([1, 2]).first()"
|
||||
// which arrives here as number 1
|
||||
// jQuery v2 allowed to silently try setting 1[HIGHLIGHT_ATTR] doing nothing
|
||||
// jQuery v3 runs in strict mode and throws an error if you attempt to set a property
|
||||
|
||||
// TODO: in firefox sometimes this throws a cross-origin access error
|
||||
try {
|
||||
const isJqueryElement = $dom.isElement($elToHighlight) && $dom.isJquery($elToHighlight)
|
||||
|
||||
if (isJqueryElement) {
|
||||
$elToHighlight.attr(HIGHLIGHT_ATTR, true)
|
||||
}
|
||||
|
||||
// TODO: throw error here if cy is undefined!
|
||||
|
||||
const $body = $$('body').clone()
|
||||
|
||||
// for the head and body, get an array of all CSS,
|
||||
// whether it's links or style tags
|
||||
// if it's same-origin, it will get the actual styles as a string
|
||||
// it it's cross-domain, it will get a reference to the link's href
|
||||
const { headStyleIds, bodyStyleIds } = snapshotsCss.getStyleIds()
|
||||
|
||||
// replaces iframes with placeholders
|
||||
replaceIframes($body)
|
||||
|
||||
// remove tags we don't want in body
|
||||
$body.find('script,link[rel=\'stylesheet\'],style').remove()
|
||||
|
||||
// here we need to figure out if we're in a remote manual environment
|
||||
// if so we need to stringify the DOM:
|
||||
// 1. grab all inputs / textareas / options and set their value on the element
|
||||
// 2. convert DOM to string: body.prop("outerHTML")
|
||||
// 3. send this string via websocket to our server
|
||||
// 4. server rebroadcasts this to our client and its stored as a property
|
||||
|
||||
// its also possible for us to store the DOM string completely on the server
|
||||
// without ever sending it back to the browser (until its requests).
|
||||
// we could just store it in memory and wipe it out intelligently.
|
||||
// this would also prevent having to store the DOM structure on the client,
|
||||
// which would reduce memory, and some CPU operations
|
||||
|
||||
// now remove it after we clone
|
||||
if (isJqueryElement) {
|
||||
$elToHighlight.removeAttr(HIGHLIGHT_ATTR)
|
||||
}
|
||||
|
||||
// preserve attributes on the <html> tag
|
||||
const htmlAttrs = getHtmlAttrs($$('html')[0])
|
||||
|
||||
const snapshot = {
|
||||
name,
|
||||
htmlAttrs,
|
||||
body: $body,
|
||||
}
|
||||
|
||||
snapshotsMap.set(snapshot, { headStyleIds, bodyStyleIds })
|
||||
|
||||
return snapshot
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createSnapshot,
|
||||
|
||||
detachDom,
|
||||
|
||||
getStyles,
|
||||
|
||||
onCssModified: snapshotsCss.onCssModified,
|
||||
|
||||
onBeforeWindowLoad: snapshotsCss.onBeforeWindowLoad,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HIGHLIGHT_ATTR,
|
||||
|
||||
create,
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
Promise = require("bluebird")
|
||||
|
||||
tryFn = (fn) ->
|
||||
## promisify this function
|
||||
Promise.try(fn)
|
||||
|
||||
create = (Cypress, state) ->
|
||||
isStable = (stable = true, event) ->
|
||||
return if state("isStable") is stable
|
||||
|
||||
## if we are going back to stable and we have
|
||||
## a whenStable callback
|
||||
if stable and whenStable = state("whenStable")
|
||||
## invoke it
|
||||
whenStable()
|
||||
|
||||
state("isStable", stable)
|
||||
|
||||
## we notify the outside world because this is what the runner uses to
|
||||
## show the 'loading spinner' during an app page loading transition event
|
||||
Cypress.action("cy:stability:changed", stable, event)
|
||||
|
||||
whenStable = (fn) ->
|
||||
## if we are not stable
|
||||
if state("isStable") is false
|
||||
return new Promise (resolve, reject) ->
|
||||
## then when we become stable
|
||||
state "whenStable", ->
|
||||
## reset this callback function
|
||||
state("whenStable", null)
|
||||
|
||||
## and invoke the original function
|
||||
tryFn(fn)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
|
||||
## else invoke it right now
|
||||
return tryFn(fn)
|
||||
|
||||
return {
|
||||
isStable
|
||||
|
||||
whenStable
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
const Promise = require('bluebird')
|
||||
|
||||
const tryFn = (fn) => {
|
||||
// promisify this function
|
||||
return Promise.try(fn)
|
||||
}
|
||||
|
||||
const create = (Cypress, state) => {
|
||||
const isStable = (stable = true, event) => {
|
||||
if (state('isStable') === stable) {
|
||||
return
|
||||
}
|
||||
|
||||
const whenStable = state('whenStable')
|
||||
|
||||
// if we are going back to stable and we have
|
||||
// a whenStable callback
|
||||
if (stable && whenStable) {
|
||||
// invoke it
|
||||
whenStable()
|
||||
}
|
||||
|
||||
state('isStable', stable)
|
||||
|
||||
// we notify the outside world because this is what the runner uses to
|
||||
// show the 'loading spinner' during an app page loading transition event
|
||||
return Cypress.action('cy:stability:changed', stable, event)
|
||||
}
|
||||
|
||||
const whenStable = (fn) => {
|
||||
// if we are not stable
|
||||
if (state('isStable') === false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// then when we become stable
|
||||
return state('whenStable', () => {
|
||||
// reset this callback function
|
||||
state('whenStable', null)
|
||||
|
||||
// and invoke the original function
|
||||
return tryFn(fn)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// else invoke it right now
|
||||
return tryFn(fn)
|
||||
}
|
||||
|
||||
return {
|
||||
isStable,
|
||||
|
||||
whenStable,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
$errUtils = require("../cypress/error_utils")
|
||||
|
||||
create = (state) ->
|
||||
return {
|
||||
timeout: (ms, delta = false) ->
|
||||
runnable = state("runnable")
|
||||
|
||||
if not runnable
|
||||
$errUtils.throwErrByPath("miscellaneous.outside_test")
|
||||
|
||||
if ms
|
||||
## if delta is true then we add (or subtract) from the
|
||||
## runnables current timeout instead of blanketingly setting it
|
||||
ms = if delta then runnable.timeout() + ms else ms
|
||||
runnable.timeout(ms)
|
||||
return @
|
||||
else
|
||||
runnable.timeout()
|
||||
|
||||
clearTimeout: ->
|
||||
runnable = state("runnable")
|
||||
|
||||
if not runnable
|
||||
$errUtils.throwErrByPath("miscellaneous.outside_test")
|
||||
|
||||
runnable.clearTimeout()
|
||||
|
||||
return @
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
const $errUtils = require('../cypress/error_utils')
|
||||
|
||||
const create = (state) => {
|
||||
return {
|
||||
timeout (ms, delta = false) {
|
||||
const runnable = state('runnable')
|
||||
|
||||
if (!runnable) {
|
||||
$errUtils.throwErrByPath('miscellaneous.outside_test')
|
||||
}
|
||||
|
||||
if (ms) {
|
||||
// if delta is true then we add (or subtract) from the
|
||||
// runnables current timeout instead of blanketingly setting it
|
||||
ms = delta ? runnable.timeout() + ms : ms
|
||||
runnable.timeout(ms)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
return runnable.timeout()
|
||||
},
|
||||
|
||||
clearTimeout () {
|
||||
const runnable = state('runnable')
|
||||
|
||||
if (!runnable) {
|
||||
$errUtils.throwErrByPath('miscellaneous.outside_test')
|
||||
}
|
||||
|
||||
runnable.clearTimeout()
|
||||
|
||||
return this
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
_ = require("lodash")
|
||||
|
||||
$errUtils = require("../cypress/error_utils")
|
||||
|
||||
validAliasApiRe = /^(\d+|all)$/
|
||||
|
||||
xhrNotWaitedOnByIndex = (state, alias, index, prop) ->
|
||||
## find the last request or response
|
||||
## which hasnt already been used.
|
||||
xhrs = state(prop) ? []
|
||||
|
||||
xhrs = _.filter xhrs, { alias }
|
||||
|
||||
## allow us to handle waiting on both
|
||||
## the request or the response part of the xhr
|
||||
privateProp = "_has#{prop}BeenWaitedOn"
|
||||
|
||||
obj = xhrs[index]
|
||||
|
||||
if obj and !obj[privateProp]
|
||||
obj[privateProp] = true
|
||||
return obj.xhr
|
||||
|
||||
create = (state) ->
|
||||
return {
|
||||
getIndexedXhrByAlias: (alias, index) ->
|
||||
if _.indexOf(alias, ".") == -1
|
||||
[str, prop] = [alias, null]
|
||||
else
|
||||
allParts = _.split(alias, '.')
|
||||
[str, prop] = [_.join(_.dropRight(allParts, 1), '.'), _.last(allParts)]
|
||||
|
||||
if prop
|
||||
if prop is "request"
|
||||
return xhrNotWaitedOnByIndex(state, str, index, "requests")
|
||||
else
|
||||
if prop isnt "response"
|
||||
$errUtils.throwErrByPath "wait.alias_invalid", {
|
||||
args: { prop, str }
|
||||
}
|
||||
|
||||
xhrNotWaitedOnByIndex(state, str, index, "responses")
|
||||
|
||||
getRequestsByAlias: (alias) ->
|
||||
if _.indexOf(alias, ".") == -1 || alias in _.keys(cy.state("aliases"))
|
||||
[alias, prop] = [alias, null]
|
||||
else
|
||||
# potentially valid prop
|
||||
allParts = _.split(alias, '.')
|
||||
[alias, prop] = [_.join(_.dropRight(allParts, 1), '.'), _.last(allParts)]
|
||||
|
||||
if prop and not validAliasApiRe.test(prop)
|
||||
$errUtils.throwErrByPath "get.alias_invalid", {
|
||||
args: { prop }
|
||||
}
|
||||
|
||||
if prop is "0"
|
||||
$errUtils.throwErrByPath "get.alias_zero", {
|
||||
args: { alias }
|
||||
}
|
||||
|
||||
## return an array of xhrs
|
||||
matching = _
|
||||
.chain(state("responses"))
|
||||
.filter({ alias: alias })
|
||||
.map("xhr")
|
||||
.value()
|
||||
|
||||
## return the whole array if prop is all
|
||||
return matching if prop is "all"
|
||||
|
||||
## else if prop its a digit and we need to return
|
||||
## the 1-based response from the array
|
||||
return matching[_.toNumber(prop) - 1] if prop
|
||||
|
||||
## else return the last matching response
|
||||
return _.last(matching)
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
create
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/* globals cy */
|
||||
const _ = require('lodash')
|
||||
|
||||
const $errUtils = require('../cypress/error_utils')
|
||||
|
||||
const validAliasApiRe = /^(\d+|all)$/
|
||||
|
||||
const xhrNotWaitedOnByIndex = (state, alias, index, prop) => {
|
||||
// find the last request or response
|
||||
// which hasnt already been used.
|
||||
let xhrs = state(prop) || []
|
||||
|
||||
xhrs = _.filter(xhrs, { alias })
|
||||
|
||||
// allow us to handle waiting on both
|
||||
// the request or the response part of the xhr
|
||||
const privateProp = `_has${prop}BeenWaitedOn`
|
||||
|
||||
const obj = xhrs[index]
|
||||
|
||||
if (obj && !obj[privateProp]) {
|
||||
obj[privateProp] = true
|
||||
|
||||
return obj.xhr
|
||||
}
|
||||
}
|
||||
|
||||
const create = (state) => {
|
||||
return {
|
||||
getIndexedXhrByAlias (alias, index) {
|
||||
let prop
|
||||
let str
|
||||
|
||||
if (_.indexOf(alias, '.') === -1) {
|
||||
str = alias
|
||||
prop = null
|
||||
} else {
|
||||
const allParts = _.split(alias, '.')
|
||||
|
||||
str = _.join(_.dropRight(allParts, 1), '.')
|
||||
prop = _.last(allParts)
|
||||
}
|
||||
|
||||
if (prop) {
|
||||
if (prop === 'request') {
|
||||
return xhrNotWaitedOnByIndex(state, str, index, 'requests')
|
||||
}
|
||||
|
||||
if (prop !== 'response') {
|
||||
$errUtils.throwErrByPath('wait.alias_invalid', {
|
||||
args: { prop, str },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return xhrNotWaitedOnByIndex(state, str, index, 'responses')
|
||||
},
|
||||
|
||||
getRequestsByAlias (alias) {
|
||||
let prop
|
||||
|
||||
if (_.indexOf(alias, '.') === -1 || _.keys(cy.state('aliases')).includes(alias)) {
|
||||
prop = null
|
||||
} else {
|
||||
// potentially valid prop
|
||||
const allParts = _.split(alias, '.')
|
||||
|
||||
alias = _.join(_.dropRight(allParts, 1), '.')
|
||||
prop = _.last(allParts)
|
||||
}
|
||||
|
||||
if (prop && !validAliasApiRe.test(prop)) {
|
||||
$errUtils.throwErrByPath('get.alias_invalid', {
|
||||
args: { prop },
|
||||
})
|
||||
}
|
||||
|
||||
if (prop === '0') {
|
||||
$errUtils.throwErrByPath('get.alias_zero', {
|
||||
args: { alias },
|
||||
})
|
||||
}
|
||||
|
||||
// return an array of xhrs
|
||||
const matching = _
|
||||
.chain(state('responses'))
|
||||
.filter({ alias })
|
||||
.map('xhr')
|
||||
.value()
|
||||
|
||||
// return the whole array if prop is all
|
||||
if (prop === 'all') {
|
||||
return matching
|
||||
}
|
||||
|
||||
// else if prop its a digit and we need to return
|
||||
// the 1-based response from the array
|
||||
if (prop) {
|
||||
return matching[_.toNumber(prop) - 1]
|
||||
}
|
||||
|
||||
// else return the last matching response
|
||||
return _.last(matching)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Timeouts from '../../../../src/cy/timeouts.coffee'
|
||||
import Timeouts from '../../../../src/cy/timeouts'
|
||||
|
||||
describe('driver/src/cy/timeouts', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user