mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-05 14:50:00 -06:00
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:
9
cli/types/index.d.ts
vendored
9
cli/types/index.d.ts
vendored
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
body {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: 4px solid blue
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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()
|
||||
673
packages/driver/test/cypress/integration/e2e/focus_blur_spec.js
Normal file
673
packages/driver/test/cypress/integration/e2e/focus_blur_spec.js
Normal 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')
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user