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 })