diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js
index d747f4f66e8..051704ad04a 100644
--- a/packages/driver/src/cy/commands/actions/click.js
+++ b/packages/driver/src/cy/commands/actions/click.js
@@ -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,
}),
}
},
diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js
index ecc52dbc0a9..edb9a0d782c 100644
--- a/packages/driver/src/cy/mouse.js
+++ b/packages/driver/src/cy/mouse.js
@@ -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 mouseUpEvents = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
+ const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault
+ const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
- const clickEvents = _.extend({}, mouseDownEvents, mouseUpEvents)
+ const clickEvents = _.extend({}, mouseDownPhase.events, mouseUpPhase.events)
return _.extend({}, { clickEvents, contextmenuEvent })
},
diff --git a/packages/driver/test/cypress/integration/commands/actions/click_spec.js b/packages/driver/test/cypress/integration/commands/actions/click_spec.js
index ab467f449d7..9ed9308a054 100644
--- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js
+++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js
@@ -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 = $('foooo')
+
+ 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 = $('foooo')
+
+ 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 = $('foooo')
+ const span2 = $('baaaar')
+
+ 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 })