fix programmatic focus/blur events, typing into currently focused (#2982)

* fix programmatic blur events, allow typing into currently focused, fix getHostContenteditable

* intercept .blur

* reference issues in tests

* make tests account for conditional number of new lines inserted

- newer browsers insert a double new line, whereas older browsers dont
- write a helper that exposes the multiplier of new lines

* cleanup, remove dead code

* make tests dynamic when browser is or isn't out of focus

* cleanup, remove old notes, add more notes

* add failing tests for when native focus / blur are called multiple times

- need to handle not firing the events conditionally based on whether
or not the element would / should receive them

* remove old code for priming focus/blur events when window is out of focus

* remove dead code

* update focus_blur spec + add chai-subset

* decaffeinate: Rename focus_blur_spec.coffee from .coffee to .js

* decaffeinate: Convert focus_blur_spec.coffee to JS

* decaffeinate: Run post-processing cleanups on focus_blur_spec.coffee

* add failing test

* fix double blur/focus events

* make document.hasFocus always return true, add test

* fix focus events when non-focusable element

* remove unneeded retrun

* fix focusing body/ bluring active element on click

* forgot to call .get() with index

* fix focus issue with body/window

* still allow firefocus on window, skip firing focus if firstfocusable is window during click

* left out return in intercept blur/focus

* cleanup test code for focus_blur spec

* add tests to type_spec, focus_blur_spec
00-00005bfe

* update focus logic for click, fix dtslint error
06-00003d9c

* add tests for selectionchange event in focus_blur spec
01-00000dae

* set dep to exact version
06-00002320

* minor formatting

* intercept focus/blur for SVGElement

* add comment to type-into-already-focused logic


Co-authored-by: Brian Mann <brian.mann86@gmail.com>
Co-authored-by: Jennifer Shehane <shehane.jennifer@gmail.com>
This commit is contained in:
Ben Kucera
2019-06-11 11:06:26 -04:00
committed by Brian Mann
parent 6772a02faf
commit 7efd9d8ab0
13 changed files with 919 additions and 396 deletions

View File

@@ -1783,6 +1783,15 @@ declare namespace Cypress {
* @see https://on.cypress.io/writefile
*/
writeFile<C extends FileContents>(filePath: string, contents: C, encoding: Encodings, options?: Partial<Loggable>): Chainable<C>
/**
* jQuery library bound to the AUT
*
* @see https://on.cypress.io/$
* @example
* cy.$$('p')
*/
$$: JQueryStatic
}
interface SinonSpyAgent<A extends sinon.SinonSpy> {

View File

@@ -31,6 +31,7 @@
"bytes": "3.1.0",
"chai": "3.5.0",
"chai-as-promised": "6.0.0",
"chai-subset": "1.6.0",
"chokidar-cli": "1.2.2",
"clone": "2.1.2",
"compression": "1.7.4",

View File

@@ -44,7 +44,6 @@ module.exports = (Commands, Cypress, cy, state, config) ->
$el = $dom.wrap(el)
domEvents = {}
$previouslyFocusedEl = null
if options.log
## figure out the options which actually change the behavior of clicks
@@ -149,9 +148,6 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## without firing the focus event
$previouslyFocused = cy.getFocused()
if el = cy.needsForceFocus()
cy.fireFocus(el)
el = $elToClick.get(0)
domEvents.mouseDown = $Mouse.mouseDown($elToClick, coords.fromViewport)
@@ -169,21 +165,17 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## retrieve the first focusable $el in our parent chain
$elToFocus = $elements.getFirstFocusableEl($elToClick)
if cy.needsFocus($elToFocus, $previouslyFocused)
cy.fireFocus($elToFocus.get(0))
## if we are currently trying to focus
## the body then calling body.focus()
## is a noop, and it will not blur the
## current element, which is all so wrong
if $elToFocus.is("body")
if $dom.isWindow($elToFocus)
# if the first focusable element from the click
# is the window, then we can skip the focus event
# since the user has clicked a non-focusable element
$focused = cy.getFocused()
## if the current focused element hasn't changed
## then blur manually
if $elements.isSame($focused, $previouslyFocused)
cy.fireBlur($focused.get(0))
if $focused
cy.fireBlur $focused.get(0)
else
# the user clicked inside a focusable element
cy.fireFocus $elToFocus.get(0)
afterMouseDown($elToClick, coords)
})

View File

@@ -28,10 +28,20 @@ module.exports = (Commands, Cypress, cy, state, config) ->
consoleProps: ->
"Applied To": $dom.getElements(options.$el)
## http://www.w3.org/TR/html5/editing.html#specially-focusable
el = options.$el.get(0)
## the body is not really focusable, but it
## can have focus on initial page load.
## this is instead a noop.
## TODO: throw on body instead (breaking change)
isBody = $dom.isJquery(options.$el) &&
$elements.isElement(options.$el.get(0)) &&
$elements.isBody(options.$el.get(0))
## http://www.w3.org/$R/html5/editing.html#specially-focusable
## ensure there is only 1 dom element in the subject
## make sure its allowed to be focusable
if not (isWin or $dom.isFocusable(options.$el))
if not (isWin or isBody or $dom.isFocusable(options.$el))
return if options.error is false
node = $dom.stringify(options.$el)
@@ -48,7 +58,6 @@ module.exports = (Commands, Cypress, cy, state, config) ->
args: { num }
})
el = options.$el.get(0)
cy.fireFocus(el)

View File

