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

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

Merged
merged 45 commits into from
Jun 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c1b761e
fix programmatic blur events, allow typing into currently focused, fi…
kuceb Dec 22, 2018
def6578
intercept .blur
kuceb Dec 27, 2018
afa9af8
reference issues in tests
kuceb Jan 2, 2019
45e85bb
Merge branch 'develop' into click-command-focus
brian-mann Feb 18, 2019
9950643
Merge branch 'develop' into click-command-focus
brian-mann Feb 18, 2019
c5a330d
make tests account for conditional number of new lines inserted
brian-mann Feb 18, 2019
bb35343
cleanup, remove dead code
brian-mann Feb 19, 2019
6980e54
make tests dynamic when browser is or isn't out of focus
brian-mann Feb 19, 2019
d657243
cleanup, remove old notes, add more notes
brian-mann Feb 19, 2019
e27ca90
add failing tests for when native focus / blur are called multiple times
brian-mann Feb 19, 2019
d4154f0
remove old code for priming focus/blur events when window is out of f…
brian-mann Feb 19, 2019
6d12abc
remove dead code
brian-mann Feb 19, 2019
c12134f
Merge remote-tracking branch 'origin/develop' into click-command-focus
kuceb Apr 15, 2019
da3b58f
update focus_blur spec + add chai-subset
kuceb Apr 15, 2019
daf458d
decaffeinate: Rename focus_blur_spec.coffee from .coffee to .js
kuceb Apr 15, 2019
a937fe8
decaffeinate: Convert focus_blur_spec.coffee to JS
kuceb Apr 15, 2019
73f9b8f
decaffeinate: Run post-processing cleanups on focus_blur_spec.coffee
kuceb Apr 15, 2019
b3eed59
add failing test
kuceb Apr 15, 2019
56d84ff
Merge branch 'tmp-click-command-focus' into click-command-focus
kuceb Apr 15, 2019
d54e831
fix double blur/focus events
kuceb Apr 15, 2019
ad17023
make document.hasFocus always return true, add test
kuceb Apr 15, 2019
6be118b
Merge branch 'develop' into click-command-focus
kuceb Apr 15, 2019
8ad4d0a
fix focus events when non-focusable element
kuceb Apr 18, 2019
26719a2
Merge branch 'develop' into click-command-focus
kuceb Apr 18, 2019
0a22f9a
remove unneeded retrun
kuceb Apr 18, 2019
f171ea0
Merge branch 'develop' of github.com:cypress-io/cypress into click-co…
kuceb Apr 18, 2019
89cfe28
Merge remote-tracking branch 'origin/click-command-focus' into click-…
kuceb Apr 18, 2019
0d7b220
fix focusing body/ bluring active element on click
kuceb Apr 18, 2019
4973c48
forgot to call .get() with index
kuceb Apr 18, 2019
605f936
Merge branch 'develop' into click-command-focus
jennifer-shehane Apr 24, 2019
fbe7ffa
fix focus issue with body/window
kuceb Apr 25, 2019
60e9490
still allow firefocus on window, skip firing focus if firstfocusable …
kuceb Apr 26, 2019
53056c2
Merge branch 'develop' into click-command-focus
kuceb Apr 26, 2019
5edd733
left out return in intercept blur/focus
kuceb Apr 26, 2019
5d1a67e
Merge remote-tracking branch 'origin/click-command-focus' into click-…
kuceb Apr 26, 2019
f17c6c6
cleanup test code for focus_blur spec
kuceb Apr 26, 2019
86dccc3
Merge remote-tracking branch 'origin/develop' into click-command-focus
kuceb May 22, 2019
cafe317
add tests to type_spec, focus_blur_spec
kuceb May 23, 2019
cafe27b
update focus logic for click, fix dtslint error
kuceb May 23, 2019
cafeffe
add tests for selectionchange event in focus_blur spec
kuceb May 24, 2019
cafe499
set dep to exact version
kuceb May 24, 2019
75520ae
minor formatting
brian-mann Jun 10, 2019
3e0a272
intercept focus/blur for SVGElement
kuceb Jun 10, 2019
5547b2e
Merge remote-tracking branch 'origin/develop' into click-command-focus
kuceb Jun 11, 2019
84ee77e
add comment to type-into-already-focused logic
kuceb Jun 11, 2019
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
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