@revoloo/cypress6
Version:
Cypress.io end to end testing tool
1,682 lines (1,293 loc) • 148 kB
JavaScript
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