Skip to content

Commit

Permalink
accept options.ensure to disable actionability checks (cypress-io#4881
Browse files Browse the repository at this point in the history
)

* accept ensure option on actionability

* remove left-in code

* update type_spec for changed error message

* remove unneeded code, allow focused to be validated for readonly

* address TODO in type.coffee about scrolling before typing into already focused

* rename ensureReceiveablility -> ensureNotDisabled, revert error message
  • Loading branch information
kuceb authored and grabartley committed Oct 6, 2019
1 parent f33c193 commit 067646d
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 64 deletions.
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) => {

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", ->
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

0 comments on commit 067646d

Please sign in to comment.