fix not sending click event to proper element (#5580)

* fix not sending click event

* add issue comment

* slightly less messy code

* refactor, use term "phase"
This commit is contained in:
Ben Kucera
2019-11-06 12:01:19 -05:00
committed by Brian Mann
parent 7ef3078f6a
commit 32073c37a7
3 changed files with 188 additions and 91 deletions
@@ -43,7 +43,7 @@ const formatMouseEvents = (events) => {
const reason = val.skipped
return {
'Event Name': key.slice(0, -5),
'Event Name': key,
'Target Element': reason,
'Prevented Default?': null,
'Stopped Propagation?': null,
@@ -52,7 +52,7 @@ const formatMouseEvents = (events) => {
}
return {
'Event Name': key.slice(0, -5),
'Event Name': key,
'Target Element': val.el,
'Prevented Default?': val.preventedDefault,
'Stopped Propagation?': val.stoppedPropagation,
@@ -265,10 +265,10 @@ module.exports = (Commands, Cypress, cy, state, config) => {
defaultOptions: { multiple: true },
positionOrX,
onReady (fromElViewport, forceEl) {
const { clickEvents1, clickEvents2, dblclickProps } = mouse.dblclick(fromElViewport, forceEl)
const { clickEvents1, clickEvents2, dblclick } = mouse.dblclick(fromElViewport, forceEl)
return {
dblclickProps,
dblclick,
clickEvents: [clickEvents1, clickEvents2],
}
},
@@ -281,8 +281,8 @@ module.exports = (Commands, Cypress, cy, state, config) => {
return {
name: 'Mouse Click Events',
data: _.concat(
formatMouseEvents(domEvents.clickEvents[0], formatMouseEvents),
formatMouseEvents(domEvents.clickEvents[1], formatMouseEvents)
formatMouseEvents(domEvents.clickEvents[0]),
formatMouseEvents(domEvents.clickEvents[1])
),
}
},
@@ -290,7 +290,7 @@ module.exports = (Commands, Cypress, cy, state, config) => {
return {
name: 'Mouse Double Click Event',
data: formatMouseEvents({
dblclickProps: domEvents.dblclickProps,
dblclick: domEvents.dblclick,
}),
}
},
+60 -50
View File
@@ -310,12 +310,12 @@ const create = (state, keyboard, focused) => {
}, mouseEvtOptionsExtend)
// TODO: pointer events should have fractional coordinates, not rounded
let pointerdownProps = sendPointerdown(
let pointerdown = sendPointerdown(
el,
pointerEvtOptions
)
const pointerdownPrevented = pointerdownProps.preventedDefault
const pointerdownPrevented = pointerdown.preventedDefault
const elIsDetached = $elements.isDetachedEl(el)
if (pointerdownPrevented || elIsDetached) {
@@ -326,31 +326,37 @@ const create = (state, keyboard, focused) => {
}
return {
pointerdownProps,
mousedownProps: {
skipped: formatReasonNotFired(reason),
targetEl: el,
events: {
pointerdown,
mousedown: {
skipped: formatReasonNotFired(reason),
},
},
}
}
let mousedownProps = sendMousedown(el, mouseEvtOptions)
let mousedown = sendMousedown(el, mouseEvtOptions)
return {
pointerdownProps,
mousedownProps,
targetEl: el,
events: {
pointerdown,
mousedown,
},
}
},
down (fromElViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) {
const $previouslyFocused = focused.getFocused()
const mouseDownEvents = mouse._downEvents(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseDownPhase = mouse._downEvents(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
// el we just send pointerdown
const el = mouseDownEvents.pointerdownProps.el
const el = mouseDownPhase.targetEl
if (mouseDownEvents.pointerdownProps.preventedDefault || mouseDownEvents.mousedownProps.preventedDefault || !$elements.isAttachedEl(el)) {
return mouseDownEvents
if (mouseDownPhase.events.pointerdown.preventedDefault || mouseDownPhase.events.mousedown.preventedDefault || !$elements.isAttachedEl(el)) {
return mouseDownPhase
}
//# retrieve the first focusable $el in our parent chain
@@ -377,7 +383,7 @@ const create = (state, keyboard, focused) => {
$selection.moveSelectionToEnd($dom.getDocumentFromElement($elToFocus[0]), { onlyIfEmptySelection: true })
}
return mouseDownEvents
return mouseDownPhase
},
/**
@@ -410,42 +416,41 @@ const create = (state, keyboard, focused) => {
* el2 = moveToCoordsOrNoop(coords)
* sendMouseup(el2)
* el3 = moveToCoordsOrNoop(coords)
* if (notDetached(el1) && el1 === el2)
* sendClick(el3)
* if (notDetached(el1))
* sendClick(ancestorOf(el1, el2))
*/
click (fromElViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) {
debug('mouse.click', { fromElViewport, forceEl })
const mouseDownEvents = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseDownPhase = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault
const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault
const mouseUpEvents = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
// Only send click event if the same element received both pointerdown and pointerup, and it's not detached.
const getSkipClickEventAndReason = () => {
const getElementToClick = () => {
// Never skip the click event when force:true
if (forceEl) {
return false
return { elToClick: forceEl }
}
if ($elements.isDetachedEl(mouseDownEvents.pointerdownProps.el)) {
return 'element was detached'
// Only send click event if mousedown element is not detached.
if ($elements.isDetachedEl(mouseDownPhase.targetEl)) {
return { skipClickEventReason: 'element was detached' }
}
if (!mouseUpEvents.pointerupProps.el || mouseDownEvents.pointerdownProps.el !== mouseUpEvents.pointerupProps.el) {
return 'mouseup and mousedown not received by same element'
}
const commonAncestor = mouseUpPhase.targetEl &&
mouseDownPhase.targetEl &&
$elements.getFirstCommonAncestor(mouseUpPhase.targetEl, mouseDownPhase.targetEl)
// No reason to skip the click event
return false
return { elToClick: commonAncestor }
}
const skipClickEvent = getSkipClickEventAndReason()
const { skipClickEventReason, elToClick } = getElementToClick()
const mouseClickEvents = mouse._mouseClickEvents(fromElViewport, mouseDownEvents.pointerdownProps.el, forceEl, skipClickEvent, mouseEvtOptionsExtend)
const mouseClickEvents = mouse._mouseClickEvents(fromElViewport, elToClick, forceEl, skipClickEventReason, mouseEvtOptionsExtend)
return _.extend({}, mouseDownEvents, mouseUpEvents, mouseClickEvents)
return _.extend({}, mouseDownPhase.events, mouseUpPhase.events, mouseClickEvents)
},
/**
@@ -471,29 +476,35 @@ const create = (state, keyboard, focused) => {
const el = forceEl || mouse.moveToCoords(fromElViewport)
let pointerupProps = sendPointerup(el, pointerEvtOptions)
let pointerup = sendPointerup(el, pointerEvtOptions)
if (skipMouseEvent || $elements.isDetachedEl($(el))) {
return {
pointerupProps,
mouseupProps: {
skipped: formatReasonNotFired('Previous event cancelled'),
targetEl: el,
events: {
pointerup,
mouseup: {
skipped: formatReasonNotFired('Previous event cancelled'),
},
},
}
}
let mouseupProps = sendMouseup(el, mouseEvtOptions)
let mouseup = sendMouseup(el, mouseEvtOptions)
return {
pointerupProps,
mouseupProps,
targetEl: el,
events: {
pointerup,
mouseup,
},
}
},
_mouseClickEvents (fromElViewport, el, forceEl, skipClickEvent, mouseEvtOptionsExtend = {}) {
if (skipClickEvent) {
return {
clickProps: {
click: {
skipped: formatReasonNotFired(skipClickEvent),
},
}
@@ -512,9 +523,9 @@ const create = (state, keyboard, focused) => {
detail: 1,
}, mouseEvtOptionsExtend)
let clickProps = sendClick(el, clickEventOptions)
let click = sendClick(el, clickEventOptions)
return { clickProps }
return { click }
},
_contextmenuEvent (fromElViewport, forceEl, mouseEvtOptionsExtend) {
@@ -530,9 +541,9 @@ const create = (state, keyboard, focused) => {
which: 3,
}, mouseEvtOptionsExtend)
let contextmenuProps = sendContextmenu(el, mouseEvtOptions)
let contextmenu = sendContextmenu(el, mouseEvtOptions)
return { contextmenuProps }
return { contextmenu }
},
dblclick (fromElViewport, forceEl, mouseEvtOptionsExtend = {}) {
@@ -553,9 +564,9 @@ const create = (state, keyboard, focused) => {
detail: 2,
}, mouseEvtOptionsExtend)
let dblclickProps = sendDblclick(el, dblclickEvtProps)
let dblclick = sendDblclick(el, dblclickEvtProps)
return { clickEvents1, clickEvents2, dblclickProps }
return { clickEvents1, clickEvents2, dblclick }
},
rightclick (fromElViewport, forceEl) {
@@ -570,15 +581,14 @@ const create = (state, keyboard, focused) => {
which: 3,
}
const mouseDownEvents = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseDownPhase = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const contextmenuEvent = mouse._contextmenuEvent(fromElViewport, forceEl)
const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault
const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault
const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseUpEvents = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const clickEvents = _.extend({}, mouseDownEvents, mouseUpEvents)
const clickEvents = _.extend({}, mouseDownPhase.events, mouseUpPhase.events)
return _.extend({}, { clickEvents, contextmenuEvent })
},
@@ -242,19 +242,18 @@ describe('src/cy/commands/actions/click', () => {
it('will not send mouseEvents/focus if pointerdown is defaultPrevented', () => {
const $btn = cy.$$('#button')
// let clicked = false
$btn.get(0).addEventListener('pointerdown', (e) => {
// clicked = true
const onEvent = cy.stub().callsFake((e) => {
e.preventDefault()
expect(e.defaultPrevented).to.be.true
})
$btn.get(0).addEventListener('pointerdown', onEvent)
attachMouseClickListeners({ $btn })
// uncomment to manually test
// cy.wrap(onEvent).should('be.called')
cy.get('#button').click().should('not.have.focus')
// cy.wrap(null).should(() => expect(clicked).ok)
cy.getAll('$btn', 'pointerdown pointerup click').each(shouldBeCalledOnce)
cy.getAll('$btn', 'mousedown mouseup').each(shouldNotBeCalled)
@@ -380,25 +379,27 @@ describe('src/cy/commands/actions/click', () => {
it('events when element moved on mousedown', () => {
const btn = cy.$$('button:first')
const div = cy.$$('div#tabindex')
const root = cy.$$('#dom')
attachFocusListeners({ btn, div })
attachMouseClickListeners({ btn, div })
attachMouseClickListeners({ btn, div, root })
attachMouseHoverListeners({ btn, div })
// let clicked = false
btn.on('mousedown', () => {
// clicked = true
const onEvent = cy.stub().callsFake(() => {
div.css(overlayStyle)
})
btn.on('mousedown', onEvent)
// uncomment to manually test
// cy.wrap(onEvent).should('be.called')
cy.contains('button').click()
// cy.wrap(null).should(() => expect(clicked).ok)
cy.getAll('btn', 'mouseover mouseenter mousedown focus').each(shouldBeCalled)
cy.getAll('btn', 'click mouseup').each(shouldNotBeCalled)
cy.getAll('div', 'mouseover mouseenter mouseup').each(shouldBeCalled)
cy.getAll('div', 'click focus').each(shouldNotBeCalled)
cy.getAll('root', 'click').each(shouldBeCalled)
})
it('events when element moved on mouseup', () => {
@@ -409,15 +410,15 @@ describe('src/cy/commands/actions/click', () => {
attachMouseClickListeners({ btn, div })
attachMouseHoverListeners({ btn, div })
// let clicked = false
btn.on('mouseup', () => {
// clicked = true
const onEvent = cy.stub().callsFake(() => {
div.css(overlayStyle)
})
btn.on('mouseup', onEvent)
// uncomment to manually test
// cy.wrap(onEvent).should('be.called')
cy.contains('button').click()
// cy.wrap(null).should(() => expect(clicked).ok)
cy.getAll('btn', 'mouseover mouseenter mousedown focus click mouseup').each(shouldBeCalled)
cy.getAll('div', 'mouseover mouseenter').each(shouldBeCalled)
@@ -432,20 +433,101 @@ describe('src/cy/commands/actions/click', () => {
attachMouseClickListeners({ btn, div })
attachMouseHoverListeners({ btn, div })
// let clicked = false
btn.on('click', () => {
// clicked = true
const onEvent = cy.stub().callsFake(() => {
div.css(overlayStyle)
})
btn.on('click', onEvent)
// uncomment to manually test
// cy.wrap(onEvent).should('be.called')
cy.contains('button').click()
// cy.wrap(null).should(() => expect(clicked).ok)
cy.getAll('btn', 'mouseover mouseenter mousedown focus click mouseup').each(shouldBeCalled)
cy.getAll('div', 'focus click mouseup mousedown').each(shouldNotBeCalled)
})
// https://github.com/cypress-io/cypress/issues/5578
it('click when mouseup el is child of mousedown el', () => {
const btn = cy.$$('button:first')
const span = $('<span>foooo</span>')
attachFocusListeners({ btn, span })
attachMouseClickListeners({ btn, span })
attachMouseHoverListeners({ btn, span })
const onEvent = cy.stub().callsFake(() => {
// clicked = true
btn.html('')
btn.append(span)
})
btn.on('mousedown', onEvent)
// uncomment to manually test
// cy.wrap(onEvent).should('be.called')
cy.contains('button').click()
cy.getAll('btn', 'mousedown focus click mouseup').each(shouldBeCalled)
cy.getAll('span', 'mouseup').each(shouldBeCalled)
cy.getAll('span', 'focus click mousedown').each(shouldNotBeCalled)
})
it('click when mousedown el is child of mouseup el', () => {
const btn = cy.$$('button:first')
const span = $('<span>foooo</span>')
attachFocusListeners({ btn, span })
attachMouseClickListeners({ btn, span })
attachMouseHoverListeners({ btn, span })
btn.html('')
btn.append(span)
const onEvent = cy.stub().callsFake(() => {
span.css({ marginLeft: 50 })
})
btn.on('mousedown', onEvent)
cy.get('button:first').click()
cy.getAll('btn', 'mousedown focus click mouseup').each(shouldBeCalled)
cy.getAll('span', 'mousedown').each(shouldBeCalled)
cy.getAll('span', 'focus click mouseup').each(shouldNotBeCalled)
})
it('no click when new element at coords is not ancestor', () => {
const btn = cy.$$('button:first')
const span1 = $('<span>foooo</span>')
const span2 = $('<span>baaaar</span>')
attachFocusListeners({ btn, span1, span2 })
attachMouseClickListeners({ btn, span1, span2 })
attachMouseHoverListeners({ btn, span1, span2 })
btn.html('')
btn.append(span1)
const onEvent = cy.stub().callsFake(() => {
btn.html('')
btn.append(span2)
})
btn.on('mousedown', onEvent)
// uncomment to manually test
// cy.wrap(onEvent).should('be.called')
cy.get('button:first').click()
cy.getAll('btn', 'mouseenter mousedown mouseup').each(shouldBeCalled)
cy.getAll('btn', 'click focus').each(shouldNotBeCalled)
cy.getAll('span1', 'mouseover mouseenter mousedown').each(shouldBeCalled)
cy.getAll('span1', 'focus click mouseup').each(shouldNotBeCalled)
cy.getAll('span2', 'mouseup mouseover mouseenter').each(shouldBeCalled)
cy.getAll('span2', 'focus click mousedown').each(shouldNotBeCalled)
})
it('does not fire a click when element has been removed on mouseup', () => {
const $btn = cy.$$('button:first')
@@ -458,6 +540,10 @@ describe('src/cy/commands/actions/click', () => {
fail('should not have gotten click')
})
cy.$$('body').on('click', (e) => {
throw new Error('should not have happened')
})
cy.contains('button').click()
})
@@ -2594,9 +2680,9 @@ describe('src/cy/commands/actions/click', () => {
},
{
'Event Name': 'click',
'Target Element': '⚠️ not fired (mouseup and mousedown not received by same element)',
'Prevented Default?': null,
'Stopped Propagation?': null,
'Target Element': { id: 'dom' },
'Prevented Default?': false,
'Stopped Propagation?': false,
'Modifiers': null,
},
])
@@ -4243,13 +4329,12 @@ describe('mouse state', () => {
})
.appendTo(btn.parent())
// let clicked = false
cover.on('mouseup', () => {
const onEvent = cy.stub().callsFake(() => {
cover.hide()
// clicked = true
})
cover.on('mouseup', onEvent)
attachFocusListeners({ btn, cover })
attachMouseHoverListeners({ btn, cover })
attachMouseClickListeners({ btn, cover })
@@ -4258,8 +4343,9 @@ describe('mouse state', () => {
btn.attr('disabled', true)
})
// uncomment to manually test
// cy.wrap(onEvent).should('be.called')
cy.get('#cover').click()
// cy.wrap(null).should(() => expect(clicked).ok)
cy.getAll('cover', 'mousedown mouseup click mouseout mouseleave').each(shouldBeCalledOnce)
cy.getAll('cover', 'focus').each(shouldNotBeCalled)
@@ -4284,13 +4370,14 @@ describe('mouse state', () => {
})
.appendTo(btn.parent())
// let clicked = false
cover.on('mouseover', () => {
const onEvent = cy.stub().callsFake(() => {
cover.hide()
// clicked = true
})
// uncomment to manually test
// cy.wrap(onEvent).should('be.called')
cover.on('mouseover', onEvent)
attachFocusListeners({ btn, cover })
attachMouseHoverListeners({ btn, cover })
attachMouseClickListeners({ btn, cover })