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

add "be.focused" and "have.focus" to assertions (#3219) #4274

Merged
merged 2 commits into from
May 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions cli/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3593,6 +3593,22 @@ declare namespace Cypress {
* @see https://on.cypress.io/assertions
*/
(chainer: 'contain', value: string): Chainable<Subject>
/**
* Assert that at least one element of the selection is focused.
* @example
* cy.get('#result').should('have.focus')
* cy.get('#result').should('be.focused')
* @see https://on.cypress.io/assertions
*/
(chainer: 'have.focus'): Chainable<Subject>
/**
* Assert that at least one element of the selection is focused.
* @example
* cy.get('#result').should('be.focused')
* cy.get('#result').should('have.focus')
* @see https://on.cypress.io/assertions
*/
(chainer: 'be.focused'): Chainable<Subject>
/**
* Assert that the selection is not empty. Note that this overrides the built-in chai assertion. If the object asserted against is not a jQuery object, the original implementation will be called.
* @example
Expand Down Expand Up @@ -3749,6 +3765,22 @@ declare namespace Cypress {
* @see https://on.cypress.io/assertions
*/
(chainer: 'not.be.visible'): Chainable<Subject>
/**
* Assert that no element of the selection is focused.
* @example
* cy.get('#result').should('not.have.focus')
* cy.get('#result').should('not.be.focused')
* @see https://on.cypress.io/assertions
*/
(chainer: 'not.have.focus'): Chainable<Subject>
/**
* Assert that no element of the selection is focused.
* @example
* cy.get('#result').should('not.be.focused')
* cy.get('#result').should('not.have.focus')
* @see https://on.cypress.io/assertions
*/
(chainer: 'not.be.focused'): Chainable<Subject>
/**
* Assert that the selection does not contain the given text, using `:contains()`. If the object asserted against is not a jQuery object, or if `contain` is not called as a function, the original implementation will be called.
* @example
Expand Down
6 changes: 6 additions & 0 deletions cli/types/tests/chainer-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@ cy.get('#result').should('be.selected')

cy.get('#result').should('be.visible')

cy.get('#result').should('be.focused')
cy.get('#result').should('not.be.focused')

cy.get('#result').should('have.focus')
cy.get('#result').should('not.have.focus')

cy.get('#result').should('be.contain', 'text')

cy.get('#result').should('have.attr', 'role')
Expand Down
43 changes: 43 additions & 0 deletions packages/driver/src/config/jquery.coffee
Original file line number Diff line number Diff line change
@@ -1,10 +1,53 @@
$ = require("jquery")
_ = require('lodash')
require("jquery.scrollto")

$dom = require("../dom")

## force jquery to have the same visible
## and hidden logic as cypress

## this prevents `is` from calling into the native .matches method
## which would prevent our `focus` code from ever being called during
## is(:focus).
## see https://github.com/jquery/sizzle/wiki#sizzlematchesselector-domelement-element-string-selector-

## this is to help to interpretor make optimizations around try/catch
tryCatchFinally = ({tryFn, catchFn, finallyFn}) ->
try
tryFn()
catch e
catchFn(e)
finally
finallyFn()

matchesSelector = $.find.matchesSelector
$.find.matchesSelector = (elem, expr) ->
isUsingFocus = _.includes(expr, ':focus')
if isUsingFocus
supportMatchesSelector = $.find.support.matchesSelector
$.find.support.matchesSelector = false

args = arguments
_this = @

return tryCatchFinally({
tryFn: ->
matchesSelector.apply(_this, args)
catchFn: (e) ->
throw e
finallyFn: ->
if isUsingFocus
$.find.support.matchesSelector = supportMatchesSelector
})


## see difference between 'filters' and 'pseudos'
## https://api.jquery.com/filter/ and https://api.jquery.com/category/selectors/

$.expr.pseudos.focus = $dom.isFocused
$.expr.filters.focus = $dom.isFocused
$.expr.pseudos.focused = $dom.isFocused
$.expr.filters.visible = $dom.isVisible
$.expr.filters.hidden = $dom.isHidden

Expand Down
16 changes: 4 additions & 12 deletions packages/driver/src/cy/focused.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,12 @@ create = (state) ->
return false

getFocused = ->
try
{ activeElement, body } = state("document")

## active element is the default if its null
## or its equal to document.body
activeElementIsDefault = ->
(not activeElement) or (activeElement is body)

if activeElementIsDefault()
return null
{ activeElement } = state("document")

if $dom.isFocused(activeElement)
return $dom.wrap(activeElement)
catch
return null

return null

return {
fireBlur
Expand Down
16 changes: 13 additions & 3 deletions packages/driver/src/cypress/chai_jquery.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ _ = require("lodash")
$ = require("jquery")
$dom = require("../dom")

selectors = "visible hidden selected checked enabled disabled".split(" ")
selectors = {
visible: "visible"
hidden: "hidden"
selected: "selected"
checked: "checked"
enabled: "enabled"
disabled: "disabled"
focus: "focused"
focused: "focused"
}

attrs = {
attr: "attribute"
css: "CSS property"
Expand Down Expand Up @@ -172,15 +182,15 @@ $chaiJquery = (chai, chaiUtils, callbacks = {}) ->
else
_super.apply(@, arguments)

_.each selectors, (selector) ->
_.each selectors, (selectorName, selector) ->
chai.Assertion.addProperty selector, ->
assert(
@,
selector,
wrap(@).is(":" + selector),
'expected #{this} to be #{exp}',
'expected #{this} not to be #{exp}',
selector
selectorName
)

_.each attrs, (description, attr) ->
Expand Down
24 changes: 24 additions & 0 deletions packages/driver/src/dom/elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,28 @@ const isSvg = function (el) {
}
}

// active element is the default if its null
// or its equal to document.body
const activeElementIsDefault = (activeElement, body) => {
return (!activeElement) || (activeElement === body)
}

const isFocused = (el) => {
try {
const doc = $document.getDocumentFromElement(el)

const { activeElement, body } = doc

if (activeElementIsDefault(activeElement, body)) {
return false
}

return doc.activeElement === el
} catch (err) {
return false
}
}

const isElement = function (obj) {
try {
if ($jquery.isJquery(obj)) {
Expand Down Expand Up @@ -757,6 +779,8 @@ module.exports = {

isType,

isFocused,

isNeedSingleValueChangeInputElement,

canSetSelectionRangeElement,
Expand Down
4 changes: 3 additions & 1 deletion packages/driver/src/dom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { isWindow, getWindowByElement } = $window
const { isDocument } = $document
const { wrap, unwrap, isJquery, query } = $jquery
const { isVisible, isHidden, getReasonIsHidden } = $visibility
const { isType, isFocusable, isElement, isScrollable, stringify, getElements, getContainsSelector, getFirstDeepestElement, isDetached, isAttached, isTextLike, isSelector, isDescendent, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent } = $elements
const { isType, isFocusable, isElement, isScrollable, isFocused, stringify, getElements, getContainsSelector, getFirstDeepestElement, isDetached, isAttached, isTextLike, isSelector, isDescendent, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent } = $elements
const { getCoordsByPosition, getElementPositioning, getElementCoordinatesByPosition, getElementAtPointFromViewport, getElementCoordinatesByPositionRelativeToXY } = $coordinates

const isDom = (obj) => {
Expand Down Expand Up @@ -42,6 +42,8 @@ module.exports = {

isScrollable,

isFocused,

isDetached,

isAttached,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$ = Cypress.$.bind(Cypress)
$ = Cypress.$
_ = Cypress._

helpers = require("../../support/helpers")
Expand Down Expand Up @@ -810,6 +810,14 @@ describe "src/cy/commands/assertions", ->
.get("body")
.get("button").should("be.visible")

it 'jquery wrapping els and selectors, not changing subject', ->
cy.wrap(cy.$$('<div></div>').appendTo('body')).should('not.be.visible')
cy.wrap(cy.$$('<div></div>')).should('not.exist')
cy.wrap(cy.$$('<div></div>').appendTo('body')).should('not.be.visible').should('exist')
cy.wrap(cy.$$('.non-existent')).should('not.be.visible').should('not.exist')
cy.wrap(cy.$$('.non-existent')).should('not.exist')
cy.wrap(cy.$$('.non-existent')).should('not.be.visible').should('not.exist')

describe "#have.length", ->
it "formats _obj with cypress", (done) ->
cy.on "log:added", (attrs, log) ->
Expand Down Expand Up @@ -1477,6 +1485,92 @@ describe "src/cy/commands/assertions", ->
"expected **<div>** not to be **empty**"
)

context "focused", ->
beforeEach ->
@div = $("<div id='div' tabindex=0></div>").appendTo($('body'))
@div.is = -> throw new Error("is called")

@div2 = $("<div id='div2' tabindex=1><button>button</button></div>").appendTo($('body'))
@div2.is = -> throw new Error("is called")

it "focus, not focus, raw dom documents", ->
expect(@div).to.not.be.focused
expect(@div[0]).to.not.be.focused
@div.focus()
expect(@div).to.be.focused
expect(@div[0]).to.be.focused

@div.blur()
expect(@div).to.not.be.focused
expect(@div[0]).to.not.be.focused


expect(@div2).not.to.be.focused
expect(@div2[0]).not.to.be.focused
@div.focus()
expect(@div2).not.to.be.focused
@div2.focus()
expect(@div2).to.be.focused

expect(@logs.length).to.eq(10)

l1 = @logs[0]
l2 = @logs[1]
l3 = @logs[2]
l4 = @logs[3]

expect(l1.get("message")).to.eq(
"expected **<div#div>** not to be **focused**"
)

expect(l2.get("message")).to.eq(
"expected **<div#div>** not to be **focused**"
)

expect(l3.get("message")).to.eq(
"expected **<div#div>** to be **focused**"
)

expect(l4.get("message")).to.eq(
"expected **<div#div>** to be **focused**"
)

it "works with focused or focus", ->
expect(@div).to.not.have.focus
expect(@div).to.not.have.focused
expect(@div).to.not.be.focus
expect(@div).to.not.be.focused

cy.get('#div').should('not.be.focused')
cy.get('#div').should('not.have.focus')

it "works with multiple elements", ->
cy.get('div:last').focus()
cy.get('div').should('have.focus')
cy.get('div:last').blur()
cy.get('div').should('not.have.focus')

it "throws when obj is not DOM", (done) ->
cy.on "fail", (err) =>
expect(@logs.length).to.eq(1)
expect(@logs[0].get("error").message).to.contain(
"expected {} to be 'focused'"
)
expect(err.message).to.include("> focus")
expect(err.message).to.include("> {}")

done()

expect({}).to.have.focus

it "calls into custom focus pseudos", ->
cy.$$('button:first').focus()
stub = cy.spy($.expr.pseudos, 'focus').as('focus')
expect(cy.$$('button:first')).to.have.focus
cy.get('button:first').should('have.focus')
.then ->
expect(stub).to.be.calledTwice

context "match", ->
beforeEach ->
@div = $("<div></div>")
Expand Down