UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

1,682 lines (1,293 loc) 148 kB
const { _, $, Promise } = Cypress const { getCommandLogWithText, findReactInstance, withMutableReporterState, clickCommandLog, attachListeners, shouldBeCalledWithCount, shouldBeCalled, shouldBeCalledOnce, shouldNotBeCalled, expectCaret, } = require('../../../support/utils') const fail = function (str) { throw new Error(str) } const mouseClickEvents = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'] const mouseHoverEvents = [ 'pointerout', 'pointerleave', 'pointerover', 'pointerenter', 'mouseout', 'mouseleave', 'mouseover', 'mouseenter', 'pointermove', 'mousemove', ] const focusEvents = ['focus', 'focusin'] const attachFocusListeners = attachListeners(focusEvents) const attachMouseClickListeners = attachListeners(mouseClickEvents) const attachMouseHoverListeners = attachListeners(mouseHoverEvents) const attachMouseDblclickListeners = attachListeners(['dblclick']) const attachContextmenuListeners = attachListeners(['contextmenu']) const overlayStyle = { position: 'fixed', top: 0, width: '100%', height: '100%', opacity: 0.5 } const getMidPoint = (el) => { const box = el.getBoundingClientRect() const midX = Math.ceil(box.left + box.width / 2 + el.ownerDocument.defaultView.scrollX) const midY = Math.ceil(box.top + box.height / 2 + el.ownerDocument.defaultView.scrollY) return { x: midX, y: midY } } const isFirefox = Cypress.isBrowser('firefox') describe('src/cy/commands/actions/click', () => { beforeEach(() => { cy.visit('/fixtures/dom.html') }) context('#click', () => { it('receives native click event', (done) => { const $btn = cy.$$('#button') $btn.on('click', (e) => { const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) const obj = _.pick(e.originalEvent, 'bubbles', 'cancelable', 'view', 'button', 'buttons', 'which', 'relatedTarget', 'altKey', 'ctrlKey', 'shiftKey', 'metaKey', 'detail', 'type') expect(obj).to.deep.eq({ bubbles: true, cancelable: true, view: cy.state('window'), button: 0, buttons: 0, which: 1, relatedTarget: null, altKey: false, ctrlKey: false, shiftKey: false, metaKey: false, detail: 1, type: 'click', }) expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() }) cy.get('#button').click() }) it('bubbles up native click event', (done) => { const click = () => { cy.state('window').removeEventListener('click', click) done() } cy.state('window').addEventListener('click', click) cy.get('#button').click() }) it('sends native mousedown event', (done) => { const $btn = cy.$$('#button') const win = cy.state('window') $btn.get(0).addEventListener('mousedown', (e) => { // calculate after scrolling const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) const obj = _.pick(e, 'bubbles', 'cancelable', 'view', 'button', 'buttons', 'which', 'relatedTarget', 'altKey', 'ctrlKey', 'shiftKey', 'metaKey', 'detail', 'type') expect(obj).to.deep.eq({ bubbles: true, cancelable: true, view: win, button: 0, buttons: 1, which: 1, relatedTarget: null, altKey: false, ctrlKey: false, shiftKey: false, metaKey: false, detail: 1, type: 'mousedown', }) expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() }) cy.get('#button').click() }) it('sends native mouseup event', (done) => { const $btn = cy.$$('#button') const win = cy.state('window') $btn.get(0).addEventListener('mouseup', (e) => { const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) const obj = _.pick(e, 'bubbles', 'cancelable', 'view', 'button', 'buttons', 'which', 'relatedTarget', 'altKey', 'ctrlKey', 'shiftKey', 'metaKey', 'detail', 'type') expect(obj).to.deep.eq({ bubbles: true, cancelable: true, view: win, button: 0, buttons: 0, which: 1, relatedTarget: null, altKey: false, ctrlKey: false, shiftKey: false, metaKey: false, detail: 1, type: 'mouseup', }) expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() }) cy.get('#button').click() }) it('sends mousedown, mouseup, click events in order', () => { const events = [] const $btn = cy.$$('#button') _.each('mousedown mouseup click'.split(' '), (event) => { $btn.get(0).addEventListener(event, () => { events.push(event) }) }) cy.get('#button').click().then(() => { expect(events).to.deep.eq(['mousedown', 'mouseup', 'click']) }) }) it('sends pointer and mouse events in order', () => { const events = [] const $btn = cy.$$('#button') _.each('pointerdown mousedown pointerup mouseup click'.split(' '), (event) => { $btn.get(0).addEventListener(event, () => { events.push(event) }) }) cy.get('#button').click().then(() => { expect(events).to.deep.eq(['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) }) }) it('records correct clientX when el scrolled', (done) => { const $btn = $(`<button id='scrolledBtn' style='position: absolute; top: 1600px; left: 1200px; width: 100px;'>foo</button>`).appendTo(cy.$$('body')) const win = cy.state('window') $btn.get(0).addEventListener('click', (e) => { const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) expect(win.scrollX).to.be.gt(0) expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) done() }) cy.get('#scrolledBtn').click() }) it('records correct clientY when el scrolled', (done) => { const $btn = $(`<button id='scrolledBtn' style='position: absolute; top: 1600px; left: 1200px; width: 100px;'>foo</button>`).appendTo(cy.$$('body')) const win = cy.state('window') $btn.get(0).addEventListener('click', (e) => { const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) expect(win.scrollY).to.be.gt(0) expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() }) cy.get('#scrolledBtn').click() }) it('will send all events even mousedown is defaultPrevented', () => { const $btn = cy.$$('#button') $btn.get(0).addEventListener('mousedown', (e) => { e.preventDefault() expect(e.defaultPrevented).to.be.true }) attachMouseClickListeners({ $btn }) cy.get('#button').click().should('not.have.focus') cy.getAll('$btn', 'pointerdown mousedown pointerup mouseup click').each(shouldBeCalled) }) it('will not send mouseEvents/focus if pointerdown is defaultPrevented', () => { const $btn = cy.$$('#button') 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.getAll('$btn', 'pointerdown pointerup click').each(shouldBeCalledOnce) cy.getAll('$btn', 'mousedown mouseup').each(shouldNotBeCalled) }) it('sends a click event', (done) => { cy.$$('#button').click(() => { done() }) cy.get('#button').click() }) it('returns the original subject', () => { const button = cy.$$('#button') cy.get('#button').click().then(($button) => { expect($button).to.match(button) }) }) it('causes focusable elements to receive focus', () => { const el = cy.$$(':text:first') attachFocusListeners({ el }) cy.get(':text:first').click().should('have.focus') cy.getAll('el', 'focus focusin').each(shouldBeCalledOnce) }) // https://github.com/cypress-io/cypress/issues/5430 it('does not attempt to click element outside viewport', (done) => { cy.timeout(100) cy.on('fail', (err) => { expect(err.message).contain('id="email-with-value"') expect(err.message).contain('hidden from view') done() }) cy.$$('#tabindex').css(overlayStyle) cy.get('#email-with-value').click() }) it('can click element outside viewport with force:true', () => { cy.$$('#tabindex').css(overlayStyle) cy.get('#email-with-value').click({ force: true }) }) it('does not fire a focus, mouseup, or click event when element has been removed on mousedown', () => { const $btn = cy.$$('button:first') $btn.on('mousedown', function () { // synchronously remove this button $(this).remove() }) $btn.on('focus', () => { fail('should not have gotten focus') }) $btn.on('focusin', () => { fail('should not have gotten focusin') }) $btn.on('mouseup', () => { fail('should not have gotten mouseup') }) $btn.on('click', () => { fail('should not have gotten click') }) cy.contains('button').click() }) it('events when element removed on pointerdown', () => { const btn = cy.$$('button:first').css({ transform: 'translateY(-50px)' }) const div = cy.$$('div#tabindex') attachFocusListeners({ btn }) attachMouseClickListeners({ btn, div }) attachMouseHoverListeners({ btn, div }) btn.on('pointerdown', () => { // synchronously remove this button btn.remove() }) cy.contains('button').click() cy.getAll('btn', 'pointerdown').each(shouldBeCalled) cy.getAll('btn', 'mousedown mouseup').each(shouldNotBeCalled) cy.getAll('div', 'pointerover pointerenter mouseover mouseenter pointerup mouseup').each(shouldBeCalled) }) it('events when element removed on pointerover', () => { const btn = cy.$$('button:first').css({ transform: 'translateY(-50px)' }) const div = cy.$$('div#tabindex') // attachFocusListeners({ btn }) attachMouseClickListeners({ btn, div }) attachMouseHoverListeners({ btn, div }) btn.on('pointerover', () => { // synchronously remove this button btn.remove() }) cy.contains('button').click() cy.getAll('btn', 'pointerover pointerenter').each(shouldBeCalled) cy.getAll('btn', 'pointerdown mousedown mouseover mouseenter').each(shouldNotBeCalled) cy.getAll('div', 'pointerover pointerenter pointerdown mousedown pointerup mouseup click').each(shouldBeCalled) }) // https://github.com/cypress-io/cypress/issues/5459 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, root }) attachMouseHoverListeners({ btn, div }) 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.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', () => { const btn = cy.$$('button:first') const div = cy.$$('div#tabindex') attachFocusListeners({ btn, div }) attachMouseClickListeners({ btn, div }) attachMouseHoverListeners({ btn, div }) 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.getAll('btn', 'mouseover mouseenter mousedown focus click mouseup').each(shouldBeCalled) cy.getAll('div', 'mouseover mouseenter').each(shouldBeCalled) cy.getAll('div', 'focus click mouseup mousedown').each(shouldNotBeCalled) }) it('events when element moved on click', () => { const btn = cy.$$('button:first') const div = cy.$$('div#tabindex') attachFocusListeners({ btn, div }) attachMouseClickListeners({ btn, div }) attachMouseHoverListeners({ btn, div }) 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.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) }) // https://github.com/cypress-io/cypress/issues/6923 it('no click when mouseUpPhase targetEl is detached', () => { 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(() => { span1.hide() btn.append(span2) }) btn.on('mousedown', onEvent) btn.on('mouseup', () => { span2.remove() }) // uncomment to manually test // cy.wrap(onEvent).should('be.called') cy.get('button:first').click() cy.getAll('btn', 'mouseenter mousedown mouseup focus').each(shouldBeCalled) cy.getAll('btn', 'click').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('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') $btn.on('mouseup', function () { // synchronously remove this button $(this).remove() }) $btn.on('click', () => { fail('btn should not have gotten click') }) cy.$$('body').on('click', (e) => { throw new Error('should not have happened') }) cy.contains('button').click() }) it('does not fire a click or mouseup when element has been removed on pointerup', () => { const $btn = cy.$$('button:first') $btn.on('pointerup', function () { // synchronously remove this button $(this).remove() }) ;['mouseup', 'click'].forEach((eventName) => { $btn.on(eventName, () => { fail(`should not have gotten ${eventName}`) }) }) cy.contains('button').click() }) it('sends modifiers', () => { const btn = cy.$$('button:first') attachMouseClickListeners({ btn }) cy.get('input:first').type('{ctrl}{shift}', { release: false }) cy.get('button:first').click() cy.getAll('btn', 'pointerdown mousedown pointerup mouseup click').each((stub) => { expect(stub).to.be.calledWithMatch({ shiftKey: true, ctrlKey: true, metaKey: false, altKey: false, }) }) }) it('silences errors on unfocusable elements', () => { cy.get('div:first').click({ force: true }) }) it('causes first focused element to receive blur', () => { let blurred = false cy.$$('input:first').blur(() => { blurred = true }) cy .get('input:first').focus() .get('input:text:last').click() .then(() => { expect(blurred).to.be.true }) }) it('inserts artificial delay of 50ms', () => { cy.spy(Promise, 'delay') cy.get('#button').click().then(() => { expect(Promise.delay).to.be.calledWith(50) }) }) it('delays 50ms before resolving', () => { cy.$$('button:first').on('click', () => { cy.spy(Promise, 'delay') }) cy.get('button:first').click({ multiple: true }).then(() => { expect(Promise.delay).to.be.calledWith(50, 'click') }) }) it('can operate on a jquery collection', () => { let clicks = 0 const buttons = cy.$$('button').slice(0, 3) buttons.click(() => { clicks += 1 return false }) // make sure we have more than 1 button expect(buttons.length).to.be.gt(1) // make sure each button received its click event cy.get('button').invoke('slice', 0, 3).click({ multiple: true }).then(($buttons) => { expect($buttons.length).to.eq(clicks) }) }) it('can cancel multiple clicks', (done) => { cy.stub(Cypress.runner, 'stop') // abort after the 3rd click const stop = _.after(3, () => { Cypress.stop() }) const clicked = cy.spy(() => { stop() }) const $anchors = cy.$$('#sequential-clicks a') $anchors.on('click', clicked) // make sure we have at least 5 anchor links expect($anchors.length).to.be.gte(5) cy.on('stop', () => { // timeout will get called synchronously // again during a click if the click function // is called const timeout = cy.spy(cy.timeout) _.delay(() => { // and we should have stopped clicking after 3 expect(clicked.callCount).to.eq(3) expect(timeout.callCount).to.eq(0) done() } , 100) }) cy.get('#sequential-clicks a').click({ multiple: true }) }) it('serially clicks a collection', () => { const throttled = cy.stub().as('clickcount') // create a throttled click function // which proves we are clicking serially const handleClick = cy.stub() .callsFake(_.throttle(throttled, 0, { leading: false })) .as('handleClick') const $anchors = cy.$$('#sequential-clicks a') $anchors.on('click', handleClick) // make sure we're clicking multiple $anchors expect($anchors.length).to.be.gt(1) cy.get('#sequential-clicks a').click({ multiple: true }).then(($els) => { expect($els).to.have.length(throttled.callCount) }) }) it('increases the timeout delta after each click', () => { const count = cy.$$('#three-buttons button').length cy.spy(cy, 'timeout') cy.get('#three-buttons button').click({ multiple: true }).then(() => { const calls = cy.timeout.getCalls() const num = _.filter(calls, (call) => _.isEqual(call.args, [50, true, 'click'])) expect(num.length).to.eq(count) }) }) // this test needs to increase the height + width of the div // when we implement scrollBy the delta of the left/top it('can click elements which are huge and the center is naturally below the fold', () => { cy.get('#massively-long-div').click() }) it('can click a tr', () => { cy.get('#table tr:first').click() }) it('places cursor at the end of input', () => { cy.get('input:first').invoke('val', 'foobar').click().then(($el) => { const el = $el.get(0) expect(el.selectionStart).to.eql(6) expect(el.selectionEnd).to.eql(6) }) cy.get('input:first').invoke('val', '').click().then(($el) => { const el = $el.get(0) expect(el.selectionStart).to.eql(0) expect(el.selectionEnd).to.eql(0) }) }) it('places cursor at the end of textarea', () => { cy.get('textarea:first').invoke('val', 'foo\nbar\nbaz').click().then(($el) => { const el = $el.get(0) expect(el.selectionStart).to.eql(11) expect(el.selectionEnd).to.eql(11) }) cy.get('textarea:first').invoke('val', '').click().then(($el) => { const el = $el.get(0) expect(el.selectionStart).to.eql(0) expect(el.selectionEnd).to.eql(0) }) }) it('places cursor at the end of [contenteditable]', () => { cy.get('[contenteditable]:first') .invoke('html', '<div><br></div>').click() .then(expectCaret(0)) cy.get('[contenteditable]:first') .invoke('html', 'foo').click() .then(expectCaret(3)) cy.get('[contenteditable]:first') .invoke('html', '<div>foo</div>').click() .then(expectCaret(3)) cy.get('[contenteditable]:first') // firefox headless: prevent contenteditable from disappearing (dont set to empty) .invoke('html', '<br>').click() .then(expectCaret(0)) }) it('can click SVG elements', () => { const onClick = cy.stub() const $svgs = cy.$$('#svgs') $svgs.click(onClick) cy.get('[data-cy=line]').click().first().click() cy.get('[data-cy=rect]').click().first().click() cy.get('[data-cy=circle]').click().first().click() .then(() => { expect(onClick.callCount).to.eq(6) }) }) it('can click a canvas', () => { const onClick = cy.stub() const $canvas = cy.$$('#canvas') $canvas.click(onClick) const ctx = $canvas.get(0).getContext('2d') ctx.fillStyle = 'green' ctx.fillRect(10, 10, 100, 100) cy.get('#canvas').click().then(() => { expect(onClick).to.be.calledOnce }) }) describe('modifier options', () => { beforeEach(() => { cy.visit('/fixtures/issue-486.html') }) it('ctrl', () => { cy.get('#button').click({ ctrlKey: true, }) cy.get('#result').should('contain', '{Ctrl}') // ctrl should be released cy.get('#button').click() cy.get('#result').should('not.contain', '{Ctrl}') cy.get('#button').click({ controlKey: true, }) cy.get('#result').should('contain', '{Ctrl}') }) it('alt', () => { cy.get('#button').click({ altKey: true, }) cy.get('#result').should('contain', '{Alt}') // alt should be released cy.get('#button').click() cy.get('#result').should('not.contain', '{Alt}') cy.get('#button').click({ optionKey: true, }) cy.get('#result').should('contain', '{Alt}') }) it('shift', () => { cy.get('#button').click({ shiftKey: true, }) cy.get('#result').should('contain', '{Shift}') // shift should be released cy.get('#button').click() cy.get('#result').should('not.contain', '{Shift}') }) it('meta', () => { cy.get('#button').click({ metaKey: true, }) cy.get('#result').should('contain', '{Meta}') // shift should be released cy.get('#button').click() cy.get('#result').should('not.contain', '{Meta}') cy.get('#button').click({ commandKey: true, }) cy.get('#result').should('contain', '{Meta}') cy.get('#button').click({ cmdKey: true, }) cy.get('#result').should('contain', '{Meta}') }) it('multiple', () => { cy.get('#button').click({ ctrlKey: true, altKey: true, shiftKey: true, metaKey: true, }) cy.get('#result').should('contain', '{Ctrl}') cy.get('#result').should('contain', '{Alt}') cy.get('#result').should('contain', '{Shift}') cy.get('#result').should('contain', '{Meta}') // modifiers should be released cy.get('#button').click() cy.get('#result').should('not.contain', '{Ctrl}') cy.get('#result').should('not.contain', '{Alt}') cy.get('#result').should('not.contain', '{Shift}') cy.get('#result').should('not.contain', '{Meta}') }) }) describe('pointer-events:none', () => { beforeEach(function () { cy.$$('<div id="ptr" style="position:absolute;width:200px;height:200px;background-color:#08c18d;">behind #ptrNone</div>').appendTo(cy.$$('#dom')) this.ptrNone = cy.$$(`<div id="ptrNone" style="position:absolute;width:400px;height:400px;background-color:salmon;pointer-events:none;opacity:0.4;text-align:right">#ptrNone</div>`).appendTo(cy.$$('#dom')) cy.$$('<div id="ptrNoneChild" style="position:absolute;top:50px;left:50px;width:200px;height:200px;background-color:red">#ptrNone > div</div>').appendTo(this.ptrNone) this.logs = [] cy.on('log:added', (attrs, log) => { this.lastLog = log this.logs.push(log) }) }) it('element behind pointer-events:none should still get click', () => { cy.get('#ptr').click() // should pass with flying colors }) it('should be able to force on pointer-events:none with force:true', () => { cy.get('#ptrNone').click({ timeout: 300, force: true }) }) it('should error with message about pointer-events', function () { const onError = cy.stub().callsFake((err) => { const { lastLog } = this expect(err.message).to.contain('has CSS `pointer-events: none`') expect(err.message).to.not.contain('inherited from') const consoleProps = lastLog.invoke('consoleProps') expect(_.keys(consoleProps)).deep.eq([ 'Command', 'Tried to Click', 'But it has CSS', 'Error', ]) expect(consoleProps['But it has CSS']).to.eq('pointer-events: none') }) cy.once('fail', onError) cy.get('#ptrNone').click({ timeout: 300 }) .then(() => { expect(onError).calledOnce }) }) it('should error with message about pointer-events and include inheritance', function () { const onError = cy.stub().callsFake((err) => { const { lastLog } = this expect(err.message).to.contain('has CSS `pointer-events: none`, inherited from this element:') expect(err.message).to.contain('<div id="ptrNone"') const consoleProps = lastLog.invoke('consoleProps') expect(_.keys(consoleProps)).deep.eq([ 'Command', 'Tried to Click', 'But it has CSS', 'Inherited From', 'Error', ]) expect(consoleProps['But it has CSS']).to.eq('pointer-events: none') expect(consoleProps['Inherited From']).to.eq(this.ptrNone.get(0)) }) cy.once('fail', onError) cy.get('#ptrNoneChild').click({ timeout: 300 }) .then(() => { expect(onError).calledOnce }) }) }) describe('actionability', () => { it('can click on inline elements that wrap lines', () => { cy.get('#overflow-link').find('.wrapped').click() }) // https://github.com/cypress-io/cypress/issues/7343 it('can click on inline elements that wrap lines where the first rect has no width', () => { cy.get('#overflow-link-width').click() }) it('can click on elements with `opacity: 0`', () => { cy.get('#opacity-0').click() }) it('can click on elements with parents that have `opacity: 0`', () => { cy.get('#opacity-0-parent').click() }) // readonly should only limit typing, not clicking it('can click on readonly inputs', () => { cy.get('#readonly-attr').click() }) it('can click on readonly submit inputs', () => { cy.get('#readonly-submit').click() }) it('can click on checkbox inputs', () => { cy.get(':checkbox:first').click() .then(($el) => { expect($el).to.be.checked }) }) it('can force click on disabled checkbox inputs', () => { cy.get(':checkbox:first') .then(($el) => { $el[0].disabled = true }) .click({ force: true }) .then(($el) => { expect($el).to.be.checked }) }) it('can click elements which are hidden until scrolled within parent container', () => { cy.get('#overflow-auto-container').contains('quux').click() }) it('does not scroll when being forced', () => { const scrolled = [] cy.on('scrolled', ($el, type) => { scrolled.push(type) }) cy .get('button:last').click({ force: true }) .then(() => { expect(scrolled).to.be.empty }) }) it('does not scroll when position sticky and display flex', () => { const scrolled = [] cy.on('scrolled', ($el, type) => { scrolled.push(type) }) cy.viewport(1000, 660) const $body = cy.$$('body') $body.children().remove() const $wrap = $('<div></div>') .attr('id', 'flex-wrap') .css({ display: 'flex', }) .prependTo($body) $(`<div><input type="text" data-cy="input" /> <br><br> <a href="#" data-cy="button"> Button </a></div>\ `) .attr('id', 'nav') .css({ position: 'sticky', top: 0, height: '100vh', width: '200px', background: '#f0f0f0', borderRight: '1px solid silver', padding: '20px', }) .appendTo($wrap) const $content = $('<div><h1>Hello</h1></div>') .attr('id', 'content') .css({ padding: '20px', flex: 1, }) .appendTo($wrap) $('<div>Long block 1</div>') .attr('id', 'long-block-1') .css({ height: '500px', border: '1px solid red', marginTop: '10px', width: '100%', }).appendTo($content) $('<div>Long block 2</div>') .attr('id', 'long-block-2') .css({ height: '500px', border: '1px solid red', marginTop: '10px', width: '100%', }).appendTo($content) $('<div>Long block 3</div>') .attr('id', 'long-block-3') .css({ height: '500px', border: '1px solid red', marginTop: '10px', width: '100%', }).appendTo($content) $('<div>Long block 4</div>') .attr('id', 'long-block-4') .css({ height: '500px', border: '1px solid red', marginTop: '10px', width: '100%', }).appendTo($content) $('<div>Long block 5</div>') .attr('id', 'long-block-5') .css({ height: '500px', border: '1px solid red', marginTop: '10px', width: '100%', }).appendTo($content) // make scrolling deterministic by ensuring we don't wait for coordsHistory // to build up cy.get('[data-cy=button]').click({ waitForAnimations: false }).then(() => { expect(scrolled).to.deep.eq(['element']) }) }) it('can specify scrollBehavior in options', () => { cy.get('input:first').then((el) => { cy.spy(el[0], 'scrollIntoView') }) cy.get('input:first').click({ scrollBehavior: 'bottom' }) cy.get('input:first').then((el) => { expect(el[0].scrollIntoView).calledWith({ block: 'end' }) }) }) it('does not scroll when scrollBehavior is false in options', () => { cy.get('input:first').then((el) => { cy.spy(el[0], 'scrollIntoView') }) cy.get('input:first').click({ scrollBehavior: false }) cy.get('input:first').then((el) => { expect(el[0].scrollIntoView).not.to.be.called }) }) it('does not scroll when scrollBehavior is false in config', { scrollBehavior: false }, () => { cy.get('input:first').then((el) => { cy.spy(el[0], 'scrollIntoView') }) cy.get('input:first').click() cy.get('input:first').then((el) => { expect(el[0].scrollIntoView).not.to.be.called }) }) it('calls scrollIntoView by default', () => { cy.get('input:first').then((el) => { cy.spy(el[0], 'scrollIntoView') }) cy.get('input:first').click() cy.get('input:first').then((el) => { expect(el[0].scrollIntoView).to.be.calledWith({ block: 'start' }) }) }) it('errors when scrollBehavior is false and element is out of view and is clicked', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('`cy.click()` failed because the center of this element is hidden from view') expect(cy.state('window').scrollY).to.equal(0) expect(cy.state('window').scrollX).to.equal(0) done() }) // make sure the input is out of view const $body = cy.$$('body') $('<div>Long block 5</div>') .css({ height: '500px', border: '1px solid red', marginTop: '10px', width: '100%', }).prependTo($body) cy.get('input:first').click({ scrollBehavior: false, timeout: 200 }) }) it('can force click on hidden elements', () => { cy.get('button:first').invoke('hide').click({ force: true }) }) it('can force click on disabled elements', () => { cy.get('input:first').invoke('prop', 'disabled', true).click({ force: true }) }) it('can forcibly click even when being covered by another element', () => { const $btn = $('<button>button covered</button>').attr('id', 'button-covered-in-span').prependTo(cy.$$('body')) $('<span>span on button</span>').css({ position: 'absolute', left: $btn.offset().left, top: $btn.offset().top, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).prependTo(cy.$$('body')) const scrolled = [] let retried = false let clicked = false cy.on('scrolled', ($el, type) => { scrolled.push(type) }) cy.on('command:retry', () => { retried = true }) $btn.on('click', () => { clicked = true }) cy.get('#button-covered-in-span').click({ force: true }).then(() => { expect(scrolled).to.be.empty expect(retried).to.be.false expect(clicked).to.be.true }) }) it('can forcibly click when being covered by element with `opacity: 0`', () => { const $btn = $('<button>button covered</button>').attr('id', 'button-covered-in-span').prependTo(cy.$$('body')) $('<span>span on button</span>').css({ opacity: 0, position: 'absolute', left: $btn.offset().left, top: $btn.offset().top, padding: 5, display: 'inline-block' }).prependTo(cy.$$('body')) let retried = false let clicked = false cy.on('command:retry', () => { retried = true }) $btn.on('click', () => { clicked = true }) cy.get('#button-covered-in-span').click({ force: true }).then(() => { expect(retried).to.be.false expect(clicked).to.be.true }) }) it('eventually clicks when covered up', () => { const $btn = $('<button>button covered</button>') .attr('id', 'button-covered-in-span') .prependTo(cy.$$('body')) const $span = $('<span>span on button</span>').css({ position: 'absolute', left: $btn.offset().left, top: $btn.offset().top, padding: 5, display: 'inline-block', backgroundColor: 'yellow', }).prependTo(cy.$$('body')) const scrolled = [] let retried = false cy.on('scrolled', ($el, type) => { scrolled.push(type) }) cy.on('command:retry', _.after(3, () => { $span.hide() retried = true })) cy.get('#button-covered-in-span').click().then(() => { expect(retried).to.be.true // - element scrollIntoView // - element scrollIntoView (retry animation coords) // - element scrollIntoView (retry covered) // - element scrollIntoView (retry covered) // - window expect(scrolled).to.deep.eq(['element', 'element', 'element', 'element']) }) }) it('scrolls the window past a fixed position element when being covered', () => { const spy = cy.spy().as('mousedown') $('<button>button covered</button>') .css({ height: 24, width: 110, }) .attr('id', 'button-covered-in-nav') .css({ width: 120, height: 20, }) .appendTo(cy.$$('#fixed-nav-test')) .mousedown(spy) $('<nav>nav on button</nav>').css({ position: 'fixed', left: 0, top: 0, padding: 20, backgroundColor: 'yellow', zIndex: 1, }).prependTo(cy.$$('body')) const scrolled = [] cy.on('scrolled', ($el, type) => { scrolled.push(type) }) // - element scrollIntoView // - element scrollIntoView (retry animation coords) // - window cy .get('#button-covered-in-nav').click() .then(($btn) => { const rect = $btn.get(0).getBoundingClientRect() const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) // this button should be 120 pixels wide expect(rect.width).to.eq(120) const obj = spy.firstCall.args[0] // clientX + clientY are relative to the document expect(scrolled).to.deep.eq(['element', 'element', 'window']) expect(obj).property('clientX').closeTo(fromElViewport.leftCenter, 1) expect(obj).property('clientY').closeTo(fromElViewport.topCenter, 1) }) }) it('scrolls the window past two fixed positioned elements when being covered', () => { $('<button>button covered</button>') .attr('id', 'button-covered-in-nav') .appendTo(cy.$$('#fixed-nav-test')) $('<nav>nav on button</nav>').css({ position: 'fixed', left: 0, top: 0, padding: 20, backgroundColor: 'yellow', zIndex: 1, }).prependTo(cy.$$('body')) $('<nav>nav2 on button</nav>').css({ position: 'fixed', left: 0, top: 40, padding: 20, backgroundColor: 'red', zIndex: 1, }).prependTo(cy.$$('body')) const scrolled = [] cy.on('scrolled', ($el, type) => { scrolled.push(type) }) // - element scrollIntoView // - element scrollIntoView (retry animation coords) // - window (nav1) // - window (nav2) cy.get('#button-covered-in-nav').click().then(() => { expect(scrolled).to.deep.eq(['element', 'element', 'window', 'window']) }) }) it('scrolls a container past a fixed position element when being covered', () => { cy.viewport(600, 450) const $body = cy.$$('body') // we must remove all of our children to // prevent the window from scrolling $body.children().remove() // this tests that our container properly scrolls! const $container = $('<div></div>') .attr('id', 'scrollable-container') .css({ position: 'relative', width: 300, height: 200, marginBottom: 100, backgroundColor: 'green', overflow: 'auto', }) .prependTo($body) $('<button>button covered</button>') .attr('id', 'button-covered-in-nav') .css({ marginTop: 500, // marginLeft: 500 marginBottom: 500, }) .appendTo($container) $('<nav>nav on button</nav>') .css({ position: 'fixed', left: 0, top: 0, padding: 20, backgroundColor: 'yellow', zIndex: 1, }) .prependTo($container) const scrolled = [] cy.on('scrolled', ($el, type) => { scrolled.push(type) }) // - element scrollIntoView // - element scrollIntoView (retry animation coords) // - window // - container cy.get('#button-covered-in-nav').click().then(() => { expect(scrolled).to.deep.eq(['element', 'element', 'window', 'container']) }) }) it('waits until element becomes visible', () => { const $btn = cy.$$('#button').hide() let retried = false cy.on('command:retry', _.after(3, () => { $btn.show() retried = true })) cy.get('#button').click().then(() => { expect(retried).to.be.true }) }) it('waits until element is no longer disabled', () => { const $btn = cy.$$('#button').prop('disabled', true) let retried = false let clicks = 0 $btn.on('click', () => { clicks += 1 }) cy.on('command:retry', _.after(3, () => { $btn.prop('disabled', false) retried = true })) cy.get('#button').click().then(() => { expect(clicks).to.eq(1) expect(retried).to.be.true }) }) it('waits until element stops animating', () => { let retries = 0 cy.on('command:retry', () => { retries += 1 }) cy.stub(cy, 'ensureElementIsNotAnimating') .throws(new Error('animating!')) .onThirdCall().returns() cy.get('button:first').click().then(() => { // - retry animation coords // - retry animation // - retry animation expect(retries).to.eq(3) expect(cy.ensureElementIsNotAnimating).to.be.calledThrice }) }) it('does not throw when waiting for animations is disabled', { waitForAnimations: false, }, () => { cy.stub(cy, 'ensureElementIsNotAnimating').throws(new Error('animating!')) cy.get('button:first').click().then(() => { expect(cy.ensureElementIsNotAnimating).not.to.be.called }) }) it('does not throw when turning off waitForAnimations in options', () => { cy.stub(cy, 'ensureElementIsNotAnimating').throws(new Error('animating!')) cy.get('button:first').click({ waitForAnimations: false }).then(() => { expect(cy.ensureElementIsNotAnimating).not.to.be.called }) }) it('passes options.animationDistanceThreshold to cy.ensureElementIsNotAnimating', () => { const $btn = cy.$$('button:first') cy.spy(cy, 'ensureElementIsNotAnimating') cy.get('button:first').click({ animationDistanceThreshold: 1000 }).then(() => { const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) const { args } = cy.ensureElementIsNotAnimating.firstCall expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) expect(args[2]).to.eq(1000) }) }) it('passes config.animationDistanceThreshold to cy.ensureElementIsNotAnimating', () => { const animationDistanceThreshold = Cypress.config('animationDistanceThreshold') const $btn = cy.$$('button:first') cy.spy(cy, 'ensureElementIsNotAnimating') cy.get('button:first').click().then(() => { const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) const { args } = cy.ensureElementIsNotAnimating.firstCall expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) expect(args[2]).to.eq(animationDistanceThreshold) }) }) describe('scroll-behavior', () => { afterEach(() => { cy.get('html').invoke('css', 'scrollBehavior', 'inherit') }) // https://github.com/cypress-io/cypress/issues/3200 it('can scroll to and click elements in html with scroll-behavior: smooth', () => { cy.get('html').invoke('css', 'scrollBehavior', 'smooth') cy.get('#table tr:first').click() }) // https://github.com/cypress-io/cypress/issues/3200 it('can scroll to and click elements in ancestor element with scroll-behavior: smooth', () => { cy.get('#dom').invoke('css', 'scrollBehavior', 'smooth') cy.get('#table tr:first').click() }) }) }) describe('assertion verification', () => { beforeEach(function () { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'assert') { this.lastLog = log } }) null }) it('eventually passes the assertion', () => { cy.$$('button:first').click(function () { _.delay(() => { $(this).addClass('clicked') } , 50) return false }) cy.get('button:first').click().should('have.class', 'clicked').then(function () { const { lastLog } = this expect(lastLog.get('name')).to.eq('assert') expect(lastLog.get('state')).to.eq('passed') expect(lastLog.get('ended')).to.be.true }) }) it('eventually passes the assertion on multiple buttons', () => { cy.$$('button').click(function () { _.delay(() => { $(this).addClass('clicked') } , 50) return false }) cy .get('button') .invoke('slice', 0, 2) .click({ multiple: true }) .should('have.class', 'clicked') }) }) describe('position argument', () => { it('can click center by default', (done) => { const $btn = $('<button>button covered</button>').attr('id', 'button-covered-in-span').css({ height: 100, width: 100 }).prependTo(cy.$$('body')) const span = $('<span>span</span>').css({ position: 'absolute', left: $btn.offset().left + 30, top: $btn.offset().top + 40, padding: 5, display: 'inline-block', backgroundCol