diff --git a/packages/driver/src/cy/actionability.coffee b/packages/driver/src/cy/actionability.coffee
index 3ad78f7ebb10..09760af5ff39 100644
--- a/packages/driver/src/cy/actionability.coffee
+++ b/packages/driver/src/cy/actionability.coffee
@@ -209,6 +209,18 @@ ensureNotAnimating = (cy, $el, coordsHistory, animationDistanceThreshold) ->
cy.ensureElementIsNotAnimating($el, coordsHistory, animationDistanceThreshold)
verify = (cy, $el, options, callbacks) ->
+
+ _.defaults(options, {
+ ensure: {
+ position: true,
+ visibility: true,
+ notDisabled: true,
+ notCovered: true,
+ notReadonly: false,
+ custom: false
+ }
+ })
+
win = $dom.getWindowByElement($el.get(0))
{ _log, force, position } = options
@@ -220,7 +232,7 @@ verify = (cy, $el, options, callbacks) ->
## if we have a position we must validate
## this ahead of time else bail early
- if position
+ if options.ensure.position and position
try
cy.ensureValidPosition(position, _log)
catch err
@@ -232,6 +244,9 @@ verify = (cy, $el, options, callbacks) ->
runAllChecks = ->
if force isnt true
+ ## ensure its 'receivable'
+ if (options.ensure.notDisabled) then cy.ensureNotDisabled($el, _log)
+
## scroll the element into view
$el.get(0).scrollIntoView()
@@ -239,10 +254,13 @@ verify = (cy, $el, options, callbacks) ->
onScroll($el, "element")
## ensure its visible
- cy.ensureVisibility($el, _log)
+ if (options.ensure.visibility) then cy.ensureVisibility($el, _log)
- ## ensure its 'receivable' (not disabled, readonly)
- cy.ensureReceivability($el, _log)
+ if options.ensure.notReadonly
+ cy.ensureNotReadonly($el, _log)
+
+ if _.isFunction(options.custom)
+ options.custom($el, _log)
## now go get all the coords for this element
coords = getCoordinatesForEl(cy, $el, options)
@@ -264,10 +282,13 @@ verify = (cy, $el, options, callbacks) ->
## to figure out if its being covered by another element.
## this calculation is relative from the viewport so we
## only care about fromViewport coords
- $elAtCoords = ensureElIsNotCovered(cy, win, $el, coords.fromViewport, options, _log, onScroll)
+ $elAtCoords = options.ensure.notCovered && ensureElIsNotCovered(cy, win, $el, coords.fromViewport, options, _log, onScroll)
## pass our final object into onReady
- return onReady($elAtCoords ? $el, coords)
+ finalEl = $elAtCoords ? $el
+ finalCoords = getCoordinatesForEl(cy, $el, options)
+
+ return onReady(finalEl, finalCoords)
## we cannot enforce async promises here because if our
## element passes every single check, we MUST fire the event
diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js
index 8475ef382cf1..7f553181fa5f 100644
--- a/packages/driver/src/cy/commands/actions/type.js
+++ b/packages/driver/src/cy/commands/actions/type.js
@@ -424,30 +424,27 @@ module.exports = function (Commands, Cypress, cy, state, config) {
const handleFocused = function () {
// if it's the body, don't need to worry about focus
- let elToCheckCurrentlyFocused
-
if (isBody) {
return type()
}
+ options.ensure = {
+ position: true,
+ visibility: true,
+ notDisabled: true,
+ notCovered: true,
+ notReadonly: true,
+ }
+
// if the subject is already the focused element, start typing
// we handle contenteditable children by getting the host contenteditable,
// and seeing if that is focused
// Checking first if element is focusable accounts for focusable els inside
// of contenteditables
- let $focused = cy.getFocused()
-
- $focused = $focused && $focused[0]
-
- if ($elements.isFocusable(options.$el)) {
- elToCheckCurrentlyFocused = options.$el[0]
- } else if ($elements.isContentEditable(options.$el[0])) {
- elToCheckCurrentlyFocused = $selection.getHostContenteditable(options.$el[0])
- }
-
- if (elToCheckCurrentlyFocused && (elToCheckCurrentlyFocused === $focused)) {
- // TODO: not scrolling here, but revisit when scroll algorithm changes
- return type()
+ if (($elements.isFocusedOrInFocused(options.$el.get(0)))) {
+ options.ensure = {
+ notReadonly: true,
+ }
}
return $actionability.verify(cy, options.$el, options, {
@@ -456,12 +453,10 @@ module.exports = function (Commands, Cypress, cy, state, config) {
},
onReady ($elToClick) {
- $focused = cy.getFocused()
-
// if we dont have a focused element
// or if we do and its not ourselves
// then issue the click
- if (!$focused || ($focused && ($focused.get(0) !== options.$el.get(0)))) {
+ if (!$elements.isFocusedOrInFocused($elToClick[0])) {
// click the element first to simulate focus
// and typical user behavior in case the window
// is out of focus
diff --git a/packages/driver/src/cy/ensures.coffee b/packages/driver/src/cy/ensures.coffee
index 6c69c1491c80..92ee3ee7d6c5 100644
--- a/packages/driver/src/cy/ensures.coffee
+++ b/packages/driver/src/cy/ensures.coffee
@@ -101,7 +101,7 @@ create = (state, expect) ->
args: { cmd, node }
})
- ensureReceivability = (subject, onFail) ->
+ ensureNotDisabled = (subject, onFail) ->
cmd = state("current").get("name")
if subject.prop("disabled")
@@ -112,6 +112,9 @@ create = (state, expect) ->
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) and subject.prop("readonly")
@@ -127,7 +130,7 @@ create = (state, expect) ->
# We overwrite the filter(":visible") in jquery
# packages/driver/src/config/jquery.coffee#L51
- # So that this effectively calls our logic
+ # 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)
@@ -325,7 +328,7 @@ create = (state, expect) ->
ensureElementIsNotAnimating
- ensureReceivability
+ ensureNotDisabled
ensureVisibility
@@ -338,6 +341,8 @@ create = (state, expect) ->
ensureValidPosition
ensureScrollability
+
+ ensureNotReadonly
}
module.exports = {
diff --git a/packages/driver/src/cypress/cy.coffee b/packages/driver/src/cypress/cy.coffee
index 98ce874a495e..fe5c18c88e37 100644
--- a/packages/driver/src/cypress/cy.coffee
+++ b/packages/driver/src/cypress/cy.coffee
@@ -680,7 +680,8 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
ensureElDoesNotHaveCSS: ensures.ensureElDoesNotHaveCSS
ensureVisibility: ensures.ensureVisibility
ensureDescendents: ensures.ensureDescendents
- ensureReceivability: ensures.ensureReceivability
+ ensureNotReadonly: ensures.ensureNotReadonly
+ ensureNotDisabled: ensures.ensureNotDisabled
ensureValidPosition: ensures.ensureValidPosition
ensureScrollability: ensures.ensureScrollability
ensureElementIsNotAnimating: ensures.ensureElementIsNotAnimating
diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee
index 54d6801baaba..b3c9ce68b5eb 100644
--- a/packages/driver/src/cypress/error_messages.coffee
+++ b/packages/driver/src/cypress/error_messages.coffee
@@ -209,6 +209,15 @@ module.exports = {
https://on.cypress.io/element-cannot-be-interacted-with
"""
+ readonly: """
+ #{cmd('{{cmd}}')} failed because this element is readonly:
+
+ {{node}}
+
+ Fix this problem, or use {force: true} to disable error checking.
+
+ https://on.cypress.io/element-cannot-be-interacted-with
+ """
invalid_position_argument: "Invalid position argument: '{{position}}'. Position may only be {{validPositions}}."
not_scrollable: """
#{cmd('{{cmd}}')} failed because this element is not scrollable:\n
diff --git a/packages/driver/src/dom/elements.js b/packages/driver/src/dom/elements.js
index 943f2308528b..b28a2339b29c 100644
--- a/packages/driver/src/dom/elements.js
+++ b/packages/driver/src/dom/elements.js
@@ -4,6 +4,7 @@ const $jquery = require('./jquery')
const $window = require('./window')
const $document = require('./document')
const $utils = require('../cypress/utils')
+const $selection = require('./selection')
const fixedOrStickyRe = /(fixed|sticky)/
@@ -344,6 +345,30 @@ const isFocused = (el) => {
}
}
+const isFocusedOrInFocused = (el) => {
+
+ const doc = $document.getDocumentFromElement(el)
+
+ const { activeElement, body } = doc
+
+ if (activeElementIsDefault(activeElement, body)) {
+ return false
+ }
+
+ let elToCheckCurrentlyFocused
+
+ if (isFocusable($(el))) {
+ elToCheckCurrentlyFocused = el
+ } else if (isContentEditable(el)) {
+ elToCheckCurrentlyFocused = $selection.getHostContenteditable(el)
+ }
+
+ if (elToCheckCurrentlyFocused && elToCheckCurrentlyFocused === activeElement) {
+ return true
+ }
+
+}
+
const isElement = function (obj) {
try {
if ($jquery.isJquery(obj)) {
@@ -813,7 +838,9 @@ const stringify = (el, form = 'long') => {
})
}
-module.exports = {
+// We extend `module.exports` to allow circular dependencies using `require`
+// Otherwise we would not be able to `require` this util from `./selection`, for example.
+_.extend(module.exports, {
isElement,
isSelector,
@@ -856,6 +883,8 @@ module.exports = {
isFocused,
+ isFocusedOrInFocused,
+
isInputAllowingImplicitFormSubmission,
isNeedSingleValueChangeInputElement,
@@ -889,4 +918,4 @@ module.exports = {
getFirstStickyPositionParent,
getFirstScrollableParent,
-}
+})
diff --git a/packages/driver/test/cypress/fixtures/dom.html b/packages/driver/test/cypress/fixtures/dom.html
index f1a56c0c3ee4..6532fd6337d6 100644
--- a/packages/driver/test/cypress/fixtures/dom.html
+++ b/packages/driver/test/cypress/fixtures/dom.html
@@ -289,7 +289,7 @@
-
@@ -356,7 +363,7 @@
5
-