@@ -328,6 +328,23 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## if it's the body, don't need to worry about focus
return type() if isBody
## 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
$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 is $focused
## TODO: not scrolling here, but revisit when scroll algorithm changes
return type()
$actionability.verify(cy, options.$el, options, {
onScroll: ($el, type) ->
Cypress.action("cy:scrolled", $el, type)
@@ -335,9 +352,6 @@ module.exports = (Commands, Cypress, cy, state, config) ->
onReady: ($elToClick) ->
$focused = cy.getFocused()
if el = cy.needsForceFocus()
cy.fireFocus(el)
## if we dont have a focused element
## or if we do and its not ourselves
## then issue the click

View File

@@ -4,16 +4,18 @@ $elements = require("../dom/elements")
$actionability = require("./actionability")
create = (state) ->
documentHasFocus = () ->
## hardcode document has focus as true
## since the test should assume the window
## is in focus the entire time
return true
fireBlur = (el) ->
win = $window.getWindowByElement(el)
hasBlurred = false
hasFocus = top.document.hasFocus()
if not hasFocus
win.focus()
## we need to bind to the blur event here
## because some browsers will not ever fire
## the blur event if the window itself is not
@@ -42,25 +44,33 @@ create = (state) ->
## fallback if our focus event never fires
## to simulate the focus + focusin
if not hasBlurred
## todo handle relatedTarget's per the spec
focusoutEvt = new FocusEvent "focusout", {
bubbles: true
cancelable: false
view: win
relatedTarget: null
}
simulateBlurEvent(el, win)
blurEvt = new FocusEvent "blur", {
bubble: false
cancelable: false
view: win
relatedTarget: null
}
simulateBlurEvent = (el, win) ->
## todo handle relatedTarget's per the spec
focusoutEvt = new FocusEvent "focusout", {
bubbles: true
cancelable: false
view: win
relatedTarget: null
}
el.dispatchEvent(blurEvt)
el.dispatchEvent(focusoutEvt)
blurEvt = new FocusEvent "blur", {
bubble: false
cancelable: false
view: win
relatedTarget: null
}
el.dispatchEvent(blurEvt)
el.dispatchEvent(focusoutEvt)
fireFocus = (el) ->
## body will never emit focus events
## so we avoid simulating this
if $elements.isBody(el)
return
## if we are focusing a different element
## dispatch any primed change events
## we have to do this because our blur
@@ -77,11 +87,6 @@ create = (state) ->
hasFocused = false
hasFocus = top.document.hasFocus()
if not hasFocus
win.focus()
## we need to bind to the focus event here
## because some browsers will not ever fire
## the focus event if the window itself is not
@@ -98,34 +103,9 @@ create = (state) ->
cleanup()
## body will never emit focus events
## so we avoid simulating this
if $elements.isBody(el)
return
## fallback if our focus event never fires
## to simulate the focus + focusin
if not hasFocused
simulate = ->
## todo handle relatedTarget's per the spec
focusinEvt = new FocusEvent "focusin", {
bubbles: true
view: win
relatedTarget: null
}
focusEvt = new FocusEvent "focus", {
view: win
relatedTarget: null
}
## not fired in the correct order per w3c spec
## because chrome chooses to fire focus before focusin
## and since we have a simulation fallback we end up
## doing it how chrome does it
## http://www.w3.org/TR/DOM-Level-3-Events/#h-events-focusevent-event-order
el.dispatchEvent(focusEvt)
el.dispatchEvent(focusinEvt)
## only blur if we have a focused element AND its not
## currently ourselves!
@@ -136,50 +116,56 @@ create = (state) ->
if not $window.isWindow(el)
fireBlur($focused.get(0))
simulate()
simulateFocusEvent(el, win)
simulateFocusEvent = (el, win) ->
## todo handle relatedTarget's per the spec
focusinEvt = new FocusEvent "focusin", {
bubbles: true
view: win
relatedTarget: null
}
focusEvt = new FocusEvent "focus", {
view: win
relatedTarget: null
}
## not fired in the correct order per w3c spec
## because chrome chooses to fire focus before focusin
## and since we have a simulation fallback we end up
## doing it how chrome does it
## http://www.w3.org/TR/DOM-Level-3-Events/#h-events-focusevent-event-order
el.dispatchEvent(focusEvt)
el.dispatchEvent(focusinEvt)
interceptFocus = (el, contentWindow, focusOption) ->
## if our document does not have focus
## then that means that we need to attempt to
## bring our window into focus, and then figure
## out if the browser fires the native focus
## event - and if it doesn't, to flag this
## element as needing focus on the next action
## command
hasFocus = top.document.hasFocus()
## normally programmatic focus calls cause "primed" focus/blur
## events if the window is not in focus
## so we fire fake events to act as if the window
## is always in focus
$focused = getFocused()
if not hasFocus
contentWindow.focus()
if $elements.isFocusable($dom.wrap(el)) && (!$focused || $focused[0] isnt el)
fireFocus(el)
return
didReceiveFocus = false
$elements.callNativeMethod(el, 'focus')
return
onFocus = ->
didReceiveFocus = true
interceptBlur = (el) ->
## normally programmatic blur calls cause "primed" focus/blur
## events if the window is not in focus
## so we fire fake events to act as if the window
## is always in focus.
$focused = getFocused()
$elements.callNativeMethod(el, "addEventListener", "focus", onFocus)
if $focused && $focused[0] is el
fireBlur(el)
return
evt = $elements.callNativeMethod(el, "focus", focusOption)
## always unbind if added listener
if onFocus
$elements.callNativeMethod(el, "removeEventListener", "focus", onFocus)
## if we didn't receive focus
if not didReceiveFocus
## then store this element as needing
## force'd focus later on
state("needsForceFocus", el)
return evt
needsForceFocus = ->
## if we have a primed focus event then
if needsForceFocus = state("needsForceFocus")
## always reset it
state("needsForceFocus", null)
## and return whatever needs force focus
return needsForceFocus
$elements.callNativeMethod(el, 'blur')
return
needsFocus = ($elToFocus, $previouslyFocusedEl) ->
$focused = getFocused()
@@ -225,7 +211,9 @@ create = (state) ->
interceptFocus
needsForceFocus
interceptBlur,
documentHasFocus,
}
module.exports = {

View File

@@ -154,11 +154,20 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
contentWindow.HTMLElement.prototype.focus = (focusOption) ->
focused.interceptFocus(this, contentWindow, focusOption)
contentWindow.HTMLElement.prototype.blur = ->
focused.interceptBlur(this)
contentWindow.SVGElement.prototype.focus = (focusOption) ->
focused.interceptFocus(this, contentWindow, focusOption)
contentWindow.SVGElement.prototype.blur = ->
focused.interceptBlur(this)
contentWindow.HTMLInputElement.prototype.select = ->
$selection.interceptSelect.call(this)
contentWindow.document.hasFocus = ->
top.document.hasFocus()
focused.documentHasFocus.call(@)
enqueue = (obj) ->
## if we have a nestedIndex it means we're processing
@@ -625,7 +634,6 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
## focused sync methods
getFocused: focused.getFocused
needsForceFocus: focused.needsForceFocus
needsFocus: focused.needsFocus
fireFocus: focused.fireFocus
fireBlur: focused.fireBlur

View File

@@ -25,8 +25,17 @@ const $utils = require('../cypress/utils')
const fixedOrStickyRe = /(fixed|sticky)/
const focusable = 'body,a[href],link[href],button,select,[tabindex],input,textarea,[contenteditable]'
const focusable = [
'a[href]',
'area[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'button:not([disabled])',
'iframe',
'[tabindex]',
'[contentEditable]',
]
const inputTypeNeedSingleValueChangeRe = /^(date|time|month|week)$/
const canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/
@@ -197,6 +206,7 @@ const nativeMethods = {
setSelectionRange: _nativeSetSelectionRange,
modify: window.Selection.prototype.modify,
focus: _nativeFocus,
hasFocus: window.document.hasFocus,
blur: _nativeBlur,
select: _nativeSelect,
}
@@ -354,7 +364,9 @@ const isElement = function (obj) {
}
const isFocusable = ($el) => {
return $el.is(focusable)
return _.some(focusable, (sel) => {
return $el.is(sel)
})
}
const isType = function ($el, type) {

View File

@@ -54,7 +54,7 @@ const _getSelectionBoundsFromContentEditable = function (el) {
const range = sel.getRangeAt(0)
//# if div[contenteditable] > text
const hostContenteditable = _getHostContenteditable(range.commonAncestorContainer)
const hostContenteditable = getHostContenteditable(range.commonAncestorContainer)
if (hostContenteditable === el) {
return {
@@ -123,10 +123,16 @@ const _insertSubstring = (curText, newText, [start, end]) => {
return curText.substring(0, start) + newText + curText.substring(end)
}
const _getHostContenteditable = function (el) {
const _hasContenteditableAttr = (el) => {
const attr = $elements.tryCallNativeMethod(el, 'getAttribute', 'contenteditable')
return attr !== undefined && attr !== null && attr !== 'false'
}
const getHostContenteditable = function (el) {
let curEl = el
while (curEl.parentElement && !$elements.tryCallNativeMethod(curEl, 'getAttribute', 'contenteditable')) {
while (curEl.parentElement && !_hasContenteditableAttr(curEl)) {
curEl = curEl.parentElement
}
@@ -134,7 +140,7 @@ const _getHostContenteditable = function (el) {
//# so act as if the original element is the host contenteditable
//# TODO: remove this when we no longer click before type and move
//# cursor to the end
if (!$elements.callNativeMethod(curEl, 'getAttribute', 'contenteditable')) {
if (!_hasContenteditableAttr(curEl)) {
return el
}
@@ -451,7 +457,7 @@ const moveSelectionToEnd = function (el) {
//# to selectAll and then collapse so we use the Selection API
const doc = $document.getDocumentFromElement(el)
const range = $elements.callNativeMethod(doc, 'createRange')
const hostContenteditable = _getHostContenteditable(el)
const hostContenteditable = getHostContenteditable(el)
let lastTextNode = _getInnerLastChild(hostContenteditable)
if (lastTextNode.tagName === 'BR') {
@@ -593,6 +599,7 @@ module.exports = {
deleteSelectionContents,
moveSelectionToEnd,
getCaretPosition,
getHostContenteditable,
moveCursorLeft,
moveCursorRight,
moveCursorUp,

View File

@@ -6,6 +6,10 @@
body {
height: 300px;
}
*:focus {
outline: 4px solid blue
}
</style>
</head>

View File

@@ -2348,6 +2348,87 @@ describe "src/cy/commands/actions/type", ->
it "respects being formatted by input event handlers"
it "accurately returns host contenteditable attr", ->
hostEl = cy.$$('<div contenteditable><div id="ce-inner1">foo</div></div>').appendTo(cy.$$('body'))
cy.get('#ce-inner1').then ($el) ->
expect($selection.getHostContenteditable($el[0])).to.eq(hostEl[0])
it "accurately returns host contenteditable=true attr", ->
hostEl = cy.$$('<div contenteditable="true"><div id="ce-inner1">foo</div></div>').appendTo(cy.$$('body'))
cy.get('#ce-inner1').then ($el) ->
expect($selection.getHostContenteditable($el[0])).to.eq(hostEl[0])
it "accurately returns host contenteditable=\"\" attr", ->
hostEl = cy.$$('<div contenteditable=""><div id="ce-inner1">foo</div></div>').appendTo(cy.$$('body'))
cy.get('#ce-inner1').then ($el) ->
expect($selection.getHostContenteditable($el[0])).to.eq(hostEl[0])
it "accurately returns host contenteditable=\"foo\" attr", ->
hostEl = cy.$$('<div contenteditable="foo"><div id="ce-inner1">foo</div></div>').appendTo(cy.$$('body'))
cy.get('#ce-inner1').then ($el) ->
expect($selection.getHostContenteditable($el[0])).to.eq(hostEl[0])
it "accurately returns same el with no falsey contenteditable=\"false\" attr", ->
hostEl = cy.$$('<div contenteditable="false"><div id="ce-inner1">foo</div></div>').appendTo(cy.$$('body'))
cy.get('#ce-inner1').then ($el) ->
expect($selection.getHostContenteditable($el[0])).to.eq($el[0])
## https://github.com/cypress-io/cypress/issues/3001
describe('skip actionability if already focused', () =>
it('inside input', () =>
cy.$$('body').append(Cypress.$('
<div style="position:relative;width:100%;height:100px;background-color:salmon;top:60px;opacity:0.5"></div>
<input type="text" id="foo">
'))
cy.$$('#foo').focus()
cy.focused().type('new text').should('have.prop', 'value', 'new text')
)
it('inside textarea', () =>
cy.$$('body').append(Cypress.$('
<div style="position:relative;width:100%;height:100px;background-color:salmon;top:60px;opacity:0.5"></div> \
<textarea id="foo"></textarea>
'))
cy.$$('#foo').focus()
cy.focused().type('new text').should('have.prop', 'value', 'new text')
)
it('inside contenteditable', () =>
cy.$$('body').append(Cypress.$('
<div style="position:relative;width:100%;height:100px;background-color:salmon;top:60px;opacity:0.5"></div>
<div id="foo" contenteditable>
<div>foo</div><div>bar</div><div>baz</div>
</div>
'))
win = cy.state('window')
doc = window.document
cy.$$('#foo').focus()
inner = cy.$$('div:contains(bar):last')
range = doc.createRange()
range.selectNodeContents(inner[0])
sel = win.getSelection()
sel.removeAllRanges()
sel.addRange(range)
cy.get('div:contains(bar):last').type('new text').should('have.prop', 'innerText', 'new text')
)
)
it "can arrow from maxlength", ->
cy.get('input:first').invoke('attr', 'maxlength', "5").type('foobar{leftarrow}')
cy.window().then (win) ->

View File

@@ -1,275 +0,0 @@
## in all browsers...
##
## activeElement is always programmatically respected and behaves identical whether window is in or out of focus
##
## browser: chrome...
##
## scenario 1: given '#one' is activeElement call programmatic el.focus() on '#two'
## - if window is in focus
## - blur will fire on '#one'
## - focus will fire on '#two'
## - if window is out of focus (the event wil be primed until the window receives focus again)
## - by clicking anywhere on the <body> (not on the element)...
## - focus on '#two' will fire first
## - blur on '#two' will fire second
## - activeElement will now be <body>
## - by clicking on another element that is focusable
## - focus on '#two' is first sent
## - blur on '#two' is then sent
## - focus is finally sent on the new focusable element we clicked
## - if instead on clicking we programmatically call .focus() back to '#one'
## - focus is fired on '#one'
## - if we were to instead click directly on '#one' then no focus or blur events are fired
## - if when clicking directly back to '#one' we prevent the 'mousedown' event
## - the focus event will fire AND the element will still be activeElement
## - had we not programmatically call .focus() ahead of time, then the focus event would
## have been not fired, and our activeElement would not have changed
##
## scenario 2 : given '#one' is activeElement call programmatic el.blur() on '#one'
## - if window is in focus
## - blur will fire on '#one'
## - if window is out of focus
## - no events will ever fire even when regaining focus
##
## browser: firefox...
## - no focus events are queued when programmatically calling element.focus() AND the window is out of focus. the events evaporate into the ether.
## - however, if calling window.focus() programmatically prior to programmatic element.focus() calls will fire all events as if the window is natively in focus
{ _ } = Cypress
it "sends delayed focus when programmatically invoked during action commands", ->
cy
.visit("http://localhost:3500/fixtures/active-elements.html")
.then ->
events = []
expect(cy.getFocused()).to.be.null
hasFocus = top.document.hasFocus()
## programmatically focus the first input element
$input = cy.$$("input:first")
$input.on "focus", (e) ->
events.push(e.originalEvent)
## when we mousedown on the input
## prevent default so that we can test
## that the force focus event is set ahead
## of time
$input.on "mousedown", (e) ->
e.preventDefault()
$input.get(0).focus()
cy
.log('top.document.hasFocus()', hasFocus)
.then ->
if hasFocus
## if we currently have focus it means
## that the browser should fire the
## native event immediately
expect(cy.state("needsForceFocus")).to.be.undefined
expect(events).to.have.length(1)
expect(events[0].isTrusted).to.be.true
else
expect(cy.state("needsForceFocus")).to.eq($input.get(0))
expect(events).to.be.empty
expect(cy.getFocused().get(0)).to.eq($input.get(0))
cy
.get("input:first").click()
.then ->
expect(cy.getFocused().get(0)).to.eq($input.get(0))
cy
.log('top.document.hasFocus()', hasFocus)
.then ->
if hasFocus
## if we had focus then no additional
## focus event is necessary
expect(events).to.have.length(1)
expect(events[0].isTrusted).to.be.true
else
expect(cy.state("needsForceFocus")).to.be.null
## we polyfill the focus event manually
expect(events).to.have.length(1)
expect(events[0].isTrusted).to.be.false
it 'sends programmatic blur when delayed due to window being out of focus', ->
cy
.visit("http://localhost:3500/fixtures/active-elements.html")
.then ->
events = []
expect(cy.getFocused()).to.be.null
hasFocus = top.document.hasFocus()
## programmatically focus the first input element
$one = cy.$$("#one")
$two = cy.$$("#two")
["focus", "blur"].forEach (evt) ->
$one.on evt, (e) ->
events.push(e.originalEvent)
$two.on evt, (e) ->
events.push(e.originalEvent)
## when we mousedown on the input
## prevent default so that we can test
## that the force focus event is set ahead
## of time
$two.on "mousedown", (e) ->
e.preventDefault()
$one.get(0).focus()
cy
.log('top.document.hasFocus()', hasFocus)
.then ->
if hasFocus
## if we currently have focus it means
## that the browser should fire the
## native event immediately
expect(cy.state("needsForceFocus")).to.be.undefined
expect(events).to.have.length(1)
expect(events[0].isTrusted).to.be.true
else
expect(cy.state("needsForceFocus")).to.eq($one.get(0))
expect(events).to.be.empty
expect(cy.getFocused().get(0)).to.eq($one.get(0))
cy
.get("#one").click()
.then ->
expect(cy.getFocused().get(0)).to.eq($one.get(0))
cy
.log('top.document.hasFocus()', hasFocus)
.then ->
if hasFocus
## if we had focus then no additional
## focus event is necessary
expect(events).to.have.length(1)
expect(events[0].isTrusted).to.be.true
else
expect(cy.state("needsForceFocus")).to.be.null
## we polyfill the focus event manually
expect(events).to.have.length(1)
expect(events[0].isTrusted).to.be.false
it "blur the activeElement when clicking the body", ->
cy
.visit("http://localhost:3500/fixtures/active-elements.html")
.then ->
events = []
expect(cy.getFocused()).to.be.null
doc = cy.state("document")
hasFocus = top.document.hasFocus()
## programmatically focus the first, then second input element
$body = cy.$$("body")
$one = cy.$$("#one")
$two = cy.$$("#two")
["focus", "blur"].forEach (evt) ->
$one.on evt, (e) ->
events.push(e.originalEvent)
$two.on evt, (e) ->
events.push(e.originalEvent)
$one.get(0).focus()
$two.get(0).focus()
cy
.log('top.document.hasFocus()', hasFocus)
.then ->
if hasFocus
## if we currently have focus it means
## that the browser should fire the
## native event immediately
expect(cy.state("needsForceFocus")).to.be.undefined
expect(events).to.have.length(3)
expect(_.toPlainObject(events[0])).to.include({
type: "focus"
isTrusted: true
target: $one.get(0)
})
expect(_.toPlainObject(events[1])).to.include({
type: "blur"
isTrusted: true
target: $one.get(0)
})
expect(_.toPlainObject(events[2])).to.include({
type: "focus"
isTrusted: true
target: $two.get(0)
})
else
expect(cy.state("needsForceFocus")).to.eq($two.get(0))
expect(events).to.be.empty
expect(cy.getFocused().get(0)).to.eq($two.get(0))
cy
.get("body").click()
.then ->
expect(doc.activeElement).to.eq($body.get(0))
cy
.log('top.document.hasFocus()', hasFocus)
.then ->
if hasFocus
## if we had focus then no additional
## focus event is necessary
expect(events).to.have.length(4)
expect(_.toPlainObject(events[3])).to.include({
type: "blur"
isTrusted: true
target: $two.get(0)
})
else
expect(cy.state("needsForceFocus")).to.be.null
## we polyfill the focus event manually
expect(events).to.have.length(2)
expect(_.toPlainObject(events[0])).to.include({
type: "focus"
isTrusted: false
target: $two.get(0)
})
expect(_.toPlainObject(events[1])).to.include({
type: "blur"
isTrusted: false
target: $two.get(0)
})
it 'opens the dropdown by force firing focus events', ->
cy
.visit('http://jedwatson.github.io/react-select/')
.then ->
hasFocus = top.document.hasFocus()
cy
.log('top.document.hasFocus()', hasFocus)
cy
.get('#state-select')
.get('div.Select-value:first')
.click()
.get('.Select-option:contains(Victoria)')
.click()

View File

@@ -0,0 +1,673 @@
/// <reference path="../../../../../../cli/types/index.d.ts" />
/* eslint arrow-body-style:'off' */
/**
* in all browsers...
*
* activeElement is always programmatically respected and behaves identical whether window is in or out of focus
*
* browser: chrome...
*
* scenario 1: given '#one' is activeElement call programmatic el.focus() on '#two'
* - if window is in focus
* - blur will fire on '#one'
* - focus will fire on '#two'
* - if window is out of focus (the event wil be primed until the window receives focus again)
* - by clicking anywhere on the <body> (not on the element)...
* - focus on '#two' will fire first
* - blur on '#two' will fire second
* - activeElement will now be <body>
* - by clicking on another element that is focusable
* - focus on '#two' is first sent
* - blur on '#two' is then sent
* - focus is finally sent on the new focusable element we clicked
* - if instead on clicking we programmatically call .focus() back to '#one'
* - focus is fired on '#one'
* - if we were to instead click directly on '#one' then no focus or blur events are fired
* - if when clicking directly back to '#one' we prevent the 'mousedown' event
* - the focus event will fire AND the element will still be activeElement
* - had we not programmatically call .focus() ahead of time, then the focus event would
* have been not fired, and our activeElement would not have changed
*
* scenario 2 : given '#one' is activeElement call programmatic el.blur() on '#one'
* - if window is in focus
* - blur will fire on '#one'
* - if window is out of focus
* - no events will ever fire even when regaining focus
* browser: firefox...
* - no focus events are queued when programmatically calling element.focus() AND the window is out of focus. the events evaporate into the ether.
* - however, if calling window.focus() programmatically prior to programmatic element.focus() calls will fire all events as if the window is natively in focus
*/
const { _ } = Cypress
const chaiSubset = require('chai-subset')
chai.use(chaiSubset)
const windowHasFocus = function () {
if (document.hasFocus()) return true
let hasFocus = false
window.addEventListener('focus', function () {
hasFocus = true
})
window.focus()
return hasFocus
}
const requireWindowInFocus = () => {
let hasFocus = windowHasFocus()
if (!hasFocus) {
expect(hasFocus, 'this test requires the window to be in focus').ok
}
}
it('can intercept blur/focus events', () => {
// Browser must be in focus
const focus = cy.spy(window.top.HTMLElement.prototype, 'focus')
const blur = cy.spy(window.top.HTMLElement.prototype, 'blur')
const handleFocus = cy.stub().as('handleFocus')
const handleBlur = cy.stub().as('handleBlur')
const resetStubs = () => {
focus.reset()
blur.reset()
handleFocus.reset()
handleBlur.reset()
}
cy
.visit('http://localhost:3500/fixtures/active-elements.html')
.then(() => {
requireWindowInFocus()
expect(cy.getFocused()).to.be.null
// programmatically focus the first, then second input element
const one = cy.$$('#one')[0]
const two = cy.$$('#two')[0]
one.addEventListener('focus', handleFocus)
two.addEventListener('focus', handleFocus)
one.addEventListener('blur', handleBlur)
two.addEventListener('blur', handleBlur)
one.focus()
expect(focus).to.calledOnce
expect(handleFocus).calledOnce
expect(blur).not.called
expect(handleBlur).not.called
resetStubs()
one.focus()
expect(focus).to.calledOnce
expect(handleFocus).not.called
expect(blur).not.called
expect(handleBlur).not.called
resetStubs()
one.blur()
expect(blur).calledOnce
expect(handleBlur).calledOnce
resetStubs()
one.blur()
expect(blur).calledOnce
expect(handleBlur).not.called
})
})
it('blur the activeElement when clicking the body', () => {
cy
.visit('http://localhost:3500/fixtures/active-elements.html')
.then(() => {
const events = []
expect(cy.getFocused()).to.be.null
const doc = cy.state('document')
// programmatically focus the first, then second input element
const $body = cy.$$('body')
const $one = cy.$$('#one')
const $two = cy.$$('#two');
['focus', 'blur'].forEach((evt) => {
$one.on(evt, (e) => {
events.push(e.originalEvent)
})
$two.on(evt, (e) => {
events.push(e.originalEvent)
})
})
$one.get(0).focus()
$two.get(0).focus()
cy.then(() => {
// if we currently have focus it means
// that the browser should fire the
// native event immediately
expect(events).to.have.length(3)
expect(_.toPlainObject(events[0])).to.include({
type: 'focus',
isTrusted: true,
target: $one.get(0),
})
expect(_.toPlainObject(events[1])).to.include({
type: 'blur',
isTrusted: true,
target: $one.get(0),
})
expect(_.toPlainObject(events[2])).to.include({
type: 'focus',
isTrusted: true,
target: $two.get(0),
})
})
cy
.get('body').click()
.then(() => {
expect(doc.activeElement).to.eq($body.get(0))
})
cy.then(() => {
// if we had focus then no additional
// focus event is necessary
expect(events).to.have.length(4)
expect(_.toPlainObject(events[3])).to.include({
type: 'blur',
isTrusted: true,
target: $two.get(0),
})
})
})
})
describe('polyfill programmatic blur events', () => {
// restore these props for the rest of the tests
let stubElementFocus
let stubElementBlur
let stubSVGFocus
let stubSVGBlur
let stubHasFocus
let oldActiveElement = null
const setActiveElement = (el) => {
Object.defineProperty(cy.state('document'), 'activeElement', {
get () {
return el
},
configurable: true,
})
}
beforeEach(() => {
oldActiveElement = Object.getOwnPropertyDescriptor(window.Document.prototype, 'activeElement')
// simulate window being out of focus by overwriting
// the focus/blur methods on HTMLElement
stubHasFocus = cy.stub(window.top.document, 'hasFocus').returns(false)
stubElementFocus = cy.stub(window.top.HTMLElement.prototype, 'focus')
stubElementBlur = cy.stub(window.top.HTMLElement.prototype, 'blur')
stubSVGFocus = cy.stub(window.top.SVGElement.prototype, 'focus')
stubSVGBlur = cy.stub(window.top.SVGElement.prototype, 'blur')
})
afterEach(() => {
Object.defineProperty(window.Document.prototype, 'activeElement', oldActiveElement)
stubHasFocus.restore()
stubElementFocus.restore()
stubElementBlur.restore()
stubSVGFocus.restore()
stubSVGBlur.restore()
})
// https://github.com/cypress-io/cypress/issues/1486
it('simulated events when window is out of focus when .focus called', () => {
cy
.visit('http://localhost:3500/fixtures/active-elements.html')
.then(() => {
// programmatically focus the first, then second input element
const $one = cy.$$('#one')
const $two = cy.$$('#two')
const stub = cy.stub().as('focus/blur event').callsFake(() => {
Cypress.log({})
});
['focus', 'blur'].forEach((evt) => {
$one.on(evt, stub)
return $two.on(evt, stub)
})
$one.get(0).focus()
// a hack here becuase we nuked the real .focus
setActiveElement($one.get(0))
$two.get(0).focus()
// cy.get('#two').click()
const getEvent = (n) => {
return stub.getCall(n).args[0].originalEvent
}
cy.wrap(null).then(() => {
expect(stub).to.be.calledThrice
expect(getEvent(0)).to.containSubset({
type: 'focus',
target: $one.get(0),
isTrusted: false,
})
expect(getEvent(1)).to.containSubset({
type: 'blur',
target: $one.get(0),
isTrusted: false,
})
expect(getEvent(2)).to.containSubset({
type: 'focus',
target: $two.get(0),
isTrusted: false,
})
})
.then(() => {
stub.reset()
setActiveElement($two.get(0))
$two.get(0).focus()
expect(stub, 'should not send focus if already focused el').not.called
})
})
}
)
// https://github.com/cypress-io/cypress/issues/1176
it('simulated events when window is out of focus when .blur called', () => {
cy
.visit('http://localhost:3500/fixtures/active-elements.html')
.then(() => {
// programmatically focus the first, then second input element
const $one = cy.$$('#one')
const $two = cy.$$('#two')
const stub = cy.stub().as('focus/blur event');
['focus', 'blur'].forEach((evt) => {
$one.on(evt, stub)
$two.on(evt, stub)
})
$one.get(0).focus()
// a hack here becuase we nuked the real .focus
setActiveElement($one.get(0))
$one.get(0).blur()
cy.then(() => {
expect(stub).calledTwice
expect(_.toPlainObject(stub.getCall(0).args[0].originalEvent)).to.containSubset({
type: 'focus',
target: $one.get(0),
isTrusted: false,
})
expect(_.toPlainObject(stub.getCall(1).args[0].originalEvent)).to.containSubset({
type: 'blur',
target: $one.get(0),
isTrusted: false,
})
})
.then(() => {
stub.reset()
setActiveElement(cy.$$('body').get(0))
$one.get(0).blur()
expect(stub, 'should not send blur if not focused el').not.called
})
})
})
// https://github.com/cypress-io/cypress/issues/1486
it('SVGElement simulated events when window is out of focus when .focus called', () => {
cy
.visit('http://localhost:3500/fixtures/active-elements.html')
.then(() => {
// programmatically focus the first, then second input element
const $one = cy.$$(`<svg id="svg-one" tabindex width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>`).appendTo(cy.$$('body'))
const $two = cy.$$(`<svg id="svg-two" tabindex width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>`).appendTo(cy.$$('body'))
const stub = cy.stub().as('focus/blur event').callsFake(() => {
Cypress.log({})
});
['focus', 'blur'].forEach((evt) => {
$one.on(evt, stub)
return $two.on(evt, stub)
})
$one.get(0).focus()
// a hack here becuase we nuked the real .focus
setActiveElement($one.get(0))
$two.get(0).focus()
// cy.get('#two').click()
const getEvent = (n) => {
return stub.getCall(n).args[0].originalEvent
}
cy.wrap(null).then(() => {
expect(stub).to.be.calledThrice
expect(getEvent(0)).to.containSubset({
type: 'focus',
target: $one.get(0),
isTrusted: false,
})
expect(getEvent(1)).to.containSubset({
type: 'blur',
target: $one.get(0),
isTrusted: false,
})
expect(getEvent(2)).to.containSubset({
type: 'focus',
target: $two.get(0),
isTrusted: false,
})
})
.then(() => {
stub.reset()
setActiveElement($two.get(0))
$two.get(0).focus()
expect(stub, 'should not send focus if already focused el').not.called
})
})
}
)
// https://github.com/cypress-io/cypress/issues/1176
it('SVGElement simulated events when window is out of focus when .blur called', () => {
cy
.visit('http://localhost:3500/fixtures/active-elements.html')
.then(() => {
// programmatically focus the first, then second input element
const $one = cy.$$(`<svg id="svg-one" tabindex width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>`).appendTo(cy.$$('body'))
const $two = cy.$$(`<svg id="svg-two" tabindex width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>`).appendTo(cy.$$('body'))
const stub = cy.stub().as('focus/blur event');
['focus', 'blur'].forEach((evt) => {
$one.on(evt, stub)
$two.on(evt, stub)
})
$one.get(0).focus()
// a hack here becuase we nuked the real .focus
setActiveElement($one.get(0))
$one.get(0).blur()
cy.then(() => {
expect(stub).calledTwice
expect(_.toPlainObject(stub.getCall(0).args[0].originalEvent)).to.containSubset({
type: 'focus',
target: $one.get(0),
isTrusted: false,
})
expect(_.toPlainObject(stub.getCall(1).args[0].originalEvent)).to.containSubset({
type: 'blur',
target: $one.get(0),
isTrusted: false,
})
})
.then(() => {
stub.reset()
setActiveElement(cy.$$('body').get(0))
$one.get(0).blur()
expect(stub, 'should not send blur if not focused el').not.called
})
})
})
it('document.hasFocus() always returns true', () => {
cy.visit('http://localhost:3500/fixtures/active-elements.html')
cy.document().then((doc) => {
expect(doc.hasFocus(), 'hasFocus returns true').eq(true)
})
})
it('does not send focus events for non-focusable elements', () => {
cy.visit('http://localhost:3500/fixtures/active-elements.html')
.then(() => {
cy.$$('<div id="no-focus">clearly not a focusable element</div>')
.appendTo(cy.$$('body'))
const stub = cy.stub()
const el1 = cy.$$('#no-focus')
const win = cy.$$(cy.state('window'))
win.on('focus', stub)
el1.on('focus', stub)
el1[0].focus()
expect(stub).not.called
})
})
})
describe('intercept blur methods correctly', () => {
beforeEach(() => {
cy.visit('http://localhost:3500/fixtures/active-elements.html').then(() => {
// cy.$$('input:first').focus()
// cy.$$('body').focus()
cy.state('document').onselectionchange = cy.stub().as('selectionchange')
})
})
it('focus <a>', () => {
const $el = cy.$$('<a href="#">foo</a>')
//
$el.appendTo(cy.$$('body'))
// cy.$$('input').focus()
// $el[0].focus()
cy.wrap($el[0]).focus()
.should('have.focus')
cy.wait(0).get('@selectionchange').should('not.be.called')
})
it('focus <select>', () => {
const $el = cy.$$('<select>')
$el.appendTo(cy.$$('body'))
$el[0].focus()
cy.wrap($el[0]).focus()
.should('have.focus')
cy.wait(0).get('@selectionchange').should('not.be.called')
})
it('focus <button>', () => {
const $el = cy.$$('<button/>')
$el.appendTo(cy.$$('body'))
$el[0].focus()
cy.wrap($el[0]).focus()
.should('have.focus')
cy.wait(0).get('@selectionchange').should('not.be.called')
})
it('focus <iframe>', () => {
const $el = cy.$$('<iframe src="" />')
$el.appendTo(cy.$$('body'))
$el[0].focus()
cy.wrap($el[0]).focus()
.should('have.focus')
cy.wait(0).get('@selectionchange').should('not.be.called')
})
it('focus [tabindex]', () => {
const $el = cy.$$('<div tabindex="1">tabindex</div>')
$el.appendTo(cy.$$('body'))
$el[0].focus()
cy.wrap($el[0]).focus()
.should('have.focus')
cy.wait(0).get('@selectionchange').should('not.be.called')
})
it('focus <textarea>', () => {
const $el = cy.$$('<textarea/>')
$el.appendTo(cy.$$('body'))
$el[0].focus()
cy.wrap($el[0]).focus()
.should('have.focus')
cy.get('@selectionchange').should('be.calledOnce')
})
it('focus [contenteditable]', () => {
const $el = cy.$$('<div contenteditable>contenteditable</div>')
$el.appendTo(cy.$$('body'))
$el[0].focus()
cy.wrap($el[0]).focus()
.should('have.focus')
cy.get('@selectionchange').should('be.calledOnce')
})
it('cannot focus a [contenteditable] child', () => {
const outer = cy.$$('<div contenteditable>contenteditable</div>').appendTo(cy.$$('body'))
const inner = cy.$$('<div>first inner contenteditable</div>').appendTo(outer)
cy.$$('<div>second inner contenteditable</div>').appendTo(outer)
cy.get('input:first').focus()
.wait(0)
.get('@selectionchange').then((stub) => stub.reset())
cy.wrap(inner).should(($el) => $el.focus)
.wait(0)
cy.get('input:first').should('have.focus')
cy.get('@selectionchange').should('not.be.called')
})
it('focus svg', () => {
const $svg = cy.$$(`<svg tabindex="1" width="900px" height="500px" viewBox="0 0 95 50" style="border: solid red 1px;"
xmlns="http://www.w3.org/2000/svg">
<g data-Name="group" stroke="green" fill="white" stroke-width="5" data-tabindex="0" >
<a xlink:href="#">
<circle cx="20" cy="25" r="5" data-Name="shape 1" data-tabindex="0" />
</a>
<a xlink:href="#">
<circle cx="40" cy="25" r="5" data-Name="shape 2" data-tabindex="0" />
</a>
<a xlink:href="#">
<circle cx="60" cy="25" r="5" data-Name="shape 3" data-tabindex="0" />
</a>
<a xlink:href="#">
<circle cx="80" cy="25" r="5" data-Name="shape 4" data-tabindex="0" />
</a>
</g>
</svg>`).appendTo(cy.$$('body'))
cy.wrap($svg).focus().should('have.focus')
})
it('focus area', () => {
cy.visit('http://localhost:3500/fixtures/active-elements.html').then(() => {
cy.$$(`
<map name="map">
<area shape="circle" coords="0,0,100"
href="#"
target="_blank" alt="area" />
</map>
<img usemap="#map" src="/__cypress/static/favicon.ico" alt="image" />
`).appendTo(cy.$$('body'))
cy.get('area').focus().should('have.focus')
})
})
})