Skip to content

Commit

Permalink
fix programmatic focus/blur events, typing into currently focused (#2982
Browse files Browse the repository at this point in the history
)

* 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>
  • Loading branch information
3 people committed Jun 11, 2019
1 parent 6772a02 commit 7efd9d8
Show file tree
Hide file tree
Showing 13 changed files with 921 additions and 398 deletions.
9 changes: 9 additions & 0 deletions cli/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
1 change: 1 addition & 0 deletions packages/driver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 9 additions & 17 deletions packages/driver/src/cy/commands/actions/click.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
})
Expand Down
15 changes: 12 additions & 3 deletions packages/driver/src/cy/commands/actions/focus.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -48,7 +58,6 @@ module.exports = (Commands, Cypress, cy, state, config) ->
args: { num }
})

el = options.$el.get(0)

cy.fireFocus(el)

Expand Down
20 changes: 17 additions & 3 deletions packages/driver/src/cy/commands/actions/type.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -328,16 +328,30 @@ 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)

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
Expand Down
168 changes: 78 additions & 90 deletions packages/driver/src/cy/focused.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

blurEvt = new FocusEvent "blur", {
bubble: false
cancelable: false
view: win
relatedTarget: null
}

el.dispatchEvent(blurEvt)
el.dispatchEvent(focusoutEvt)
simulateBlurEvent(el, win)

simulateBlurEvent = (el, win) ->
## todo handle relatedTarget's per the spec
focusoutEvt = new FocusEvent "focusout", {
bubbles: true
cancelable: false
view: win
relatedTarget: null
}

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
Expand All @@ -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
Expand All @@ -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!
Expand All @@ -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()

if not hasFocus
contentWindow.focus()

didReceiveFocus = false

onFocus = ->
didReceiveFocus = true

$elements.callNativeMethod(el, "addEventListener", "focus", onFocus)

evt = $elements.callNativeMethod(el, "focus", focusOption)
## 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()

## always unbind if added listener
if onFocus
$elements.callNativeMethod(el, "removeEventListener", "focus", onFocus)
if $elements.isFocusable($dom.wrap(el)) && (!$focused || $focused[0] isnt el)
fireFocus(el)
return

## if we didn't receive focus
if not didReceiveFocus
## then store this element as needing
## force'd focus later on
state("needsForceFocus", el)
$elements.callNativeMethod(el, 'focus')
return

return evt
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()

needsForceFocus = ->
## if we have a primed focus event then
if needsForceFocus = state("needsForceFocus")
## always reset it
state("needsForceFocus", null)
if $focused && $focused[0] is el
fireBlur(el)
return

## and return whatever needs force focus
return needsForceFocus
$elements.callNativeMethod(el, 'blur')
return

needsFocus = ($elToFocus, $previouslyFocusedEl) ->
$focused = getFocused()
Expand Down Expand Up @@ -225,7 +211,9 @@ create = (state) ->

interceptFocus

needsForceFocus
interceptBlur,

documentHasFocus,
}

module.exports = {
Expand Down
12 changes: 10 additions & 2 deletions packages/driver/src/cypress/cy.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 7efd9d8

Please sign in to comment.