Skip to content

Commit

Permalink
accept options.ensure to disable actionability checks (#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 jennifer-shehane committed Sep 24, 2019
1 parent 6ba8d7c commit f32a921
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

3 comments on commit f32a921

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f32a921 Sep 24, 2019

Choose a reason for hiding this comment

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

AppVeyor has built the win32 ia32 version of the Test Runner.

You can install this pre-release platform-specific build using instructions at https://on.cypress.io/installing-cypress#Install-pre-release-version.

You will need to use custom CYPRESS_INSTALL_BINARY url and install Cypress using an url instead of the version.

set CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/3.5.0/win32-ia32/appveyor-develop-f32a921c9ae967469a772d606a305be281307c57-27638299/cypress.zip
npm install https://cdn.cypress.io/beta/binary/3.5.0/win32-ia32/appveyor-develop-f32a921c9ae967469a772d606a305be281307c57-27638299/cypress.zip

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f32a921 Sep 24, 2019

Choose a reason for hiding this comment

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

AppVeyor has built the win32 x64 version of the Test Runner.

You can install this pre-release platform-specific build using instructions at https://on.cypress.io/installing-cypress#Install-pre-release-version.

You will need to use custom CYPRESS_INSTALL_BINARY url and install Cypress using an url instead of the version.

set CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/3.5.0/win32-x64/appveyor-develop-f32a921c9ae967469a772d606a305be281307c57-27638299/cypress.zip
npm install https://cdn.cypress.io/beta/binary/3.5.0/win32-x64/appveyor-develop-f32a921c9ae967469a772d606a305be281307c57-27638299/cypress.zip

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f32a921 Sep 24, 2019

Choose a reason for hiding this comment

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

Circle has built the darwin x64 version of the Test Runner.

You can install this pre-release platform-specific build using instructions at https://on.cypress.io/installing-cypress#Install-pre-release-version.

You will need to use custom CYPRESS_INSTALL_BINARY url and install Cypress using an url instead of the version.

export CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/binary/3.5.0/darwin-x64/circle-develop-f32a921c9ae967469a772d606a305be281307c57-154543/cypress.zip
npm install https://cdn.cypress.io/beta/npm/3.5.0/circle-develop-f32a921c9ae967469a772d606a305be281307c57-154542/cypress.tgz

Please sign in to comment.