Merge pull request #7216 from sainthkh/decaff-driver-3

This commit is contained in:
Jennifer Shehane
2020-05-05 14:24:04 +06:30
committed by GitHub
23 changed files with 1632 additions and 1370 deletions
-116
View File
@@ -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
}
+145
View File
@@ -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,
}
-351
View File
@@ -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
}
+415
View File
@@ -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,
}
-96
View File
@@ -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
}
+118
View File
@@ -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,
}
-221
View File
@@ -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
}
+252
View File
@@ -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,
}
-86
View File
@@ -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
}
+107
View File
@@ -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
},
}
-21
View File
@@ -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
}
+25
View File
@@ -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,
}
-118
View File
@@ -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
}
+143
View File
@@ -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,
}
-196
View File
@@ -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>&lt;iframe&gt; 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
}
+216
View File
@@ -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>&lt;iframe&gt; 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,
}
-48
View File
@@ -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
}
+60
View File
@@ -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,
}
-33
View File
@@ -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
}
+40
View File
@@ -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,
}
-83
View File
@@ -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
}
+110
View File
@@ -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(() => {