Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

accept options.ensure to disable actionability checks #4881

Merged
merged 8 commits into from
Sep 24, 2019
33 changes: 27 additions & 6 deletions packages/driver/src/cy/actionability.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -232,17 +244,23 @@ 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()

if onScroll
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)
Expand All @@ -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
Expand Down
31 changes: 13 additions & 18 deletions packages/driver/src/cy/commands/actions/type.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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
Expand Down
11 changes: 8 additions & 3 deletions packages/driver/src/cy/ensures.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -325,7 +328,7 @@ create = (state, expect) ->

ensureElementIsNotAnimating

ensureReceivability
ensureNotDisabled

ensureVisibility

Expand All @@ -338,6 +341,8 @@ create = (state, expect) ->
ensureValidPosition

ensureScrollability

ensureNotReadonly
}

module.exports = {
Expand Down
3 changes: 2 additions & 1 deletion packages/driver/src/cypress/cy.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/driver/src/cypress/error_messages.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 31 additions & 2 deletions packages/driver/src/dom/elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)/

Expand Down Expand Up @@ -344,6 +345,30 @@ const isFocused = (el) => {
}
}

const isFocusedOrInFocused = (el) => {
kuceb marked this conversation as resolved.
Show resolved Hide resolved

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)) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -856,6 +883,8 @@ module.exports = {

isFocused,

isFocusedOrInFocused,

isInputAllowingImplicitFormSubmission,

isNeedSingleValueChangeInputElement,
Expand Down Expand Up @@ -889,4 +918,4 @@ module.exports = {
getFirstStickyPositionParent,

getFirstScrollableParent,
}
})
11 changes: 9 additions & 2 deletions packages/driver/test/cypress/fixtures/dom.html
Original file line number Diff line number Diff line change
Expand Up @@ -289,14 +289,21 @@
<input type="submit" value ="submit me2" />
</form>

<form>
<form id="readonly">
<!-- All values are valid readonly -->
<input id="readonly-attr" readonly />
<input id="readonly-readonly" readonly="readonly" />
<input id="readonly-empty-str" readonly="" />

<!-- Although not strictly part of spec, Chrome respects any string -->
<input id="readonly-str" readonly="abc" />

<!-- readonly doesn't enforce on non typeable inputs -->
<input type="checkbox" id="readonly-checkbox" readonly />
<input type="submit" id="readonly-submit" value="readonly click" readonly />
<select name="hunter" readonly>
<option value="gon-val" readonly>gon</option>
</select>
</form>
</div>

Expand Down Expand Up @@ -356,7 +363,7 @@
<a href="#">5</a>
</div>

<select name="maps">
<select id="select-maps" name="maps">
<option value="de_dust2">dust2</option>
<option value="de_aztec">inferno</option>
<option value="de_nuke">nuke</option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ describe "src/cy/commands/actions/check", ->
done("should not fire change event")

cy.get(checkbox).check()

## readonly should only be limited to inputs, not checkboxes
it "can check readonly checkboxes", ->
cy.get('#readonly-checkbox').check().then ($checkbox) ->
expect($checkbox).to.be.checked

it "does not require visibility with force: true", ->
checkbox = ":checkbox[name='birds']"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -613,11 +613,19 @@ describe('src/cy/commands/actions/click', () => {
})

describe('actionability', () => {

it('can click on inline elements that wrap lines', () => {
cy.get('#overflow-link').find('.wrapped').click()
})

// readonly should only limit typing, not clicking
it('can click on readonly inputs', () => {
cy.get('#readonly-attr').click()
})

it('can click on readonly submit inputs', () => {
cy.get('#readonly-submit').click()
})

it('can click elements which are hidden until scrolled within parent container', () => {
cy.get('#overflow-auto-container').contains('quux').click()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe "src/cy/commands/actions/focus", ->
.get("input:first").focus()
.get("#focus input").focus()
.then ->
expect(blurred).to.be.true
expect(blurred).to.be.true

it "matches cy.focused()", ->
button = cy.$$("#button")
Expand Down Expand Up @@ -153,6 +153,14 @@ describe "src/cy/commands/actions/focus", ->
cy.get("[data-cy=rect]").focus().then ->
expect(onFocus).to.be.calledOnce

it "can focus on readonly inputs", ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is the test that is effectively testing this entire PR - it needs a link above the issue to the issue that it's fixing.

onFocus = cy.stub()

cy.$$("#readonly-attr").focus(onFocus)

cy.get("#readonly-attr").focus().then ->
expect(onFocus).to.be.calledOnce

describe "assertion verification", ->
beforeEach ->
cy.on "log:added", (attrs, log) =>
Expand Down Expand Up @@ -290,6 +298,7 @@ describe "src/cy/commands/actions/focus", ->

cy.focus()

## TODO: dont skip this
it.skip "slurps up failed promises", (done) ->
cy.timeout(1000)

Expand Down
Loading