From 9cd40ad840258fbac88a60118d68b43f3b93b080 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 18 Jul 2019 11:45:48 -0400 Subject: [PATCH 01/36] cleanup type_spec, allow unused-vars for args --- .eslintrc | 9 +- .../integration/commands/actions/type_spec.js | 250 +++++++++--------- 2 files changed, 130 insertions(+), 129 deletions(-) diff --git a/.eslintrc b/.eslintrc index 22f00333db5e..45e7bd5a85dc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,5 +5,12 @@ "extends": [ "plugin:@cypress/dev/general" ], - "rules": {} + "rules": { + "no-unused-vars": [ + "error", + { + "args": "none" + } + ] + } } diff --git a/packages/driver/test/cypress/integration/commands/actions/type_spec.js b/packages/driver/test/cypress/integration/commands/actions/type_spec.js index ab9efea49736..76a6f615dc5d 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -35,6 +35,10 @@ describe('src/cy/commands/actions/type', () => { // so this test is browser version agnostic const newLines = el.innerText + // disregard the last new line, and divide by 2... + // this tells us how many multiples of new lines + // the browser inserts for new lines other than + // the last new line this.multiplierNumNewLines = (newLines.length - 1) / 2 }) }) @@ -42,7 +46,7 @@ describe('src/cy/commands/actions/type', () => { beforeEach(function () { const doc = cy.state('document') - return $(doc.body).empty().html(this.body) + $(doc.body).empty().html(this.body) }) context('#type', () => { @@ -133,7 +137,7 @@ describe('src/cy/commands/actions/type', () => { }) it('delays 50ms before resolving', () => { - cy.$$(':text:first').on('change', () => { + cy.$$(':text:first').on('change', (e) => { cy.spy(Promise, 'delay') }) @@ -330,7 +334,7 @@ describe('src/cy/commands/actions/type', () => { }) describe('input types where no extra formatting required', () => { - return _.each([ + _.each([ 'password', 'email', 'number', @@ -913,25 +917,25 @@ describe('src/cy/commands/actions/type', () => { it('overwrites text when selectAll in click handler', () => { cy.$$('#input-without-value').val('0').click(function () { - return $(this).select() + $(this).select() }) }) it('overwrites text when selectAll in mouseup handler', () => { cy.$$('#input-without-value').val('0').mouseup(function () { - return $(this).select() + $(this).select() }) }) it('overwrites text when selectAll in mouseup handler', () => { cy.$$('#input-without-value').val('0').mouseup(function () { - return $(this).select() + $(this).select() }) }) it('responsive to keydown handler', () => { cy.$$('#input-without-value').val('1234').keydown(function () { - return $(this).get(0).setSelectionRange(0, 0) + $(this).get(0).setSelectionRange(0, 0) }) cy.get('#input-without-value').type('56').then(($input) => { @@ -941,7 +945,7 @@ describe('src/cy/commands/actions/type', () => { it('responsive to keyup handler', () => { cy.$$('#input-without-value').val('1234').keyup(function () { - return $(this).get(0).setSelectionRange(0, 0) + $(this).get(0).setSelectionRange(0, 0) }) cy.get('#input-without-value').type('56').then(($input) => { @@ -951,7 +955,7 @@ describe('src/cy/commands/actions/type', () => { it('responsive to input handler', () => { cy.$$('#input-without-value').val('1234').keyup(function () { - return $(this).get(0).setSelectionRange(0, 0) + $(this).get(0).setSelectionRange(0, 0) }) cy.get('#input-without-value').type('56').then(($input) => { @@ -961,7 +965,7 @@ describe('src/cy/commands/actions/type', () => { it('responsive to change handler', () => { cy.$$('#input-without-value').val('1234').change(function () { - return $(this).get(0).setSelectionRange(0, 0) + $(this).get(0).setSelectionRange(0, 0) }) // no change event should be fired @@ -981,7 +985,7 @@ describe('src/cy/commands/actions/type', () => { const val = $input.val() // setting value updates cursor to the end of input - return $input.val(`${val + key}-`) + $input.val(`${val + key}-`) }) cy.get('#input-without-value').type('foo').then(($input) => { @@ -994,10 +998,10 @@ describe('src/cy/commands/actions/type', () => { const $input = $(e.target) - return _.defer(() => { + _.defer(() => { const val = $input.val() - return $input.val(`${val}-`) + $input.val(`${val}-`) }) }) @@ -1065,7 +1069,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input when textInput is preventedDefault', (done) => { - cy.$$('#input-without-value').get(0).addEventListener('input', () => { + cy.$$('#input-without-value').get(0).addEventListener('input', (e) => { done('should not have received input event') }) @@ -1240,11 +1244,11 @@ describe('src/cy/commands/actions/type', () => { it('overwrites text when input has selected range of text in click handler', () => { // e.preventDefault() cy.$$('#input-with-value').mouseup((e) => { - return e.target.setSelectionRange(1, 1) + e.target.setSelectionRange(1, 1) }) const select = (e) => { - return e.target.select() + e.target.select() } cy @@ -1451,7 +1455,7 @@ describe('src/cy/commands/actions/type', () => { cy.get('#generic-iframe') .then(($iframe) => { - return $iframe.load(() => { + $iframe.load(() => { loaded = true }) }).scrollIntoView() @@ -1479,7 +1483,7 @@ describe('src/cy/commands/actions/type', () => { }) }) - // NOTE: fix this with 4.0 updates + // TODO: fix this with 4.0 updates describe.skip('element reference loss', () => { it('follows the focus of the cursor', () => { let charCount = 0 @@ -1489,7 +1493,7 @@ describe('src/cy/commands/actions/type', () => { cy.$$('input').eq(1).focus() } - return charCount++ + charCount++ }) cy.get('input:first').type('foobar').then(() => { @@ -1549,7 +1553,7 @@ describe('src/cy/commands/actions/type', () => { }) it('fires input event', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done() }) @@ -1587,7 +1591,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -1597,7 +1601,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done('input should not have fired') }) @@ -1632,7 +1636,7 @@ describe('src/cy/commands/actions/type', () => { // select the 'ar' characters cy .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.get(0).setSelectionRange(1, 3) + $input.get(0).setSelectionRange(1, 3) }).get(':text:first').type('{backspace}').then(($input) => { expect($input).to.have.value('b') }) @@ -1655,7 +1659,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -1690,7 +1694,7 @@ describe('src/cy/commands/actions/type', () => { // select the 'ar' characters cy .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.get(0).setSelectionRange(1, 3) + $input.get(0).setSelectionRange(1, 3) }).get(':text:first').type('{del}').then(($input) => { expect($input).to.have.value('b') }) @@ -1713,7 +1717,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -1723,19 +1727,19 @@ describe('src/cy/commands/actions/type', () => { }) it('does fire input event when value changes', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done() }) // select the 'a' characters cy .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.get(0).setSelectionRange(0, 1) + $input.get(0).setSelectionRange(0, 1) }).get(':text:first').type('{del}') }) it('does not fire input event when value does not change', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done('should not have fired input') }) @@ -1776,7 +1780,7 @@ describe('src/cy/commands/actions/type', () => { // select the 'a' character cy .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.get(0).setSelectionRange(1, 2) + $input.get(0).setSelectionRange(1, 2) }).get(':text:first').type('{leftarrow}n').then(($input) => { expect($input).to.have.value('bnar') }) @@ -1786,7 +1790,7 @@ describe('src/cy/commands/actions/type', () => { // select the 'a' character cy .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.get(0).setSelectionRange(0, 1) + $input.get(0).setSelectionRange(0, 1) }).get(':text:first').type('{leftarrow}n').then(($input) => { expect($input).to.have.value('nbar') }) @@ -1805,13 +1809,13 @@ describe('src/cy/commands/actions/type', () => { done() }) - cy.get(':text:first').invoke('val', 'ab').type('{leftarrow}').then(() => { + cy.get(':text:first').invoke('val', 'ab').type('{leftarrow}').then(($input) => { done() }) }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -1821,7 +1825,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done('input should not have fired') }) @@ -1849,7 +1853,7 @@ describe('src/cy/commands/actions/type', () => { it('can move the cursor from the beginning to beginning + 1', () => { // select the beginning cy.get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.get(0).setSelectionRange(0, 0) + $input.get(0).setSelectionRange(0, 0) }).get(':text:first').type('{rightarrow}n').then(($input) => { expect($input).to.have.value('bnar') }) @@ -1865,7 +1869,7 @@ describe('src/cy/commands/actions/type', () => { // select the 'a' character cy .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.get(0).setSelectionRange(1, 2) + $input.get(0).setSelectionRange(1, 2) }).get(':text:first').type('{rightarrow}n').then(($input) => { expect($input).to.have.value('banr') }) @@ -1893,13 +1897,13 @@ describe('src/cy/commands/actions/type', () => { done() }) - cy.get(':text:first').invoke('val', 'ab').type('{rightarrow}').then(() => { + cy.get(':text:first').invoke('val', 'ab').type('{rightarrow}').then(($input) => { done() }) }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -1909,7 +1913,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done('input should not have fired') }) @@ -1947,13 +1951,13 @@ describe('src/cy/commands/actions/type', () => { done() }) - cy.get('#comments').type('{home}').then(() => { + cy.get('#comments').type('{home}').then(($input) => { done() }) }) it('does not fire textInput event', (done) => { - cy.$$('#comments').on('textInput', () => { + cy.$$('#comments').on('textInput', (e) => { done('textInput should not have fired') }) @@ -1963,7 +1967,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$('#comments').on('input', () => { + cy.$$('#comments').on('input', (e) => { done('input should not have fired') }) @@ -2018,13 +2022,13 @@ describe('src/cy/commands/actions/type', () => { done() }) - cy.get('#comments').type('{end}').then(() => { + cy.get('#comments').type('{end}').then(($input) => { done() }) }) it('does not fire textInput event', (done) => { - cy.$$('#comments').on('textInput', () => { + cy.$$('#comments').on('textInput', (e) => { done('textInput should not have fired') }) @@ -2034,7 +2038,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$('#comments').on('input', () => { + cy.$$('#comments').on('input', (e) => { done('input should not have fired') }) @@ -2093,13 +2097,13 @@ describe('src/cy/commands/actions/type', () => { done() }) - cy.get('#comments').type('{uparrow}').then(() => { + cy.get('#comments').type('{uparrow}').then(($input) => { done() }) }) it('does not fire textInput event', (done) => { - cy.$$('#comments').on('textInput', () => { + cy.$$('#comments').on('textInput', (e) => { done('textInput should not have fired') }) @@ -2109,7 +2113,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$('#comments').on('input', () => { + cy.$$('#comments').on('input', (e) => { done('input should not have fired') }) @@ -2144,7 +2148,7 @@ describe('src/cy/commands/actions/type', () => { cy.document().then((doc) => { ce.focus() - return doc.getSelection().selectAllChildren(line) + doc.getSelection().selectAllChildren(line) }) cy.get('[contenteditable]:first') @@ -2186,13 +2190,13 @@ describe('src/cy/commands/actions/type', () => { done() }) - cy.get('#comments').type('{downarrow}').then(() => { + cy.get('#comments').type('{downarrow}').then(($input) => { done() }) }) it('does not fire textInput event', (done) => { - cy.$$('#comments').on('textInput', () => { + cy.$$('#comments').on('textInput', (e) => { done('textInput should not have fired') }) @@ -2202,7 +2206,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$('#comments').on('input', () => { + cy.$$('#comments').on('input', (e) => { done('input should not have fired') }) @@ -2239,7 +2243,7 @@ describe('src/cy/commands/actions/type', () => { cy.document().then((doc) => { ce.focus() - return doc.getSelection().selectAllChildren(line) + doc.getSelection().selectAllChildren(line) }) cy.get('[contenteditable]:first') @@ -2273,7 +2277,7 @@ describe('src/cy/commands/actions/type', () => { context('{enter}', () => { it('sets which and keyCode to 13 and prevents EOL insertion', (done) => { - cy.$$('#input-types textarea').on('keypress', _.after(2, () => { + cy.$$('#input-types textarea').on('keypress', _.after(2, (e) => { done('should not have received keypress event') })) @@ -2310,7 +2314,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -2320,7 +2324,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done('input should not have fired') }) @@ -2376,7 +2380,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -2386,7 +2390,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done('input should not have fired') }) @@ -2428,7 +2432,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -2438,7 +2442,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done('input should not have fired') }) @@ -2480,7 +2484,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire textInput event', (done) => { - cy.$$(':text:first').on('textInput', () => { + cy.$$(':text:first').on('textInput', (e) => { done('textInput should not have fired') }) @@ -2490,7 +2494,7 @@ describe('src/cy/commands/actions/type', () => { }) it('does not fire input event', (done) => { - cy.$$(':text:first').on('input', () => { + cy.$$(':text:first').on('input', (e) => { done('input should not have fired') }) @@ -2750,37 +2754,26 @@ describe('src/cy/commands/actions/type', () => { }) afterEach(function () { - return this.$input.off('keydown') + this.$input.off('keydown') }) - it('sends keydown event for new modifiers', function (done) { - let event = null + it('sends keydown event for new modifiers', function () { + const spy = cy.spy().as('keydown') - this.$input.on('keydown', (e) => { - event = e - }) + this.$input.on('keydown', spy) cy.get('input:text:first').type('{shift}').then(() => { - expect(event.shiftKey).to.be.true - expect(event.which).to.equal(16) - - done() + expect(spy).to.be.calledWithMatch({ which: 16 }) }) }) - it('does not send keydown event for already activated modifiers', function (done) { - let triggered = false + it('does not send keydown event for already activated modifiers', function () { + const spy = cy.spy().as('keydown') - this.$input.on('keydown', (e) => { - if ((e.which === 18) || (e.which === 17)) { - triggered = true - } - }) + this.$input.on('keydown', spy) cy.get('input:text:first').type('{cmd}{alt}').then(() => { - expect(triggered).to.be.false - - done() + expect(spy).to.not.be.called }) }) }) @@ -3005,7 +2998,7 @@ describe('src/cy/commands/actions/type', () => { cy.get(':text:first').invoke('val', 'foo').clear().type('o').click().then(($el) => { expect(changed).to.eq(0) - return $el + $el }).blur() .then(() => { expect(changed).to.eq(1) @@ -3244,10 +3237,10 @@ describe('src/cy/commands/actions/type', () => { // https://github.com/cypress-io/cypress/issues/3001 describe('skip actionability if already focused', () => { it('inside input', () => { - cy.$$('body').append(Cypress.$('\ + cy.$$('body').append(Cypress.$(/*html*/`\
\ \ -')) +`)) cy.$$('#foo').focus() @@ -3256,10 +3249,10 @@ describe('src/cy/commands/actions/type', () => { it('inside textarea', () => { - cy.$$('body').append(Cypress.$('\ + cy.$$('body').append(Cypress.$(/*html*/`\
\ \ -')) +`)) cy.$$('#foo').focus() @@ -3268,12 +3261,12 @@ describe('src/cy/commands/actions/type', () => { it('inside contenteditable', () => { - cy.$$('body').append(Cypress.$('\ + cy.$$('body').append(Cypress.$(/*html*/`\
\
\
foo
bar
baz
\
\ -')) +`)) const win = cy.state('window') const doc = window.document @@ -3297,7 +3290,7 @@ describe('src/cy/commands/actions/type', () => { it('can arrow from maxlength', () => { cy.get('input:first').invoke('attr', 'maxlength', '5').type('foobar{leftarrow}') - cy.window().then(() => { + cy.window().then((win) => { expect($selection.getSelectionBounds(Cypress.$('input:first').get(0))) .to.deep.eq({ start: 4, end: 4 }) }) @@ -3306,7 +3299,7 @@ describe('src/cy/commands/actions/type', () => { it('won\'t arrowright past length', () => { cy.get('input:first').type('foo{rightarrow}{rightarrow}{rightarrow}bar{rightarrow}') - cy.window().then(() => { + cy.window().then((win) => { expect($selection.getSelectionBounds(Cypress.$('input:first').get(0))) .to.deep.eq({ start: 6, end: 6 }) }) @@ -3315,7 +3308,7 @@ describe('src/cy/commands/actions/type', () => { it('won\'t arrowleft before word', () => { cy.get('input:first').type(`oo{leftarrow}{leftarrow}{leftarrow}f${'{leftarrow}'.repeat(5)}`) - cy.window().then(() => { + cy.window().then((win) => { expect($selection.getSelectionBounds(Cypress.$('input:first').get(0))) .to.deep.eq({ start: 0, end: 0 }) }) @@ -3324,7 +3317,7 @@ describe('src/cy/commands/actions/type', () => { it('leaves caret at the end of contenteditable', () => { cy.get('[contenteditable]:first').type('foobar') - cy.window().then(() => { + cy.window().then((win) => { expect($selection.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) .to.deep.eq({ start: 6, end: 6 }) }) @@ -3337,7 +3330,7 @@ describe('src/cy/commands/actions/type', () => { el.innerHTML = 'foo' cy.get('[contenteditable]:first').type('bar') - cy.window().then(() => { + cy.window().then((win) => { expect($selection.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) .to.deep.eq({ start: 6, end: 6 }) }) @@ -3346,7 +3339,7 @@ describe('src/cy/commands/actions/type', () => { it('can move the caret left on contenteditable', () => { cy.get('[contenteditable]:first').type('foo{leftarrow}{leftarrow}') - cy.window().then(() => { + cy.window().then((win) => { expect($selection.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) .to.deep.eq({ start: 1, end: 1 }) }) @@ -3361,7 +3354,7 @@ describe('src/cy/commands/actions/type', () => { it('leaves caret at the end of input', () => { cy.get(':text:first').type('foobar') - cy.window().then(() => { + cy.window().then((win) => { expect($selection.getSelectionBounds(Cypress.$(':text:first').get(0))) .to.deep.eq({ start: 6, end: 6 }) }) @@ -3370,7 +3363,7 @@ describe('src/cy/commands/actions/type', () => { it('leaves caret at the end of textarea', () => { cy.get('#comments').type('foobar') - cy.window().then(() => { + cy.window().then((win) => { expect($selection.getSelectionBounds(Cypress.$('#comments').get(0))) .to.deep.eq({ start: 6, end: 6 }) }) @@ -3436,7 +3429,7 @@ describe('src/cy/commands/actions/type', () => { it('enter and \\n should act the same for [contenteditable]', () => { // non breaking white space const cleanseText = (text) => { - return text.split('\u00a0').join(' ') + text.split('\u00a0').join(' ') } const expectMatchInnerText = ($el, innerText) => { @@ -3500,7 +3493,7 @@ describe('src/cy/commands/actions/type', () => { this.$forms.find('#single-input').submit((e) => { e.preventDefault() - return events.push('submit') + events.push('submit') }) cy.on('log:added', (attrs, log) => { @@ -3511,7 +3504,7 @@ describe('src/cy/commands/actions/type', () => { return events.push(`${log.get('name')}:log:${state}`) }) - return events.push(`${log.get('name')}:log:${state}`) + events.push(`${log.get('name')}:log:${state}`) } }) @@ -3531,6 +3524,7 @@ describe('src/cy/commands/actions/type', () => { this.$forms.find('#single-input').submit((e) => { e.preventDefault() + submits += 1 }) @@ -3879,7 +3873,7 @@ describe('src/cy/commands/actions/type', () => { context('disabled default button', () => { beforeEach(function () { - return this.$forms.find('#multiple-inputs-and-multiple-submits').find('button').prop('disabled', true) + this.$forms.find('#multiple-inputs-and-multiple-submits').find('button').prop('disabled', true) }) it('will not receive click event', function (done) { @@ -3912,13 +3906,13 @@ describe('src/cy/commands/actions/type', () => { } }) - return null + null }) it('eventually passes the assertion', () => { cy.$$('input:first').keyup(function () { - return _.delay(() => { - return $(this).addClass('typed') + _.delay(() => { + $(this).addClass('typed') } , 100) }) @@ -3940,7 +3934,7 @@ describe('src/cy/commands/actions/type', () => { this.lastLog = log }) - return null + null }) it('passes in $el', () => { @@ -3979,7 +3973,7 @@ describe('src/cy/commands/actions/type', () => { } cy - .get('#comments').type('foobarbaz').then(() => { + .get('#comments').type('foobarbaz').then(($txt) => { expectToHaveValueAndCoords() }).get('#comments').clear().type('onetwothree').then(() => { expectToHaveValueAndCoords() @@ -3998,7 +3992,7 @@ describe('src/cy/commands/actions/type', () => { } cy - .get('#comments').focus().type('foobarbaz').then(() => { + .get('#comments').focus().type('foobarbaz').then(($txt) => { expectToHaveValueAndNoCoords() }).get('#comments').clear().type('onetwothree').then(() => { expectToHaveValueAndNoCoords() @@ -4012,7 +4006,7 @@ describe('src/cy/commands/actions/type', () => { cy.on('log:added', (attrs, log) => { logs.push(log) if (log.get('name') === 'type') { - return types.push(log) + types.push(log) } }) @@ -4167,10 +4161,10 @@ describe('src/cy/commands/actions/type', () => { cy.on('log:added', (attrs, log) => { this.lastLog = log - return this.logs.push(log) + this.logs.push(log) }) - return null + null }) it('throws when not a dom subject', (done) => { @@ -4184,10 +4178,10 @@ describe('src/cy/commands/actions/type', () => { it('throws when subject is not in the document', (done) => { let typed = 0 - const input = cy.$$('input:first').keypress(() => { + const input = cy.$$('input:first').keypress((e) => { typed += 1 - return input.remove() + input.remove() }) cy.on('fail', (err) => { @@ -4236,7 +4230,7 @@ describe('src/cy/commands/actions/type', () => { cy.get('textarea,:text').then(function ($inputs) { this.num = $inputs.length - return $inputs + $inputs }).type('foo') cy.on('fail', (err) => { @@ -4746,7 +4740,7 @@ describe('src/cy/commands/actions/type', () => { 'week', ] - return inputTypes.forEach((type) => { + inputTypes.forEach((type) => { it(type, () => { cy.get(`#${type}-with-value`).clear().then(($input) => { expect($input.val()).to.equal('') @@ -4763,13 +4757,13 @@ describe('src/cy/commands/actions/type', () => { } }) - return null + null }) it('eventually passes the assertion', () => { cy.$$('input:first').keyup(function () { - return _.delay(() => { - return $(this).addClass('cleared') + _.delay(() => { + $(this).addClass('cleared') } , 100) }) @@ -4786,8 +4780,8 @@ describe('src/cy/commands/actions/type', () => { it('eventually passes the assertion on multiple inputs', () => { cy.$$('input').keyup(function () { - return _.delay(() => { - return $(this).addClass('cleared') + _.delay(() => { + $(this).addClass('cleared') } , 100) }) @@ -4805,14 +4799,14 @@ describe('src/cy/commands/actions/type', () => { cy.on('log:added', (attrs, log) => { this.lastLog = log - return this.logs.push(log) + this.logs.push(log) }) - return null + null }) it('throws when not a dom subject', (done) => { - cy.on('fail', () => { + cy.on('fail', (err) => { done() }) @@ -4822,10 +4816,10 @@ describe('src/cy/commands/actions/type', () => { it('throws when subject is not in the document', (done) => { let cleared = 0 - const input = cy.$$('input:first').val('123').keydown(() => { + const input = cy.$$('input:first').val('123').keydown((e) => { cleared += 1 - return input.remove() + input.remove() }) cy.on('fail', (err) => { @@ -4999,7 +4993,7 @@ describe('src/cy/commands/actions/type', () => { this.lastLog = log }) - return null + null }) it('logs immediately before resolving', () => { @@ -5026,12 +5020,12 @@ describe('src/cy/commands/actions/type', () => { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'clear') { - return logs.push(log) + logs.push(log) } }) cy.get('input').invoke('slice', 0, 2).clear().then(() => { - return _.each(logs, (log) => { + _.each(logs, (log) => { expect(log.get('state')).to.eq('passed') expect(log.get('ended')).to.be.true @@ -5040,7 +5034,7 @@ describe('src/cy/commands/actions/type', () => { }) it('snapshots after clicking', () => { - cy.get('input:first').clear().then(function () { + cy.get('input:first').clear().then(function ($input) { const { lastLog } = this expect(lastLog.get('snapshots').length).to.eq(1) From 3671278809e73deac7c838faaf152d8bda2165b1 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 18 Jul 2019 11:48:31 -0400 Subject: [PATCH 02/36] fix missing return --- .../test/cypress/integration/commands/actions/type_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/test/cypress/integration/commands/actions/type_spec.js b/packages/driver/test/cypress/integration/commands/actions/type_spec.js index 76a6f615dc5d..a3f4d05101c8 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -2998,7 +2998,7 @@ describe('src/cy/commands/actions/type', () => { cy.get(':text:first').invoke('val', 'foo').clear().type('o').click().then(($el) => { expect(changed).to.eq(0) - $el + return $el }).blur() .then(() => { expect(changed).to.eq(1) From e38ffa58c92b6a8a952b3a51447a76c33fb1de0d Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 16 Jul 2019 16:42:10 -0400 Subject: [PATCH 03/36] rename mouse/keyboard --- packages/driver/src/{cypress => cy}/keyboard.js | 0 packages/driver/src/{cypress => cy}/mouse.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/driver/src/{cypress => cy}/keyboard.js (100%) rename packages/driver/src/{cypress => cy}/mouse.js (100%) diff --git a/packages/driver/src/cypress/keyboard.js b/packages/driver/src/cy/keyboard.js similarity index 100% rename from packages/driver/src/cypress/keyboard.js rename to packages/driver/src/cy/keyboard.js diff --git a/packages/driver/src/cypress/mouse.js b/packages/driver/src/cy/mouse.js similarity index 100% rename from packages/driver/src/cypress/mouse.js rename to packages/driver/src/cy/mouse.js From e31ae9c822764ee76979cf19d8e0a6e4d0908280 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 16 Jul 2019 16:47:41 -0400 Subject: [PATCH 04/36] apply changes on this branch with rename --- .eslintrc | 4 +- packages/driver/src/cy/actionability.coffee | 5 +- .../driver/src/cy/commands/actions/click.js | 562 +++-- .../src/cy/commands/actions/trigger.coffee | 10 +- .../src/cy/commands/actions/type.coffee | 25 +- packages/driver/src/cy/keyboard.js | 1201 ++++----- packages/driver/src/cy/mouse.js | 721 +++++- packages/driver/src/cypress.coffee | 2 +- packages/driver/src/cypress/cy.coffee | 9 + .../driver/src/cypress/error_messages.coffee | 4 +- packages/driver/src/dom/coordinates.js | 61 +- packages/driver/src/dom/elements.js | 97 +- packages/driver/src/dom/window.js | 4 + .../driver/test/cypress/fixtures/dom.html | 13 +- .../test/cypress/fixtures/issue-2956.html | 134 + .../commands/actions/click_spec.js | 2151 +++++++++++++++-- 16 files changed, 3842 insertions(+), 1161 deletions(-) create mode 100644 packages/driver/test/cypress/fixtures/issue-2956.html diff --git a/.eslintrc b/.eslintrc index 45e7bd5a85dc..aabf1ee70b7c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,9 +8,7 @@ "rules": { "no-unused-vars": [ "error", - { - "args": "none" - } + {} ] } } diff --git a/packages/driver/src/cy/actionability.coffee b/packages/driver/src/cy/actionability.coffee index b8fd3fcc4948..e54099d97973 100644 --- a/packages/driver/src/cy/actionability.coffee +++ b/packages/driver/src/cy/actionability.coffee @@ -267,7 +267,10 @@ verify = (cy, $el, options, callbacks) -> $elAtCoords = ensureElIsNotCovered(cy, win, $el, coords.fromViewport, options, _log, onScroll) ## pass our final object into onReady - return onReady($elAtCoords ? $el, coords) + finalEl = $elAtCoords ? $el + finalCoords = getCoordinatesForEl(cy, $el, options) + + return onReady(finalEl, finalCoords) ## we cannot enforce async promises here because if our ## element passes every single check, we MUST fire the event diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index 814519567f71..bf3d1ca6395d 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -1,21 +1,20 @@ const _ = require('lodash') +const $ = require('jquery') const Promise = require('bluebird') - -const $Mouse = require('../../../cypress/mouse') - const $dom = require('../../../dom') const $utils = require('../../../cypress/utils') -const $elements = require('../../../dom/elements') -const $selection = require('../../../dom/selection') const $actionability = require('../../actionability') module.exports = (Commands, Cypress, cy, state, config) => { + const mouse = cy.internal.mouse + return Commands.addAll({ prevSubject: 'element' }, { click (subject, positionOrX, y, options = {}) { - //# TODO handle pointer-events: none - //# http://caniuse.com/#feat=pointer-events + //# TODO handle pointer-events: none + //# http://caniuse.com/#feat=pointer-events - let position; let x; + let position + let x ({ options, position, x, y } = $actionability.getPositionFromArguments(positionOrX, y, options)) @@ -37,20 +36,16 @@ module.exports = (Commands, Cypress, cy, state, config) => { //# and we did not pass the multiple flag if ((options.multiple === false) && (options.$el.length > 1)) { $utils.throwErrByPath('click.multiple_elements', { - args: { num: options.$el.length }, + args: { cmd: 'click', num: options.$el.length }, }) } - state('window') - const click = (el) => { let deltaOptions const $el = $dom.wrap(el) - const domEvents = {} - if (options.log) { - //# figure out the options which actually change the behavior of clicks + //# figure out the options which actually change the behavior of clicks deltaOptions = $utils.filterOutOptions(options) options._log = Cypress.log({ @@ -62,24 +57,18 @@ module.exports = (Commands, Cypress, cy, state, config) => { } if (options.errorOnSelect && $el.is('select')) { - $utils.throwErrByPath('click.on_select_element', { onFail: options._log }) + $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'click' }, onFail: options._log }) } - const afterMouseDown = function ($elToClick, coords) { - //# we need to use both of these - let consoleObj - const { fromWindow, fromViewport } = coords - - //# handle mouse events removing DOM elements - //# https://www.w3.org/TR/uievents/#event-type-click (scroll up slightly) + //# we want to add this delay delta to our + //# runnables timeout so we prevent it from + //# timing out from multiple clicks + cy.timeout($actionability.delay, true, 'click') - if ($dom.isAttached($elToClick)) { - domEvents.mouseUp = $Mouse.mouseUp($elToClick, fromViewport) - } + const createLog = (domEvents, fromWindowCoords) => { + let consoleObj - if ($dom.isAttached($elToClick)) { - domEvents.click = $Mouse.click($elToClick, fromViewport) - } + const elClicked = domEvents.moveEvents.el if (options._log) { consoleObj = options._log.invoke('consoleProps') @@ -87,39 +76,28 @@ module.exports = (Commands, Cypress, cy, state, config) => { const consoleProps = function () { consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { - 'Applied To': $dom.getElements($el), - 'Elements': $el.length, - 'Coords': _.pick(fromWindow, 'x', 'y'), //# always absolute + 'Applied To': $dom.getElements(options.$el), + 'Elements': options.$el.length, + 'Coords': _.pick(fromWindowCoords, 'x', 'y'), //# always absolute 'Options': deltaOptions, }) - if ($el.get(0) !== $elToClick.get(0)) { - //# only do this if $elToClick isnt $el - consoleObj['Actual Element Clicked'] = $dom.getElements($elToClick) + if (options.$el.get(0) !== elClicked) { + //# only do this if $elToClick isnt $el + consoleObj['Actual Element Clicked'] = $dom.getElements($(elClicked)) } - consoleObj.groups = function () { - const groups = [{ - name: 'MouseDown', - items: _.pick(domEvents.mouseDown, 'preventedDefault', 'stoppedPropagation', 'modifiers'), - }] - - if (domEvents.mouseUp) { - groups.push({ - name: 'MouseUp', - items: _.pick(domEvents.mouseUp, 'preventedDefault', 'stoppedPropagation', 'modifiers'), - }) - } - - if (domEvents.click) { - groups.push({ - name: 'Click', - items: _.pick(domEvents.click, 'preventedDefault', 'stoppedPropagation', 'modifiers'), - }) - } - - return groups - } + consoleObj.table = _.extend((consoleObj.table || {}), { + 1: () => { + return formatMoveEventsTable(domEvents.moveEvents.events) + }, + 2: () => { + return { + name: 'Mouse Click Events', + data: formatMouseEvents(domEvents.clickEvents), + } + }, + }) return consoleObj } @@ -127,11 +105,11 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Promise .delay($actionability.delay, 'click') .then(() => { - //# display the red dot at these coords + //# display the red dot at these coords if (options._log) { - //# because we snapshot and output a command per click - //# we need to manually snapshot + end them - options._log.set({ coords: fromWindow, consoleProps }) + //# because we snapshot and output a command per click + //# we need to manually snapshot + end them + options._log.set({ coords: fromWindowCoords, consoleProps }) } //# we need to split this up because we want the coordinates @@ -144,11 +122,6 @@ module.exports = (Commands, Cypress, cy, state, config) => { }).return(null) } - //# we want to add this delay delta to our - //# runnables timeout so we prevent it from - //# timing out from multiple clicks - cy.timeout($actionability.delay, true, 'click') - //# must use callbacks here instead of .then() //# because we're issuing the clicks synchonrously //# once we establish the coordinates and the element @@ -158,56 +131,22 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Cypress.action('cy:scrolled', $el, type) }, - onReady ($elToClick, coords) { - //# record the previously focused element before - //# issuing the mousedown because browsers may - //# automatically shift the focus to the element - //# without firing the focus event - const $previouslyFocused = cy.getFocused() + onReady: ($elToClick, coords) => { - el = $elToClick.get(0) + const { fromWindow, fromViewport } = coords - domEvents.mouseDown = $Mouse.mouseDown($elToClick, coords.fromViewport) + const forceEl = options.force && $elToClick.get(0) - //# if mousedown was canceled then or caused - //# our element to be removed from the DOM - //# just resolve after mouse down and dont - //# send a focus event - if (domEvents.mouseDown.preventedDefault || !$dom.isAttached($elToClick)) { - return afterMouseDown($elToClick, coords) - } - - if ($elements.isInput(el) || $elements.isTextarea(el) || $elements.isContentEditable(el)) { - if (!$elements.isNeedSingleValueChangeInputElement(el)) { - $selection.moveSelectionToEnd(el) - } - } + const moveEvents = mouse.mouseMove(fromViewport, forceEl) - //# retrieve the first focusable $el in our parent chain - const $elToFocus = $elements.getFirstFocusableEl($elToClick) + const clickEvents = mouse.mouseClick(fromViewport, forceEl) - if (cy.needsFocus($elToFocus, $previouslyFocused)) { - if ($dom.isWindow($elToFocus)) { - // if the first focusable element from the click - // is the window, then we can skip the focus event - // since the user has clicked a non-focusable element - const $focused = cy.getFocused() - - if ($focused) { - cy.fireBlur($focused.get(0)) - } - } else { - // the user clicked inside a focusable element - cy.fireFocus($elToFocus.get(0)) - } - } - - return afterMouseDown($elToClick, coords) + return createLog({ moveEvents, clickEvents }, fromWindow) }, }) .catch((err) => { - //# snapshot only on click failure + //# snapshot only on click failure err.onFail = function () { if (options._log) { return options._log.snapshot() @@ -239,66 +178,407 @@ module.exports = (Commands, Cypress, cy, state, config) => { //# update dblclick to use the click //# logic and just swap out the event details? - dblclick (subject, options = {}) { - _.defaults(options, - { log: true }) + dblclick (subject, positionOrX, y, options = {}) { + + let position + let x + + ({ options, position, x, y } = $actionability.getPositionFromArguments(positionOrX, y, options)) + + _.defaults(options, { + $el: subject, + log: true, + verify: true, + force: false, + // TODO: 4.0 make this false by default + multiple: true, + position, + x, + y, + errorOnSelect: true, + waitForAnimations: config('waitForAnimations'), + animationDistanceThreshold: config('animationDistanceThreshold'), + }) - const dblclicks = [] + //# throw if we're trying to click multiple elements + //# and we did not pass the multiple flag + if ((options.multiple === false) && (options.$el.length > 1)) { + $utils.throwErrByPath('click.multiple_elements', { + args: { cmd: 'dblclick', num: options.$el.length }, + }) + } const dblclick = (el) => { - let log + let deltaOptions const $el = $dom.wrap(el) + if (options.log) { + //# figure out the options which actually change the behavior of clicks + deltaOptions = $utils.filterOutOptions(options) + + options._log = Cypress.log({ + message: deltaOptions, + $el, + }) + + options._log.snapshot('before', { next: 'after' }) + } + + if (options.errorOnSelect && $el.is('select')) { + $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'dblclick' }, onFail: options._log }) + } + //# we want to add this delay delta to our //# runnables timeout so we prevent it from //# timing out from multiple clicks cy.timeout($actionability.delay, true, 'dblclick') + const createLog = (domEvents, fromWindowCoords) => { + let consoleObj + + const elClicked = domEvents.moveEvents.el + + if (options._log) { + consoleObj = options._log.invoke('consoleProps') + } + + const consoleProps = function () { + consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { + 'Applied To': $dom.getElements(options.$el), + 'Elements': options.$el.length, + 'Coords': _.pick(fromWindowCoords, 'x', 'y'), //# always absolute + 'Options': deltaOptions, + }) + + if (options.$el.get(0) !== elClicked) { + //# only do this if $elToClick isnt $el + consoleObj['Actual Element Clicked'] = $dom.getElements(elClicked) + } + + consoleObj.table = _.extend((consoleObj.table || {}), { + 1: () => { + return formatMoveEventsTable(domEvents.moveEvents.events) + }, + 2: () => { + return { + name: 'Mouse Click Events', + data: _.concat( + formatMouseEvents(domEvents.clickEvents[0], formatMouseEvents), + formatMouseEvents(domEvents.clickEvents[1], formatMouseEvents) + ), + } + }, + 3: () => { + return { + name: 'Mouse Dblclick Event', + data: formatMouseEvents({ dblclickProps: domEvents.dblclickProps }), + } + }, + }) + + return consoleObj + } + + return Promise + .delay($actionability.delay, 'dblclick') + .then(() => { + //# display the red dot at these coords + if (options._log) { + //# because we snapshot and output a command per click + //# we need to manually snapshot + end them + options._log.set({ coords: fromWindowCoords, consoleProps }) + } + + //# we need to split this up because we want the coordinates + //# to mutate our passed in options._log but we dont necessary + //# want to snapshot and end our command if we're a different + //# action like (cy.type) and we're borrowing the click action + if (options._log && options.log) { + return options._log.snapshot().end() + } + }).return(null) + } + + //# must use callbacks here instead of .then() + //# because we're issuing the clicks synchonrously + //# once we establish the coordinates and the element + //# passes all of the internal checks + return $actionability.verify(cy, $el, options, { + onScroll ($el, type) { + return Cypress.action('cy:scrolled', $el, type) + }, + + onReady: ($elToClick, coords) => { + + const { fromWindow, fromViewport } = coords + const forceEl = options.force && $elToClick.get(0) + const moveEvents = mouse.mouseMove(fromViewport, forceEl) + const { clickEvents1, clickEvents2, dblclickProps } = mouse.dblclick(fromViewport, forceEl) + + return createLog({ + moveEvents, + clickEvents: [clickEvents1, clickEvents2], + dblclickProps, + }, fromWindow) + }, + }) + .catch((err) => { + //# snapshot only on click failure + err.onFail = function () { + if (options._log) { + return options._log.snapshot() + } + } + + //# if we give up on waiting for actionability then + //# lets throw this error and log the command + return $utils.throwErr(err, { onFail: options._log }) + }) + } + + return Promise + .each(options.$el.toArray(), dblclick) + .then(() => { + let verifyAssertions + + if (options.verify === false) { + return options.$el + } + + return (verifyAssertions = () => { + return cy.verifyUpcomingAssertions(options.$el, options, { + onRetry: verifyAssertions, + }) + })() + }) + }, + + rightclick (subject, positionOrX, y, options = {}) { + + let position + let x + + ({ options, position, x, y } = $actionability.getPositionFromArguments(positionOrX, y, options)) + + _.defaults(options, { + $el: subject, + log: true, + verify: true, + force: false, + multiple: false, + position, + x, + y, + errorOnSelect: true, + waitForAnimations: config('waitForAnimations'), + animationDistanceThreshold: config('animationDistanceThreshold'), + }) + + //# throw if we're trying to click multiple elements + //# and we did not pass the multiple flag + if ((options.multiple === false) && (options.$el.length > 1)) { + $utils.throwErrByPath('click.multiple_elements', { + args: { cmd: 'rightclick', num: options.$el.length }, + }) + } + + const rightclick = (el) => { + let deltaOptions + const $el = $dom.wrap(el) + if (options.log) { - log = Cypress.log({ + //# figure out the options which actually change the behavior of clicks + deltaOptions = $utils.filterOutOptions(options) + + options._log = Cypress.log({ + message: deltaOptions, $el, - consoleProps () { - return { - 'Applied To': $dom.getElements($el), - 'Elements': $el.length, - } - }, }) + + options._log.snapshot('before', { next: 'after' }) } - cy.ensureVisibility($el, log) + if (options.errorOnSelect && $el.is('select')) { + $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'rightclick' }, onFail: options._log }) + } - const p = cy.now('focus', $el, { $el, error: false, verify: false, log: false }).then(() => { - const event = new MouseEvent('dblclick', { - bubbles: true, - cancelable: true, - }) + //# we want to add this delay delta to our + //# runnables timeout so we prevent it from + //# timing out from multiple clicks + cy.timeout($actionability.delay, true, 'rightclick') - el.dispatchEvent(event) + const createLog = (domEvents, fromWindowCoords) => { + let consoleObj - // $el.cySimulate("dblclick") + const elClicked = domEvents.moveEvents.el - // log.snapshot() if log + if (options._log) { + consoleObj = options._log.invoke('consoleProps') + } - //# need to return null here to prevent - //# chaining thenable promises - return null - }).delay($actionability.delay, 'dblclick') + const consoleProps = function () { + consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { + 'Applied To': $dom.getElements(options.$el), + 'Elements': options.$el.length, + 'Coords': _.pick(fromWindowCoords, 'x', 'y'), //# always absolute + 'Options': deltaOptions, + }) - dblclicks.push(p) + if (options.$el.get(0) !== elClicked) { + //# only do this if $elToClick isnt $el + consoleObj['Actual Element Clicked'] = $dom.getElements(elClicked) + } - return p - } + consoleObj.table = _.extend((consoleObj.table || {}), { + 1: () => { + return formatMoveEventsTable(domEvents.moveEvents.events) + }, + 2: () => { + return { + name: 'Mouse Click Events', + data: formatMouseEvents(domEvents.clickEvents, formatMouseEvents), + } + }, + 3: () => { + return { + name: 'Contextmenu Event', + data: formatMouseEvents(domEvents.contextmenuEvent), + } + }, + }) + + return consoleObj + } + + return Promise + .delay($actionability.delay, 'rightclick') + .then(() => { + //# display the red dot at these coords + if (options._log) { + //# because we snapshot and output a command per click + //# we need to manually snapshot + end them + options._log.set({ coords: fromWindowCoords, consoleProps }) + } + + //# we need to split this up because we want the coordinates + //# to mutate our passed in options._log but we dont necessary + //# want to snapshot and end our command if we're a different + //# action like (cy.type) and we're borrowing the click action + if (options._log && options.log) { + return options._log.snapshot().end() + } + }).return(null) + } + + //# must use callbacks here instead of .then() + //# because we're issuing the clicks synchonrously + //# once we establish the coordinates and the element + //# passes all of the internal checks + return $actionability.verify(cy, $el, options, { + onScroll ($el, type) { + return Cypress.action('cy:scrolled', $el, type) + }, - //# create a new promise and chain off of it using reduce to insert - //# the artificial delays. we have to set this as cancelable for it - //# to propogate since this is an "inner" promise + onReady: ($elToClick, coords) => { + + const { fromWindow, fromViewport } = coords + const forceEl = options.force && $elToClick.get(0) + const moveEvents = mouse.mouseMove(fromViewport, forceEl) + const { clickEvents, contextmenuEvent } = mouse.rightclick(fromViewport, forceEl) + + return createLog({ + moveEvents, + clickEvents, + contextmenuEvent, + }, fromWindow) + }, + }) + .catch((err) => { + //# snapshot only on click failure + err.onFail = function () { + if (options._log) { + return options._log.snapshot() + } + } + + //# if we give up on waiting for actionability then + //# lets throw this error and log the command + return $utils.throwErr(err, { onFail: options._log }) + }) + } - //# return our original subject when our promise resolves return Promise - .resolve(subject.toArray()) - .each(dblclick) - .return(subject) + .each(options.$el.toArray(), rightclick) + .then(() => { + let verifyAssertions + + if (options.verify === false) { + return options.$el + } + + return (verifyAssertions = () => { + return cy.verifyUpcomingAssertions(options.$el, options, { + onRetry: verifyAssertions, + }) + })() + }) }, }) } + +const formatMoveEventsTable = (events) => { + + return { + name: `Mouse Move Events${events ? '' : ' (skipped)'}`, + data: _.map(events, (obj) => { + const key = _.keys(obj)[0] + const val = obj[_.keys(obj)[0]] + + if (val.skipped) { + const reason = val.skipped + + return { + 'Event Name': key, + 'Target Element': reason, + 'Prevented Default?': null, + 'Stopped Propagation?': null, + // 'Modifiers': null, + } + } + + return { + 'Event Name': key, + 'Target Element': val.el, + 'Prevented Default?': val.preventedDefault, + 'Stopped Propagation?': val.stoppedPropagation, + // 'Modifiers': val.modifiers ? val.modifiers : null, + } + }), + } +} + +const formatMouseEvents = (events) => { + return _.map(events, (val, key) => { + + if (val.skipped) { + + const reason = val.skipped + + return { + 'Event Name': key.slice(0, -5), + 'Target Element': reason, + 'Prevented Default?': null, + 'Stopped Propagation?': null, + 'Modifiers': null, + } + } + + return { + 'Event Name': key.slice(0, -5), + 'Target Element': val.el, + 'Prevented Default?': val.preventedDefault, + 'Stopped Propagation?': val.stoppedPropagation, + 'Modifiers': val.modifiers ? val.modifiers : null, + } + }) +} diff --git a/packages/driver/src/cy/commands/actions/trigger.coffee b/packages/driver/src/cy/commands/actions/trigger.coffee index 5696fb998f8d..9f47fd3fe43c 100644 --- a/packages/driver/src/cy/commands/actions/trigger.coffee +++ b/packages/driver/src/cy/commands/actions/trigger.coffee @@ -2,6 +2,8 @@ _ = require("lodash") Promise = require("bluebird") $dom = require("../../../dom") +$elements = require("../../../dom/elements") +$window = require("../../../dom/window") $utils = require("../../../cypress/utils") $actionability = require("../../actionability") @@ -90,11 +92,15 @@ module.exports = (Commands, Cypress, cy, state, config) -> coords: fromWindow }) + docCoords = $elements.getFromDocCoords(fromViewport.x, fromViewport.y, $window.getWindowByElement($elToClick.get(0))) + eventOptions = _.extend({ clientX: fromViewport.x clientY: fromViewport.y - pageX: fromWindow.x - pageY: fromWindow.y + screenX: fromViewport.x + screenY: fromViewport.y + pageX: docCoords.x + pageY: docCoords.y }, eventOptions) dispatch($elToClick.get(0), eventName, eventOptions) diff --git a/packages/driver/src/cy/commands/actions/type.coffee b/packages/driver/src/cy/commands/actions/type.coffee index 2272f44371bc..2efb697b691a 100644 --- a/packages/driver/src/cy/commands/actions/type.coffee +++ b/packages/driver/src/cy/commands/actions/type.coffee @@ -5,7 +5,6 @@ moment = require("moment") $dom = require("../../../dom") $elements = require("../../../dom/elements") $selection = require("../../../dom/selection") -$Keyboard = require("../../../cypress/keyboard") $utils = require("../../../cypress/utils") $actionability = require("../../actionability") @@ -17,8 +16,7 @@ weekRegex = /^\d{4}-W(0[1-9]|[1-4]\d|5[0-3])$/ timeRegex = /^([0-1]\d|2[0-3]):[0-5]\d(:[0-5]\d)?(\.[0-9]{1,3})?$/ module.exports = (Commands, Cypress, cy, state, config) -> - Cypress.on "test:before:run", -> - $Keyboard.resetModifiers(state("document"), state("window")) + keyboard = cy.internal.keyboard Commands.addAll({ prevSubject: "element" }, { type: (subject, chars, options = {}) -> @@ -46,8 +44,9 @@ module.exports = (Commands, Cypress, cy, state, config) -> getRow = (id, key, which) -> table[id] or do -> table[id] = (obj = {}) - modifiers = $Keyboard.activeModifiers() - obj.modifiers = modifiers.join(", ") if modifiers.length + modifiers = keyboard.modifiersToString(keyboard.getActiveModifiers(state)) + if modifiers + obj.modifiers = modifiers if key obj.typed = key obj.which = which if which @@ -71,11 +70,15 @@ module.exports = (Commands, Cypress, cy, state, config) -> "Typed": chars "Applied To": $dom.getElements(options.$el) "Options": deltaOptions - "table": -> - { - name: "Key Events Table" - data: getTableData() - columns: ["typed", "which", "keydown", "keypress", "textInput", "input", "keyup", "change", "modifiers"] + "table": { + ## mouse events tables will take up slots 1 and 2 if they're present + ## this preserves the order of the tables + 3: -> + { + name: "Keyboard Events" + data: getTableData() + columns: ["typed", "which", "keydown", "keypress", "textInput", "input", "keyup", "change", "modifiers"] + } } } } @@ -249,7 +252,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> isContentEditable = $elements.isContentEditable(options.$el.get(0)) isTextarea = $elements.isTextarea(options.$el.get(0)) - $Keyboard.type({ + keyboard.type({ $el: options.$el chars: options.chars delay: options.delay diff --git a/packages/driver/src/cy/keyboard.js b/packages/driver/src/cy/keyboard.js index 36ef6c46d622..565d739044f7 100644 --- a/packages/driver/src/cy/keyboard.js +++ b/packages/driver/src/cy/keyboard.js @@ -2,7 +2,7 @@ const _ = require('lodash') const Promise = require('bluebird') const $elements = require('../dom/elements') const $selection = require('../dom/selection') -const $Cypress = require('../cypress') +const $document = require('../dom/document') const isSingleDigitRe = /^\d$/ const isStartingDigitRe = /^\d/ @@ -31,711 +31,722 @@ const keyStandardMap = { '{pagedown}': 'PageDown', } -const $Keyboard = { - keyToStandard (key) { - return keyStandardMap[key] || key - }, - - charCodeMap: { - 33: 49, //# ! --- 1 - 64: 50, //# @ --- 2 - 35: 51, //# # --- 3 - 36: 52, //# $ --- 4 - 37: 53, //# % --- 5 - 94: 54, //# ^ --- 6 - 38: 55, //# & --- 7 - 42: 56, //# * --- 8 - 40: 57, //# ( --- 9 - 41: 48, //# ) --- 0 - 59: 186, //# ; --- 186 - 58: 186, //# : --- 186 - 61: 187, //# = --- 187 - 43: 187, //# + --- 187 - 44: 188, //# , --- 188 - 60: 188, //# < --- 188 - 45: 189, //# - --- 189 - 95: 189, //# _ --- 189 - 46: 190, //# . --- 190 - 62: 190, //# > --- 190 - 47: 191, //# / --- 191 - 63: 191, //# ? --- 191 - 96: 192, //# ` --- 192 - 126: 192, //# ~ --- 192 - 91: 219, //# [ --- 219 - 123: 219, //# { --- 219 - 92: 220, //# \ --- 220 - 124: 220, //# | --- 220 - 93: 221, //# ] --- 221 - 125: 221, //# } --- 221 - 39: 222, //# ' --- 222 - 34: 222, //# " --- 222 - }, - - modifierCodeMap: { - alt: 18, - ctrl: 17, - meta: 91, - shift: 16, - }, - - specialChars: { - '{selectall}': $selection.selectAll, - - //# charCode = 46 - //# no keyPress - //# no textInput - //# yes input (if value is actually changed) - '{del}' (el, options) { - options.charCode = 46 - options.keypress = false - options.textInput = false - options.setKey = '{del}' - - return this.ensureKey(el, null, options, () => { - $selection.getSelectionBounds(el) - - if ($selection.isCollapsed(el)) { - //# if there's no text selected, delete the prev char - //# if deleted char, send the input event - options.input = $selection.deleteRightOfCursor(el) - - return - } +const keyToStandard = (key) => { + return keyStandardMap[key] || key +} - //# text is selected, so delete the selection - //# contents and send the input event - $selection.deleteSelectionContents(el) - options.input = true +const charCodeMap = { + 33: 49, //# ! --- 1 + 64: 50, //# @ --- 2 + 35: 51, //# # --- 3 + 36: 52, //# $ --- 4 + 37: 53, //# % --- 5 + 94: 54, //# ^ --- 6 + 38: 55, //# & --- 7 + 42: 56, //# * --- 8 + 40: 57, //# ( --- 9 + 41: 48, //# ) --- 0 + 59: 186, //# ; --- 186 + 58: 186, //# : --- 186 + 61: 187, //# = --- 187 + 43: 187, //# + --- 187 + 44: 188, //# , --- 188 + 60: 188, //# < --- 188 + 45: 189, //# - --- 189 + 95: 189, //# _ --- 189 + 46: 190, //# . --- 190 + 62: 190, //# > --- 190 + 47: 191, //# / --- 191 + 63: 191, //# ? --- 191 + 96: 192, //# ` --- 192 + 126: 192, //# ~ --- 192 + 91: 219, //# [ --- 219 + 123: 219, //# { --- 219 + 92: 220, //# \ --- 220 + 124: 220, //# | --- 220 + 93: 221, //# ] --- 221 + 125: 221, //# } --- 221 + 39: 222, //# ' --- 222 + 34: 222, //# " --- 222 +} - }) - }, +const modifierCodeMap = { + alt: 18, + ctrl: 17, + meta: 91, + shift: 16, +} - //# charCode = 45 - //# no keyPress - //# no textInput - //# no input - '{insert}' (el, options) { - options.charCode = 45 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{insert}' - - return this.ensureKey(el, null, options) - }, +const initialModifiers = { + alt: false, + ctrl: false, + meta: false, + shift: false, +} - //# charCode = 8 - //# no keyPress - //# no textInput - //# yes input (if value is actually changed) - '{backspace}' (el, options) { - options.charCode = 8 - options.keypress = false - options.textInput = false - options.setKey = '{backspace}' +const modifierChars = { + '{alt}': 'alt', + '{option}': 'alt', - return this.ensureKey(el, null, options, () => { + '{ctrl}': 'ctrl', + '{control}': 'ctrl', - if ($selection.isCollapsed(el)) { - //# if there's no text selected, delete the prev char - //# if deleted char, send the input event - options.input = $selection.deleteLeftOfCursor(el) + '{meta}': 'meta', + '{command}': 'meta', + '{cmd}': 'meta', - return - } + '{shift}': 'shift', +} - //# text is selected, so delete the selection - //# contents and send the input event - $selection.deleteSelectionContents(el) - options.input = true +const getKeyCode = (key) => { + const code = key.charCodeAt(0) - }) - }, + return charCodeMap[code] != null ? charCodeMap[code] : code +} - //# charCode = 27 - //# no keyPress - //# no textInput - //# no input - '{esc}' (el, options) { - options.charCode = 27 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{esc}' - - return this.ensureKey(el, null, options) - }, +const getAsciiCode = (key) => { + const code = key.charCodeAt(0) - // "{tab}": (el, rng) -> + return code +} - '{{}' (el, options) { - options.key = '{' +const isModifier = (chars) => { + return _.has(modifierChars, chars) +} - return this.typeKey(el, options.key, options) - }, +const toModifiersEventOptions = (modifiers) => { + return { + altKey: modifiers.alt, + ctrlKey: modifiers.ctrl, + metaKey: modifiers.meta, + shiftKey: modifiers.shift, + } +} - //# charCode = 13 - //# yes keyPress - //# no textInput - //# no input - //# yes change (if input is different from last change event) - '{enter}' (el, options) { - options.charCode = 13 - options.textInput = false - options.input = false - options.setKey = '{enter}' - - return this.ensureKey(el, '\n', options, () => { - $selection.replaceSelectionContents(el, '\n') - - return options.onEnterPressed(options.id) - }) - }, +const fromModifierEventOptions = (eventOptions) => { + return _.pickBy({ + alt: eventOptions.altKey, + ctrl: eventOptions.ctrlKey, + meta: eventOptions.metaKey, + shift: eventOptions.shiftKey, - //# charCode = 37 - //# no keyPress - //# no textInput - //# no input - '{leftarrow}' (el, options) { - options.charCode = 37 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{leftarrow}' - - return this.ensureKey(el, null, options, () => { - return $selection.moveCursorLeft(el) - }) - }, + }, (x) => { + return !!x + }) +} - //# charCode = 39 - //# no keyPress - //# no textInput - //# no input - '{rightarrow}' (el, options) { - options.charCode = 39 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{rightarrow}' - - return this.ensureKey(el, null, options, () => { - return $selection.moveCursorRight(el) - }) - }, +const getActiveModifiers = (state) => { + return _.clone(state('keyboardModifiers')) || _.clone(initialModifiers) +} - //# charCode = 38 - //# no keyPress - //# no textInput - //# no input - '{uparrow}' (el, options) { - options.charCode = 38 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{uparrow}' - - return this.ensureKey(el, null, options, () => { - return $selection.moveCursorUp(el) - }) - }, +const modifiersToString = (modifiers) => { + return _.keys( + _.pickBy(modifiers, (val) => { + return val + }) + ) + .join(', ') +} - //# charCode = 40 - //# no keyPress - //# no textInput - //# no input - '{downarrow}' (el, options) { - options.charCode = 40 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{downarrow}' - - return this.ensureKey(el, null, options, () => { - return $selection.moveCursorDown(el) - }) +const create = function (state) { + + const kb = { + + specialChars: { + '{selectall}': $selection.selectAll, + + //# charCode = 46 + //# no keyPress + //# no textInput + //# yes input (if value is actually changed) + '{del}' (el, options) { + options.charCode = 46 + options.keypress = false + options.textInput = false + options.setKey = '{del}' + + return kb.ensureKey(el, null, options, () => { + + if ($selection.isCollapsed(el)) { + //# if there's no text selected, delete the prev char + //# if deleted char, send the input event + options.input = $selection.deleteRightOfCursor(el) + + return + } + + //# text is selected, so delete the selection + //# contents and send the input event + $selection.deleteSelectionContents(el) + options.input = true + + }) + }, + + //# charCode = 8 + //# no keyPress + //# no textInput + //# no input + '{insert}' (el, options) { + options.charCode = 45 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{insert}' + + return kb.ensureKey(el, null, options) + }, + + //# charCode = 8 + //# no keyPress + //# no textInput + //# yes input (if value is actually changed) + '{backspace}' (el, options) { + options.charCode = 8 + options.keypress = false + options.textInput = false + options.setKey = '{backspace}' + + return kb.ensureKey(el, null, options, () => { + + if ($selection.isCollapsed(el)) { + //# if there's no text selected, delete the prev char + //# if deleted char, send the input event + options.input = $selection.deleteLeftOfCursor(el) + + return + } + + //# text is selected, so delete the selection + //# contents and send the input event + $selection.deleteSelectionContents(el) + options.input = true + + }) + }, + + //# charCode = 27 + //# no keyPress + //# no textInput + //# no input + '{esc}' (el, options) { + options.charCode = 27 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{esc}' + + return kb.ensureKey(el, null, options) + }, + + '{{}' (el, options) { + options.key = '{' + + return kb.typeKey(el, options.key, options) + }, + + //# charCode = 13 + //# yes keyPress + //# no textInput + //# no input + //# yes change (if input is different from last change event) + '{enter}' (el, options) { + options.charCode = 13 + options.textInput = false + options.input = false + options.setKey = '{enter}' + + return kb.ensureKey(el, '\n', options, () => { + $selection.replaceSelectionContents(el, '\n') + + return options.onEnterPressed(options.id) + }) + }, + + //# charCode = 37 + //# no keyPress + //# no textInput + //# no input + '{leftarrow}' (el, options) { + options.charCode = 37 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{leftarrow}' + + return kb.ensureKey(el, null, options, () => { + return $selection.moveCursorLeft(el) + }) + }, + + //# charCode = 39 + //# no keyPress + //# no textInput + //# no input + '{rightarrow}' (el, options) { + options.charCode = 39 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{rightarrow}' + + return kb.ensureKey(el, null, options, () => { + return $selection.moveCursorRight(el) + }) + }, + + //# charCode = 38 + //# no keyPress + //# no textInput + //# no input + '{uparrow}' (el, options) { + options.charCode = 38 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{uparrow}' + + return kb.ensureKey(el, null, options, () => { + return $selection.moveCursorUp(el) + }) + }, + + //# charCode = 40 + //# no keyPress + //# no textInput + //# no input + '{downarrow}' (el, options) { + options.charCode = 40 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{downarrow}' + + return kb.ensureKey(el, null, options, () => { + return $selection.moveCursorDown(el) + }) + }, + + // charCode = 36 + // no keyPress + // no textInput + // no input + '{home}' (el, options) { + options.charCode = 36 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{home}' + + return this.ensureKey(el, null, options, function () { + return $selection.moveCursorToLineStart(el) + }) + }, + // charCode = 35 + // no keyPress + // no textInput + // no input + '{end}' (el, options) { + options.charCode = 35 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{end}' + + return this.ensureKey(el, null, options, function () { + return $selection.moveCursorToLineEnd(el) + }) + }, + // charCode = 33 + // no keyPress + // no textInput + // no input + '{pageup}' (el, options) { + options.charCode = 33 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{pageup}' + + return this.ensureKey(el, null, options) + }, + // charCode = 34 + // no keyPress + // no textInput + // no input + '{pagedown}' (el, options) { + options.charCode = 34 + options.keypress = false + options.textInput = false + options.input = false + options.setKey = '{pagedown}' + + return this.ensureKey(el, null, options) + }, }, - //# charCode = 36 - //# no keyPress - //# no textInput - //# no input - '{home}' (el, options) { - options.charCode = 36 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{home}' - - return this.ensureKey(el, null, options, () => { - return $selection.moveCursorToLineStart(el) - }) + isSpecialChar (chars) { + return _.has(kb.specialChars, chars) }, - //# charCode = 35 - //# no keyPress - //# no textInput - //# no input - '{end}' (el, options) { - options.charCode = 35 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{end}' - - return this.ensureKey(el, null, options, () => { - return $selection.moveCursorToLineEnd(el) + type (options = {}) { + _.defaults(options, { + delay: 10, + disableSpecialCharSequences: false, + onEvent () { }, + onBeforeEvent () { }, + onBeforeType () { }, + onValueChange () { }, + onEnterPressed () { }, + onNoMatchingSpecialChars () { }, + onBeforeSpecialCharAction () { }, }) - }, - //# charCode = 33 - //# no keyPress - //# no textInput - //# no input - '{pageup}' (el, options) { - options.charCode = 33 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{pageup}' - - return this.ensureKey(el, null, options) - }, + const el = options.$el.get(0) - //# charCode = 34 - //# no keyPress - //# no textInput - //# no input - '{pagedown}' (el, options) { - options.charCode = 34 - options.keypress = false - options.textInput = false - options.input = false - options.setKey = '{pagedown}' - - return this.ensureKey(el, null, options) - }, - }, - - modifierChars: { - '{alt}': 'alt', - '{option}': 'alt', - - '{ctrl}': 'ctrl', - '{control}': 'ctrl', - - '{meta}': 'meta', - '{command}': 'meta', - '{cmd}': 'meta', - - '{shift}': 'shift', - }, - - boundsAreEqual (bounds) { - return bounds[0] === bounds[1] - }, - - type (options = {}) { - _.defaults(options, { - delay: 10, - disableSpecialCharSequences: false, - onEvent () {}, - onBeforeEvent () {}, - onBeforeType () {}, - onValueChange () {}, - onEnterPressed () {}, - onNoMatchingSpecialChars () {}, - onBeforeSpecialCharAction () {}, - }) + let keys = options.chars + + if (!options.disableSpecialCharSequences) { + keys = options.chars.split(charsBetweenCurlyBracesRe).map((chars) => { + if (charsBetweenCurlyBracesRe.test(chars)) { + // allow special chars and modifiers to be case-insensitive + return chars.toLowerCase() + } - const el = options.$el.get(0) + return chars + }) + } - let keys = options.chars + options.onBeforeType(kb.countNumIndividualKeyStrokes(keys)) - if (!options.disableSpecialCharSequences) { - keys = options.chars.split(charsBetweenCurlyBracesRe).map((chars) => { - if (charsBetweenCurlyBracesRe.test(chars)) { - //# allow special chars and modifiers to be case-insensitive - return chars.toLowerCase() + //# should make each keystroke async to mimic + //# how keystrokes come into javascript naturally + return Promise + .each(keys, (key) => { + return kb.typeChars(el, key, options) + }).then(() => { + if (options.release !== false) { + return kb.resetModifiers($document.getDocumentFromElement(el)) } - - return chars }) - } - - options.onBeforeType(this.countNumIndividualKeyStrokes(keys)) - - //# should make each keystroke async to mimic - //# how keystrokes come into javascript naturally - return Promise - .each(keys, (key) => { - return this.typeChars(el, key, options) - }).then(() => { - if (options.release !== false) { - return this.resetModifiers(el, options.window) - } - }) - }, - - countNumIndividualKeyStrokes (keys) { - return _.reduce(keys, (memo, chars) => { - //# special chars count as 1 keystroke - if (this.isSpecialChar(chars)) { - return memo + 1 - //# modifiers don't count as keystrokes - } + }, + + countNumIndividualKeyStrokes (keys) { + return _.reduce(keys, (memo, chars) => { + //# special chars count as 1 keystroke + if (kb.isSpecialChar(chars)) { + return memo + 1 + //# modifiers don't count as keystrokes + } + + if (isModifier(chars)) { + return memo + } + + return memo + chars.length - if (this.isModifier(chars)) { - return memo } + , 0) + }, - return memo + chars.length + typeChars (el, chars, options) { + options = _.clone(options) - } - , 0) - }, + // switch(false) executes blocks whose case === false + switch (false) { + case !kb.isSpecialChar(chars): + return Promise + .resolve(kb.handleSpecialChars(el, chars, options)) + .delay(options.delay) - typeChars (el, chars, options) { - options = _.clone(options) + case !isModifier(chars): + return Promise + .resolve(kb.handleModifier(el, chars, options)) + .delay(options.delay) + + case !charsBetweenCurlyBracesRe.test(chars): { + + //# between curly braces, but not a valid special + //# char or modifier + const allChars = _.keys(kb.specialChars).concat(_.keys(modifierChars)).join(', ') - switch (false) { - case !this.isSpecialChar(chars): { - return Promise - .resolve(this.handleSpecialChars(el, chars, options)) - .delay(options.delay) - } - case !this.isModifier(chars): { - return Promise - .resolve(this.handleModifier(el, chars, options)) - .delay(options.delay) - } - case !charsBetweenCurlyBracesRe.test(chars): { - //# between curly braces, but not a valid special - //# char or modifier - const allChars = _.keys(this.specialChars).concat(_.keys(this.modifierChars)).join(', ') - - return Promise - .resolve(options.onNoMatchingSpecialChars(chars, allChars)) - .delay(options.delay) - } - default: { - return Promise - .each(chars.split(''), (char) => { return Promise - .resolve(this.typeKey(el, char, options)) + .resolve(options.onNoMatchingSpecialChars(chars, allChars)) .delay(options.delay) - }) + } + + default: + return Promise + .each(chars.split(''), (char) => { + return Promise + .resolve(kb.typeKey(el, char, options)) + .delay(options.delay) + }) } - } - }, + }, - getKeyCode (key) { - const code = key.charCodeAt(0) + simulateKey (el, eventType, key, options) { + //# bail if we've said not to fire this specific event + //# in our options - return this.charCodeMap[code] != null ? this.charCodeMap[code] : code - }, + let charCode + let keyCode + let which - getAsciiCode (key) { - const code = key.charCodeAt(0) + if (options[eventType] === false) { + return true + } - return code - }, + key = options.key != null ? options.key : key - expectedValueDoesNotMatchCurrentValue (expected, rng) { - return expected !== rng.all() - }, + let keys = true + let otherKeys = true - moveCaretToEnd (rng) { - const len = rng.length() + const event = new Event(eventType, { + bubbles: true, + cancelable: eventType !== 'input', + }) - return rng.bounds([len, len]) - }, + switch (eventType) { + case 'keydown': case 'keyup': + keyCode = options.charCode != null ? options.charCode : getKeyCode(key.toUpperCase()) - simulateKey (el, eventType, key, options) { - //# bail if we've said not to fire this specific event - //# in our options + charCode = 0 + // keyCode = keyCode + which = keyCode + break - let charCode - let keyCode - let which + case 'keypress': { - if (options[eventType] === false) { - return true - } + const asciiCode = options.charCode != null ? options.charCode : getAsciiCode(key) - key = options.key != null ? options.key : key + charCode = asciiCode + keyCode = asciiCode + which = asciiCode + break + } - let keys = true - let otherKeys = true + case 'textInput': + charCode = 0 + keyCode = 0 + which = 0 + otherKeys = false - const event = new Event(eventType, { - bubbles: true, - cancelable: eventType !== 'input', - }) + _.extend(event, { + data: key, + }) - switch (eventType) { - case 'keydown': case 'keyup': { - keyCode = options.charCode != null ? options.charCode : this.getKeyCode(key.toUpperCase()) + break - charCode = 0 - which = keyCode - break + case 'input': + keys = false + otherKeys = false + break + default: + break } - case 'keypress': { - const asciiCode = options.charCode != null ? options.charCode : this.getAsciiCode(key) - charCode = asciiCode - keyCode = asciiCode - which = asciiCode - break - } - case 'textInput': { - charCode = 0 - keyCode = 0 - which = 0 - otherKeys = false + if (otherKeys) { _.extend(event, { - data: key, + location: 0, + repeat: false, }) - break - } - - case 'input': { - keys = false - otherKeys = false - break + _.extend(event, toModifiersEventOptions(getActiveModifiers(state))) } - default: null - } - - if (otherKeys) { - _.extend(event, { - location: 0, - repeat: false, - }) + if (keys) { + // special key like "{enter}" might have 'key = \n' + // in which case the original intent will be in options.setKey + // "normal" keys will have their value in "key" argument itself + const standardKey = keyToStandard(options.setKey || key) - this.mixinModifiers(event) - } - - if (keys) { - // special key like "{enter}" might have 'key = \n' - // in which case the original intent will be in options.setKey - // "normal" keys will have their value in "key" argument itself - const standardKey = $Keyboard.keyToStandard(options.setKey || key) - - _.extend(event, { - charCode, - detail: 0, - key: standardKey, - keyCode, - layerX: 0, - layerY: 0, - pageX: 0, - pageY: 0, - view: options.window, - which, - }) - } + _.extend(event, { + charCode, + detail: 0, + key: standardKey, + keyCode, + layerX: 0, + layerY: 0, + pageX: 0, + pageY: 0, + view: options.window, + which, + }) + } - const args = [options.id, key, eventType, which] + const args = [options.id, key, eventType, which] - //# give the driver a chance to bail on this event - //# if we return false here - if (options.onBeforeEvent.apply(this, args) === false) { - return - } + //# give the driver a chance to bail on this event + //# if we return false here + if (options.onBeforeEvent.apply(this, args) === false) { + return + } - const dispatched = el.dispatchEvent(event) + const dispatched = el.dispatchEvent(event) - args.push(dispatched) + args.push(dispatched) - options.onEvent.apply(this, args) + options.onEvent.apply(this, args) - return dispatched - }, + return dispatched + }, - typeKey (el, key, options) { - return this.ensureKey(el, key, options, () => { + typeKey (el, key, options) { + return kb.ensureKey(el, key, options, () => { - const isDigit = isSingleDigitRe.test(key) - const isNumberInputType = $elements.isInput(el) && $elements.isType(el, 'number') + const isDigit = isSingleDigitRe.test(key) + const isNumberInputType = $elements.isInput(el) && $elements.isType(el, 'number') - if (isNumberInputType) { - const { selectionStart } = el - const valueLength = $elements.getNativeProp(el, 'value').length - const isDigitsInText = isStartingDigitRe.test(options.chars) - const isValidCharacter = (key === '.') || ((key === '-') && valueLength) - const { prevChar } = options + if (isNumberInputType) { + const { selectionStart } = el + const valueLength = $elements.getNativeProp(el, 'value').length + const isDigitsInText = isStartingDigitRe.test(options.chars) + const isValidCharacter = (key === '.') || ((key === '-') && valueLength) + const { prevChar } = options - if (!isDigit && (isDigitsInText || !isValidCharacter || (selectionStart !== 0))) { - options.prevChar = key + if (!isDigit && (isDigitsInText || !isValidCharacter || (selectionStart !== 0))) { + options.prevChar = key - return - } + return + } - //# only type '.' and '-' if it is the first symbol and there already is a value, or if - //# '.' or '-' are appended to a digit. If not, value cannot be set. - if (isDigit && ((prevChar === '.') || ((prevChar === '-') && !valueLength))) { - options.prevChar = key - key = prevChar + key + //# only type '.' and '-' if it is the first symbol and there already is a value, or if + //# '.' or '-' are appended to a digit. If not, value cannot be set. + if (isDigit && ((prevChar === '.') || ((prevChar === '-') && !valueLength))) { + options.prevChar = key + key = prevChar + key + } } - } - return options.updateValue(el, key) - }) - }, + return options.updateValue(el, key) + }) + }, - ensureKey (el, key, options, fn) { - _.defaults(options, { - prevText: null, - }) + ensureKey (el, key, options, fn) { + _.defaults(options, { + prevText: null, + }) - options.id = _.uniqueId('char') - // options.beforeKey = el.value + options.id = _.uniqueId('char') + // options.beforeKey = el.value - const maybeUpdateValueAndFireInput = () => { - //# only call this function if we haven't been told not to - if (fn && (options.onBeforeSpecialCharAction(options.id, options.key) !== false)) { - let prevText + const maybeUpdateValueAndFireInput = () => { + //# only call this function if we haven't been told not to + if (fn && (options.onBeforeSpecialCharAction(options.id, options.key) !== false)) { + let prevText - if (!$elements.isContentEditable(el)) { - prevText = $elements.getNativeProp(el, 'value') - } + if (!$elements.isContentEditable(el)) { + prevText = $elements.getNativeProp(el, 'value') + } - fn.call(this) + fn.call(this) - if ((options.prevText === null) && !$elements.isContentEditable(el)) { - options.prevText = prevText - options.onValueChange(options.prevText, el) + if ((options.prevText === null) && !$elements.isContentEditable(el)) { + options.prevText = prevText + options.onValueChange(options.prevText, el) + } } - } - - return this.simulateKey(el, 'input', key, options) - } - if (this.simulateKey(el, 'keydown', key, options)) { - if (this.simulateKey(el, 'keypress', key, options)) { - if (this.simulateKey(el, 'textInput', key, options)) { + return kb.simulateKey(el, 'input', key, options) + } - let ml + if (kb.simulateKey(el, 'keydown', key, options)) { + if (kb.simulateKey(el, 'keypress', key, options)) { + if (kb.simulateKey(el, 'textInput', key, options)) { - if ($elements.isInput(el) || $elements.isTextarea(el)) { - ml = el.maxLength - } + let ml - //# maxlength is -1 by default when omitted - //# but could also be null or undefined :-/ - //# only cafe if we are trying to type a key - if (((ml === 0) || (ml > 0)) && key) { - //# check if we should update the value - //# and fire the input event - //# as long as we're under maxlength + if ($elements.isInput(el) || $elements.isTextarea(el)) { + ml = el.maxLength + } - if ($elements.getNativeProp(el, 'value').length < ml) { + //# maxlength is -1 by default when omitted + //# but could also be null or undefined :-/ + //# only cafe if we are trying to type a key + if (((ml === 0) || (ml > 0)) && key) { + //# check if we should update the value + //# and fire the input event + //# as long as we're under maxlength + + if ($elements.getNativeProp(el, 'value').length < ml) { + maybeUpdateValueAndFireInput() + } + } else { maybeUpdateValueAndFireInput() } - } else { - maybeUpdateValueAndFireInput() } } } - } - - return this.simulateKey(el, 'keyup', key, options) - }, - - isSpecialChar (chars) { - let needle - - return (needle = chars, _.keys(this.specialChars).includes(needle)) - }, - handleSpecialChars (el, chars, options) { - options.key = chars - - return this.specialChars[chars].call(this, el, options) - }, - - modifiers: { - alt: false, - ctrl: false, - meta: false, - shift: false, - }, + return kb.simulateKey(el, 'keyup', key, options) + }, - isModifier (chars) { - let needle + handleSpecialChars (el, chars, options) { + options.key = chars - return (needle = chars, _.keys(this.modifierChars).includes(needle)) - }, + return kb.specialChars[chars].call(this, el, options) + }, - handleModifier (el, chars, options) { - const modifier = this.modifierChars[chars] + handleModifier (el, chars, options) { + const modifier = modifierChars[chars] - //# do nothing if already activated - if (this.modifiers[modifier]) { - return - } + //# do nothing if already activated + if (getActiveModifiers(state)[modifier]) { + return + } - this.modifiers[modifier] = true + const _activeModifiers = getActiveModifiers(state) - return this.simulateModifier(el, 'keydown', modifier, options) - }, + _activeModifiers[modifier] = true - simulateModifier (el, eventType, modifier, options) { - return this.simulateKey(el, eventType, null, _.extend(options, { - charCode: this.modifierCodeMap[modifier], - id: _.uniqueId('char'), - key: `<${modifier}>`, - })) - }, + state('keyboardModifiers', _activeModifiers) - mixinModifiers (event) { - return _.extend(event, { - altKey: this.modifiers.alt, - ctrlKey: this.modifiers.ctrl, - metaKey: this.modifiers.meta, - shiftKey: this.modifiers.shift, - }) - }, + return kb.simulateModifier(el, 'keydown', modifier, options) + }, - activeModifiers () { - return _.reduce(this.modifiers, (memo, isActivated, modifier) => { - if (isActivated) { - memo.push(modifier) - } + simulateModifier (el, eventType, modifier, options) { + return kb.simulateKey(el, eventType, null, _.extend(options, { + charCode: modifierCodeMap[modifier], + id: _.uniqueId('char'), + key: `<${modifier}>`, + })) + }, - return memo - } - , []) - }, + // keyup should be sent to the activeElement or body if null + resetModifiers (doc) { - resetModifiers (el, window) { - return (() => { - const result = [] + const activeEl = $elements.getActiveElByDocument(doc) + const activeModifiers = getActiveModifiers(state) - for (let modifier in this.modifiers) { - const isActivated = this.modifiers[modifier] + for (let modifier in activeModifiers) { + const isActivated = activeModifiers[modifier] - this.modifiers[modifier] = false + activeModifiers[modifier] = false + state('keyboardModifiers', _.clone(activeModifiers)) if (isActivated) { - result.push(this.simulateModifier(el, 'keyup', modifier, { + kb.simulateModifier(activeEl, 'keyup', modifier, { window, - onBeforeEvent () {}, - onEvent () {}, - })) - } else { - result.push(undefined) + onBeforeEvent () { }, + onEvent () { }, + }) } } + }, + toModifiersEventOptions, + modifiersToString, + getActiveModifiers, + modifierChars, + } - return result - })() - }, + return kb } -$Cypress.Keyboard = $Keyboard - -module.exports = $Keyboard +module.exports = { + create, + toModifiersEventOptions, + getActiveModifiers, + modifierChars, + modifiersToString, + fromModifierEventOptions, +} diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index 11118a096956..96bd97d3a4e7 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -1,138 +1,655 @@ -const $Keyboard = require('./keyboard') const $dom = require('../dom') +const $elements = require('../dom/elements') +const $ = require('jquery') +const _ = require('lodash') +const $Keyboard = require('./keyboard') +const $selection = require('../dom/selection') +const debug = require('debug')('driver:mouse') -const { stopPropagation } = window.MouseEvent.prototype +/** + * @typedef Coords + * @property {number} x + * @property {number} y + * @property {Document} doc + */ -module.exports = { - mouseDown ($elToClick, fromViewport) { - const el = $elToClick.get(0) - - const win = $dom.getWindowByElement(el) - - const mdownEvtProps = $Keyboard.mixinModifiers({ - bubbles: true, - cancelable: true, - view: win, - clientX: fromViewport.x, - clientY: fromViewport.y, - buttons: 1, - detail: 1, - }) - - const mdownEvt = new window.MouseEvent('mousedown', mdownEvtProps) - - //# ensure this property exists on older chromium versions - if (mdownEvt.buttons == null) { - mdownEvt.buttons = 1 - } +const getLastHoveredEl = (state) => { + let lastHoveredEl = state('mouseLastHoveredEl') + const lastHoveredElAttached = lastHoveredEl && $elements.isAttachedEl(lastHoveredEl) - mdownEvt.stopPropagation = function (...args) { - this._hasStoppedPropagation = true + if (!lastHoveredElAttached) { + lastHoveredEl = null + state('mouseLastHoveredEl', lastHoveredEl) + } - return stopPropagation.apply(this, args) - } + return lastHoveredEl - const canceled = !el.dispatchEvent(mdownEvt) +} - const props = { - preventedDefault: canceled, - stoppedPropagation: !!mdownEvt._hasStoppedPropagation, - } +const getMouseCoords = (state) => { + return state('mouseCoords') +} - const modifiers = $Keyboard.activeModifiers() +const create = (state, focused) => { + + const mouse = { + + _getDefaultMouseOptions (x, y, win) { + const _activeModifiers = $Keyboard.getActiveModifiers(state) + const modifiersEventOptions = $Keyboard.toModifiersEventOptions(_activeModifiers) + const coordsEventOptions = toCoordsEventOptions(x, y, win) + + return _.extend({ + view: win, + // allow propagation out of root of shadow-dom + // https://developer.mozilla.org/en-US/docs/Web/API/Event/composed + composed: true, + // only for events involving moving cursor + relatedTarget: null, + }, modifiersEventOptions, coordsEventOptions) + }, + + /** + * @param {Coords} coords + * @param {HTMLElement} forceEl + */ + mouseMove (coords, forceEl) { + debug('mousemove', coords) + + const lastHoveredEl = getLastHoveredEl(state) + + const targetEl = mouse.getElAtCoordsOrForce(coords, forceEl) + + // if coords are same AND we're already hovered on the element, don't send move events + if (_.isEqual({ x: coords.x, y: coords.y }, getMouseCoords(state)) && lastHoveredEl === targetEl) return { el: targetEl } + + const events = mouse._mouseMoveEvents(targetEl, coords) + + const resultEl = mouse.getElAtCoordsOrForce(coords, forceEl) + + if (resultEl !== targetEl) { + mouse._mouseMoveEvents(resultEl, coords) + } + + return { el: resultEl, fromEl: lastHoveredEl, events } + }, + + /** + * @param {HTMLElement} el + * @param {Coords} coords + */ + _mouseMoveEvents (el, coords) { + + // events are not fired on disabled elements, so we don't have to take that into account + const win = $dom.getWindowByElement(el) + const { x, y } = coords + + const defaultOptions = mouse._getDefaultMouseOptions(x, y, win) + const defaultMouseOptions = _.extend({}, defaultOptions, { + button: 0, + which: 0, + buttons: 0, + }) + + const defaultPointerOptions = _.extend({}, defaultOptions, { + button: -1, + which: 0, + buttons: 0, + pointerId: 1, + pointerType: 'mouse', + isPrimary: true, + }) + + const notFired = () => { + return { + skipped: formatReasonNotFired('Already on Coordinates'), + } + } + let pointerout = _.noop + let pointerleave = _.noop + let pointerover = notFired + let pointerenter = _.noop + let mouseout = _.noop + let mouseleave = _.noop + let mouseover = notFired + let mouseenter = _.noop + let pointermove = notFired + let mousemove = notFired + + const lastHoveredEl = getLastHoveredEl(state) + + const hoveredElChanged = el !== lastHoveredEl + let commonAncestor = null + + if (hoveredElChanged && lastHoveredEl) { + commonAncestor = $elements.getFirstCommonAncestor(el, lastHoveredEl) + pointerout = () => { + sendPointerout(lastHoveredEl, _.extend({}, defaultPointerOptions, { relatedTarget: el })) + } + + mouseout = () => { + sendMouseout(lastHoveredEl, _.extend({}, defaultMouseOptions, { relatedTarget: el })) + } + + let curParent = lastHoveredEl + + const elsToSendMouseleave = [] + + while (curParent && curParent !== commonAncestor) { + elsToSendMouseleave.push(curParent) + curParent = curParent.parentNode + } + + pointerleave = () => { + elsToSendMouseleave.forEach((elToSend) => { + sendPointerleave(elToSend, _.extend({}, defaultPointerOptions, { relatedTarget: el })) + }) + } + + mouseleave = () => { + elsToSendMouseleave.forEach((elToSend) => { + sendMouseleave(elToSend, _.extend({}, defaultMouseOptions, { relatedTarget: el })) + }) + } + + } + + if (hoveredElChanged) { + if (el && $elements.isAttachedEl(el)) { + + mouseover = () => { + return sendMouseover(el, _.extend({}, defaultMouseOptions, { relatedTarget: lastHoveredEl })) + } + + pointerover = () => { + return sendPointerover(el, _.extend({}, defaultPointerOptions, { relatedTarget: lastHoveredEl })) + } + + let curParent = el + const elsToSendMouseenter = [] + + while (curParent && curParent.ownerDocument && curParent !== commonAncestor) { + elsToSendMouseenter.push(curParent) + curParent = curParent.parentNode + } + + elsToSendMouseenter.reverse() + + pointerenter = () => { + return elsToSendMouseenter.forEach((elToSend) => { + sendPointerenter(elToSend, _.extend({}, defaultPointerOptions, { relatedTarget: lastHoveredEl })) + }) + } + + mouseenter = () => { + return elsToSendMouseenter.forEach((elToSend) => { + sendMouseenter(elToSend, _.extend({}, defaultMouseOptions, { relatedTarget: lastHoveredEl })) + }) + } + } + + } + + // if (!Cypress.config('mousemoveBeforeMouseover') && el) { + pointermove = () => { + return sendPointermove(el, defaultPointerOptions) + } + + mousemove = () => { + return sendMousemove(el, defaultMouseOptions) + } + + const events = [] + + pointerout() + pointerleave() + events.push({ pointerover: pointerover() }) + pointerenter() + mouseout() + mouseleave() + events.push({ mouseover: mouseover() }) + mouseenter() + state('mouseLastHoveredEl', $elements.isAttachedEl(el) ? el : null) + state('mouseCoords', { x, y }) + events.push({ pointermove: pointermove() }) + events.push({ mousemove: mousemove() }) + + return events + + }, + + /** + * + * @param {Coords} coords + * @param {HTMLElement} forceEl + * @returns {HTMLElement} + */ + getElAtCoordsOrForce ({ x, y, doc }, forceEl) { + if (forceEl) { + return forceEl + } + + const el = doc.elementFromPoint(x, y) + + // mouse._mouseMoveEvents(el, { x, y }) + + return el + + }, + + /** + * + * @param {Coords} coords + * @param {HTMLElement} forceEl + */ + moveToCoordsOrForce (coords, forceEl) { + if (forceEl) { + return forceEl + } + + const { el } = mouse.mouseMove(coords) + + return el + }, + + /** + * @param {Coords} coords + * @param {HTMLElement} forceEl + */ + _mouseDownEvents (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + + const { x, y } = coords + const el = mouse.moveToCoordsOrForce(coords, forceEl) + + const win = $dom.getWindowByElement(el) + + const defaultOptions = mouse._getDefaultMouseOptions(x, y, win) + + const pointerEvtOptions = _.extend({}, defaultOptions, { + button: 0, + which: 1, + buttons: 1, + detail: 0, + pressure: 0.5, + pointerType: 'mouse', + pointerId: 1, + isPrimary: true, + relatedTarget: null, + }, pointerEvtOptionsExtend) + + const mouseEvtOptions = _.extend({}, defaultOptions, { + button: 0, + which: 1, + buttons: 1, + detail: 1, + }, mouseEvtOptionsExtend) + + // TODO: pointer events should have fractional coordinates, not rounded + let pointerdownProps = sendPointerdown( + el, + pointerEvtOptions + ) - if (modifiers.length) { - props.modifiers = modifiers.join(', ') - } + const pointerdownPrevented = pointerdownProps.preventedDefault + const elIsDetached = $elements.isDetachedEl(el) + + if (pointerdownPrevented || elIsDetached) { + let reason = 'pointerdown was cancelled' + + if (elIsDetached) { + reason = 'Element was detached' + } - return props - }, + return { + pointerdownProps, + mousedownProps: { + skipped: formatReasonNotFired(reason), + }, + } + } + + let mousedownProps = sendMousedown(el, mouseEvtOptions) - mouseUp ($elToClick, fromViewport) { - const el = $elToClick.get(0) + return { + pointerdownProps, + mousedownProps, + } - const win = $dom.getWindowByElement(el) + }, - const mupEvtProps = $Keyboard.mixinModifiers({ - bubbles: true, - cancelable: true, - view: win, - clientX: fromViewport.x, - clientY: fromViewport.y, - buttons: 0, - detail: 1, - }) + mouseDown (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { - const mupEvt = new MouseEvent('mouseup', mupEvtProps) + const $previouslyFocused = focused.getFocused() - //# ensure this property exists on older chromium versions - if (mupEvt.buttons == null) { - mupEvt.buttons = 0 - } + const mouseDownEvents = mouse._mouseDownEvents(coords, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) - mupEvt.stopPropagation = function (...args) { - this._hasStoppedPropagation = true + // el we just send pointerdown + const el = mouseDownEvents.pointerdownProps.el - return stopPropagation.apply(this, args) - } + if (mouseDownEvents.pointerdownProps.preventedDefault || mouseDownEvents.mousedownProps.preventedDefault || !$elements.isAttachedEl(el)) { + return mouseDownEvents + } - const canceled = !el.dispatchEvent(mupEvt) + if ($elements.isInput(el) || $elements.isTextarea(el) || $elements.isContentEditable(el)) { + if (!$elements.isNeedSingleValueChangeInputElement(el)) { + $selection.moveSelectionToEnd(el) + } + } - const props = { - preventedDefault: canceled, - stoppedPropagation: !!mupEvt._hasStoppedPropagation, - } + //# retrieve the first focusable $el in our parent chain + const $elToFocus = $elements.getFirstFocusableEl($(el)) - const modifiers = $Keyboard.activeModifiers() + if (focused.needsFocus($elToFocus, $previouslyFocused)) { - if (modifiers.length) { - props.modifiers = modifiers.join(', ') - } + if ($dom.isWindow($elToFocus)) { + // if the first focusable element from the click + // is the window, then we can skip the focus event + // since the user has clicked a non-focusable element + const $focused = focused.getFocused() - return props - }, + if ($focused) { + focused.fireBlur($focused.get(0)) + } + } else { + // the user clicked inside a focusable element + focused.fireFocus($elToFocus.get(0)) + } - click ($elToClick, fromViewport) { - const el = $elToClick.get(0) + } - const win = $dom.getWindowByElement(el) + return mouseDownEvents + }, - const clickEvtProps = $Keyboard.mixinModifiers({ - bubbles: true, - cancelable: true, - view: win, - clientX: fromViewport.x, - clientY: fromViewport.y, - buttons: 0, - detail: 1, - }) + /** + * @param {HTMLElement} el + * @param {Window} win + * @param {Coords} fromViewport + * @param {HTMLElement} forceEl + */ + mouseUp (fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + debug('mouseUp', { fromViewport, forceEl, skipMouseEvent }) - const clickEvt = new MouseEvent('click', clickEvtProps) + return mouse._mouseUpEvents(fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + }, - //# ensure this property exists on older chromium versions - if (clickEvt.buttons == null) { - clickEvt.buttons = 0 - } + mouseClick (fromViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + debug('mouseClick', { fromViewport, forceEl }) + const mouseDownEvents = mouse.mouseDown(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) - clickEvt.stopPropagation = function (...args) { - this._hasStoppedPropagation = true + const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault - return stopPropagation.apply(this, args) - } + const mouseUpEvents = mouse.mouseUp(fromViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) - const canceled = !el.dispatchEvent(clickEvt) + const skipClickEvent = $elements.isDetachedEl(mouseDownEvents.pointerdownProps.el) - const props = { - preventedDefault: canceled, - stoppedPropagation: !!clickEvt._hasStoppedPropagation, - } + const mouseClickEvents = mouse._mouseClickEvents(fromViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend) + + return _.extend({}, mouseDownEvents, mouseUpEvents, mouseClickEvents) + + }, + + /** + * @param {Coords} fromViewport + * @param {HTMLElement} el + * @param {HTMLElement} forceEl + * @param {Window} win + */ + _mouseUpEvents (fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + + const win = state('window') + + let defaultOptions = mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win) + + const pointerEvtOptions = _.extend({}, defaultOptions, { + buttons: 0, + pressure: 0.5, + pointerType: 'mouse', + pointerId: 1, + isPrimary: true, + detail: 0, + }, pointerEvtOptionsExtend) + + let mouseEvtOptions = _.extend({}, defaultOptions, { + buttons: 0, + detail: 1, + }, mouseEvtOptionsExtend) + + const el = mouse.moveToCoordsOrForce(fromViewport, forceEl) + + let pointerupProps = sendPointerup(el, pointerEvtOptions) + + if (skipMouseEvent || $elements.isDetachedEl($(el))) { + return { + pointerupProps, + mouseupProps: { + skipped: formatReasonNotFired('Previous event cancelled'), + }, + } + } + + let mouseupProps = sendMouseup(el, mouseEvtOptions) + + return { + pointerupProps, + mouseupProps, + } + + }, + + _mouseClickEvents (fromViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend = {}) { + const el = mouse.moveToCoordsOrForce(fromViewport, forceEl) + + const win = $dom.getWindowByElement(el) + + const defaultOptions = mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win) + + const clickEventOptions = _.extend({}, defaultOptions, { + buttons: 0, + detail: 1, + }, mouseEvtOptionsExtend) + + if (skipClickEvent) { + return { + clickProps: { + skipped: formatReasonNotFired('Element was detached'), + }, + } + } + + let clickProps = sendClick(el, clickEventOptions) + + return { clickProps } + }, + + _contextmenuEvent (fromViewport, forceEl, mouseEvtOptionsExtend) { + const el = mouse.moveToCoordsOrForce(fromViewport, forceEl) + + const win = $dom.getWindowByElement(el) + const defaultOptions = mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win) + + const mouseEvtOptions = _.extend({}, defaultOptions, { + button: 2, + buttons: 2, + detail: 0, + which: 3, + }, mouseEvtOptionsExtend) + + let contextmenuProps = sendContextmenu(el, mouseEvtOptions) + + return { contextmenuProps } + }, + + dblclick (fromViewport, forceEl, mouseEvtOptionsExtend = {}) { + const click = (clickNum) => { + const clickEvents = mouse.mouseClick(fromViewport, forceEl, {}, { detail: clickNum }) + + return clickEvents + } + + const clickEvents1 = click(1) + const clickEvents2 = click(2) + + const el = mouse.moveToCoordsOrForce(fromViewport, forceEl) + const win = $dom.getWindowByElement(el) + + const dblclickEvtProps = _.extend(mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win), { + buttons: 0, + detail: 2, + }, mouseEvtOptionsExtend) + + let dblclickProps = sendDblclick(el, dblclickEvtProps) + + return { clickEvents1, clickEvents2, dblclickProps } + }, - const modifiers = $Keyboard.activeModifiers() + rightclick (fromViewport, forceEl) { - if (modifiers.length) { - props.modifiers = modifiers.join(', ') + const pointerEvtOptionsExtend = { + button: 2, + buttons: 2, + which: 3, + } + const mouseEvtOptionsExtend = { + button: 2, + buttons: 2, + which: 3, + } + + const mouseDownEvents = mouse.mouseDown(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + + const contextmenuEvent = mouse._contextmenuEvent(fromViewport, forceEl) + + const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault + + const mouseUpEvents = mouse.mouseUp(fromViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + + const clickEvents = _.extend({}, mouseDownEvents, mouseUpEvents) + + return _.extend({}, { clickEvents, contextmenuEvent }) + + }, + } + + return mouse +} + +const { stopPropagation } = window.MouseEvent.prototype + +const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, constructor) => { + evtOptions = _.extend({}, evtOptions, { bubbles, cancelable }) + const _eventModifiers = $Keyboard.fromModifierEventOptions(evtOptions) + const modifiers = $Keyboard.modifiersToString(_eventModifiers) + + const evt = new constructor(evtName, _.extend({}, evtOptions, { bubbles, cancelable })) + + if (bubbles) { + evt.stopPropagation = function (...args) { + evt._hasStoppedPropagation = true + + return stopPropagation.apply(this, ...args) } + } + + const preventedDefault = !el.dispatchEvent(evt) + + return { + stoppedPropagation: !!evt._hasStoppedPropagation, + preventedDefault, + el, + modifiers, + } + +} + +const sendPointerEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { + const constructor = el.ownerDocument.defaultView.PointerEvent + + return sendEvent(evtName, el, evtOptions, bubbles, cancelable, constructor) +} +const sendMouseEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { + // IE doesn't have event constructors, so you should use document.createEvent('mouseevent') + // https://dom.spec.whatwg.org/#dom-document-createevent + const constructor = el.ownerDocument.defaultView.MouseEvent + + return sendEvent(evtName, el, evtOptions, bubbles, cancelable, constructor) +} - return props - }, +const sendPointerup = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerup', true, true) +} +const sendPointerdown = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerdown', true, true) +} +const sendPointermove = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointermove', true, true) +} +const sendPointerover = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerover', true, true) +} +const sendPointerenter = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerenter', false, false) +} +const sendPointerleave = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerleave', false, false) +} +const sendPointerout = (el, evtOptions) => { + return sendPointerEvent(el, evtOptions, 'pointerout', true, true) +} + +const sendMouseup = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseup', true, true) +} +const sendMousedown = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mousedown', true, true) +} +const sendMousemove = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mousemove', true, true) +} +const sendMouseover = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseover', true, true) +} +const sendMouseenter = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseenter', false, false) +} +const sendMouseleave = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseleave', false, false) +} +const sendMouseout = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'mouseout', true, true) +} +const sendClick = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'click', true, true) +} +const sendDblclick = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'dblclick', true, true) +} +const sendContextmenu = (el, evtOptions) => { + return sendMouseEvent(el, evtOptions, 'contextmenu', true, true) +} + +const formatReasonNotFired = (reason) => { + return `⚠️ not fired (${reason})` +} + +const toCoordsEventOptions = (x, y, win) => { + + // these are the coords from the document, ignoring scroll position + const fromDocCoords = $elements.getFromDocCoords(x, y, win) + + return { + clientX: x, + clientY: y, + screenX: x, + screenY: y, + x, + y, + pageX: fromDocCoords.x, + pageY: fromDocCoords.y, + layerX: fromDocCoords.x, + layerY: fromDocCoords.y, + } +} + +module.exports = { + create, } diff --git a/packages/driver/src/cypress.coffee b/packages/driver/src/cypress.coffee index 5a1608ae59ae..0c345a918bde 100644 --- a/packages/driver/src/cypress.coffee +++ b/packages/driver/src/cypress.coffee @@ -16,7 +16,7 @@ $Cookies = require("./cypress/cookies") $Cy = require("./cypress/cy") $Events = require("./cypress/events") $SetterGetter = require("./cypress/setter_getter") -$Keyboard = require("./cypress/keyboard") +$Keyboard = require("./cy/keyboard") $Log = require("./cypress/log") $Location = require("./cypress/location") $LocalStorage = require("./cypress/local_storage") diff --git a/packages/driver/src/cypress/cy.coffee b/packages/driver/src/cypress/cy.coffee index 15d5554f8da3..badcdaf9c313 100644 --- a/packages/driver/src/cypress/cy.coffee +++ b/packages/driver/src/cypress/cy.coffee @@ -13,6 +13,8 @@ $Events = require("./events") $Errors = require("../cy/errors") $Ensures = require("../cy/ensures") $Focused = require("../cy/focused") +$Mouse = require("../cy/mouse") +$Keyboard = require("../cy/keyboard") $Location = require("../cy/location") $Assertions = require("../cy/assertions") $Listeners = require("../cy/listeners") @@ -80,6 +82,8 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> jquery = $jQuery.create(state) location = $Location.create(state) focused = $Focused.create(state) + keyboard = $Keyboard.create(state) + mouse = $Mouse.create(state, focused) timers = $Timers.create() { expect } = $Chai.create(specWindow, assertions.assert) @@ -648,6 +652,11 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> fireFocus: focused.fireFocus fireBlur: focused.fireBlur + internal: { + mouse: mouse + keyboard: keyboard + } + ## timer sync methods pauseTimers: timers.pauseTimers diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index 576efa39d462..334ad078a707 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -121,8 +121,8 @@ module.exports = { invalid_argument: "#{cmd('clearLocalStorage')} must be called with either a string or regular expression." click: - multiple_elements: "#{cmd('click')} can only be called on a single element. Your subject contained {{num}} elements. Pass { multiple: true } if you want to serially click each element." - on_select_element: "#{cmd('click')} cannot be called on a element. Use #{cmd('select')} command instead to change the value." clock: already_created: "#{cmd('clock')} can only be called once per test. Use the clock returned from the previous call." diff --git a/packages/driver/src/dom/coordinates.js b/packages/driver/src/dom/coordinates.js index fffc727c918a..1cadf4c40bbb 100644 --- a/packages/driver/src/dom/coordinates.js +++ b/packages/driver/src/dom/coordinates.js @@ -1,4 +1,5 @@ const $window = require('./window') +const $elements = require('./elements') const getElementAtPointFromViewport = (doc, x, y) => { return doc.elementFromPoint(x, y) @@ -11,6 +12,7 @@ const getElementPositioning = ($el) => { const el = $el[0] const win = $window.getWindowByElement(el) + let autFrame // properties except for width / height // are relative to the top left of the viewport @@ -22,12 +24,52 @@ const getElementPositioning = ($el) => { // const rect = el.getBoundingClientRect() const rect = el.getClientRects()[0] || el.getBoundingClientRect() - const center = getCenterCoordinates(rect) + function calculateAutIframeCoords (rect, el) { + let x = 0 //rect.left + let y = 0 //rect.top + let curWindow = el.ownerDocument.defaultView + let frame + + const isAutIframe = (win) => { + !$elements.getNativeProp(win.parent, 'frameElement') + } + + while (!isAutIframe(curWindow) && window.parent !== window) { + frame = $elements.getNativeProp(curWindow, 'frameElement') + curWindow = curWindow.parent + + if (curWindow && $elements.getNativeProp(curWindow, 'frameElement')) { + const frameRect = frame.getBoundingClientRect() + + x += frameRect.left + y += frameRect.top + } + // Cypress will sometimes miss the Iframe if coords are too small + // remove this when test-runner is extracted out + } + + autFrame = curWindow + + const ret = { + left: x + rect.left, + top: y + rect.top, + right: x + rect.right, + bottom: y + rect.top, + width: rect.width, + height: rect.height, + } + + return ret + + } + + const rectCenter = getCenterCoordinates(rect) + + const rectFromAut = calculateAutIframeCoords(rect, el) + const rectFromAutCenter = getCenterCoordinates(rectFromAut) // add the center coordinates // because its useful to any caller - const topCenter = center.y - const leftCenter = center.x return { scrollTop: el.scrollTop, @@ -39,14 +81,15 @@ const getElementPositioning = ($el) => { left: rect.left, right: rect.right, bottom: rect.bottom, - topCenter, - leftCenter, + topCenter: rectCenter.y, + leftCenter: rectCenter.x, + doc: win.document, }, fromWindow: { - top: rect.top + win.pageYOffset, - left: rect.left + win.pageXOffset, - topCenter: topCenter + win.pageYOffset, - leftCenter: leftCenter + win.pageXOffset, + top: rectFromAut.top + autFrame.pageYOffset, + left: rectFromAut.left + autFrame.pageXOffset, + topCenter: rectFromAutCenter.y + autFrame.pageYOffset, + leftCenter: rectFromAutCenter.x + autFrame.pageXOffset, }, } } diff --git a/packages/driver/src/dom/elements.js b/packages/driver/src/dom/elements.js index 1f4a6a80c446..6ffa2ace842f 100644 --- a/packages/driver/src/dom/elements.js +++ b/packages/driver/src/dom/elements.js @@ -1,21 +1,3 @@ -/* eslint-disable - default-case, - no-case-declarations, - no-cond-assign, - no-const-assign, - no-dupe-keys, - no-undef, - one-var, - prefer-rest-params, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const _ = require('lodash') const $ = require('jquery') const $jquery = require('./jquery') @@ -181,12 +163,14 @@ const _getType = function () { const nativeGetters = { value: _getValue, - selectionStart: descriptor('HTMLInputElement', 'selectionStart').get, isContentEditable: _isContentEditable, isCollapsed: descriptor('Selection', 'isCollapsed').get, selectionStart: _getSelectionStart, selectionEnd: _getSelectionEnd, type: _getType, + activeElement: descriptor('Document', 'activeElement').get, + body: descriptor('Document', 'body').get, + frameElement: Object.getOwnPropertyDescriptor(window, 'frameElement').get, } const nativeSetters = { @@ -263,7 +247,7 @@ const setNativeProp = function (obj, prop, val) { if (!nativeProp) { const fns = _.keys(nativeSetters).join(', ') - throw new Error(`attempted to use a native setter prop called: ${fn}. Available props are: ${fns}`) + throw new Error(`attempted to use a native setter prop called: ${prop}. Available props are: ${fns}`) } let retProp = nativeProp.call(obj, val) @@ -329,6 +313,10 @@ const isBody = (el) => { return getTagName(el) === 'body' } +const isIframe = (el) => { + return getTagName(el) === 'iframe' +} + const isHTML = (el) => { return getTagName(el) === 'html' } @@ -402,6 +390,34 @@ const isAncestor = ($el, $maybeAncestor) => { return $el.parents().index($maybeAncestor) >= 0 } +const getFirstCommonAncestor = (el1, el2) => { + const el1Ancestors = [el1].concat(getAllParents(el1)) + let curEl = el2 + + while (curEl) { + if (el1Ancestors.indexOf(curEl) !== -1) { + return curEl + } + + curEl = curEl.parentNode + } + + return curEl +} + +const getAllParents = (el) => { + let curEl = el.parentNode + const allParents = [] + + while (curEl) { + allParents.push(curEl) + curEl = curEl.parentNode + } + + return allParents + +} + const isChild = ($el, $maybeChild) => { return $el.children().index($maybeChild) >= 0 } @@ -457,6 +473,20 @@ const isAttached = function ($el) { return $document.hasActiveWindow(doc) && _.every(els, isIn) } +/** + * @param {HTMLElement} el + */ +const isDetachedEl = (el) => { + return !isAttachedEl(el) +} + +/** + * @param {HTMLElement} el + */ +const isAttachedEl = function (el) { + return isAttached($(el)) +} + const isSame = function ($el1, $el2) { const el1 = $jquery.unwrap($el1) const el2 = $jquery.unwrap($el2) @@ -566,6 +596,13 @@ const isScrollable = ($el) => { return false } +const getFromDocCoords = (x, y, win) => { + return { + x: win.scrollX + x, + y: win.scrollY + y, + } +} + const isDescendent = ($el1, $el2) => { if (!$el2) { return false @@ -618,6 +655,14 @@ const getFirstFocusableEl = ($el) => { return getFirstFocusableEl($el.parent()) } +const getActiveElByDocument = (doc) => { + const activeEl = getNativeProp(doc, 'activeElement') + + if (activeEl) return activeEl + + return getNativeProp(doc, 'body') +} + const getFirstParentWithTagName = ($el, tagName) => { // return null if we're at body/html/document // cuz that means nothing has fixed position @@ -845,6 +890,10 @@ module.exports = { isDetached, + isAttachedEl, + + isDetachedEl, + isAncestor, isChild, @@ -869,6 +918,8 @@ module.exports = { isInput, + isIframe, + isTextarea, isType, @@ -895,12 +946,18 @@ module.exports = { getElements, + getFromDocCoords, + getFirstFocusableEl, + getActiveElByDocument, + getContainsSelector, getFirstDeepestElement, + getFirstCommonAncestor, + getFirstParentWithTagName, getFirstFixedOrStickyPositionParent, diff --git a/packages/driver/src/dom/window.js b/packages/driver/src/dom/window.js index 6c27324bebcf..b6c91a675f3f 100644 --- a/packages/driver/src/dom/window.js +++ b/packages/driver/src/dom/window.js @@ -1,6 +1,10 @@ const $jquery = require('./jquery') const $document = require('./document') +/** + * @param {HTMLElement} el + * @returns {Window} + */ const getWindowByElement = function (el) { if (isWindow(el)) { return el diff --git a/packages/driver/test/cypress/fixtures/dom.html b/packages/driver/test/cypress/fixtures/dom.html index f1a56c0c3ee4..e41f0fc685bf 100644 --- a/packages/driver/test/cypress/fixtures/dom.html +++ b/packages/driver/test/cypress/fixtures/dom.html @@ -134,13 +134,6 @@ -
- Sakura - Naruto - - -
-
@@ -549,6 +542,12 @@ +
+ Sakura + Naruto + + +
iframe:
diff --git a/packages/driver/test/cypress/fixtures/issue-2956.html b/packages/driver/test/cypress/fixtures/issue-2956.html new file mode 100644 index 000000000000..4536d323874b --- /dev/null +++ b/packages/driver/test/cypress/fixtures/issue-2956.html @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+ +
+ +
+ +
+ + + + + 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 31a4b692d438..028494e1dc80 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -1,11 +1,31 @@ const $ = Cypress.$.bind(Cypress) const { _ } = Cypress const { Promise } = Cypress +const chaiSubset = require('chai-subset') + +chai.use(chaiSubset) 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 allMouseEvents = [...mouseClickEvents, ...mouseHoverEvents, ...focusEvents] + describe('src/cy/commands/actions/click', () => { before(() => { cy @@ -18,7 +38,10 @@ describe('src/cy/commands/actions/click', () => { beforeEach(function () { const doc = cy.state('document') - return $(doc.body).empty().html(this.body) + $(doc.body).empty().html(this.body) + // scroll back to top of page before every test + // since this is a side-effect + doc.documentElement.scrollTop = 0 }) context('#click', () => { @@ -144,8 +167,23 @@ describe('src/cy/commands/actions/click', () => { const $btn = cy.$$('#button') _.each('mousedown mouseup click'.split(' '), (event) => { - return $btn.get(0).addEventListener(event, () => { - return events.push(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) }) }) @@ -229,6 +267,7 @@ describe('src/cy/commands/actions/click', () => { .then(() => { expect(onError).calledOnce }) + }) }) @@ -267,25 +306,37 @@ describe('src/cy/commands/actions/click', () => { }) it('will send all events even mousedown is defaultPrevented', () => { - const events = [] const $btn = cy.$$('#button') $btn.get(0).addEventListener('mousedown', (e) => { e.preventDefault() - expect(e.defaultPrevented).to.be.true }) - _.each('mouseup click'.split(' '), (event) => { - return $btn.get(0).addEventListener(event, () => { - return events.push(event) - }) - }) + attachMouseClickListeners({ $btn }) - cy.get('#button').click().then(() => { - expect(events).to.deep.eq(['mouseup', 'click']) + 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') + + $btn.get(0).addEventListener('pointerdown', (e) => { + e.preventDefault() + + expect(e.defaultPrevented).to.be.true }) + + attachMouseClickListeners({ $btn }) + + 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) => { @@ -304,14 +355,15 @@ describe('src/cy/commands/actions/click', () => { }) }) - it('causes focusable elements to receive focus', (done) => { - const $text = cy.$$(':text:first') + it('causes focusable elements to receive focus', () => { - $text.focus(() => { - done() - }) + const el = cy.$$(':text:first') + + attachFocusListeners({ el }) - cy.get(':text:first').click() + cy.get(':text:first').click().should('have.focus') + + cy.getAll('el', 'focus focusin').each(shouldBeCalledOnce) }) it('does not fire a focus, mouseup, or click event when element has been removed on mousedown', () => { @@ -319,26 +371,69 @@ describe('src/cy/commands/actions/click', () => { $btn.on('mousedown', function () { // synchronously remove this button - return $(this).remove() + $(this).remove() }) $btn.on('focus', () => { - return fail('should not have gotten focus') + fail('should not have gotten focus') }) $btn.on('focusin', () => { - return fail('should not have gotten focusin') + fail('should not have gotten focusin') }) $btn.on('mouseup', () => { - return fail('should not have gotten mouseup') + fail('should not have gotten mouseup') }) $btn.on('click', () => { - return fail('should not have gotten click') + fail('should not have gotten click') + }) + + cy.contains('button').click() + }) + + it('events when element removed on pointerdown', () => { + const btn = cy.$$('button:first') + const div = cy.$$('div#tabindex') + + attachFocusListeners({ btn }) + attachMouseClickListeners({ btn, div }) + attachMouseHoverListeners({ div }) + + btn.on('pointerdown', () => { + // synchronously remove this button + + btn.remove() + }) + + // return + 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') + 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) }) it('does not fire a click when element has been removed on mouseup', () => { @@ -346,19 +441,54 @@ describe('src/cy/commands/actions/click', () => { $btn.on('mouseup', function () { // synchronously remove this button - return $(this).remove() + $(this).remove() }) $btn.on('click', () => { - return fail('should not have gotten click') + fail('should not have gotten click') }) cy.contains('button').click() }) - it('silences errors on unfocusable elements', () => { - cy.$$('div:first') + 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 }) }) @@ -366,7 +496,7 @@ describe('src/cy/commands/actions/click', () => { let blurred = false cy.$$('input:first').blur(() => { - return blurred = true + blurred = true }) cy @@ -423,7 +553,7 @@ describe('src/cy/commands/actions/click', () => { }) const clicked = cy.spy(() => { - return stop() + stop() }) const $anchors = cy.$$('#sequential-clicks a') @@ -439,7 +569,7 @@ describe('src/cy/commands/actions/click', () => { // is called const timeout = cy.spy(cy.timeout) - return _.delay(() => { + _.delay(() => { // and we should have stopped clicking after 3 expect(clicked.callCount).to.eq(3) @@ -454,24 +584,23 @@ describe('src/cy/commands/actions/click', () => { }) it('serially clicks a collection', () => { - let clicks = 0 + const throttled = cy.stub().as('clickcount') // create a throttled click function // which proves we are clicking serially - const throttled = _.throttle(() => { - return clicks += 1 - } - , 5, { leading: false }) + const handleClick = cy.stub() + .callsFake(_.throttle(throttled, 0, { leading: false })) + .as('handleClick') - const anchors = cy.$$('#sequential-clicks a') + const $anchors = cy.$$('#sequential-clicks a') - anchors.click(throttled) + $anchors.on('click', handleClick) - // make sure we're clicking multiple anchors - expect(anchors.length).to.be.gt(1) + // make sure we're clicking multiple $anchors + expect($anchors.length).to.be.gt(1) - cy.get('#sequential-clicks a').click({ multiple: true }).then(($anchors) => { - expect($anchors.length).to.eq(clicks) + cy.get('#sequential-clicks a').click({ multiple: true }).then(($els) => { + expect($els).to.have.length(throttled.callCount) }) }) @@ -622,6 +751,28 @@ describe('src/cy/commands/actions/click', () => { }) }) + it('can click inside an iframe', () => { + cy.get('iframe') + .should(($iframe) => { + // wait for iframe to load + expect($iframe.contents().find('body').html()).ok + }) + .then(($iframe) => { + // cypress does not wrap this as a DOM element (does not wrap in jquery) + // return cy.wrap($iframe[0].contentDocument.body) + return cy.wrap($iframe.contents().find('body')) + }) + + .within(() => { + cy.get('a#hashchange') + // .should($el => $el[0].click()) + .click() + }) + .then(($body) => { + expect($body[0].ownerDocument.defaultView.location.hash).eq('#hashchange') + }) + }) + describe('actionability', () => { it('can click on inline elements that wrap lines', () => { @@ -636,7 +787,7 @@ describe('src/cy/commands/actions/click', () => { const scrolled = [] cy.on('scrolled', ($el, type) => { - return scrolled.push(type) + scrolled.push(type) }) cy @@ -650,17 +801,14 @@ describe('src/cy/commands/actions/click', () => { const scrolled = [] cy.on('scrolled', ($el, type) => { - return scrolled.push(type) + scrolled.push(type) }) cy.viewport(1000, 660) const $body = cy.$$('body') - $body.css({ - padding: 0, - margin: 0, - }).children().remove() + $body.children().remove() const $wrap = $('
') .attr('id', 'flex-wrap') @@ -764,15 +912,15 @@ describe('src/cy/commands/actions/click', () => { let clicked = false cy.on('scrolled', ($el, type) => { - return scrolled.push(type) + scrolled.push(type) }) cy.on('command:retry', () => { - return retried = true + retried = true }) $btn.on('click', () => { - return clicked = true + clicked = true }) cy.get('#button-covered-in-span').click({ force: true }).then(() => { @@ -801,7 +949,7 @@ describe('src/cy/commands/actions/click', () => { let retried = false cy.on('scrolled', ($el, type) => { - return scrolled.push(type) + scrolled.push(type) }) cy.on('command:retry', _.after(3, () => { @@ -822,9 +970,12 @@ describe('src/cy/commands/actions/click', () => { }) it('scrolls the window past a fixed position element when being covered', () => { + const spy = cy.spy().as('mousedown') + $('') .attr('id', 'button-covered-in-nav') .appendTo(cy.$$('#fixed-nav-test')) + .mousedown(spy) $('').css({ position: 'fixed', @@ -838,14 +989,17 @@ describe('src/cy/commands/actions/click', () => { const scrolled = [] cy.on('scrolled', ($el, type) => { - return scrolled.push(type) + scrolled.push(type) }) // - element scrollIntoView // - element scrollIntoView (retry animation coords) // - window - cy.get('#button-covered-in-nav').click().then(() => { + cy.get('#button-covered-in-nav').click() + .then(() => { expect(scrolled).to.deep.eq(['element', 'element', 'window']) + expect(spy.args[0][0]).property('clientX').closeTo(60, 2) + expect(spy.args[0][0]).property('clientY').eq(68) }) }) @@ -875,7 +1029,7 @@ describe('src/cy/commands/actions/click', () => { const scrolled = [] cy.on('scrolled', ($el, type) => { - return scrolled.push(type) + scrolled.push(type) }) // - element scrollIntoView @@ -932,7 +1086,7 @@ describe('src/cy/commands/actions/click', () => { const scrolled = [] cy.on('scrolled', ($el, type) => { - return scrolled.push(type) + scrolled.push(type) }) // - element scrollIntoView @@ -966,7 +1120,7 @@ describe('src/cy/commands/actions/click', () => { let clicks = 0 $btn.on('click', () => { - return clicks += 1 + clicks += 1 }) cy.on('command:retry', _.after(3, () => { @@ -985,7 +1139,7 @@ describe('src/cy/commands/actions/click', () => { let retries = 0 cy.on('command:retry', () => { - return retries += 1 + retries += 1 }) cy.stub(cy, 'ensureElementIsNotAnimating') @@ -1062,13 +1216,13 @@ describe('src/cy/commands/actions/click', () => { } }) - return null + null }) it('eventually passes the assertion', () => { cy.$$('button:first').click(function () { _.delay(() => { - return $(this).addClass('clicked') + $(this).addClass('clicked') } , 50) @@ -1088,7 +1242,7 @@ describe('src/cy/commands/actions/click', () => { it('eventually passes the assertion on multiple buttons', () => { cy.$$('button').click(function () { _.delay(() => { - return $(this).addClass('clicked') + $(this).addClass('clicked') } , 50) @@ -1248,8 +1402,6 @@ describe('src/cy/commands/actions/click', () => { it('can pass options along with position', (done) => { const $btn = $('').attr('id', 'button-covered-in-span').css({ height: 100, width: 100 }).prependTo(cy.$$('body')) - $('span').css({ position: 'absolute', left: $btn.offset().left + 80, top: $btn.offset().top + 80, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).appendTo(cy.$$('body')) - $btn.on('click', () => { done() }) @@ -1282,8 +1434,6 @@ describe('src/cy/commands/actions/click', () => { it('can pass options along with x, y', (done) => { const $btn = $('').attr('id', 'button-covered-in-span').css({ height: 100, width: 100 }).prependTo(cy.$$('body')) - $('span').css({ position: 'absolute', left: $btn.offset().left + 50, top: $btn.offset().top + 65, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).appendTo(cy.$$('body')) - $btn.on('click', () => { done() }) @@ -1348,8 +1498,8 @@ describe('src/cy/commands/actions/click', () => { const input = cy.$$('input:first') _.each('focus focusin mousedown mouseup click'.split(' '), (event) => { - return input.get(0).addEventListener(event, () => { - return events.push(event) + input.get(0).addEventListener(event, () => { + events.push(event) }) }) @@ -1403,27 +1553,17 @@ describe('src/cy/commands/actions/click', () => { expect(onFocus).not.to.be.called }) }) - }) - - // it "events", -> - // $btn = cy.$$("button") - // win = $(cy.state("window")) - - // _.each {"btn": btn, "win": win}, (type, key) -> - // _.each "focus mousedown mouseup click".split(" "), (event) -> - // # _.each "focus focusin focusout mousedown mouseup click".split(" "), (event) -> - // type.get(0).addEventListener event, (e) -> - // if key is "btn" - // # e.preventDefault() - // e.stopPropagation() - - // console.log "#{key} #{event}", e - // $btn.on "mousedown", (e) -> - // console.log("btn mousedown") - // e.preventDefault() - - // win.on "mousedown", -> console.log("win mousedown") + it('will fire pointerdown event', () => { + // cy.get('input').eq(1).click() + // cy.get('input').eq(2).click() + // cy.get('input').eq(4).click() + cy.get('textarea:first').click() + // cy.get('input').eq(3).click() + cy.get('input:first').click() + // cy.get('input').eq(1).click() + }) + }) describe('errors', () => { beforeEach(function () { @@ -1434,10 +1574,10 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { this.lastLog = log - return this.logs.push(log) + this.logs.push(log) }) - return null + null }) it('throws when not a dom subject', (done) => { @@ -1449,15 +1589,14 @@ describe('src/cy/commands/actions/click', () => { }) it('throws when attempting to click multiple elements', (done) => { - const num = cy.$$('button').length cy.on('fail', (err) => { - expect(err.message).to.eq(`cy.click() can only be called on a single element. Your subject contained ${num} elements. Pass { multiple: true } if you want to serially click each element.`) + expect(err.message).to.eq('cy.click() can only be called on a single element. Your subject contained 4 elements. Pass { multiple: true } if you want to serially click each element.') done() }) - cy.get('button').click() + cy.get('.badge-multi').click() }) it('throws when subject is not in the document', (done) => { @@ -1492,16 +1631,23 @@ describe('src/cy/commands/actions/click', () => { cy.click() }) + // Array(1).fill().map(()=> it('throws when any member of the subject isnt visible', function (done) { - cy.timeout(250) + + // sometimes the command will timeout early with + // Error: coordsHistory must be at least 2 sets of coords + cy.timeout(300) cy.$$('#three-buttons button').show().last().hide() cy.on('fail', (err) => { - const { lastLog } = this + const { lastLog, logs } = this + const logsArr = logs.map((log) => { + return log.get().consoleProps() + }) - expect(this.logs.length).to.eq(4) + expect(logsArr).to.have.length(4) expect(lastLog.get('error')).to.eq(err) expect(err.message).to.include('cy.click() failed because this element is not visible') @@ -1676,7 +1822,7 @@ describe('src/cy/commands/actions/click', () => { let clicks = 0 cy.$$('button:first').on('click', () => { - return clicks += 1 + clicks += 1 }) cy.on('fail', (err) => { @@ -1697,7 +1843,7 @@ describe('src/cy/commands/actions/click', () => { expect(err.message).not.to.include('undefined') expect(lastLog.get('name')).to.eq('assert') expect(lastLog.get('state')).to.eq('failed') - expect(lastLog.get('error')).to.be.an.instanceof(chai.AssertionError) + expect(lastLog.get('error')).to.be.an.instanceof(window.chai.AssertionError) done() }) @@ -1723,10 +1869,10 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { this.lastLog = log - return this.logs.push(log) + this.logs.push(log) }) - return null + null }) it('logs immediately before resolving', (done) => { @@ -1781,7 +1927,7 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'click') { - return clicks.push(log) + clicks.push(log) } }) @@ -1798,7 +1944,7 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'click') { - return logs.push(log) + logs.push(log) } }) @@ -1823,12 +1969,12 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'click') { - return logs.push(log) + logs.push(log) } }) cy.get('#three-buttons button').click({ multiple: true }).then(() => { - return _.each(logs, (log) => { + _.each(logs, (log) => { expect(log.get('state')).to.eq('passed') expect(log.get('ended')).to.be.true @@ -1857,7 +2003,7 @@ describe('src/cy/commands/actions/click', () => { expect(logCoords.x).to.be.closeTo(fromWindow.x, 1) // ensure we are within 1 expect(logCoords.y).to.be.closeTo(fromWindow.y, 1) // ensure we are within 1 expect(console.Command).to.eq('click') - expect(console['Applied To']).to.eq(lastLog.get('$el').get(0)) + expect(console['Applied To'], 'applied to').to.eq(lastLog.get('$el').get(0)) expect(console.Elements).to.eq(1) expect(console.Coords.x).to.be.closeTo(fromWindow.x, 1) // ensure we are within 1 @@ -1886,29 +2032,75 @@ describe('src/cy/commands/actions/click', () => { }) cy.get('input:first').click().then(function () { - expect(this.lastLog.invoke('consoleProps').groups()).to.deep.eq([ - { - name: 'MouseDown', - items: { - preventedDefault: true, - stoppedPropagation: true, + + const consoleProps = this.lastLog.invoke('consoleProps') + + expect(consoleProps.table[1]()).to.containSubset({ + 'name': 'Mouse Move Events', + 'data': [ + { + 'Event Name': 'pointerover', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, }, - }, - { - name: 'MouseUp', - items: { - preventedDefault: false, - stoppedPropagation: false, + { + 'Event Name': 'mouseover', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, }, - }, - { - name: 'Click', - items: { - preventedDefault: false, - stoppedPropagation: false, + { + 'Event Name': 'pointermove', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, }, - }, - ]) + { + 'Event Name': 'mousemove', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + ], + }) + + expect(consoleProps.table[2]()).to.containSubset({ + name: 'Mouse Click Events', + data: [ + { + 'Event Name': 'pointerdown', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'mousedown', + 'Target Element': { id: 'input' }, + 'Prevented Default?': true, + 'Stopped Propagation?': true, + }, + { + 'Event Name': 'pointerup', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'mouseup', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'click', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + ], + }) + }) }) @@ -1918,27 +2110,36 @@ describe('src/cy/commands/actions/click', () => { }) cy.get('input:first').click().then(function () { - expect(this.lastLog.invoke('consoleProps').groups()).to.deep.eq([ + expect(this.lastLog.invoke('consoleProps').table[2]().data).to.containSubset([ { - name: 'MouseDown', - items: { - preventedDefault: false, - stoppedPropagation: false, - }, + 'Event Name': 'pointerdown', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, }, { - name: 'MouseUp', - items: { - preventedDefault: true, - stoppedPropagation: true, - }, + 'Event Name': 'mousedown', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, }, { - name: 'Click', - items: { - preventedDefault: false, - stoppedPropagation: false, - }, + 'Event Name': 'pointerup', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'mouseup', + 'Target Element': { id: 'input' }, + 'Prevented Default?': true, + 'Stopped Propagation?': true, + }, + { + 'Event Name': 'click', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, }, ]) }) @@ -1950,29 +2151,106 @@ describe('src/cy/commands/actions/click', () => { }) cy.get('input:first').click().then(function () { - expect(this.lastLog.invoke('consoleProps').groups()).to.deep.eq([ + expect(this.lastLog.invoke('consoleProps').table[2]().data).to.containSubset([ { - name: 'MouseDown', - items: { - preventedDefault: false, - stoppedPropagation: false, - }, + 'Event Name': 'pointerdown', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, }, { - name: 'MouseUp', - items: { - preventedDefault: false, - stoppedPropagation: false, - }, + 'Event Name': 'mousedown', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, }, { - name: 'Click', - items: { - preventedDefault: true, - stoppedPropagation: true, - }, + 'Event Name': 'pointerup', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'mouseup', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'click', + 'Target Element': { id: 'input' }, + 'Prevented Default?': true, + 'Stopped Propagation?': true, + }, + ]) + }) + }) + + it('#consoleProps groups skips mouse move events if no mouse move', () => { + const btn = cy.$$('span#not-hidden') + + attachMouseClickListeners({ btn }) + attachMouseHoverListeners({ btn }) + + cy.get('span#not-hidden').click().click() + + cy.getAll('btn', 'mousemove mouseover').each(shouldBeCalledOnce) + cy.getAll('btn', 'pointerdown mousedown pointerup mouseup click').each(shouldBeCalledNth(2)) + .then(function () { + + const { logs } = this + const logsArr = logs.map((x) => x.invoke('consoleProps')) + + const lastClickProps = _.filter(logsArr, { Command: 'click' })[1] + const consoleProps = lastClickProps + + expect(_.map(consoleProps.table, (x) => x())).to.containSubset([ + { + 'name': 'Mouse Move Events (skipped)', + 'data': [], + }, + { + 'name': 'Mouse Click Events', + 'data': [ + { + 'Event Name': 'pointerdown', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'mousedown', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'pointerup', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'mouseup', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'click', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + ], }, ]) + }) }) @@ -1982,53 +2260,46 @@ describe('src/cy/commands/actions/click', () => { }) cy.get('input:first').type('{ctrl}{shift}', { release: false }).click().then(function () { - expect(this.lastLog.invoke('consoleProps').groups()).to.deep.eq([ + expect(this.lastLog.invoke('consoleProps').table[2]().data).to.containSubset([ { - name: 'MouseDown', - items: { - preventedDefault: false, - stoppedPropagation: false, - modifiers: 'ctrl, shift', - }, + 'Event Name': 'pointerdown', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': 'ctrl, shift', + }, { - name: 'MouseUp', - items: { - preventedDefault: false, - stoppedPropagation: false, - modifiers: 'ctrl, shift', - }, + 'Event Name': 'mousedown', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': 'ctrl, shift', + }, { - name: 'Click', - items: { - preventedDefault: true, - stoppedPropagation: true, - modifiers: 'ctrl, shift', - }, - }, - ]) - - cy.get('body').type('{ctrl}') - }) - }) // clear modifiers + 'Event Name': 'pointerup', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': 'ctrl, shift', - it('#consoleProps when no mouseup or click', () => { - const $btn = cy.$$('button:first') - - $btn.on('mousedown', function () { - // synchronously remove this button - return $(this).remove() - }) + }, + { + 'Event Name': 'mouseup', + 'Target Element': { id: 'input' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': 'ctrl, shift', - cy.contains('button').click().then(function () { - expect(this.lastLog.invoke('consoleProps').groups()).to.deep.eq([ + }, { - name: 'MouseDown', - items: { - preventedDefault: false, - stoppedPropagation: false, - }, + 'Event Name': 'click', + 'Target Element': { id: 'input' }, + 'Prevented Default?': true, + 'Stopped Propagation?': true, + 'Modifiers': 'ctrl, shift', + }, ]) }) @@ -2039,24 +2310,45 @@ describe('src/cy/commands/actions/click', () => { $btn.on('mouseup', function () { // synchronously remove this button - return $(this).remove() + $(this).remove() }) cy.contains('button').click().then(function () { - expect(this.lastLog.invoke('consoleProps').groups()).to.deep.eq([ + expect(this.lastLog.invoke('consoleProps').table[2]().data).to.containSubset([ { - name: 'MouseDown', - items: { - preventedDefault: false, - stoppedPropagation: false, - }, + 'Event Name': 'pointerdown', + 'Target Element': { id: 'button' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, }, { - name: 'MouseUp', - items: { - preventedDefault: false, - stoppedPropagation: false, - }, + 'Event Name': 'mousedown', + 'Target Element': { id: 'button' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'pointerup', + 'Target Element': { id: 'button' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'mouseup', + 'Target Element': { id: 'button' }, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'click', + 'Target Element': '⚠️ not fired (Element was detached)', + 'Prevented Default?': null, + 'Stopped Propagation?': null, + 'Modifiers': null, }, ]) }) @@ -2067,11 +2359,11 @@ describe('src/cy/commands/actions/click', () => { $btn.on('mouseup', function () { // synchronously remove this button - return $(this).remove() + $(this).remove() }) $btn.on('click', () => { - return fail('should not have gotten click') + fail('should not have gotten click') }) cy.contains('button').click() @@ -2093,7 +2385,7 @@ describe('src/cy/commands/actions/click', () => { context('#dblclick', () => { it('sends a dblclick event', (done) => { - cy.$$('#button').dblclick(() => { + cy.$$('#button').on('dblclick', () => { done() }) @@ -2108,25 +2400,19 @@ describe('src/cy/commands/actions/click', () => { }) }) - it('causes focusable elements to receive focus', (done) => { - const $text = cy.$$(':text:first') - - $text.focus(() => { - done() - }) - - cy.get(':text:first').dblclick() + it('causes focusable elements to receive focus', () => { + cy.get(':text:first').dblclick().should('have.focus') }) it('silences errors on unfocusable elements', () => { - cy.get('div:first').dblclick() + cy.get('div:first').dblclick({ force: true }) }) it('causes first focused element to receive blur', () => { let blurred = false cy.$$('input:first').blur(() => { - return blurred = true + blurred = true }) cy @@ -2165,12 +2451,12 @@ describe('src/cy/commands/actions/click', () => { }) }) - // NOTE: fix this once we implement aborting / restoring / reset + // TODO: fix this once we implement aborting / restoring / reset it.skip('can cancel multiple dblclicks', function (done) { let dblclicks = 0 const spy = this.sandbox.spy(() => { - return this.Cypress.abort() + this.Cypress.abort() }) // abort after the 3rd dblclick @@ -2181,7 +2467,7 @@ describe('src/cy/commands/actions/click', () => { anchors.dblclick(() => { dblclicks += 1 - return dblclicked() + dblclicked() }) // make sure we have at least 5 anchor links @@ -2193,7 +2479,7 @@ describe('src/cy/commands/actions/click', () => { // is called const timeout = this.sandbox.spy(cy, '_timeout') - return _.delay(() => { + _.delay(() => { // abort should only have been called once expect(spy.callCount).to.eq(1) @@ -2210,25 +2496,95 @@ describe('src/cy/commands/actions/click', () => { cy.get('#sequential-clicks a').dblclick() }) + it('serially dblclicks a collection of anchors to the top of the page', () => { + + const throttled = cy.stub().as('clickcount') + + // create a throttled click function + // which proves we are clicking serially + const handleClick = cy.stub() + .callsFake(_.throttle(throttled, 5, { leading: false })) + .as('handleClick') + + const $anchors = cy.$$('#sequential-clicks a') + + $anchors.on('click', handleClick) + cy.$$('div#dom').on('click', cy.stub().as('topClick')) + .on('dblclick', cy.stub().as('topDblclick')) + + // make sure we're clicking multiple $anchors + expect($anchors.length).to.be.gt(1) + + cy.get('#sequential-clicks a').dblclick({ multiple: true }).then(($els) => { + expect($els).to.have.length(throttled.callCount) + cy.get('@topDblclick').should('have.property', 'callCount', $els.length) + }) + }) + it('serially dblclicks a collection', () => { - let dblclicks = 0 - // create a throttled dblclick function - // which proves we are dblclicking serially - const throttled = _.throttle(() => { - return dblclicks += 1 - } - , 5, { leading: false }) + const throttled = cy.stub().as('clickcount') - const anchors = cy.$$('#sequential-clicks a') + // create a throttled click function + // which proves we are clicking serially + const handleClick = cy.stub() + .callsFake(_.throttle(throttled, 5, { leading: false })) + .as('handleClick') + + const $anchors = cy.$$('#three-buttons button') + + $anchors.on('dblclick', handleClick) + + // make sure we're clicking multiple $anchors + expect($anchors.length).to.be.gt(1) + + cy.get('#three-buttons button').dblclick({ multiple: true }).then(($els) => { + expect($els).to.have.length(throttled.callCount) + }) + }) + + it('correctly sets the detail property on mouse events', () => { + const btn = cy.$$('button:first') + + attachMouseClickListeners({ btn }) + attachMouseDblclickListeners({ btn }) + cy.get('button:first').dblclick() + cy.getAll('btn', 'mousedown mouseup click').each((spy) => { + expect(spy.firstCall).calledWithMatch({ detail: 1 }) + }) + + cy.getAll('btn', 'mousedown mouseup click').each((spy) => { + expect(spy.lastCall).to.be.calledWithMatch({ detail: 2 }) + }) + + cy.getAll('btn', 'dblclick').each((spy) => { + expect(spy).to.be.calledOnce + expect(spy.firstCall).to.be.calledWithMatch({ detail: 2 }) + }) + + // pointer events do not set change detail prop + cy.getAll('btn', 'pointerdown pointerup').each((spy) => { + expect(spy).to.be.calledWithMatch({ detail: 0 }) + }) + }) + + it('sends modifiers', () => { - anchors.dblclick(throttled) + const btn = cy.$$('button:first') - // make sure we're dblclicking multiple anchors - expect(anchors.length).to.be.gt(1) + attachMouseClickListeners({ btn }) + attachMouseDblclickListeners({ btn }) - cy.get('#sequential-clicks a').dblclick().then(($anchors) => { - expect($anchors.length).to.eq(dblclicks) + cy.get('input:first').type('{ctrl}{shift}', { release: false }) + cy.get('button:first').dblclick() + + cy.getAll('btn', 'pointerdown mousedown pointerup mouseup click dblclick').each((stub) => { + expect(stub).to.be.calledWithMatch({ + shiftKey: true, + ctrlKey: true, + metaKey: false, + altKey: false, + }) }) }) @@ -2257,10 +2613,10 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { this.lastLog = log - return this.logs.push(log) + this.logs.push(log) }) - return null + null }) it('throws when not a dom subject', (done) => { @@ -2291,18 +2647,6 @@ describe('src/cy/commands/actions/click', () => { cy.get('button:first').dblclick().dblclick() }) - it('throws when any member of the subject isnt visible', (done) => { - cy.$$('button').slice(0, 3).show().last().hide() - - cy.on('fail', (err) => { - expect(err.message).to.include('cy.dblclick() failed because this element is not visible') - - done() - }) - - cy.get('button').invoke('slice', 0, 3).dblclick() - }) - it('logs once when not dom subject', function (done) { cy.on('fail', (err) => { const { lastLog } = this @@ -2317,12 +2661,15 @@ describe('src/cy/commands/actions/click', () => { }) it('throws when any member of the subject isnt visible', function (done) { + cy.timeout(600) cy.$$('#three-buttons button').show().last().hide() cy.on('fail', (err) => { const { lastLog } = this - expect(this.logs.length).to.eq(4) + const logs = _.cloneDeep(this.logs) + + expect(logs).to.have.length(4) expect(lastLog.get('error')).to.eq(err) expect(err.message).to.include('cy.dblclick() failed because this element is not visible') @@ -2340,10 +2687,10 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { this.lastLog = log - return this.logs.push(log) + this.logs.push(log) }) - return null + null }) it('logs immediately before resolving', (done) => { @@ -2365,9 +2712,9 @@ describe('src/cy/commands/actions/click', () => { cy.get('button:first').dblclick().then(function () { const { lastLog } = this - expect(lastLog.get('snapshots').length).to.eq(1) - - expect(lastLog.get('snapshots')[0]).to.be.an('object') + expect(lastLog.get('snapshots')).to.have.length(2) + expect(lastLog.get('snapshots')[0]).to.containSubset({ name: 'before' }) + expect(lastLog.get('snapshots')[1]).to.containSubset({ name: 'after' }) }) }) @@ -2383,7 +2730,7 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'dblclick') { - return dblclicks.push(log) + dblclicks.push(log) } }) @@ -2400,7 +2747,7 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'dblclick') { - return logs.push(log) + logs.push(log) } }) @@ -2418,13 +2765,1283 @@ describe('src/cy/commands/actions/click', () => { cy.get('button').first().dblclick().then(function () { const { lastLog } = this - expect(lastLog.invoke('consoleProps')).to.deep.eq({ - Command: 'dblclick', - 'Applied To': lastLog.get('$el').get(0), - Elements: 1, + const consoleProps = lastLog.invoke('consoleProps') + + expect(consoleProps).to.containSubset({ + 'Command': 'dblclick', + 'Applied To': {}, + 'Elements': 1, + 'Coords': { + 'x': 34, + 'y': 548, + }, + 'Options': { + 'multiple': true, + }, + 'table': {}, }) - }) - }) - }) - }) -}) + + const tables = _.map(consoleProps.table, ((x) => x())) + + expect(tables).to.containSubset([ + { + 'name': 'Mouse Move Events', + 'data': [ + { + 'Event Name': 'pointerover', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'mouseover', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'pointermove', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + { + 'Event Name': 'mousemove', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + }, + ], + }, + { + 'name': 'Mouse Click Events', + 'data': [ + { + 'Event Name': 'pointerdown', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'mousedown', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'pointerup', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'mouseup', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'click', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'pointerdown', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'mousedown', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'pointerup', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'mouseup', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + { + 'Event Name': 'click', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + ], + }, + { + 'name': 'Mouse Dblclick Event', + 'data': [ + { + 'Event Name': 'dblclick', + 'Target Element': {}, + 'Prevented Default?': false, + 'Stopped Propagation?': false, + 'Modifiers': null, + }, + ], + }, + ]) + }) + }) + }) + }) + + context('#rightclick', () => { + + it('can rightclick', () => { + const el = cy.$$('button:first') + + attachMouseClickListeners({ el }) + attachContextmenuListeners({ el }) + + cy.get('button:first').rightclick().should('have.focus') + + cy.getAll('el', 'pointerdown mousedown contextmenu pointerup mouseup').each(shouldBeCalled) + cy.getAll('el', 'click').each(shouldNotBeCalled) + + cy.getAll('el', 'pointerdown mousedown pointerup mouseup').each((stub) => { + expect(stub.firstCall.args[0]).to.containSubset({ + button: 2, + buttons: 2, + which: 3, + }) + }) + + cy.getAll('el', 'contextmenu').each((stub) => { + expect(stub.firstCall.args[0]).to.containSubset({ + altKey: false, + bubbles: true, + target: el.get(0), + button: 2, + buttons: 2, + cancelable: true, + data: undefined, + detail: 0, + eventPhase: 2, + handleObj: { type: 'contextmenu', origType: 'contextmenu', data: undefined }, + relatedTarget: null, + shiftKey: false, + type: 'contextmenu', + view: cy.state('window'), + which: 3, + }) + }) + }) + + it('can rightclick disabled', () => { + const el = cy.$$('input:first') + + el.get(0).disabled = true + + attachMouseClickListeners({ el }) + attachFocusListeners({ el }) + attachContextmenuListeners({ el }) + + cy.get('input:first').rightclick({ force: true }) + + cy.getAll('el', 'mousedown contextmenu mouseup').each(shouldNotBeCalled) + cy.getAll('el', 'pointerdown pointerup').each(shouldBeCalled) + }) + + it('rightclick cancel contextmenu', () => { + const el = cy.$$('button:first') + + // canceling contextmenu prevents the native contextmenu + // likely we want to call attention to this, since we cannot + // reproduce the native contextmenu + el.on('contextmenu', () => false) + + attachMouseClickListeners({ el }) + attachFocusListeners({ el }) + attachContextmenuListeners({ el }) + + cy.get('button:first').rightclick().should('have.focus') + + cy.getAll('el', 'pointerdown mousedown contextmenu pointerup mouseup').each(shouldBeCalled) + cy.getAll('el', 'click').each(shouldNotBeCalled) + }) + + it('rightclick cancel mousedown', () => { + const el = cy.$$('button:first') + + el.on('mousedown', () => false) + + attachMouseClickListeners({ el }) + attachFocusListeners({ el }) + attachContextmenuListeners({ el }) + + cy.get('button:first').rightclick().should('not.have.focus') + + cy.getAll('el', 'pointerdown mousedown contextmenu pointerup mouseup').each(shouldBeCalled) + cy.getAll('el', 'focus click').each(shouldNotBeCalled) + + }) + + it('rightclick cancel pointerdown', () => { + const el = cy.$$('button:first') + + el.on('pointerdown', () => false) + + attachMouseClickListeners({ el }) + attachFocusListeners({ el }) + attachContextmenuListeners({ el }) + + cy.get('button:first').rightclick() + + cy.getAll('el', 'pointerdown pointerup contextmenu').each(shouldBeCalled) + cy.getAll('el', 'mousedown mouseup').each(shouldNotBeCalled) + + }) + + it('rightclick remove el on pointerdown', () => { + const el = cy.$$('button:first') + + el.on('pointerdown', () => el.get(0).remove()) + + attachMouseClickListeners({ el }) + attachFocusListeners({ el }) + attachContextmenuListeners({ el }) + + cy.get('button:first').rightclick().should('not.exist') + + cy.getAll('el', 'pointerdown').each(shouldBeCalled) + cy.getAll('el', 'mousedown mouseup contextmenu pointerup').each(shouldNotBeCalled) + + }) + + it('rightclick remove el on mouseover', () => { + const el = cy.$$('button:first') + const el2 = cy.$$('div#tabindex') + + el.on('mouseover', () => el.get(0).remove()) + + attachMouseClickListeners({ el, el2 }) + attachMouseHoverListeners({ el, el2 }) + attachFocusListeners({ el, el2 }) + attachContextmenuListeners({ el, el2 }) + + cy.get('button:first').rightclick().should('not.exist') + cy.get(el2.selector).should('have.focus') + + cy.getAll('el', 'pointerover mouseover').each(shouldBeCalledOnce) + cy.getAll('el', 'pointerdown mousedown pointerup mouseup contextmenu').each(shouldNotBeCalled) + cy.getAll('el2', 'focus pointerdown pointerup contextmenu').each(shouldBeCalled) + + }) + + describe('errors', () => { + beforeEach(function () { + Cypress.config('defaultCommandTimeout', 100) + + this.logs = [] + + cy.on('log:added', (attrs, log) => { + this.lastLog = log + + this.logs.push(log) + }) + + null + }) + + it('throws when not a dom subject', (done) => { + cy.on('fail', () => { + done() + }) + + cy.rightclick() + }) + + it('throws when subject is not in the document', (done) => { + let rightclicked = 0 + + const $button = cy.$$('button:first').on('contextmenu', () => { + rightclicked += 1 + $button.remove() + + return false + }) + + cy.on('fail', (err) => { + expect(rightclicked).to.eq(1) + expect(err.message).to.include('cy.rightclick() failed because this element') + + done() + }) + + cy.get('button:first').rightclick().rightclick() + }) + + it('logs once when not dom subject', function (done) { + cy.on('fail', (err) => { + const { lastLog } = this + + expect(this.logs.length).to.eq(1) + expect(lastLog.get('error')).to.eq(err) + + done() + }) + + cy.rightclick() + }) + + it('throws when any member of the subject isnt visible', function (done) { + cy.timeout(300) + cy.$$('#three-buttons button').show().last().hide() + + cy.on('fail', (err) => { + const { lastLog } = this + + expect(this.logs.length).to.eq(4) + expect(lastLog.get('error')).to.eq(err) + expect(err.message).to.include('cy.rightclick() failed because this element is not visible') + + done() + }) + + cy.get('#three-buttons button').rightclick({ multiple: true }) + }) + }) + + describe('.log', () => { + beforeEach(function () { + this.logs = [] + + cy.on('log:added', (attrs, log) => { + this.lastLog = log + + this.logs.push(log) + }) + + null + }) + + it('logs immediately before resolving', (done) => { + const $button = cy.$$('button:first') + + cy.on('log:added', (attrs, log) => { + if (log.get('name') === 'rightclick') { + expect(log.get('state')).to.eq('pending') + expect(log.get('$el').get(0)).to.eq($button.get(0)) + + done() + } + }) + + cy.get('button:first').rightclick() + }) + + it('snapshots after clicking', () => { + cy.get('button:first').rightclick().then(function () { + const { lastLog } = this + + expect(lastLog.get('snapshots')).to.have.length(2) + expect(lastLog.get('snapshots')[0]).to.containSubset({ name: 'before' }) + expect(lastLog.get('snapshots')[1]).to.containSubset({ name: 'after' }) + }) + }) + + it('returns only the $el for the element of the subject that was rightclicked', () => { + const rightclicks = [] + + // append two buttons + const $button = () => { + return $('").appendTo cy.$$("body") + + win = cy.state("window") + + $btn.on "mouseover", (e) => + { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + + expect(e.pageX).to.be.closeTo(win.pageXOffset + e.clientX, 1) + expect(e.pageY).to.be.closeTo(win.pageYOffset + e.clientY, 1) + done() + + cy.get("#scrolledBtn").trigger("mouseover") + it "does not change the subject", -> $input = cy.$$("input:first") @@ -788,3 +802,5 @@ describe "src/cy/commands/actions/trigger", -> expect(eventOptions.clientY).to.be.be.a("number") expect(eventOptions.pageX).to.be.be.a("number") expect(eventOptions.pageY).to.be.be.a("number") + expect(eventOptions.screenX).to.be.be.a("number").and.eq(eventOptions.clientX) + expect(eventOptions.screenY).to.be.be.a("number").and.eq(eventOptions.clientY) From 76f830729e34142fbe2edce62364d2f3ad07116b Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Mon, 29 Jul 2019 16:18:28 -0400 Subject: [PATCH 11/36] re-run build From 5a123bf94dcf39b0bddf803a0b9e96e5867d5fde Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 8 Aug 2019 13:54:55 -0400 Subject: [PATCH 12/36] add typedefs --- cli/types/index.d.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index 540db9629ace..dcf0f812cd94 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -616,7 +616,14 @@ declare namespace Cypress { * * @see https://on.cypress.io/dblclick */ - dblclick(options?: Partial): Chainable + dblclick(options?: Partial): Chainable + + /** + * Right-click a DOM element. + * + * @see https://on.cypress.io/rightclick + */ + rightclick(options?: Partial): Chainable /** * Set a debugger and log what the previous command yields. From 53771dc53ed21c14e643ec212cdef07f97144708 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 8 Aug 2019 13:59:54 -0400 Subject: [PATCH 13/36] temp 08/08/19 [skip ci] --- cli/types/tests/chainer-examples.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/types/tests/chainer-examples.ts b/cli/types/tests/chainer-examples.ts index 020b447e7e0f..4825f497c032 100644 --- a/cli/types/tests/chainer-examples.ts +++ b/cli/types/tests/chainer-examples.ts @@ -452,3 +452,7 @@ cy.writeFile('../file.path', '', { flag: 'a+', encoding: 'utf-8' }) + +cy.get('foo').click() +cy.get('foo').rightclick() +cy.get('foo').dblclick() From 3db8363b8cc6d96237a5bd3e4d901cfff9cbc725 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 8 Aug 2019 14:26:39 -0400 Subject: [PATCH 14/36] fix type_spec, click_spec --- packages/driver/src/cy/commands/actions/type.js | 6 +++++- .../test/cypress/integration/commands/actions/click_spec.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index 0da258071548..732c6b02dd20 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -52,7 +52,11 @@ module.exports = function (Commands, Cypress, cy, state, config) { let obj table[id] = (obj = {}) - obj.modifiers = $Keyboard.modifiersToString(keyboard.getActiveModifiers()) + const modifiers = $Keyboard.modifiersToString(keyboard.getActiveModifiers()) + + if (modifiers) { + obj.modifiers = modifiers + } if (key) { obj.typed = key 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 97b11ac2226e..d9e9dc9d7c5c 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -3316,7 +3316,7 @@ describe('mouse state', () => { doc: cy.state('document'), } - cy.internal.mouse.mouseMove(coords) + cy.devices.mouse.mouseMove(coords) expect(mouseenter).to.be.calledOnce expect(cy.state('mouseCoords')).ok }) From 977a19e0811412a7b2082927d43a4015df4be469 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Sun, 11 Aug 2019 12:54:29 -0400 Subject: [PATCH 15/36] fix console table event table logging --- .../commands/actions/click_spec.js | 29 +++++++++++++++++++ .../integration/commands/actions/type_spec.js | 25 ++++++++++++++++ packages/driver/test/cypress/support/utils.js | 24 +++++++++++++++ packages/runner/src/lib/logger.js | 18 +++++++++--- 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 packages/driver/test/cypress/support/utils.js 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 d9e9dc9d7c5c..3128218911c3 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -2,6 +2,7 @@ const $ = Cypress.$.bind(Cypress) const { _ } = Cypress const { Promise } = Cypress const chaiSubset = require('chai-subset') +const { getCommandLogWithText, findReactInstance, withMutableReporterState } = require('../../../support/utils') chai.use(chaiSubset) @@ -2341,6 +2342,34 @@ describe('src/cy/commands/actions/click', () => { }) }) + it('can print table of keys on click', () => { + const spyTableName = cy.spy(top.console, 'groupCollapsed') + const spyTableData = cy.spy(top.console, 'table') + + cy.get('input:first').click() + + cy.wrap(null) + .should(() => { + spyTableName.reset() + spyTableData.reset() + + return withMutableReporterState(() => { + + const commandLogEl = getCommandLogWithText('click') + + const reactCommandInstance = findReactInstance(commandLogEl) + + reactCommandInstance.props.appState.isRunning = false + + $(commandLogEl).find('.command-wrapper').click() + + expect(spyTableName).calledWith('Mouse Move Events') + expect(spyTableName).calledWith('Mouse Click Events') + expect(spyTableData).calledTwice + }) + }) + }) + it('does not fire a click when element has been removed on mouseup', () => { const $btn = cy.$$('button:first') diff --git a/packages/driver/test/cypress/integration/commands/actions/type_spec.js b/packages/driver/test/cypress/integration/commands/actions/type_spec.js index 6d8c2ede870b..bc43e5c046a9 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -2,6 +2,7 @@ const $ = Cypress.$.bind(Cypress) const { _ } = Cypress const { Promise } = Cypress const $selection = require('../../../../../src/dom/selection') +const { getCommandLogWithText, findReactInstance, withMutableReporterState } = require('../../../support/utils') // trim new lines at the end of innerText // due to changing browser versions implementing @@ -4133,6 +4134,30 @@ describe('src/cy/commands/actions/type', () => { }) }) + it('can print table of keys on click', () => { + cy.get('input:first').type('foo') + + .then(() => { + return withMutableReporterState(() => { + const spyTableName = cy.spy(top.console, 'groupCollapsed') + const spyTableData = cy.spy(top.console, 'table') + + const commandLogEl = getCommandLogWithText('foo') + + const reactCommandInstance = findReactInstance(commandLogEl) + + reactCommandInstance.props.appState.isRunning = false + + $(commandLogEl).find('.command-wrapper').click() + + expect(spyTableName.firstCall).calledWith('Mouse Move Events') + expect(spyTableName.secondCall).calledWith('Mouse Click Events') + expect(spyTableName.thirdCall).calledWith('Keyboard Events') + expect(spyTableData).calledThrice + }) + }) + }) + // table.data.forEach (item, i) -> // expect(item).to.deep.eq(expectedTable[i]) diff --git a/packages/driver/test/cypress/support/utils.js b/packages/driver/test/cypress/support/utils.js new file mode 100644 index 000000000000..c4b253cf3242 --- /dev/null +++ b/packages/driver/test/cypress/support/utils.js @@ -0,0 +1,24 @@ +export const getCommandLogWithText = (text) => cy.$$(`.command-wrapper:contains(${text}):visible`, top.document).parentsUntil('li').last().parent()[0] + +export const findReactInstance = function (dom) { + let key = Object.keys(dom).find((key) => key.startsWith('__reactInternalInstance$')) + let internalInstance = dom[key] + + if (internalInstance == null) return null + + return internalInstance._debugOwner + ? internalInstance._debugOwner.stateNode + : internalInstance.return.stateNode + +} + +export const withMutableReporterState = (fn) => { + top.Runner.configureMobx({ enforceActions: 'never' }) + + return Cypress.Promise.try(() => { + return fn() + }).then(() => { + top.Runner.configureMobx({ enforceActions: 'strict' }) + }) + +} diff --git a/packages/runner/src/lib/logger.js b/packages/runner/src/lib/logger.js index d39f0c711ccf..0cd7546691ad 100644 --- a/packages/runner/src/lib/logger.js +++ b/packages/runner/src/lib/logger.js @@ -76,8 +76,6 @@ export default { if (!groups) return - delete consoleProps.groups - return _.map(groups, (group) => { group.items = this._formatted(group.items) @@ -86,6 +84,18 @@ export default { }, _logTable (consoleProps) { + + if (isMultiEntryTable(consoleProps.table)) { + _.each( + _.sortBy(consoleProps.table, (val, key) => key), + (table) => { + return this._logTable({ table }) + } + ) + + return + } + const table = this._getTable(consoleProps) if (!table) return @@ -104,8 +114,8 @@ export default { if (!table) return - delete consoleProps.table - return table }, } + +const isMultiEntryTable = (table) => !_.isFunction(table) && !_.some(_.keys(table).map(isNaN).filter(Boolean), true) From ef536c58ec96985cdd566236d08da8405e5cf2da Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Mon, 12 Aug 2019 11:47:16 -0400 Subject: [PATCH 16/36] fix spec utils --- packages/driver/test/cypress/support/utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/driver/test/cypress/support/utils.js b/packages/driver/test/cypress/support/utils.js index c4b253cf3242..c6e7000e8c38 100644 --- a/packages/driver/test/cypress/support/utils.js +++ b/packages/driver/test/cypress/support/utils.js @@ -15,6 +15,10 @@ export const findReactInstance = function (dom) { export const withMutableReporterState = (fn) => { top.Runner.configureMobx({ enforceActions: 'never' }) + const currentTestLog = findReactInstance(cy.$$('.runnable-active', top.document)[0]) + + currentTestLog.props.model.isOpen = true + return Cypress.Promise.try(() => { return fn() }).then(() => { From 9202ff620cd2ef30798148d46f9b004569fbd9eb Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Wed, 14 Aug 2019 18:23:05 -0400 Subject: [PATCH 17/36] fix invalid clicking-into-iframe spec --- .../test/cypress/integration/commands/actions/click_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3128218911c3..6901262a0f30 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -743,12 +743,12 @@ describe('src/cy/commands/actions/click', () => { cy.get('iframe') .should(($iframe) => { // wait for iframe to load - expect($iframe.contents().find('body').html()).ok + expect($iframe.first().contents().find('body').html()).ok }) .then(($iframe) => { // cypress does not wrap this as a DOM element (does not wrap in jquery) // return cy.wrap($iframe[0].contentDocument.body) - return cy.wrap($iframe.contents().find('body')) + return cy.wrap($iframe.first().contents().find('body')) }) .within(() => { From 44eb7404241c16a3dd41197c25a97174cfb4ff05 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 15 Aug 2019 13:10:51 -0400 Subject: [PATCH 18/36] address review, cleanup --- packages/driver/src/cy/keyboard.js | 10 ++----- packages/driver/src/cy/mouse.js | 43 +++++++++++++----------------- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/packages/driver/src/cy/keyboard.js b/packages/driver/src/cy/keyboard.js index daef0d7b4c77..60c703916758 100644 --- a/packages/driver/src/cy/keyboard.js +++ b/packages/driver/src/cy/keyboard.js @@ -57,14 +57,7 @@ const fromModifierEventOptions = (eventOptions) => { }, Boolean) } -const modifiersToString = (modifiers) => { - return _.keys( - _.pickBy(modifiers, (val) => { - return val - }) - ) - .join(', ') -} +const modifiersToString = (modifiers) => _.keys(_.pickBy(modifiers, Boolean)).join(', ') const create = (state) => { const kb = { @@ -680,6 +673,7 @@ const create = (state) => { return kb.specialChars[chars].call(this, el, options) }, + isModifier (chars) { return _.includes(_.keys(kb.modifierChars), chars) }, diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index f65777dd43f9..13ede096088c 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -4,7 +4,7 @@ const $ = require('jquery') const _ = require('lodash') const $Keyboard = require('./keyboard') const $selection = require('../dom/selection') -const debug = require('debug')('driver:mouse') +const debug = require('debug')('cypress:driver:mouse') /** * @typedef Coords @@ -23,7 +23,16 @@ const getLastHoveredEl = (state) => { } return lastHoveredEl +} +const defaultPointerDownUpOptions = { + pointerType: 'mouse', + pointerId: 1, + isPrimary: true, + detail: 0, + // pressure 0.5 is default for mouse that doesn't support pressure + // https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pressure + pressure: 0.5, } const getMouseCoords = (state) => { @@ -188,7 +197,6 @@ const create = (state, keyboard, focused) => { } - // if (!Cypress.config('mousemoveBeforeMouseover') && el) { pointermove = () => { return sendPointermove(el, defaultPointerOptions) } @@ -213,7 +221,6 @@ const create = (state, keyboard, focused) => { events.push({ mousemove: mousemove() }) return events - }, /** @@ -229,10 +236,7 @@ const create = (state, keyboard, focused) => { const el = doc.elementFromPoint(x, y) - // mouse._mouseMoveEvents(el, { x, y }) - return el - }, /** @@ -264,14 +268,10 @@ const create = (state, keyboard, focused) => { const defaultOptions = mouse._getDefaultMouseOptions(x, y, win) const pointerEvtOptions = _.extend({}, defaultOptions, { + ...defaultPointerDownUpOptions, button: 0, which: 1, buttons: 1, - detail: 0, - pressure: 0.5, - pointerType: 'mouse', - pointerId: 1, - isPrimary: true, relatedTarget: null, }, pointerEvtOptionsExtend) @@ -399,12 +399,8 @@ const create = (state, keyboard, focused) => { let defaultOptions = mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win) const pointerEvtOptions = _.extend({}, defaultOptions, { + ...defaultPointerDownUpOptions, buttons: 0, - pressure: 0.5, - pointerType: 'mouse', - pointerId: 1, - isPrimary: true, - detail: 0, }, pointerEvtOptionsExtend) let mouseEvtOptions = _.extend({}, defaultOptions, { @@ -533,12 +529,12 @@ const create = (state, keyboard, focused) => { const { stopPropagation } = window.MouseEvent.prototype -const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, constructor) => { +const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, Constructor) => { evtOptions = _.extend({}, evtOptions, { bubbles, cancelable }) const _eventModifiers = $Keyboard.fromModifierEventOptions(evtOptions) const modifiers = $Keyboard.modifiersToString(_eventModifiers) - const evt = new constructor(evtName, _.extend({}, evtOptions, { bubbles, cancelable })) + const evt = new Constructor(evtName, _.extend({}, evtOptions, { bubbles, cancelable })) if (bubbles) { evt.stopPropagation = function (...args) { @@ -560,16 +556,16 @@ const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, } const sendPointerEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { - const constructor = el.ownerDocument.defaultView.PointerEvent + const Constructor = el.ownerDocument.defaultView.PointerEvent - return sendEvent(evtName, el, evtOptions, bubbles, cancelable, constructor) + return sendEvent(evtName, el, evtOptions, bubbles, cancelable, Constructor) } const sendMouseEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { - // IE doesn't have event constructors, so you should use document.createEvent('mouseevent') + // TODO: IE doesn't have event constructors, so you should use document.createEvent('mouseevent') // https://dom.spec.whatwg.org/#dom-document-createevent - const constructor = el.ownerDocument.defaultView.MouseEvent + const Constructor = el.ownerDocument.defaultView.MouseEvent - return sendEvent(evtName, el, evtOptions, bubbles, cancelable, constructor) + return sendEvent(evtName, el, evtOptions, bubbles, cancelable, Constructor) } const sendPointerup = (el, evtOptions) => { @@ -630,7 +626,6 @@ const formatReasonNotFired = (reason) => { } const toCoordsEventOptions = (x, y, win) => { - // these are the coords from the document, ignoring scroll position const fromDocCoords = $elements.getFromDocCoords(x, y, win) From 109763853e3c11ce434c6aa84b94370d3e116445 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Wed, 25 Sep 2019 15:57:01 -0400 Subject: [PATCH 19/36] temp 09/25/19 [skip ci] --- packages/driver/src/cy/mouse.js | 3 ++ .../commands/actions/click_spec.js | 45 +++++++++---------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index 13ede096088c..4ccaca0d05d6 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -70,6 +70,7 @@ const create = (state, keyboard, focused) => { // if coords are same AND we're already hovered on the element, don't send move events if (_.isEqual({ x: coords.x, y: coords.y }, getMouseCoords(state)) && lastHoveredEl === targetEl) return { el: targetEl } + debug('mousemove events') const events = mouse._mouseMoveEvents(targetEl, coords) const resultEl = mouse.getElAtCoordsOrForce(coords, forceEl) @@ -565,6 +566,8 @@ const sendMouseEvent = (el, evtOptions, evtName, bubbles = false, cancelable = f // https://dom.spec.whatwg.org/#dom-document-createevent const Constructor = el.ownerDocument.defaultView.MouseEvent + debug('send event:', evtName) + return sendEvent(evtName, el, evtOptions, bubbles, cancelable, Constructor) } 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 7a149ef7a0de..84954c1b4319 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -3769,42 +3769,37 @@ describe('mouse state', () => { cy.get('#inner').should('not.be.visible') }) - it('will respect changes to dom in event handlers', () => { + it.only('will respect changes to dom in event handlers', () => { - allMouseEvents.forEach((evt) => { - cy.$$('#sq4').on(evt, cy.spy().as(`sq4:${evt}`)) - cy.$$('#outer').on(evt, cy.spy().as(`outer:${evt}`)) - cy.$$('input:first').on(evt, cy.spy().as(`input:${evt}`)) - }) + const els = { + sq4: cy.$$('#sq4'), + outer: cy.$$('#outer'), + input: cy.$$('input:first'), + } + + attachListeners(['mouseenter', 'mouseexit']) + + attachMouseClickListeners(els) + attachMouseHoverListeners(els) + + return cy.get('#sq4').click() - cy.get('#outer').click({ timeout: 200 }) + cy.get('#outer').click() - cy.getAll('@sq4:mouseover @sq4:mousedown @sq4:mouseup @sq4:click').each((spy) => { - expect(spy).to.be.calledTwice - }) + cy.getAll('@sq4:mouseover @sq4:mousedown @sq4:mouseup @sq4:click').each(shouldBeCalledNth(2)) - cy.getAll('@sq4:mouseout').each((spy) => { - expect(spy).to.be.calledOnce - }) + cy.getAll('@sq4:mouseout').each(shouldBeCalledOnce) - cy.getAll('@outer:mousedown @outer:mouseup @outer:click').each((spy) => { - expect(spy).to.not.be.called - }) + cy.getAll('@outer:mousedown @outer:mouseup @outer:click').each(shouldNotBeCalled) - cy.getAll('@outer:mouseover @outer:mouseout').each((spy) => { - expect(spy).to.be.calledOnce - }) + cy.getAll('@outer:mouseover @outer:mouseout').each(shouldBeCalledOnce) cy.get('input:first').click().should('not.have.focus') - cy.getAll('@input:mouseover @input:mouseout').each((spy) => { - expect(spy).to.be.calledOnce - }) + cy.getAll('@input:mouseover @input:mouseout').each(shouldBeCalledOnce) - cy.getAll('@input:mousedown @input:mouseup @input:click').each((spy) => { - expect(spy).to.not.be.called - }) + cy.getAll('@input:mousedown @input:mouseup @input:click').each(shouldNotBeCalled) }) // it('will continue to send mouseleave events', function (done) { From f7ab2ebd376fd0be8d7952a417476ea384acecb0 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 26 Sep 2019 12:57:46 -0400 Subject: [PATCH 20/36] add test for clicking checkbox, cleanup click_spec --- .../commands/actions/click_spec.js | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) 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 84954c1b4319..e915178f9bfa 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -775,6 +775,24 @@ describe('src/cy/commands/actions/click', () => { 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() }) @@ -2961,7 +2979,6 @@ describe('src/cy/commands/actions/click', () => { cancelable: true, data: undefined, detail: 0, - eventPhase: 2, handleObj: { type: 'contextmenu', origType: 'contextmenu', data: undefined }, relatedTarget: null, shiftKey: false, @@ -3065,7 +3082,7 @@ describe('src/cy/commands/actions/click', () => { attachContextmenuListeners({ el, el2 }) cy.get('button:first').rightclick().should('not.exist') - cy.get(el2.selector).should('have.focus') + cy.get('div#tabindex').should('have.focus') cy.getAll('el', 'pointerover mouseover').each(shouldBeCalledOnce) cy.getAll('el', 'pointerdown mousedown pointerup mouseup contextmenu').each(shouldNotBeCalled) @@ -3769,7 +3786,7 @@ describe('mouse state', () => { cy.get('#inner').should('not.be.visible') }) - it.only('will respect changes to dom in event handlers', () => { + it('will respect changes to dom in event handlers', () => { const els = { sq4: cy.$$('#sq4'), @@ -3782,8 +3799,6 @@ describe('mouse state', () => { attachMouseClickListeners(els) attachMouseHoverListeners(els) - return - cy.get('#sq4').click() cy.get('#outer').click() From fbc43ebdaa2282d099d08acbfbce00449403a6bf Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Fri, 27 Sep 2019 15:42:08 -0400 Subject: [PATCH 21/36] document mouse click and mouse move algo, adjust mouse move, add test for recursive movement --- packages/driver/src/cy/mouse.js | 37 +++++++++++++++---- .../test/cypress/fixtures/issue-2956.html | 20 +++++++++- .../commands/actions/click_spec.js | 33 +++++++++++------ 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index 4ccaca0d05d6..b2fd2e6f1ed4 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -70,21 +70,23 @@ const create = (state, keyboard, focused) => { // if coords are same AND we're already hovered on the element, don't send move events if (_.isEqual({ x: coords.x, y: coords.y }, getMouseCoords(state)) && lastHoveredEl === targetEl) return { el: targetEl } - debug('mousemove events') const events = mouse._mouseMoveEvents(targetEl, coords) const resultEl = mouse.getElAtCoordsOrForce(coords, forceEl) - if (resultEl !== targetEl) { - mouse._mouseMoveEvents(resultEl, coords) - } - return { el: resultEl, fromEl: lastHoveredEl, events } }, /** * @param {HTMLElement} el * @param {Coords} coords + * Steps to perform mouse move: + * - send out events to elLastHovered (bubbles) + * - send leave events to all Elements until commonAncestor + * - send over events to elToHover (bubbles) + * - send enter events to all elements from commonAncestor + * - send move events to elToHover (bubbles) + * - elLastHovered = elToHover */ _mouseMoveEvents (el, coords) { @@ -371,7 +373,28 @@ const create = (state, keyboard, focused) => { return mouse._mouseUpEvents(fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) }, + /** + * + * Steps to perform click: + * + * moveToCoordsOrNoop = (coords, el) => { + * elAtPoint = getElementFromPoint(coords) + * if (elAtPoint !== el) + * sendMouseMoveEvents(elAtPoint, el) + * return getElementFromPoint(coords) + * } + * + * coords = getCoords(elSubject) + * el1 = moveToCoordsOrNoop(coords, elLastHovered) + * mouseDown(el1) + * el2 = moveToCoordsOrNoop(coords, el1) + * mouseUp(el2) + * el3 = moveToCoordsOrNoop(coords, el2) + * if (notDetached(el1)) + * sendClick(el3) + */ mouseClick (fromViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + debug('mouseClick', { fromViewport, forceEl }) const mouseDownEvents = mouse.mouseDown(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) @@ -545,6 +568,8 @@ const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, } } + debug('event:', evtName, el) + const preventedDefault = !el.dispatchEvent(evt) return { @@ -566,8 +591,6 @@ const sendMouseEvent = (el, evtOptions, evtName, bubbles = false, cancelable = f // https://dom.spec.whatwg.org/#dom-document-createevent const Constructor = el.ownerDocument.defaultView.MouseEvent - debug('send event:', evtName) - return sendEvent(evtName, el, evtOptions, bubbles, cancelable, Constructor) } diff --git a/packages/driver/test/cypress/fixtures/issue-2956.html b/packages/driver/test/cypress/fixtures/issue-2956.html index 4536d323874b..cc2b6a1729ba 100644 --- a/packages/driver/test/cypress/fixtures/issue-2956.html +++ b/packages/driver/test/cypress/fixtures/issue-2956.html @@ -48,6 +48,10 @@ display: inherit; } +#sq6.hover { + margin-left: 100px; +} + button { display: inherit; position: relative; @@ -76,6 +80,16 @@ inner.classList.remove('active'); } +function setHover() { + // console.log('hover') + this.classList.add('hover'); +} + +function unsetHover() { + // console.log('unhover') + this.classList.remove('hover'); +} + function color() { if (outer.classList.contains('yellow')) { outer.classList.remove('yellow'); @@ -106,6 +120,10 @@ e.preventDefault() }) +const sq6 = document.getElementById('sq6') +sq6.addEventListener("mouseenter", setHover); +sq6.addEventListener("mouseleave", unsetHover); + } @@ -128,7 +146,7 @@
- +
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 e915178f9bfa..efd077fa3e8a 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -3817,17 +3817,28 @@ describe('mouse state', () => { cy.getAll('@input:mousedown @input:mouseup @input:click').each(shouldNotBeCalled) }) - // it('will continue to send mouseleave events', function (done) { - // cy.once('fail', (err) => { - // expect(err.message).to.contain('is being covered') - // done() - // }) - - // cy.get('#sq4').click() - // cy.timeout(500) - // cy.get('#outer').click() - // cy.get('input:last').click()//.click({ timeout: 200 }) - // }) + + it('can click on a recursively moving element', () => { + + const sq6 = cy.$$('#sq6') + + /* + * the square moves back-forth on mouseleave/mouseenter + * so: + * - move phase, mouseover sent to sq, sq leaves + * - before mousedown events, move phase, sq returns, mousedown sent to sq + * - before mouseup events, move phase, mouseover sent to sq, sq leaves, mouseup sent to body + * - before click events, move events sent, sq returns, click sent to sq + */ + attachListeners(['mouseover'])({ sq6 }) + attachMouseClickListeners({ sq6 }) + + cy.get('#sq6') + .click() + + cy.getAll('sq6', 'mousedown pointerdown click').each(shouldBeCalledOnce) + cy.getAll('sq6', 'mouseover').each(shouldBeCalledNth(2)) + }) }) it('handles disabled attr', () => { From 83a35edaa9c180b29bdf9fa6615e6d52add132de Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Sun, 29 Sep 2019 18:00:22 -0400 Subject: [PATCH 22/36] cleanup / formatting --- .../driver/src/cy/commands/actions/click.js | 297 +++++++++--------- packages/driver/src/cy/mouse.js | 23 +- .../commands/actions/click_spec.js | 91 +++--- 3 files changed, 204 insertions(+), 207 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index 286586fe5df0..02e6f858904c 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -5,14 +5,68 @@ const $dom = require('../../../dom') const $utils = require('../../../cypress/utils') const $actionability = require('../../actionability') +const formatMoveEventsTable = (events) => { + return { + name: `Mouse Move Events${events ? '' : ' (skipped)'}`, + data: _.map(events, (obj) => { + const key = _.keys(obj)[0] + const val = obj[_.keys(obj)[0]] + + if (val.skipped) { + const reason = val.skipped + + // no modifiers can be present + // on move events + return { + 'Event Name': key, + 'Target Element': reason, + 'Prevented Default?': null, + 'Stopped Propagation?': null, + } + } + + // no modifiers can be present + // on move events + return { + 'Event Name': key, + 'Target Element': val.el, + 'Prevented Default?': val.preventedDefault, + 'Stopped Propagation?': val.stoppedPropagation, + } + }), + } +} + +const formatMouseEvents = (events) => { + return _.map(events, (val, key) => { + if (val.skipped) { + + const reason = val.skipped + + return { + 'Event Name': key.slice(0, -5), + 'Target Element': reason, + 'Prevented Default?': null, + 'Stopped Propagation?': null, + 'Modifiers': null, + } + } + + return { + 'Event Name': key.slice(0, -5), + 'Target Element': val.el, + 'Prevented Default?': val.preventedDefault, + 'Stopped Propagation?': val.stoppedPropagation, + 'Modifiers': val.modifiers ? val.modifiers : null, + } + }) +} + module.exports = (Commands, Cypress, cy, state, config) => { const { mouse } = cy.devices return Commands.addAll({ prevSubject: 'element' }, { click (subject, positionOrX, y, options = {}) { - //# TODO handle pointer-events: none - //# http://caniuse.com/#feat=pointer-events - let position let x @@ -32,8 +86,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { animationDistanceThreshold: config('animationDistanceThreshold'), }) - //# throw if we're trying to click multiple elements - //# and we did not pass the multiple flag + // throw if we're trying to click multiple elements + // and we did not pass the multiple flag if ((options.multiple === false) && (options.$el.length > 1)) { $utils.throwErrByPath('click.multiple_elements', { args: { cmd: 'click', num: options.$el.length }, @@ -45,7 +99,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { const $el = $dom.wrap(el) if (options.log) { - //# figure out the options which actually change the behavior of clicks + // figure out the options which actually change the behavior of clicks deltaOptions = $utils.filterOutOptions(options) options._log = Cypress.log({ @@ -60,9 +114,9 @@ module.exports = (Commands, Cypress, cy, state, config) => { $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'click' }, onFail: options._log }) } - //# we want to add this delay delta to our - //# runnables timeout so we prevent it from - //# timing out from multiple clicks + // we want to add this delay delta to our + // runnables timeout so we prevent it from + // timing out from multiple clicks cy.timeout($actionability.delay, true, 'click') const createLog = (domEvents, fromWindowCoords) => { @@ -78,12 +132,12 @@ module.exports = (Commands, Cypress, cy, state, config) => { consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { 'Applied To': $dom.getElements(options.$el), 'Elements': options.$el.length, - 'Coords': _.pick(fromWindowCoords, 'x', 'y'), //# always absolute + 'Coords': _.pick(fromWindowCoords, 'x', 'y'), // always absolute 'Options': deltaOptions, }) if (options.$el.get(0) !== elClicked) { - //# only do this if $elToClick isnt $el + // only do this if $elToClick isnt $el consoleObj['Actual Element Clicked'] = $dom.getElements($(elClicked)) } @@ -105,34 +159,34 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Promise .delay($actionability.delay, 'click') .then(() => { - //# display the red dot at these coords + // display the red dot at these coords if (options._log) { - //# because we snapshot and output a command per click - //# we need to manually snapshot + end them + // because we snapshot and output a command per click + // we need to manually snapshot + end them options._log.set({ coords: fromWindowCoords, consoleProps }) } - //# we need to split this up because we want the coordinates - //# to mutate our passed in options._log but we dont necessary - //# want to snapshot and end our command if we're a different - //# action like (cy.type) and we're borrowing the click action + // we need to split this up because we want the coordinates + // to mutate our passed in options._log but we dont necessary + // want to snapshot and end our command if we're a different + // action like (cy.type) and we're borrowing the click action if (options._log && options.log) { return options._log.snapshot().end() } - }).return(null) + }) + .return(null) } - //# must use callbacks here instead of .then() - //# because we're issuing the clicks synchonrously - //# once we establish the coordinates and the element - //# passes all of the internal checks + // must use callbacks here instead of .then() + // because we're issuing the clicks synchonrously + // once we establish the coordinates and the element + // passes all of the internal checks return $actionability.verify(cy, $el, options, { onScroll ($el, type) { return Cypress.action('cy:scrolled', $el, type) }, - onReady: ($elToClick, coords) => { - + onReady ($elToClick, coords) { const { fromWindow, fromViewport } = coords const forceEl = options.force && $elToClick.get(0) @@ -146,15 +200,15 @@ module.exports = (Commands, Cypress, cy, state, config) => { }, }) .catch((err) => { - //# snapshot only on click failure + // snapshot only on click failure err.onFail = function () { if (options._log) { return options._log.snapshot() } } - //# if we give up on waiting for actionability then - //# lets throw this error and log the command + // if we give up on waiting for actionability then + // lets throw this error and log the command return $utils.throwErr(err, { onFail: options._log }) }) } @@ -162,24 +216,23 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Promise .each(options.$el.toArray(), click) .then(() => { - let verifyAssertions - if (options.verify === false) { return options.$el } - return (verifyAssertions = () => { + const verifyAssertions = () => { return cy.verifyUpcomingAssertions(options.$el, options, { onRetry: verifyAssertions, }) - })() + } + + return verifyAssertions() }) }, - //# update dblclick to use the click - //# logic and just swap out the event details? + // update dblclick to use the click + // logic and just swap out the event details? dblclick (subject, positionOrX, y, options = {}) { - let position let x @@ -200,8 +253,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { animationDistanceThreshold: config('animationDistanceThreshold'), }) - //# throw if we're trying to click multiple elements - //# and we did not pass the multiple flag + // throw if we're trying to click multiple elements + // and we did not pass the multiple flag if ((options.multiple === false) && (options.$el.length > 1)) { $utils.throwErrByPath('click.multiple_elements', { args: { cmd: 'dblclick', num: options.$el.length }, @@ -213,7 +266,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { const $el = $dom.wrap(el) if (options.log) { - //# figure out the options which actually change the behavior of clicks + // figure out the options which actually change the behavior of clicks deltaOptions = $utils.filterOutOptions(options) options._log = Cypress.log({ @@ -228,9 +281,9 @@ module.exports = (Commands, Cypress, cy, state, config) => { $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'dblclick' }, onFail: options._log }) } - //# we want to add this delay delta to our - //# runnables timeout so we prevent it from - //# timing out from multiple clicks + // we want to add this delay delta to our + // runnables timeout so we prevent it from + // timing out from multiple clicks cy.timeout($actionability.delay, true, 'dblclick') const createLog = (domEvents, fromWindowCoords) => { @@ -246,12 +299,12 @@ module.exports = (Commands, Cypress, cy, state, config) => { consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { 'Applied To': $dom.getElements(options.$el), 'Elements': options.$el.length, - 'Coords': _.pick(fromWindowCoords, 'x', 'y'), //# always absolute + 'Coords': _.pick(fromWindowCoords, 'x', 'y'), // always absolute 'Options': deltaOptions, }) if (options.$el.get(0) !== elClicked) { - //# only do this if $elToClick isnt $el + // only do this if $elToClick isnt $el consoleObj['Actual Element Clicked'] = $dom.getElements(elClicked) } @@ -282,34 +335,34 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Promise .delay($actionability.delay, 'dblclick') .then(() => { - //# display the red dot at these coords + // display the red dot at these coords if (options._log) { - //# because we snapshot and output a command per click - //# we need to manually snapshot + end them + // because we snapshot and output a command per click + // we need to manually snapshot + end them options._log.set({ coords: fromWindowCoords, consoleProps }) } - //# we need to split this up because we want the coordinates - //# to mutate our passed in options._log but we dont necessary - //# want to snapshot and end our command if we're a different - //# action like (cy.type) and we're borrowing the click action + // we need to split this up because we want the coordinates + // to mutate our passed in options._log but we dont necessary + // want to snapshot and end our command if we're a different + // action like (cy.type) and we're borrowing the click action if (options._log && options.log) { return options._log.snapshot().end() } - }).return(null) + }) + .return(null) } - //# must use callbacks here instead of .then() - //# because we're issuing the clicks synchonrously - //# once we establish the coordinates and the element - //# passes all of the internal checks + // must use callbacks here instead of .then() + // because we're issuing the clicks synchonrously + // once we establish the coordinates and the element + // passes all of the internal checks return $actionability.verify(cy, $el, options, { onScroll ($el, type) { return Cypress.action('cy:scrolled', $el, type) }, - onReady: ($elToClick, coords) => { - + onReady ($elToClick, coords) { const { fromWindow, fromViewport } = coords const forceEl = options.force && $elToClick.get(0) const moveEvents = mouse.mouseMove(fromViewport, forceEl) @@ -323,15 +376,15 @@ module.exports = (Commands, Cypress, cy, state, config) => { }, }) .catch((err) => { - //# snapshot only on click failure + // snapshot only on click failure err.onFail = function () { if (options._log) { return options._log.snapshot() } } - //# if we give up on waiting for actionability then - //# lets throw this error and log the command + // if we give up on waiting for actionability then + // lets throw this error and log the command return $utils.throwErr(err, { onFail: options._log }) }) } @@ -339,22 +392,21 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Promise .each(options.$el.toArray(), dblclick) .then(() => { - let verifyAssertions - if (options.verify === false) { return options.$el } - return (verifyAssertions = () => { + const verifyAssertions = () => { return cy.verifyUpcomingAssertions(options.$el, options, { onRetry: verifyAssertions, }) - })() + } + + return verifyAssertions() }) }, rightclick (subject, positionOrX, y, options = {}) { - let position let x @@ -374,8 +426,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { animationDistanceThreshold: config('animationDistanceThreshold'), }) - //# throw if we're trying to click multiple elements - //# and we did not pass the multiple flag + // throw if we're trying to click multiple elements + // and we did not pass the multiple flag if ((options.multiple === false) && (options.$el.length > 1)) { $utils.throwErrByPath('click.multiple_elements', { args: { cmd: 'rightclick', num: options.$el.length }, @@ -387,7 +439,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { const $el = $dom.wrap(el) if (options.log) { - //# figure out the options which actually change the behavior of clicks + // figure out the options which actually change the behavior of clicks deltaOptions = $utils.filterOutOptions(options) options._log = Cypress.log({ @@ -402,9 +454,9 @@ module.exports = (Commands, Cypress, cy, state, config) => { $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'rightclick' }, onFail: options._log }) } - //# we want to add this delay delta to our - //# runnables timeout so we prevent it from - //# timing out from multiple clicks + // we want to add this delay delta to our + // runnables timeout so we prevent it from + // timing out from multiple clicks cy.timeout($actionability.delay, true, 'rightclick') const createLog = (domEvents, fromWindowCoords) => { @@ -420,12 +472,12 @@ module.exports = (Commands, Cypress, cy, state, config) => { consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { 'Applied To': $dom.getElements(options.$el), 'Elements': options.$el.length, - 'Coords': _.pick(fromWindowCoords, 'x', 'y'), //# always absolute + 'Coords': _.pick(fromWindowCoords, 'x', 'y'), // always absolute 'Options': deltaOptions, }) if (options.$el.get(0) !== elClicked) { - //# only do this if $elToClick isnt $el + // only do this if $elToClick isnt $el consoleObj['Actual Element Clicked'] = $dom.getElements(elClicked) } @@ -453,34 +505,34 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Promise .delay($actionability.delay, 'rightclick') .then(() => { - //# display the red dot at these coords + // display the red dot at these coords if (options._log) { - //# because we snapshot and output a command per click - //# we need to manually snapshot + end them + // because we snapshot and output a command per click + // we need to manually snapshot + end them options._log.set({ coords: fromWindowCoords, consoleProps }) } - //# we need to split this up because we want the coordinates - //# to mutate our passed in options._log but we dont necessary - //# want to snapshot and end our command if we're a different - //# action like (cy.type) and we're borrowing the click action + // we need to split this up because we want the coordinates + // to mutate our passed in options._log but we dont necessary + // want to snapshot and end our command if we're a different + // action like (cy.type) and we're borrowing the click action if (options._log && options.log) { return options._log.snapshot().end() } - }).return(null) + }) + .return(null) } - //# must use callbacks here instead of .then() - //# because we're issuing the clicks synchonrously - //# once we establish the coordinates and the element - //# passes all of the internal checks + // must use callbacks here instead of .then() + // because we're issuing the clicks synchonrously + // once we establish the coordinates and the element + // passes all of the internal checks return $actionability.verify(cy, $el, options, { onScroll ($el, type) { return Cypress.action('cy:scrolled', $el, type) }, - onReady: ($elToClick, coords) => { - + onReady ($elToClick, coords) { const { fromWindow, fromViewport } = coords const forceEl = options.force && $elToClick.get(0) const moveEvents = mouse.mouseMove(fromViewport, forceEl) @@ -494,15 +546,15 @@ module.exports = (Commands, Cypress, cy, state, config) => { }, }) .catch((err) => { - //# snapshot only on click failure + // snapshot only on click failure err.onFail = function () { if (options._log) { return options._log.snapshot() } } - //# if we give up on waiting for actionability then - //# lets throw this error and log the command + // if we give up on waiting for actionability then + // lets throw this error and log the command return $utils.throwErr(err, { onFail: options._log }) }) } @@ -510,75 +562,18 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Promise .each(options.$el.toArray(), rightclick) .then(() => { - let verifyAssertions - if (options.verify === false) { return options.$el } - return (verifyAssertions = () => { + const verifyAssertions = () => { return cy.verifyUpcomingAssertions(options.$el, options, { onRetry: verifyAssertions, }) - })() - }) - }, - }) -} - -const formatMoveEventsTable = (events) => { - - return { - name: `Mouse Move Events${events ? '' : ' (skipped)'}`, - data: _.map(events, (obj) => { - const key = _.keys(obj)[0] - const val = obj[_.keys(obj)[0]] - - if (val.skipped) { - const reason = val.skipped - - return { - 'Event Name': key, - 'Target Element': reason, - 'Prevented Default?': null, - 'Stopped Propagation?': null, - // 'Modifiers': null, } - } - return { - 'Event Name': key, - 'Target Element': val.el, - 'Prevented Default?': val.preventedDefault, - 'Stopped Propagation?': val.stoppedPropagation, - // 'Modifiers': val.modifiers ? val.modifiers : null, - } - }), - } -} - -const formatMouseEvents = (events) => { - return _.map(events, (val, key) => { - - if (val.skipped) { - - const reason = val.skipped - - return { - 'Event Name': key.slice(0, -5), - 'Target Element': reason, - 'Prevented Default?': null, - 'Stopped Propagation?': null, - 'Modifiers': null, - } - } - - return { - 'Event Name': key.slice(0, -5), - 'Target Element': val.el, - 'Prevented Default?': val.preventedDefault, - 'Stopped Propagation?': val.stoppedPropagation, - 'Modifiers': val.modifiers ? val.modifiers : null, - } + return verifyAssertions() + }) + }, }) } diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index b2fd2e6f1ed4..b622cd03190d 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -1,7 +1,7 @@ -const $dom = require('../dom') -const $elements = require('../dom/elements') const $ = require('jquery') const _ = require('lodash') +const $dom = require('../dom') +const $elements = require('../dom/elements') const $Keyboard = require('./keyboard') const $selection = require('../dom/selection') const debug = require('debug')('cypress:driver:mouse') @@ -375,21 +375,22 @@ const create = (state, keyboard, focused) => { /** * - * Steps to perform click: + * Steps to perform a click: * - * moveToCoordsOrNoop = (coords, el) => { + * moveToCoordsOrNoop = (coords) => { * elAtPoint = getElementFromPoint(coords) - * if (elAtPoint !== el) - * sendMouseMoveEvents(elAtPoint, el) + * if (elAtPoint !== elLastHovered) + * sendMouseMoveEvents({to: elAtPoint, from: elLastHovered}) + * elLastHovered = elAtPoint * return getElementFromPoint(coords) * } * * coords = getCoords(elSubject) - * el1 = moveToCoordsOrNoop(coords, elLastHovered) - * mouseDown(el1) - * el2 = moveToCoordsOrNoop(coords, el1) - * mouseUp(el2) - * el3 = moveToCoordsOrNoop(coords, el2) + * el1 = moveToCoordsOrNoop(coords) + * sendMousedown(el1) + * el2 = moveToCoordsOrNoop(coords) + * sendMouseup(el2) + * el3 = moveToCoordsOrNoop(coords) * if (notDetached(el1)) * sendClick(el3) */ 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 efd077fa3e8a..3f169097586a 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -27,6 +27,42 @@ const focusEvents = ['focus', 'focusin'] const allMouseEvents = [...mouseClickEvents, ...mouseHoverEvents, ...focusEvents] +const attachListeners = (listenerArr) => { + return (els) => { + _.each(els, (el, elName) => { + return listenerArr.forEach((evtName) => { + el.on(evtName, cy.stub().as(`${elName}:${evtName}`)) + }) + }) + } +} + +const attachFocusListeners = attachListeners(focusEvents) +const attachMouseClickListeners = attachListeners(mouseClickEvents) +const attachMouseHoverListeners = attachListeners(mouseHoverEvents) +const attachMouseDblclickListeners = attachListeners(['dblclick']) +const attachContextmenuListeners = attachListeners(['contextmenu']) + +const getAllFn = (...aliases) => { + if (aliases.length > 1) { + return getAllFn((_.isArray(aliases[1]) ? aliases[1] : aliases[1].split(' ')).map((alias) => `@${aliases[0]}:${alias}`).join(' ')) + } + + return Cypress.Promise.all( + aliases[0].split(' ').map((alias) => { + return cy.now('get', alias) + }) + ) +} + +Cypress.Commands.add('getAll', getAllFn) + +const wrapped = (obj) => cy.wrap(obj, { log: false }) +const shouldBeCalled = (stub) => wrapped(stub).should('be.called') +const shouldBeCalledOnce = (stub) => wrapped(stub).should('be.calledOnce') +const shouldBeCalledWithCount = (num) => (stub) => wrapped(stub).should('have.callCount', num) +const shouldNotBeCalled = (stub) => wrapped(stub).should('not.be.called') + describe('src/cy/commands/actions/click', () => { beforeEach(() => { cy.visit('/fixtures/dom.html') @@ -387,19 +423,22 @@ describe('src/cy/commands/actions/click', () => { attachFocusListeners({ btn }) attachMouseClickListeners({ btn, div }) - attachMouseHoverListeners({ div }) + attachMouseHoverListeners({ btn, div }) btn.on('pointerdown', () => { // synchronously remove this button - btn.remove() }) - // return cy.contains('button').click() cy.getAll('btn', 'pointerdown').each(shouldBeCalled) cy.getAll('btn', 'mousedown mouseup').each(shouldNotBeCalled) + + // the browser is in control of whether or not the pointerdown event + // so this test *may* not necessarily pass in all browsers, but it's + // worth adding to help specify the current expected behavior + cy.getAll('div', 'pointerdown').each(shouldNotBeCalled) cy.getAll('div', 'pointerover pointerenter mouseover mouseenter pointerup mouseup').each(shouldBeCalled) }) @@ -413,7 +452,6 @@ describe('src/cy/commands/actions/click', () => { btn.on('pointerover', () => { // synchronously remove this button - btn.remove() }) @@ -457,7 +495,6 @@ describe('src/cy/commands/actions/click', () => { }) it('sends modifiers', () => { - const btn = cy.$$('button:first') attachMouseClickListeners({ btn }) @@ -739,6 +776,7 @@ describe('src/cy/commands/actions/click', () => { }) }) + // https://github.com/cypress-io/cypress/issues/4347 it('can click inside an iframe', () => { cy.get('iframe') .should(($iframe) => { @@ -2209,7 +2247,7 @@ describe('src/cy/commands/actions/click', () => { cy.get('span#not-hidden').click().click() cy.getAll('btn', 'mousemove mouseover').each(shouldBeCalledOnce) - cy.getAll('btn', 'pointerdown mousedown pointerup mouseup click').each(shouldBeCalledNth(2)) + cy.getAll('btn', 'pointerdown mousedown pointerup mouseup click').each(shouldBeCalledWithCount(2)) .then(function () { const { logs } = this @@ -2539,7 +2577,6 @@ describe('src/cy/commands/actions/click', () => { }) it('serially dblclicks a collection of anchors to the top of the page', () => { - const throttled = cy.stub().as('clickcount') // create a throttled click function @@ -3802,7 +3839,7 @@ describe('mouse state', () => { cy.get('#sq4').click() cy.get('#outer').click() - cy.getAll('@sq4:mouseover @sq4:mousedown @sq4:mouseup @sq4:click').each(shouldBeCalledNth(2)) + cy.getAll('@sq4:mouseover @sq4:mousedown @sq4:mouseup @sq4:click').each(shouldBeCalledWithCount(2)) cy.getAll('@sq4:mouseout').each(shouldBeCalledOnce) @@ -3837,7 +3874,7 @@ describe('mouse state', () => { .click() cy.getAll('sq6', 'mousedown pointerdown click').each(shouldBeCalledOnce) - cy.getAll('sq6', 'mouseover').each(shouldBeCalledNth(2)) + cy.getAll('sq6', 'mouseover').each(shouldBeCalledWithCount(2)) }) }) @@ -4054,39 +4091,3 @@ describe('mouse state', () => { }) }) - -const attachListeners = (listenerArr) => { - return (els) => { - _.each(els, (el, elName) => { - return listenerArr.forEach((evtName) => { - el.on(evtName, cy.stub().as(`${elName}:${evtName}`)) - }) - }) - } -} - -const attachFocusListeners = attachListeners(focusEvents) -const attachMouseClickListeners = attachListeners(mouseClickEvents) -const attachMouseHoverListeners = attachListeners(mouseHoverEvents) -const attachMouseDblclickListeners = attachListeners(['dblclick']) -const attachContextmenuListeners = attachListeners(['contextmenu']) - -const getAllFn = (...aliases) => { - if (aliases.length > 1) { - return getAllFn((_.isArray(aliases[1]) ? aliases[1] : aliases[1].split(' ')).map((alias) => `@${aliases[0]}:${alias}`).join(' ')) - } - - return Cypress.Promise.all( - aliases[0].split(' ').map((alias) => { - return cy.now('get', alias) - }) - ) -} - -Cypress.Commands.add('getAll', getAllFn) - -const _wrapLogFalse = (obj) => cy.wrap(obj, { log: false }) -const shouldBeCalled = (stub) => _wrapLogFalse(stub).should('be.called') -const shouldBeCalledOnce = (stub) => _wrapLogFalse(stub).should('be.calledOnce') -const shouldBeCalledNth = (num) => (stub) => _wrapLogFalse(stub).should('have.callCount', num) -const shouldNotBeCalled = (stub) => _wrapLogFalse(stub).should('not.be.called') From 7a229f9868d83bb7a91f549d2aa8bee5ebad673e Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Sun, 29 Sep 2019 21:07:55 -0400 Subject: [PATCH 23/36] scope the getCommandLogWithText to the active runnable to prevent leakage --- packages/driver/test/cypress/support/utils.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/driver/test/cypress/support/utils.js b/packages/driver/test/cypress/support/utils.js index c6e7000e8c38..cb082d319c92 100644 --- a/packages/driver/test/cypress/support/utils.js +++ b/packages/driver/test/cypress/support/utils.js @@ -1,4 +1,11 @@ -export const getCommandLogWithText = (text) => cy.$$(`.command-wrapper:contains(${text}):visible`, top.document).parentsUntil('li').last().parent()[0] +export const getCommandLogWithText = (text) => { + return cy + .$$(`.runnable-active .command-wrapper:contains(${text}):visible`, top.document) + .parentsUntil('li') + .last() + .parent() + .get(0) +} export const findReactInstance = function (dom) { let key = Object.keys(dom).find((key) => key.startsWith('__reactInternalInstance$')) @@ -19,9 +26,8 @@ export const withMutableReporterState = (fn) => { currentTestLog.props.model.isOpen = true - return Cypress.Promise.try(() => { - return fn() - }).then(() => { + return Cypress.Promise.try(fn) + .then(() => { top.Runner.configureMobx({ enforceActions: 'strict' }) }) From adb6f2c0cf17b0b717a88c6298a648d7aa8a518c Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Sun, 29 Sep 2019 21:09:56 -0400 Subject: [PATCH 24/36] cleanup tests, make values static so tests pass consistently - use attach listener alias APIs - remove magic number constants - fix some incoherent html / script tag stuff --- .../driver/test/cypress/fixtures/dom.html | 10 + .../test/cypress/fixtures/issue-2956.html | 227 ++++++------- .../commands/actions/click_spec.js | 301 +++++++++++------- 3 files changed, 292 insertions(+), 246 deletions(-) diff --git a/packages/driver/test/cypress/fixtures/dom.html b/packages/driver/test/cypress/fixtures/dom.html index d9c8b4707857..3ccffd2a3271 100644 --- a/packages/driver/test/cypress/fixtures/dom.html +++ b/packages/driver/test/cypress/fixtures/dom.html @@ -6,6 +6,12 @@
DOM Fixture diff --git a/packages/driver/test/cypress/fixtures/issue-2956.html b/packages/driver/test/cypress/fixtures/issue-2956.html index cc2b6a1729ba..a0edfbfce47e 100644 --- a/packages/driver/test/cypress/fixtures/issue-2956.html +++ b/packages/driver/test/cypress/fixtures/issue-2956.html @@ -3,150 +3,129 @@ - - - - + + sq6.addEventListener("mouseenter", setHover); + sq6.addEventListener("mouseleave", unsetHover); + } + -
-
- +
+
+ +
-
-
-
- -
- -
- -
- -
- +
+
+
+ +
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 3f169097586a..136949f7cf52 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -25,8 +25,6 @@ const mouseHoverEvents = [ ] const focusEvents = ['focus', 'focusin'] -const allMouseEvents = [...mouseClickEvents, ...mouseHoverEvents, ...focusEvents] - const attachListeners = (listenerArr) => { return (els) => { _.each(els, (el, elName) => { @@ -869,11 +867,10 @@ describe('src/cy/commands/actions/click', () => { }) .prependTo($body) - $(`\ -\ -`) + $(`\ + `) .attr('id', 'nav') .css({ position: 'sticky', @@ -1026,6 +1023,10 @@ describe('src/cy/commands/actions/click', () => { $('') .attr('id', 'button-covered-in-nav') + .css({ + width: 120, + height: 20, + }) .appendTo(cy.$$('#fixed-nav-test')) .mousedown(spy) @@ -1047,11 +1048,21 @@ describe('src/cy/commands/actions/click', () => { // - element scrollIntoView // - element scrollIntoView (retry animation coords) // - window - cy.get('#button-covered-in-nav').click() - .then(() => { + cy + .get('#button-covered-in-nav').click() + .then(($btn) => { + const rect = $btn.get(0).getBoundingClientRect() + const { fromViewport } = 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(spy.args[0][0]).property('clientX').closeTo(60, 2) - expect(spy.args[0][0]).property('clientY').eq(68) + expect(obj).property('clientX').closeTo(fromViewport.leftCenter, 1) + expect(obj).property('clientY').closeTo(fromViewport.topCenter, 1) }) }) @@ -1923,8 +1934,6 @@ describe('src/cy/commands/actions/click', () => { this.logs.push(log) }) - - null }) it('logs immediately before resolving', (done) => { @@ -2045,23 +2054,26 @@ describe('src/cy/commands/actions/click', () => { }) it('#consoleProps', () => { - cy.get('button').first().click().then(function ($button) { + cy.get('button').first().click().then(function ($btn) { const { lastLog } = this - const console = lastLog.invoke('consoleProps') - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($button) - const logCoords = lastLog.get('coords') + const rect = $btn.get(0).getBoundingClientRect() + const consoleProps = lastLog.invoke('consoleProps') + const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + + // this button should be 60 pixels wide + expect(rect.width).to.eq(60) - expect(logCoords.x).to.be.closeTo(fromWindow.x, 1) // ensure we are within 1 - expect(logCoords.y).to.be.closeTo(fromWindow.y, 1) // ensure we are within 1 - expect(console.Command).to.eq('click') - expect(console['Applied To'], 'applied to').to.eq(lastLog.get('$el').get(0)) - expect(console.Elements).to.eq(1) - expect(console.Coords.x).to.be.closeTo(fromWindow.x, 1) // ensure we are within 1 + expect(consoleProps.Coords.x).to.be.closeTo(fromWindow.x, 1) // ensure we are within 1 + expect(consoleProps.Coords.y).to.be.closeTo(fromWindow.y, 1) // ensure we are within 1 - expect(console.Coords.y).to.be.closeTo(fromWindow.y, 1) + expect(consoleProps).to.containSubset({ + 'Command': 'click', + 'Applied To': lastLog.get('$el').get(0), + 'Elements': 1, + }) }) - }) // ensure we are within 1 + }) it('#consoleProps actual element clicked', () => { const $btn = $('').appendTo(cy.$$('body')) @@ -797,6 +718,84 @@ describe('src/cy/commands/actions/click', () => { }) }) + describe('pointer-events:none', () => { + beforeEach(function () { + cy.$$('
behind #ptrNone
').appendTo(cy.$$('#dom')) + this.ptrNone = cy.$$('
#ptrNone
').appendTo(cy.$$('#dom')) + cy.$$('
#ptrNone > 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('
{ + expect(onError).calledOnce + }) + }) + }) + describe('actionability', () => { it('can click on inline elements that wrap lines', () => { cy.get('#overflow-link').find('.wrapped').click() From 9f09c3cb1b579572fcd8a80c26592ad9b56aa9ea Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Mon, 30 Sep 2019 00:32:51 -0400 Subject: [PATCH 27/36] refactor actions to DRY up duplicated logic between click/dblclick/rightclick - tighten up the mouse.js method names, name them consistently --- .../driver/src/cy/commands/actions/click.js | 683 ++++++------------ packages/driver/src/cy/mouse.js | 43 +- .../commands/actions/click_spec.js | 7 +- 3 files changed, 243 insertions(+), 490 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index 02e6f858904c..0536814020f4 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -65,514 +65,273 @@ const formatMouseEvents = (events) => { module.exports = (Commands, Cypress, cy, state, config) => { const { mouse } = cy.devices - return Commands.addAll({ prevSubject: 'element' }, { - click (subject, positionOrX, y, options = {}) { - let position - let x - - ({ options, position, x, y } = $actionability.getPositionFromArguments(positionOrX, y, options)) - - _.defaults(options, { - $el: subject, - log: true, - verify: true, - force: false, - multiple: false, - position, - x, - y, - errorOnSelect: true, - waitForAnimations: config('waitForAnimations'), - animationDistanceThreshold: config('animationDistanceThreshold'), + const mouseEvent = (eventName, { subject, positionOrX, y, options, onReady, onTable }) => { + let position + let x + + ({ options, position, x, y } = $actionability.getPositionFromArguments(positionOrX, y, options)) + + _.defaults(options, { + $el: subject, + log: true, + verify: true, + force: false, + // TODO: 4.0 make this false by default + multiple: false, + position, + x, + y, + errorOnSelect: true, + waitForAnimations: config('waitForAnimations'), + animationDistanceThreshold: config('animationDistanceThreshold'), + }) + + // throw if we're trying to click multiple elements + // and we did not pass the multiple flag + if ((options.multiple === false) && (options.$el.length > 1)) { + $utils.throwErrByPath('click.multiple_elements', { + args: { cmd: eventName, num: options.$el.length }, }) + } - // throw if we're trying to click multiple elements - // and we did not pass the multiple flag - if ((options.multiple === false) && (options.$el.length > 1)) { - $utils.throwErrByPath('click.multiple_elements', { - args: { cmd: 'click', num: options.$el.length }, + const perform = (el) => { + let deltaOptions + const $el = $dom.wrap(el) + + if (options.log) { + // figure out the options which actually change the behavior of clicks + deltaOptions = $utils.filterOutOptions(options) + + options._log = Cypress.log({ + message: deltaOptions, + $el, }) + + options._log.snapshot('before', { next: 'after' }) } - const click = (el) => { - let deltaOptions - const $el = $dom.wrap(el) + if (options.errorOnSelect && $el.is('select')) { + $utils.throwErrByPath('click.on_select_element', { + args: { cmd: eventName }, + onFail: options._log, + }) + } - if (options.log) { - // figure out the options which actually change the behavior of clicks - deltaOptions = $utils.filterOutOptions(options) + // we want to add this delay delta to our + // runnables timeout so we prevent it from + // timing out from multiple clicks + cy.timeout($actionability.delay, true, eventName) - options._log = Cypress.log({ - message: deltaOptions, - $el, - }) + const createLog = (domEvents, fromWindowCoords) => { + let consoleObj - options._log.snapshot('before', { next: 'after' }) - } + const elClicked = domEvents.moveEvents.el - if (options.errorOnSelect && $el.is('select')) { - $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'click' }, onFail: options._log }) + if (options._log) { + consoleObj = options._log.invoke('consoleProps') } - // we want to add this delay delta to our - // runnables timeout so we prevent it from - // timing out from multiple clicks - cy.timeout($actionability.delay, true, 'click') + const consoleProps = function () { + consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { + 'Applied To': $dom.getElements(options.$el), + 'Elements': options.$el.length, + 'Coords': _.pick(fromWindowCoords, 'x', 'y'), // always absolute + 'Options': deltaOptions, + }) + + if (options.$el.get(0) !== elClicked) { + // only do this if $elToClick isnt $el + consoleObj['Actual Element Clicked'] = $dom.getElements($(elClicked)) + } - const createLog = (domEvents, fromWindowCoords) => { - let consoleObj + consoleObj.table = _.extend((consoleObj.table || {}), onTable(domEvents)) - const elClicked = domEvents.moveEvents.el + return consoleObj + } + return Promise + .delay($actionability.delay, 'click') + .then(() => { + // display the red dot at these coords if (options._log) { - consoleObj = options._log.invoke('consoleProps') + // because we snapshot and output a command per click + // we need to manually snapshot + end them + options._log.set({ coords: fromWindowCoords, consoleProps }) } - const consoleProps = function () { - consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { - 'Applied To': $dom.getElements(options.$el), - 'Elements': options.$el.length, - 'Coords': _.pick(fromWindowCoords, 'x', 'y'), // always absolute - 'Options': deltaOptions, - }) - - if (options.$el.get(0) !== elClicked) { - // only do this if $elToClick isnt $el - consoleObj['Actual Element Clicked'] = $dom.getElements($(elClicked)) - } - - consoleObj.table = _.extend((consoleObj.table || {}), { - 1: () => { - return formatMoveEventsTable(domEvents.moveEvents.events) - }, - 2: () => { - return { - name: 'Mouse Click Events', - data: formatMouseEvents(domEvents.clickEvents), - } - }, - }) - - return consoleObj + // we need to split this up because we want the coordinates + // to mutate our passed in options._log but we dont necessary + // want to snapshot and end our command if we're a different + // action like (cy.type) and we're borrowing the click action + if (options._log && options.log) { + return options._log.snapshot().end() } + }) + .return(null) + } - return Promise - .delay($actionability.delay, 'click') - .then(() => { - // display the red dot at these coords - if (options._log) { - // because we snapshot and output a command per click - // we need to manually snapshot + end them - options._log.set({ coords: fromWindowCoords, consoleProps }) - } - - // we need to split this up because we want the coordinates - // to mutate our passed in options._log but we dont necessary - // want to snapshot and end our command if we're a different - // action like (cy.type) and we're borrowing the click action - if (options._log && options.log) { - return options._log.snapshot().end() - } - }) - .return(null) - } - - // must use callbacks here instead of .then() - // because we're issuing the clicks synchonrously - // once we establish the coordinates and the element - // passes all of the internal checks - return $actionability.verify(cy, $el, options, { - onScroll ($el, type) { - return Cypress.action('cy:scrolled', $el, type) - }, - - onReady ($elToClick, coords) { - const { fromWindow, fromViewport } = coords + // must use callbacks here instead of .then() + // because we're issuing the clicks synchonrously + // once we establish the coordinates and the element + // passes all of the internal checks + return $actionability.verify(cy, $el, options, { + onScroll ($el, type) { + return Cypress.action('cy:scrolled', $el, type) + }, - const forceEl = options.force && $elToClick.get(0) + onReady ($elToClick, coords) { + const { fromWindow, fromViewport } = coords - const moveEvents = mouse.mouseMove(fromViewport, forceEl) + const forceEl = options.force && $elToClick.get(0) - const clickEvents = mouse.mouseClick(fromViewport, forceEl) + const moveEvents = mouse.move(fromViewport, forceEl) - return createLog({ moveEvents, clickEvents }, fromWindow) + const onReadyProps = onReady(fromViewport, forceEl) - }, - }) - .catch((err) => { - // snapshot only on click failure - err.onFail = function () { - if (options._log) { - return options._log.snapshot() - } + return createLog({ + moveEvents, + ...onReadyProps, + }, fromWindow) + }, + }) + .catch((err) => { + // snapshot only on click failure + err.onFail = function () { + if (options._log) { + return options._log.snapshot() } - - // if we give up on waiting for actionability then - // lets throw this error and log the command - return $utils.throwErr(err, { onFail: options._log }) - }) - } - - return Promise - .each(options.$el.toArray(), click) - .then(() => { - if (options.verify === false) { - return options.$el } - const verifyAssertions = () => { - return cy.verifyUpcomingAssertions(options.$el, options, { - onRetry: verifyAssertions, - }) - } - - return verifyAssertions() + // if we give up on waiting for actionability then + // lets throw this error and log the command + return $utils.throwErr(err, { onFail: options._log }) }) - }, + } - // update dblclick to use the click - // logic and just swap out the event details? - dblclick (subject, positionOrX, y, options = {}) { - let position - let x - - ({ options, position, x, y } = $actionability.getPositionFromArguments(positionOrX, y, options)) - - _.defaults(options, { - $el: subject, - log: true, - verify: true, - force: false, - // TODO: 4.0 make this false by default - multiple: true, - position, - x, - y, - errorOnSelect: true, - waitForAnimations: config('waitForAnimations'), - animationDistanceThreshold: config('animationDistanceThreshold'), - }) + return Promise + .each(options.$el.toArray(), perform) + .then(() => { + if (options.verify === false) { + return options.$el + } - // throw if we're trying to click multiple elements - // and we did not pass the multiple flag - if ((options.multiple === false) && (options.$el.length > 1)) { - $utils.throwErrByPath('click.multiple_elements', { - args: { cmd: 'dblclick', num: options.$el.length }, + const verifyAssertions = () => { + return cy.verifyUpcomingAssertions(options.$el, options, { + onRetry: verifyAssertions, }) } - const dblclick = (el) => { - let deltaOptions - const $el = $dom.wrap(el) - - if (options.log) { - // figure out the options which actually change the behavior of clicks - deltaOptions = $utils.filterOutOptions(options) - - options._log = Cypress.log({ - message: deltaOptions, - $el, - }) - - options._log.snapshot('before', { next: 'after' }) - } - - if (options.errorOnSelect && $el.is('select')) { - $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'dblclick' }, onFail: options._log }) - } - - // we want to add this delay delta to our - // runnables timeout so we prevent it from - // timing out from multiple clicks - cy.timeout($actionability.delay, true, 'dblclick') - - const createLog = (domEvents, fromWindowCoords) => { - let consoleObj - - const elClicked = domEvents.moveEvents.el + return verifyAssertions() + }) + } - if (options._log) { - consoleObj = options._log.invoke('consoleProps') + return Commands.addAll({ prevSubject: 'element' }, { + click (subject, positionOrX, y, options = {}) { + return mouseEvent('click', { + y, + subject, + options, + positionOrX, + onReady (fromViewport, forceEl) { + const clickEvents = mouse.click(fromViewport, forceEl) + + return { + clickEvents, } - - const consoleProps = function () { - consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { - 'Applied To': $dom.getElements(options.$el), - 'Elements': options.$el.length, - 'Coords': _.pick(fromWindowCoords, 'x', 'y'), // always absolute - 'Options': deltaOptions, - }) - - if (options.$el.get(0) !== elClicked) { - // only do this if $elToClick isnt $el - consoleObj['Actual Element Clicked'] = $dom.getElements(elClicked) - } - - consoleObj.table = _.extend((consoleObj.table || {}), { - 1: () => { - return formatMoveEventsTable(domEvents.moveEvents.events) - }, - 2: () => { - return { - name: 'Mouse Click Events', - data: _.concat( - formatMouseEvents(domEvents.clickEvents[0], formatMouseEvents), - formatMouseEvents(domEvents.clickEvents[1], formatMouseEvents) - ), - } - }, - 3: () => { - return { - name: 'Mouse Dblclick Event', - data: formatMouseEvents({ dblclickProps: domEvents.dblclickProps }), - } - }, - }) - - return consoleObj + }, + onTable (domEvents) { + return { + 1: () => { + return formatMoveEventsTable(domEvents.moveEvents.events) + }, + 2: () => { + return { + name: 'Mouse Click Events', + data: formatMouseEvents(domEvents.clickEvents), + } + }, } + }, + }) + }, - return Promise - .delay($actionability.delay, 'dblclick') - .then(() => { - // display the red dot at these coords - if (options._log) { - // because we snapshot and output a command per click - // we need to manually snapshot + end them - options._log.set({ coords: fromWindowCoords, consoleProps }) - } - - // we need to split this up because we want the coordinates - // to mutate our passed in options._log but we dont necessary - // want to snapshot and end our command if we're a different - // action like (cy.type) and we're borrowing the click action - if (options._log && options.log) { - return options._log.snapshot().end() - } - }) - .return(null) - } + dblclick (subject, positionOrX, y, options = {}) { + // TODO: 4.0 make this false by default + options.multiple = true - // must use callbacks here instead of .then() - // because we're issuing the clicks synchonrously - // once we establish the coordinates and the element - // passes all of the internal checks - return $actionability.verify(cy, $el, options, { - onScroll ($el, type) { - return Cypress.action('cy:scrolled', $el, type) - }, - - onReady ($elToClick, coords) { - const { fromWindow, fromViewport } = coords - const forceEl = options.force && $elToClick.get(0) - const moveEvents = mouse.mouseMove(fromViewport, forceEl) - const { clickEvents1, clickEvents2, dblclickProps } = mouse.dblclick(fromViewport, forceEl) - - return createLog({ - moveEvents, - clickEvents: [clickEvents1, clickEvents2], - dblclickProps, - }, fromWindow) - }, - }) - .catch((err) => { - // snapshot only on click failure - err.onFail = function () { - if (options._log) { - return options._log.snapshot() - } + return mouseEvent('dblclick', { + y, + subject, + options, + positionOrX, + onReady (fromViewport, forceEl) { + const { clickEvents1, clickEvents2, dblclickProps } = mouse.dblclick(fromViewport, forceEl) + + return { + dblclickProps, + clickEvents: [clickEvents1, clickEvents2], } - - // if we give up on waiting for actionability then - // lets throw this error and log the command - return $utils.throwErr(err, { onFail: options._log }) - }) - } - - return Promise - .each(options.$el.toArray(), dblclick) - .then(() => { - if (options.verify === false) { - return options.$el - } - - const verifyAssertions = () => { - return cy.verifyUpcomingAssertions(options.$el, options, { - onRetry: verifyAssertions, - }) - } - - return verifyAssertions() + }, + onTable (domEvents) { + return { + 1: () => { + return formatMoveEventsTable(domEvents.moveEvents.events) + }, + 2: () => { + return { + name: 'Mouse Click Events', + data: _.concat( + formatMouseEvents(domEvents.clickEvents[0], formatMouseEvents), + formatMouseEvents(domEvents.clickEvents[1], formatMouseEvents) + ), + } + }, + 3: () => { + return { + name: 'Mouse Double Click Event', + data: formatMouseEvents({ + dblclickProps: domEvents.dblclickProps, + }), + } + }, + } + }, }) }, rightclick (subject, positionOrX, y, options = {}) { - let position - let x - - ({ options, position, x, y } = $actionability.getPositionFromArguments(positionOrX, y, options)) - - _.defaults(options, { - $el: subject, - log: true, - verify: true, - force: false, - multiple: false, - position, - x, + return mouseEvent('rightclick', { y, - errorOnSelect: true, - waitForAnimations: config('waitForAnimations'), - animationDistanceThreshold: config('animationDistanceThreshold'), - }) - - // throw if we're trying to click multiple elements - // and we did not pass the multiple flag - if ((options.multiple === false) && (options.$el.length > 1)) { - $utils.throwErrByPath('click.multiple_elements', { - args: { cmd: 'rightclick', num: options.$el.length }, - }) - } - - const rightclick = (el) => { - let deltaOptions - const $el = $dom.wrap(el) - - if (options.log) { - // figure out the options which actually change the behavior of clicks - deltaOptions = $utils.filterOutOptions(options) - - options._log = Cypress.log({ - message: deltaOptions, - $el, - }) - - options._log.snapshot('before', { next: 'after' }) - } - - if (options.errorOnSelect && $el.is('select')) { - $utils.throwErrByPath('click.on_select_element', { args: { cmd: 'rightclick' }, onFail: options._log }) - } - - // we want to add this delay delta to our - // runnables timeout so we prevent it from - // timing out from multiple clicks - cy.timeout($actionability.delay, true, 'rightclick') - - const createLog = (domEvents, fromWindowCoords) => { - let consoleObj - - const elClicked = domEvents.moveEvents.el - - if (options._log) { - consoleObj = options._log.invoke('consoleProps') + subject, + options, + positionOrX, + onReady (fromViewport, forceEl) { + const { clickEvents, contextmenuEvent } = mouse.rightclick(fromViewport, forceEl) + + return { + clickEvents, + contextmenuEvent, } - - const consoleProps = function () { - consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { - 'Applied To': $dom.getElements(options.$el), - 'Elements': options.$el.length, - 'Coords': _.pick(fromWindowCoords, 'x', 'y'), // always absolute - 'Options': deltaOptions, - }) - - if (options.$el.get(0) !== elClicked) { - // only do this if $elToClick isnt $el - consoleObj['Actual Element Clicked'] = $dom.getElements(elClicked) - } - - consoleObj.table = _.extend((consoleObj.table || {}), { - 1: () => { - return formatMoveEventsTable(domEvents.moveEvents.events) - }, - 2: () => { - return { - name: 'Mouse Click Events', - data: formatMouseEvents(domEvents.clickEvents, formatMouseEvents), - } - }, - 3: () => { - return { - name: 'Contextmenu Event', - data: formatMouseEvents(domEvents.contextmenuEvent), - } - }, - }) - - return consoleObj + }, + onTable (domEvents) { + return { + 1: () => { + return formatMoveEventsTable(domEvents.moveEvents.events) + }, + 2: () => { + return { + name: 'Mouse Click Events', + data: formatMouseEvents(domEvents.clickEvents, formatMouseEvents), + } + }, + 3: () => { + return { + name: 'Mouse Right Click Event', + data: formatMouseEvents(domEvents.contextmenuEvent), + } + }, } - - return Promise - .delay($actionability.delay, 'rightclick') - .then(() => { - // display the red dot at these coords - if (options._log) { - // because we snapshot and output a command per click - // we need to manually snapshot + end them - options._log.set({ coords: fromWindowCoords, consoleProps }) - } - - // we need to split this up because we want the coordinates - // to mutate our passed in options._log but we dont necessary - // want to snapshot and end our command if we're a different - // action like (cy.type) and we're borrowing the click action - if (options._log && options.log) { - return options._log.snapshot().end() - } - }) - .return(null) - } - - // must use callbacks here instead of .then() - // because we're issuing the clicks synchonrously - // once we establish the coordinates and the element - // passes all of the internal checks - return $actionability.verify(cy, $el, options, { - onScroll ($el, type) { - return Cypress.action('cy:scrolled', $el, type) - }, - - onReady ($elToClick, coords) { - const { fromWindow, fromViewport } = coords - const forceEl = options.force && $elToClick.get(0) - const moveEvents = mouse.mouseMove(fromViewport, forceEl) - const { clickEvents, contextmenuEvent } = mouse.rightclick(fromViewport, forceEl) - - return createLog({ - moveEvents, - clickEvents, - contextmenuEvent, - }, fromWindow) - }, - }) - .catch((err) => { - // snapshot only on click failure - err.onFail = function () { - if (options._log) { - return options._log.snapshot() - } - } - - // if we give up on waiting for actionability then - // lets throw this error and log the command - return $utils.throwErr(err, { onFail: options._log }) - }) - } - - return Promise - .each(options.$el.toArray(), rightclick) - .then(() => { - if (options.verify === false) { - return options.$el - } - - const verifyAssertions = () => { - return cy.verifyUpcomingAssertions(options.$el, options, { - onRetry: verifyAssertions, - }) - } - - return verifyAssertions() + }, }) }, }) diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index b622cd03190d..ee065e6df2a4 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -60,8 +60,8 @@ const create = (state, keyboard, focused) => { * @param {Coords} coords * @param {HTMLElement} forceEl */ - mouseMove (coords, forceEl) { - debug('mousemove', coords) + move (coords, forceEl) { + debug('mouse.move', coords) const lastHoveredEl = getLastHoveredEl(state) @@ -70,7 +70,7 @@ const create = (state, keyboard, focused) => { // if coords are same AND we're already hovered on the element, don't send move events if (_.isEqual({ x: coords.x, y: coords.y }, getMouseCoords(state)) && lastHoveredEl === targetEl) return { el: targetEl } - const events = mouse._mouseMoveEvents(targetEl, coords) + const events = mouse._moveEvents(targetEl, coords) const resultEl = mouse.getElAtCoordsOrForce(coords, forceEl) @@ -88,8 +88,7 @@ const create = (state, keyboard, focused) => { * - send move events to elToHover (bubbles) * - elLastHovered = elToHover */ - _mouseMoveEvents (el, coords) { - + _moveEvents (el, coords) { // events are not fired on disabled elements, so we don't have to take that into account const win = $dom.getWindowByElement(el) const { x, y } = coords @@ -252,7 +251,7 @@ const create = (state, keyboard, focused) => { return forceEl } - const { el } = mouse.mouseMove(coords) + const { el } = mouse.move(coords) return el }, @@ -261,7 +260,7 @@ const create = (state, keyboard, focused) => { * @param {Coords} coords * @param {HTMLElement} forceEl */ - _mouseDownEvents (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + _downEvents (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { const { x, y } = coords const el = mouse.moveToCoordsOrForce(coords, forceEl) @@ -318,11 +317,10 @@ const create = (state, keyboard, focused) => { }, - mouseDown (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { - + down (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { const $previouslyFocused = focused.getFocused() - const mouseDownEvents = mouse._mouseDownEvents(coords, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseDownEvents = mouse._downEvents(coords, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) // el we just send pointerdown const el = mouseDownEvents.pointerdownProps.el @@ -341,7 +339,6 @@ const create = (state, keyboard, focused) => { const $elToFocus = $elements.getFirstFocusableEl($(el)) if (focused.needsFocus($elToFocus, $previouslyFocused)) { - if ($dom.isWindow($elToFocus)) { // if the first focusable element from the click // is the window, then we can skip the focus event @@ -367,10 +364,10 @@ const create = (state, keyboard, focused) => { * @param {Coords} fromViewport * @param {HTMLElement} forceEl */ - mouseUp (fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { - debug('mouseUp', { fromViewport, forceEl, skipMouseEvent }) + up (fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + debug('mouse.up', { fromViewport, forceEl, skipMouseEvent }) - return mouse._mouseUpEvents(fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + return mouse._upEvents(fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) }, /** @@ -394,14 +391,14 @@ const create = (state, keyboard, focused) => { * if (notDetached(el1)) * sendClick(el3) */ - mouseClick (fromViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + click (fromViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + debug('mouse.click', { fromViewport, forceEl }) - debug('mouseClick', { fromViewport, forceEl }) - const mouseDownEvents = mouse.mouseDown(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseDownEvents = mouse.down(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault - const mouseUpEvents = mouse.mouseUp(fromViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseUpEvents = mouse.up(fromViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) const skipClickEvent = $elements.isDetachedEl(mouseDownEvents.pointerdownProps.el) @@ -417,7 +414,7 @@ const create = (state, keyboard, focused) => { * @param {HTMLElement} forceEl * @param {Window} win */ - _mouseUpEvents (fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + _upEvents (fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { const win = state('window') @@ -500,7 +497,7 @@ const create = (state, keyboard, focused) => { dblclick (fromViewport, forceEl, mouseEvtOptionsExtend = {}) { const click = (clickNum) => { - const clickEvents = mouse.mouseClick(fromViewport, forceEl, {}, { detail: clickNum }) + const clickEvents = mouse.click(fromViewport, forceEl, {}, { detail: clickNum }) return clickEvents } @@ -522,7 +519,6 @@ const create = (state, keyboard, focused) => { }, rightclick (fromViewport, forceEl) { - const pointerEvtOptionsExtend = { button: 2, buttons: 2, @@ -534,18 +530,17 @@ const create = (state, keyboard, focused) => { which: 3, } - const mouseDownEvents = mouse.mouseDown(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseDownEvents = mouse.down(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) const contextmenuEvent = mouse._contextmenuEvent(fromViewport, forceEl) const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault - const mouseUpEvents = mouse.mouseUp(fromViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseUpEvents = mouse.up(fromViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) const clickEvents = _.extend({}, mouseDownEvents, mouseUpEvents) 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 a9ac2ac47454..bef2ff653dd5 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -707,7 +707,6 @@ describe('src/cy/commands/actions/click', () => { // return cy.wrap($iframe[0].contentDocument.body) return cy.wrap($iframe.first().contents().find('body')) }) - .within(() => { cy.get('a#hashchange') // .should($el => $el[0].click()) @@ -2981,7 +2980,7 @@ describe('src/cy/commands/actions/click', () => { ], }, { - 'name': 'Mouse Dblclick Event', + 'name': 'Mouse Double Click Event', 'data': [ { 'Event Name': 'dblclick', @@ -3380,7 +3379,7 @@ describe('src/cy/commands/actions/click', () => { ], }, { - 'name': 'Contextmenu Event', + 'name': 'Mouse Right Click Event', 'data': [ { 'Event Name': 'contextmenu', @@ -3420,7 +3419,7 @@ describe('mouse state', () => { doc: cy.state('document'), } - cy.devices.mouse.mouseMove(coords) + cy.devices.mouse.move(coords) expect(mouseenter).to.be.calledOnce expect(cy.state('mouseCoords')).ok }) From 807ada5f50e168b063a0aa01b16d4e518dc6a366 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Mon, 30 Sep 2019 00:33:24 -0400 Subject: [PATCH 28/36] remove comment --- packages/driver/src/cy/commands/actions/click.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index 0536814020f4..473bd4fe8f77 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -76,7 +76,6 @@ module.exports = (Commands, Cypress, cy, state, config) => { log: true, verify: true, force: false, - // TODO: 4.0 make this false by default multiple: false, position, x, From b4b160af6779431a6985739053730b02b2770d28 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Fri, 4 Oct 2019 16:35:50 -0400 Subject: [PATCH 29/36] fix iframe coords and test --- .../driver/src/cy/commands/actions/click.js | 20 +- .../src/cy/commands/actions/trigger.coffee | 10 +- packages/driver/src/dom/coordinates.js | 32 +-- .../commands/actions/click_spec.js | 226 ++++++++++++------ .../integration/e2e/domSnapshots.spec.js | 26 +- packages/driver/test/cypress/support/utils.js | 30 ++- 6 files changed, 227 insertions(+), 117 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index 473bd4fe8f77..166596d6dcc4 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -4,6 +4,7 @@ const Promise = require('bluebird') const $dom = require('../../../dom') const $utils = require('../../../cypress/utils') const $actionability = require('../../actionability') +const debug = require('debug')('cypress:driver:click') const formatMoveEventsTable = (events) => { return { @@ -65,7 +66,7 @@ const formatMouseEvents = (events) => { module.exports = (Commands, Cypress, cy, state, config) => { const { mouse } = cy.devices - const mouseEvent = (eventName, { subject, positionOrX, y, options, onReady, onTable }) => { + const mouseAction = (eventName, { subject, positionOrX, y, options, onReady, onTable }) => { let position let x @@ -121,7 +122,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { // timing out from multiple clicks cy.timeout($actionability.delay, true, eventName) - const createLog = (domEvents, fromWindowCoords) => { + const createLog = (domEvents, fromWindowCoords, fromAutWindowCoords) => { let consoleObj const elClicked = domEvents.moveEvents.el @@ -155,7 +156,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { if (options._log) { // because we snapshot and output a command per click // we need to manually snapshot + end them - options._log.set({ coords: fromWindowCoords, consoleProps }) + options._log.set({ coords: fromAutWindowCoords, consoleProps }) } // we need to split this up because we want the coordinates @@ -179,8 +180,9 @@ module.exports = (Commands, Cypress, cy, state, config) => { }, onReady ($elToClick, coords) { - const { fromWindow, fromViewport } = coords + const { fromViewport, fromAutWindow, fromWindow } = coords + debug('got coords', { fromViewport, fromAutWindow }) const forceEl = options.force && $elToClick.get(0) const moveEvents = mouse.move(fromViewport, forceEl) @@ -190,7 +192,9 @@ module.exports = (Commands, Cypress, cy, state, config) => { return createLog({ moveEvents, ...onReadyProps, - }, fromWindow) + }, + fromWindow, + fromAutWindow) }, }) .catch((err) => { @@ -226,7 +230,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Commands.addAll({ prevSubject: 'element' }, { click (subject, positionOrX, y, options = {}) { - return mouseEvent('click', { + return mouseAction('click', { y, subject, options, @@ -258,7 +262,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { // TODO: 4.0 make this false by default options.multiple = true - return mouseEvent('dblclick', { + return mouseAction('dblclick', { y, subject, options, @@ -299,7 +303,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { }, rightclick (subject, positionOrX, y, options = {}) { - return mouseEvent('rightclick', { + return mouseAction('rightclick', { y, subject, options, diff --git a/packages/driver/src/cy/commands/actions/trigger.coffee b/packages/driver/src/cy/commands/actions/trigger.coffee index 9f47fd3fe43c..4142193bd812 100644 --- a/packages/driver/src/cy/commands/actions/trigger.coffee +++ b/packages/driver/src/cy/commands/actions/trigger.coffee @@ -84,23 +84,21 @@ module.exports = (Commands, Cypress, cy, state, config) -> Cypress.action("cy:scrolled", $el, type) onReady: ($elToClick, coords) -> - { fromWindow, fromViewport } = coords + { fromWindow, fromViewport, fromAutWindow } = coords if options._log ## display the red dot at these coords options._log.set({ - coords: fromWindow + coords: fromAutWindow }) - docCoords = $elements.getFromDocCoords(fromViewport.x, fromViewport.y, $window.getWindowByElement($elToClick.get(0))) - eventOptions = _.extend({ clientX: fromViewport.x clientY: fromViewport.y screenX: fromViewport.x screenY: fromViewport.y - pageX: docCoords.x - pageY: docCoords.y + pageX: fromWindow.x + pageY: fromWindow.y }, eventOptions) dispatch($elToClick.get(0), eventName, eventOptions) diff --git a/packages/driver/src/dom/coordinates.js b/packages/driver/src/dom/coordinates.js index 2bde9d76c1c2..b6f9d259be4e 100644 --- a/packages/driver/src/dom/coordinates.js +++ b/packages/driver/src/dom/coordinates.js @@ -5,9 +5,7 @@ const getElementAtPointFromViewport = (doc, x, y) => { return doc.elementFromPoint(x, y) } -const isAutIframe = (win) => { - !$elements.getNativeProp(win.parent, 'frameElement') -} +const isAutIframe = (win) => !$elements.getNativeProp(win.parent, 'frameElement') const getElementPositioning = ($el) => { /** @@ -34,18 +32,17 @@ const getElementPositioning = ($el) => { let curWindow = el.ownerDocument.defaultView let frame - while (!isAutIframe(curWindow) && window.parent !== window) { + while (!isAutIframe(curWindow) && curWindow.parent !== curWindow) { frame = $elements.getNativeProp(curWindow, 'frameElement') - curWindow = curWindow.parent - if (curWindow && $elements.getNativeProp(curWindow, 'frameElement')) { + if (curWindow && frame) { const frameRect = frame.getBoundingClientRect() x += frameRect.left y += frameRect.top } - // Cypress will sometimes miss the Iframe if coords are too small - // remove this when test-runner is extracted out + + curWindow = curWindow.parent } autFrame = curWindow @@ -60,12 +57,12 @@ const getElementPositioning = ($el) => { } } - const rectCenter = getCenterCoordinates(rect) const rectFromAut = getRectFromAutIframe(rect, el) const rectFromAutCenter = getCenterCoordinates(rectFromAut) // add the center coordinates // because its useful to any caller + const rectCenter = getCenterCoordinates(rect) const topCenter = rectCenter.y const leftCenter = rectCenter.x @@ -224,7 +221,7 @@ const getElementCoordinatesByPosition = ($el, position) => { // but also from the viewport so // whoever is calling us can use it // however they'd like - const { width, height, fromViewport, fromWindow } = positionProps + const { width, height, fromViewport, fromWindow, fromAutWindow } = positionProps // dynamically call the by transforming the nam=> e // bottom -> getBottomCoordinates @@ -259,14 +256,21 @@ const getElementCoordinatesByPosition = ($el, position) => { fromWindow.x = windowTargetCoords.x fromWindow.y = windowTargetCoords.y + const autTargetCoords = fn({ + width, + height, + top: fromAutWindow.top, + left: fromAutWindow.left, + }) + + fromAutWindow.x = autTargetCoords.x + fromAutWindow.y = autTargetCoords.y + // return an object with both sets // of normalized coordinates for both // the window and the viewport return { - width, - height, - fromViewport, - fromWindow, + ...positionProps, } } 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 fd7769a38f11..ddb39462ed02 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -2,7 +2,7 @@ const $ = Cypress.$.bind(Cypress) const { _ } = Cypress const { Promise } = Cypress const chaiSubset = require('chai-subset') -const { getCommandLogWithText, findReactInstance, withMutableReporterState } = require('../../../support/utils') +const { getCommandLogWithText, findReactInstance, withMutableReporterState, clickCommandLog } = require('../../../support/utils') chai.use(chaiSubset) @@ -215,7 +215,7 @@ describe('src/cy/commands/actions/click', () => { }) it('records correct clientX when el scrolled', (done) => { - const $btn = $('').appendTo(cy.$$('body')) + const $btn = $(``).appendTo(cy.$$('body')) const win = cy.state('window') @@ -232,7 +232,7 @@ describe('src/cy/commands/actions/click', () => { }) it('records correct clientY when el scrolled', (done) => { - const $btn = $('').appendTo(cy.$$('body')) + const $btn = $(``).appendTo(cy.$$('body')) const win = cy.state('window') @@ -695,25 +695,81 @@ describe('src/cy/commands/actions/click', () => { }) }) - // https://github.com/cypress-io/cypress/issues/4347 - it('can click inside an iframe', () => { - cy.get('iframe') - .should(($iframe) => { - // wait for iframe to load - expect($iframe.first().contents().find('body').html()).ok + describe('pointer-events:none', () => { + beforeEach(function () { + cy.$$('
behind #ptrNone
').appendTo(cy.$$('#dom')) + this.ptrNone = cy.$$(`
#ptrNone
`).appendTo(cy.$$('#dom')) + cy.$$('
#ptrNone > div
').appendTo(this.ptrNone) + + this.logs = [] + cy.on('log:added', (attrs, log) => { + this.lastLog = log + + this.logs.push(log) + }) }) - .then(($iframe) => { - // cypress does not wrap this as a DOM element (does not wrap in jquery) - // return cy.wrap($iframe[0].contentDocument.body) - return cy.wrap($iframe.first().contents().find('body')) + + it('element behind pointer-events:none should still get click', () => { + cy.get('#ptr').click() // should pass with flying colors }) - .within(() => { - cy.get('a#hashchange') - // .should($el => $el[0].click()) - .click() + + it('should be able to force on pointer-events:none with force:true', () => { + cy.get('#ptrNone').click({ timeout: 300, force: true }) }) - .then(($body) => { - expect($body[0].ownerDocument.defaultView.location.hash).eq('#hashchange') + + 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('
{ + expect(onError).calledOnce + }) }) }) @@ -1839,7 +1895,7 @@ describe('src/cy/commands/actions/click', () => { expect(lastLog.get('snapshots')[1].name).to.eq('after') expect(err.message).to.include('cy.click() failed because this element is not visible:') expect(err.message).to.include('>button ...') - expect(err.message).to.include('\'\' is not visible because it has CSS property: \'position: fixed\' and its being covered') + expect(err.message).to.include(`'' is not visible because it has CSS property: 'position: fixed' and its being covered`) expect(err.message).to.include('>span on...') const console = lastLog.invoke('consoleProps') @@ -1884,7 +1940,7 @@ describe('src/cy/commands/actions/click', () => { it('throws when provided invalid position', function (done) { cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Invalid position argument: \'foo\'. Position may only be topLeft, top, topRight, left, center, right, bottomLeft, bottom, bottomRight.') + expect(err.message).to.eq(`Invalid position argument: 'foo'. Position may only be topLeft, top, topRight, left, center, right, bottomLeft, bottom, bottomRight.`) done() }) @@ -1995,7 +2051,7 @@ describe('src/cy/commands/actions/click', () => { // append two buttons const button = () => { - return $('') + return $(``) } cy.$$('body').append(button()).append(button()) @@ -2432,33 +2488,6 @@ describe('src/cy/commands/actions/click', () => { }) }) - it('can print table of keys on click', () => { - const spyTableName = cy.spy(top.console, 'groupCollapsed') - const spyTableData = cy.spy(top.console, 'table') - - cy.get('input:first').click() - - cy.wrap(null) - .should(() => { - spyTableName.reset() - spyTableData.reset() - - return withMutableReporterState(() => { - const commandLogEl = getCommandLogWithText('click') - - const reactCommandInstance = findReactInstance(commandLogEl) - - reactCommandInstance.props.appState.isRunning = false - - $(commandLogEl).find('.command-wrapper').click() - - expect(spyTableName).calledWith('Mouse Move Events') - expect(spyTableName).calledWith('Mouse Click Events') - expect(spyTableData).calledTwice - }) - }) - }) - it('does not fire a click when element has been removed on mouseup', () => { const $btn = cy.$$('button:first') @@ -2826,7 +2855,7 @@ describe('src/cy/commands/actions/click', () => { // append two buttons const $button = () => { - return $('`) .css({ float: 'left', display: 'block', @@ -3933,7 +3962,7 @@ describe('mouse state', () => { }) it('handles disabled attr added on mousedown', () => { - const btn = cy.$$(/*html*/'`) .css({ float: 'left', display: 'block', @@ -3961,7 +3990,7 @@ describe('mouse state', () => { }) it('can click new element after mousemove sequence', () => { - const btn = cy.$$(/*html*/'`) .css({ float: 'left', display: 'block', @@ -3970,7 +3999,7 @@ describe('mouse state', () => { }) .appendTo(cy.$$('body')) - const cover = cy.$$(/*html*/'
').css({ + const cover = cy.$$(/*html*/`
`).css({ backgroundColor: 'blue', position: 'relative', height: 50, @@ -3991,18 +4020,13 @@ describe('mouse state', () => { expect(stub).to.not.be.called }) - // should we send mouseover to newly hovered els? - // cy.getAll('@mouseover').each((stub) => { - // expect(stub).to.be.calledOnce - // }) - cy.getAll('btn', 'pointerdown mousedown mouseup pointerup click').each((stub) => { expect(stub).to.be.calledOnce }) }) it('can click new element after mousemove sequence [disabled]', () => { - const btn = cy.$$(/*html*/'`) .css({ float: 'left', display: 'block', @@ -4011,7 +4035,7 @@ describe('mouse state', () => { }) .appendTo(cy.$$('body')) - const cover = cy.$$(/*html*/'
').css({ + const cover = cy.$$(/*html*/`
`).css({ backgroundColor: 'blue', position: 'relative', height: 50, @@ -4048,7 +4072,7 @@ describe('mouse state', () => { }) it('can target new element after mousedown sequence', () => { - const btn = cy.$$(/*html*/'`) .css({ float: 'left', display: 'block', @@ -4057,7 +4081,7 @@ describe('mouse state', () => { }) .appendTo(cy.$$('body')) - const cover = cy.$$(/*html*/'
').css({ + const cover = cy.$$(/*html*/`
`).css({ backgroundColor: 'blue', position: 'relative', height: 50, @@ -4084,7 +4108,7 @@ describe('mouse state', () => { }) it('can target new element after mouseup sequence', () => { - const btn = cy.$$(/*html*/'`) .css({ float: 'left', display: 'block', @@ -4093,7 +4117,7 @@ describe('mouse state', () => { }) .appendTo(cy.$$('body')) - const cover = cy.$$(/*html*/'
').css({ + const cover = cy.$$(/*html*/`
`).css({ backgroundColor: 'blue', position: 'relative', height: 50, @@ -4124,7 +4148,7 @@ describe('mouse state', () => { }) it('responds to changes in move handlers', () => { - const btn = cy.$$(/*html*/'`) .css({ float: 'left', display: 'block', @@ -4133,7 +4157,7 @@ describe('mouse state', () => { }) .appendTo(cy.$$('body')) - const cover = cy.$$(/*html*/'
').css({ + const cover = cy.$$(/*html*/`
`).css({ backgroundColor: 'blue', position: 'relative', height: 50, @@ -4161,4 +4185,70 @@ describe('mouse state', () => { }) + describe('user experience', () => { + + beforeEach(() => { + cy.visit('/fixtures/dom.html') + }) + + // https://github.com/cypress-io/cypress/issues/4347 + it('can render element highlight inside iframe', () => { + + cy.get('iframe:first') + .should(($iframe) => { + // wait for iframe to load + expect($iframe.first().contents().find('body').html()).ok + }) + .then(($iframe) => { + // cypress does not wrap this as a DOM element (does not wrap in jquery) + return cy.wrap($iframe.first().contents().find('body')) + }) + .within(() => { + cy.get('a#hashchange') + .click() + }) + .then(($body) => { + expect($body[0].ownerDocument.defaultView.location.hash).eq('#hashchange') + }) + + clickCommandLog('click') + .then(() => { + cy.get('.__cypress-highlight').then(($target) => { + const targetRect = $target[0].getBoundingClientRect() + const iframeRect = cy.$$('iframe')[0].getBoundingClientRect() + + expect(targetRect.top).gt(iframeRect.top) + expect(targetRect.bottom).lt(iframeRect.bottom) + }) + }) + }) + + it('can print table of keys on click', () => { + const spyTableName = cy.spy(top.console, 'groupCollapsed') + const spyTableData = cy.spy(top.console, 'table') + + cy.get('input:first').click() + + cy.wrap(null) + .should(() => { + spyTableName.reset() + spyTableData.reset() + + return withMutableReporterState(() => { + const commandLogEl = getCommandLogWithText('click') + + const reactCommandInstance = findReactInstance(commandLogEl.get(0)) + + reactCommandInstance.props.appState.isRunning = false + + commandLogEl.find('.command-wrapper').click() + + expect(spyTableName).calledWith('Mouse Move Events') + expect(spyTableName).calledWith('Mouse Click Events') + expect(spyTableData).calledTwice + }) + }) + }) + }) + }) diff --git a/packages/driver/test/cypress/integration/e2e/domSnapshots.spec.js b/packages/driver/test/cypress/integration/e2e/domSnapshots.spec.js index 471d045251d8..07d0c930dd3c 100644 --- a/packages/driver/test/cypress/integration/e2e/domSnapshots.spec.js +++ b/packages/driver/test/cypress/integration/e2e/domSnapshots.spec.js @@ -1,5 +1,4 @@ -const { withMutableReporterState, findReactInstance, getCommandLogWithText } = require('../../support/utils') -const { $ } = Cypress +const { clickCommandLog } = require('../../support/utils') describe('rect highlight', () => { beforeEach(() => { @@ -19,24 +18,6 @@ describe('rect highlight', () => { }) }) -const getAndPin = (sel) => { - cy.get(sel) - - cy.wait(0) - .then(() => { - withMutableReporterState(() => { - - const commandLogEl = getCommandLogWithText(sel) - - const reactCommandInstance = findReactInstance(commandLogEl) - - reactCommandInstance.props.appState.isRunning = false - - $(commandLogEl).find('.command-wrapper').click() - }) - }) -} - const shouldHaveCorrectHighlightPositions = () => { return cy.wrap(null, { timeout: 400 }).should(() => { const dims = { @@ -50,6 +31,11 @@ const shouldHaveCorrectHighlightPositions = () => { }) } +const getAndPin = (sel) => { + cy.get(sel) + clickCommandLog(sel) +} + const expectToBeInside = (rectInner, rectOuter, mes = 'rect to be inside rect') => { try { expect(rectInner.width, 'width').lte(rectOuter.width) diff --git a/packages/driver/test/cypress/support/utils.js b/packages/driver/test/cypress/support/utils.js index d7fd2992e8f9..fe0fbcfc5fcd 100644 --- a/packages/driver/test/cypress/support/utils.js +++ b/packages/driver/test/cypress/support/utils.js @@ -1,4 +1,12 @@ -export const getCommandLogWithText = (text) => cy.$$(`.runnable-active .command-wrapper:contains(${text}):visible`, top.document).parentsUntil('li').last().parent()[0] +const { $ } = Cypress + +export const getCommandLogWithText = (text) => { + return cy + .$$(`.runnable-active .command-wrapper:contains(${text}):visible`, top.document) + .parentsUntil('li') + .last() + .parent() +} export const findReactInstance = function (dom) { let key = Object.keys(dom).find((key) => key.startsWith('__reactInternalInstance$')) @@ -12,6 +20,26 @@ export const findReactInstance = function (dom) { } +export const clickCommandLog = (sel) => { + return cy.wait(0) + .then(() => { + withMutableReporterState(() => { + + const commandLogEl = getCommandLogWithText(sel) + + const reactCommandInstance = findReactInstance(commandLogEl[0]) + + if (!reactCommandInstance) { + assert(false, 'failed to get command log React instance') + } + + reactCommandInstance.props.appState.isRunning = false + + $(commandLogEl).find('.command-wrapper').click() + }) + }) +} + export const withMutableReporterState = (fn) => { top.Runner.configureMobx({ enforceActions: 'never' }) From 5bb77e4d85f62489e095a9a462bb8f3f7e686ace Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Mon, 7 Oct 2019 11:15:18 -0400 Subject: [PATCH 30/36] rename fromDocCoords->fromWindowCoords, fix type_spec ux test --- packages/driver/src/cy/mouse.js | 12 ++-- packages/driver/src/dom/elements.js | 4 +- .../integration/commands/actions/type_spec.js | 55 +++++++++---------- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index ee065e6df2a4..ee1c8ee7ad47 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -648,8 +648,8 @@ const formatReasonNotFired = (reason) => { } const toCoordsEventOptions = (x, y, win) => { - // these are the coords from the document, ignoring scroll position - const fromDocCoords = $elements.getFromDocCoords(x, y, win) + // these are the coords from the element's window, ignoring scroll position + const fromWindowCoords = $elements.getFromWindowCoords(x, y, win) return { clientX: x, @@ -658,10 +658,10 @@ const toCoordsEventOptions = (x, y, win) => { screenY: y, x, y, - pageX: fromDocCoords.x, - pageY: fromDocCoords.y, - layerX: fromDocCoords.x, - layerY: fromDocCoords.y, + pageX: fromWindowCoords.x, + pageY: fromWindowCoords.y, + layerX: fromWindowCoords.x, + layerY: fromWindowCoords.y, } } diff --git a/packages/driver/src/dom/elements.js b/packages/driver/src/dom/elements.js index 2cd5191fe529..e6c6d2bae680 100644 --- a/packages/driver/src/dom/elements.js +++ b/packages/driver/src/dom/elements.js @@ -634,7 +634,7 @@ const isScrollable = ($el) => { return false } -const getFromDocCoords = (x, y, win) => { +const getFromWindowCoords = (x, y, win) => { return { x: win.scrollX + x, y: win.scrollY + y, @@ -990,7 +990,7 @@ _.extend(module.exports, { getElements, - getFromDocCoords, + getFromWindowCoords, getFirstFocusableEl, diff --git a/packages/driver/test/cypress/integration/commands/actions/type_spec.js b/packages/driver/test/cypress/integration/commands/actions/type_spec.js index 18b3e495d6dc..d5df66e08fe9 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -4134,35 +4134,6 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('can print table of keys on click', () => { - cy.get('input:first').type('foo') - - .then(() => { - return withMutableReporterState(() => { - const spyTableName = cy.spy(top.console, 'groupCollapsed') - const spyTableData = cy.spy(top.console, 'table') - - const commandLogEl = getCommandLogWithText('foo') - - const reactCommandInstance = findReactInstance(commandLogEl) - - reactCommandInstance.props.appState.isRunning = false - - $(commandLogEl).find('.command-wrapper').click() - - expect(spyTableName.firstCall).calledWith('Mouse Move Events') - expect(spyTableName.secondCall).calledWith('Mouse Click Events') - expect(spyTableName.thirdCall).calledWith('Keyboard Events') - expect(spyTableData).calledThrice - }) - }) - }) - - // table.data.forEach (item, i) -> - // expect(item).to.deep.eq(expectedTable[i]) - - // expect(table.data).to.deep.eq(expectedTable) - it('has no modifiers when there are none activated', () => { cy.get(':text:first').type('f').then(function () { const table = this.lastLog.invoke('consoleProps').table[3]() @@ -5098,4 +5069,30 @@ https://on.cypress.io/type`) }) }) }) + + describe('user experience', () => { + it('can print table of keys on click', () => { + cy.get('input:first').type('foo') + + .then(() => { + return withMutableReporterState(() => { + const spyTableName = cy.spy(top.console, 'groupCollapsed') + const spyTableData = cy.spy(top.console, 'table') + + const commandLogEl = getCommandLogWithText('foo') + + const reactCommandInstance = findReactInstance(commandLogEl[0]) + + reactCommandInstance.props.appState.isRunning = false + + $(commandLogEl).find('.command-wrapper').click() + + expect(spyTableName.firstCall).calledWith('Mouse Move Events') + expect(spyTableName.secondCall).calledWith('Mouse Click Events') + expect(spyTableName.thirdCall).calledWith('Keyboard Events') + expect(spyTableData).calledThrice + }) + }) + }) + }) }) From a7b45f388dfbb0840ec5a1f8c30bf39f145e7e04 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Fri, 11 Oct 2019 16:26:55 -0400 Subject: [PATCH 31/36] tighten up method, remove unnecessary arg --- packages/driver/src/cy/mouse.js | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index ee065e6df2a4..28abca117353 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -65,14 +65,14 @@ const create = (state, keyboard, focused) => { const lastHoveredEl = getLastHoveredEl(state) - const targetEl = mouse.getElAtCoordsOrForce(coords, forceEl) + const targetEl = forceEl || mouse.getElAtCoords(coords) // if coords are same AND we're already hovered on the element, don't send move events if (_.isEqual({ x: coords.x, y: coords.y }, getMouseCoords(state)) && lastHoveredEl === targetEl) return { el: targetEl } const events = mouse._moveEvents(targetEl, coords) - const resultEl = mouse.getElAtCoordsOrForce(coords, forceEl) + const resultEl = forceEl || mouse.getElAtCoords(coords) return { el: resultEl, fromEl: lastHoveredEl, events } }, @@ -228,14 +228,9 @@ const create = (state, keyboard, focused) => { /** * * @param {Coords} coords - * @param {HTMLElement} forceEl * @returns {HTMLElement} */ - getElAtCoordsOrForce ({ x, y, doc }, forceEl) { - if (forceEl) { - return forceEl - } - + getElAtCoords ({ x, y, doc }) { const el = doc.elementFromPoint(x, y) return el @@ -244,13 +239,8 @@ const create = (state, keyboard, focused) => { /** * * @param {Coords} coords - * @param {HTMLElement} forceEl */ - moveToCoordsOrForce (coords, forceEl) { - if (forceEl) { - return forceEl - } - + moveToCoords (coords) { const { el } = mouse.move(coords) return el @@ -263,7 +253,7 @@ const create = (state, keyboard, focused) => { _downEvents (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { const { x, y } = coords - const el = mouse.moveToCoordsOrForce(coords, forceEl) + const el = forceEl || mouse.moveToCoords(coords) const win = $dom.getWindowByElement(el) @@ -430,7 +420,7 @@ const create = (state, keyboard, focused) => { detail: 1, }, mouseEvtOptionsExtend) - const el = mouse.moveToCoordsOrForce(fromViewport, forceEl) + const el = forceEl || mouse.moveToCoords(fromViewport) let pointerupProps = sendPointerup(el, pointerEvtOptions) @@ -453,7 +443,7 @@ const create = (state, keyboard, focused) => { }, _mouseClickEvents (fromViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend = {}) { - const el = mouse.moveToCoordsOrForce(fromViewport, forceEl) + const el = forceEl || mouse.moveToCoords(fromViewport) const win = $dom.getWindowByElement(el) @@ -478,7 +468,7 @@ const create = (state, keyboard, focused) => { }, _contextmenuEvent (fromViewport, forceEl, mouseEvtOptionsExtend) { - const el = mouse.moveToCoordsOrForce(fromViewport, forceEl) + const el = forceEl || mouse.moveToCoords(fromViewport) const win = $dom.getWindowByElement(el) const defaultOptions = mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win) @@ -505,7 +495,7 @@ const create = (state, keyboard, focused) => { const clickEvents1 = click(1) const clickEvents2 = click(2) - const el = mouse.moveToCoordsOrForce(fromViewport, forceEl) + const el = forceEl || mouse.moveToCoords(fromViewport) const win = $dom.getWindowByElement(el) const dblclickEvtProps = _.extend(mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win), { From 67443b53a649a09ebf645ed6998880c430206d23 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Fri, 11 Oct 2019 17:53:01 -0400 Subject: [PATCH 32/36] add debug logic for retries to console.error() non cypress errors --- packages/driver/src/cy/actionability.coffee | 2 +- packages/driver/src/cy/commands/actions/click.js | 2 -- packages/driver/src/cy/retries.coffee | 12 +++++++++++- packages/driver/src/cypress/log.coffee | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/driver/src/cy/actionability.coffee b/packages/driver/src/cy/actionability.coffee index fce7ac3952a1..2f761ebdb379 100644 --- a/packages/driver/src/cy/actionability.coffee +++ b/packages/driver/src/cy/actionability.coffee @@ -201,7 +201,7 @@ ensureNotAnimating = (cy, $el, coordsHistory, animationDistanceThreshold) -> ## if we dont have at least 2 points ## then automatically retry if coordsHistory.length < 2 - throw new Error("coordsHistory must be at least 2 sets of coords") + throw $utils.cypressErr("coordsHistory must be at least 2 sets of coords") ## verify that our element is not currently animating ## by verifying it is still at the same coordinates within diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index 166596d6dcc4..e96a5cd9d0fb 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -4,7 +4,6 @@ const Promise = require('bluebird') const $dom = require('../../../dom') const $utils = require('../../../cypress/utils') const $actionability = require('../../actionability') -const debug = require('debug')('cypress:driver:click') const formatMoveEventsTable = (events) => { return { @@ -182,7 +181,6 @@ module.exports = (Commands, Cypress, cy, state, config) => { onReady ($elToClick, coords) { const { fromViewport, fromAutWindow, fromWindow } = coords - debug('got coords', { fromViewport, fromAutWindow }) const forceEl = options.force && $elToClick.get(0) const moveEvents = mouse.move(fromViewport, forceEl) diff --git a/packages/driver/src/cy/retries.coffee b/packages/driver/src/cy/retries.coffee index 33f6fe346f29..ca7959720302 100644 --- a/packages/driver/src/cy/retries.coffee +++ b/packages/driver/src/cy/retries.coffee @@ -1,7 +1,8 @@ _ = require("lodash") Promise = require("bluebird") - +debug = require('debug')('cypress:driver:retries') $utils = require("../cypress/utils") +{ CypressErrorRe } = require("../cypress/log") create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) -> return { @@ -28,6 +29,15 @@ create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) - _name: current?.get("name") }) + { error } = options + + ## TODO: remove this once the codeframe PR is in since that + ## correctly handles not rewrapping errors so that stack + ## traces are correctly displayed + if debug.enabled and error and not CypressErrorRe.test(error.name) + debug('retrying due to caught error...') + console.error(error) + interval = options.interval ? options._interval ## we calculate the total time we've been retrying diff --git a/packages/driver/src/cypress/log.coffee b/packages/driver/src/cypress/log.coffee index a1c3674ad01b..1353edb925c9 100644 --- a/packages/driver/src/cypress/log.coffee +++ b/packages/driver/src/cypress/log.coffee @@ -502,6 +502,8 @@ create = (Cypress, cy, state, config) -> return logFn module.exports = { + CypressErrorRe + reduceMemory toSerializedJSON From 07e2f720797221603dc16d2a83ae9fc36ef797f5 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Fri, 11 Oct 2019 18:04:09 -0400 Subject: [PATCH 33/36] renamed fromWindow -> fromElWindow, and fromViewport -> fromElViewport --- packages/driver/src/cy/actionability.coffee | 26 ++++---- .../driver/src/cy/commands/actions/click.js | 26 ++++---- .../src/cy/commands/actions/trigger.coffee | 14 ++-- .../driver/src/cy/commands/screenshot.coffee | 14 ++-- packages/driver/src/cy/mouse.js | 58 ++++++++-------- packages/driver/src/dom/coordinates.js | 62 ++++++++--------- packages/driver/src/dom/visibility.js | 10 +-- .../commands/actions/check_spec.coffee | 12 ++-- .../commands/actions/click_spec.js | 66 +++++++++---------- .../commands/actions/scroll_spec.coffee | 26 ++++---- .../commands/actions/select_spec.coffee | 6 +- .../commands/actions/trigger_spec.coffee | 42 ++++++------ .../integration/commands/actions/type_spec.js | 14 ++-- .../integration/dom/coordinates_spec.coffee | 48 +++++++------- 14 files changed, 212 insertions(+), 212 deletions(-) diff --git a/packages/driver/src/cy/actionability.coffee b/packages/driver/src/cy/actionability.coffee index 2f761ebdb379..6a195c636a51 100644 --- a/packages/driver/src/cy/actionability.coffee +++ b/packages/driver/src/cy/actionability.coffee @@ -36,19 +36,19 @@ getPositionFromArguments = (positionOrX, y, options) -> return {options, position, x, y} -ensureElIsNotCovered = (cy, win, $el, fromViewport, options, log, onScroll) -> +ensureElIsNotCovered = (cy, win, $el, fromElViewport, options, log, onScroll) -> $elAtCoords = null - getElementAtPointFromViewport = (fromViewport) -> + getElementAtPointFromViewport = (fromElViewport) -> ## get the element at point from the viewport based ## on the desired x/y normalized coordinations - if elAtCoords = $dom.getElementAtPointFromViewport(win.document, fromViewport.x, fromViewport.y) + if elAtCoords = $dom.getElementAtPointFromViewport(win.document, fromElViewport.x, fromElViewport.y) $elAtCoords = $dom.wrap(elAtCoords) - ensureDescendents = (fromViewport) -> + ensureDescendents = (fromElViewport) -> ## figure out the deepest element we are about to interact ## with at these coordinates - $elAtCoords = getElementAtPointFromViewport(fromViewport) + $elAtCoords = getElementAtPointFromViewport(fromElViewport) cy.ensureElDoesNotHaveCSS($el, 'pointer-events', 'none', log) cy.ensureDescendents($el, $elAtCoords, log) @@ -57,8 +57,8 @@ ensureElIsNotCovered = (cy, win, $el, fromViewport, options, log, onScroll) -> ensureDescendentsAndScroll = -> try - ## use the initial coords fromViewport - ensureDescendents(fromViewport) + ## use the initial coords fromElViewport + ensureDescendents(fromElViewport) catch err ## if we're being covered by a fixed position element then ## we're going to attempt to continously scroll the element @@ -146,16 +146,16 @@ ensureElIsNotCovered = (cy, win, $el, fromViewport, options, log, onScroll) -> ## now that we've changed scroll positions ## we must recalculate whether this element is covered ## since the element's top / left positions change. - fromViewport = getCoordinatesForEl(cy, $el, options).fromViewport + fromElViewport = getCoordinatesForEl(cy, $el, options).fromElViewport ## this is a relative calculation based on the viewport ## so these are the only coordinates we care about - ensureDescendents(fromViewport) + ensureDescendents(fromElViewport) catch err ## we failed here, but before scrolling the next container ## we need to first verify that the element covering up ## is the same one as before our scroll - if $elAtCoords = getElementAtPointFromViewport(fromViewport) + if $elAtCoords = getElementAtPointFromViewport(fromElViewport) ## get the fixed element again $fixed = getFixedOrStickyEl($elAtCoords) @@ -277,7 +277,7 @@ verify = (cy, $el, options, callbacks) -> ## (see https://github.com/cypress-io/cypress/pull/1478) sticky = !!getStickyEl($el) - coordsHistory.push(if sticky then coords.fromViewport else coords.fromWindow) + coordsHistory.push(if sticky then coords.fromElViewport else coords.fromElWindow) ## then we ensure the element isnt animating ensureNotAnimating(cy, $el, coordsHistory, options.animationDistanceThreshold) @@ -285,8 +285,8 @@ verify = (cy, $el, options, callbacks) -> ## now that we know our element isn't animating its time ## to figure out if its being covered by another element. ## this calculation is relative from the viewport so we - ## only care about fromViewport coords - $elAtCoords = options.ensure.notCovered && ensureElIsNotCovered(cy, win, $el, coords.fromViewport, options, _log, onScroll) + ## only care about fromElViewport coords + $elAtCoords = options.ensure.notCovered && ensureElIsNotCovered(cy, win, $el, coords.fromElViewport, options, _log, onScroll) ## pass our final object into onReady finalEl = $elAtCoords ? $el diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index e96a5cd9d0fb..cd83ad7cbd14 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -121,7 +121,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { // timing out from multiple clicks cy.timeout($actionability.delay, true, eventName) - const createLog = (domEvents, fromWindowCoords, fromAutWindowCoords) => { + const createLog = (domEvents, fromElWindow, fromAutWindow) => { let consoleObj const elClicked = domEvents.moveEvents.el @@ -134,7 +134,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { consoleObj = _.defaults(consoleObj != null ? consoleObj : {}, { 'Applied To': $dom.getElements(options.$el), 'Elements': options.$el.length, - 'Coords': _.pick(fromWindowCoords, 'x', 'y'), // always absolute + 'Coords': _.pick(fromElWindow, 'x', 'y'), // always absolute 'Options': deltaOptions, }) @@ -155,7 +155,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { if (options._log) { // because we snapshot and output a command per click // we need to manually snapshot + end them - options._log.set({ coords: fromAutWindowCoords, consoleProps }) + options._log.set({ coords: fromAutWindow, consoleProps }) } // we need to split this up because we want the coordinates @@ -179,19 +179,19 @@ module.exports = (Commands, Cypress, cy, state, config) => { }, onReady ($elToClick, coords) { - const { fromViewport, fromAutWindow, fromWindow } = coords + const { fromElViewport, fromElWindow, fromAutWindow } = coords const forceEl = options.force && $elToClick.get(0) - const moveEvents = mouse.move(fromViewport, forceEl) + const moveEvents = mouse.move(fromElViewport, forceEl) - const onReadyProps = onReady(fromViewport, forceEl) + const onReadyProps = onReady(fromElViewport, forceEl) return createLog({ moveEvents, ...onReadyProps, }, - fromWindow, + fromElWindow, fromAutWindow) }, }) @@ -233,8 +233,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { subject, options, positionOrX, - onReady (fromViewport, forceEl) { - const clickEvents = mouse.click(fromViewport, forceEl) + onReady (fromElViewport, forceEl) { + const clickEvents = mouse.click(fromElViewport, forceEl) return { clickEvents, @@ -265,8 +265,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { subject, options, positionOrX, - onReady (fromViewport, forceEl) { - const { clickEvents1, clickEvents2, dblclickProps } = mouse.dblclick(fromViewport, forceEl) + onReady (fromElViewport, forceEl) { + const { clickEvents1, clickEvents2, dblclickProps } = mouse.dblclick(fromElViewport, forceEl) return { dblclickProps, @@ -306,8 +306,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { subject, options, positionOrX, - onReady (fromViewport, forceEl) { - const { clickEvents, contextmenuEvent } = mouse.rightclick(fromViewport, forceEl) + onReady (fromElViewport, forceEl) { + const { clickEvents, contextmenuEvent } = mouse.rightclick(fromElViewport, forceEl) return { clickEvents, diff --git a/packages/driver/src/cy/commands/actions/trigger.coffee b/packages/driver/src/cy/commands/actions/trigger.coffee index 4142193bd812..b27b4e7cc34e 100644 --- a/packages/driver/src/cy/commands/actions/trigger.coffee +++ b/packages/driver/src/cy/commands/actions/trigger.coffee @@ -84,7 +84,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> Cypress.action("cy:scrolled", $el, type) onReady: ($elToClick, coords) -> - { fromWindow, fromViewport, fromAutWindow } = coords + { fromElWindow, fromElViewport, fromAutWindow } = coords if options._log ## display the red dot at these coords @@ -93,12 +93,12 @@ module.exports = (Commands, Cypress, cy, state, config) -> }) eventOptions = _.extend({ - clientX: fromViewport.x - clientY: fromViewport.y - screenX: fromViewport.x - screenY: fromViewport.y - pageX: fromWindow.x - pageY: fromWindow.y + clientX: fromElViewport.x + clientY: fromElViewport.y + screenX: fromElViewport.x + screenY: fromElViewport.y + pageX: fromElWindow.x + pageY: fromElWindow.y }, eventOptions) dispatch($elToClick.get(0), eventName, eventOptions) diff --git a/packages/driver/src/cy/commands/screenshot.coffee b/packages/driver/src/cy/commands/screenshot.coffee index f0752e38b558..a0f215b10ffa 100644 --- a/packages/driver/src/cy/commands/screenshot.coffee +++ b/packages/driver/src/cy/commands/screenshot.coffee @@ -137,23 +137,23 @@ takeElementScreenshot = ($el, state, automationOptions) -> numScreenshots = Math.ceil(elPosition.height / viewportHeight) scrolls = _.map _.times(numScreenshots), (index) -> - y = elPosition.fromWindow.top + (viewportHeight * index) + y = elPosition.fromElWindow.top + (viewportHeight * index) afterScroll = -> elPosition = $dom.getElementPositioning($el) - x = Math.min(viewportWidth, elPosition.fromViewport.left) + x = Math.min(viewportWidth, elPosition.fromElViewport.left) width = Math.min(viewportWidth - x, elPosition.width) if numScreenshots is 1 return { x: x - y: elPosition.fromViewport.top + y: elPosition.fromElViewport.top width: width height: elPosition.height } if index + 1 is numScreenshots - overlap = (numScreenshots - 1) * viewportHeight + elPosition.fromViewport.top - heightLeft = elPosition.fromViewport.bottom - overlap + overlap = (numScreenshots - 1) * viewportHeight + elPosition.fromElViewport.top + heightLeft = elPosition.fromElViewport.bottom - overlap { x: x y: overlap @@ -163,10 +163,10 @@ takeElementScreenshot = ($el, state, automationOptions) -> else { x: x - y: Math.max(0, elPosition.fromViewport.top) + y: Math.max(0, elPosition.fromElViewport.top) width: width ## TODO: try simplifying to just 'viewportHeight' - height: Math.min(viewportHeight, elPosition.fromViewport.top + elPosition.height) + height: Math.min(viewportHeight, elPosition.fromElViewport.top + elPosition.height) } { y, afterScroll } diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index 6baed0c7a650..b556ab0831ac 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -307,10 +307,10 @@ const create = (state, keyboard, focused) => { }, - down (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + down (fromElViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { const $previouslyFocused = focused.getFocused() - const mouseDownEvents = mouse._downEvents(coords, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseDownEvents = mouse._downEvents(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) // el we just send pointerdown const el = mouseDownEvents.pointerdownProps.el @@ -351,13 +351,13 @@ const create = (state, keyboard, focused) => { /** * @param {HTMLElement} el * @param {Window} win - * @param {Coords} fromViewport + * @param {Coords} fromElViewport * @param {HTMLElement} forceEl */ - up (fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { - debug('mouse.up', { fromViewport, forceEl, skipMouseEvent }) + up (fromElViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + debug('mouse.up', { fromElViewport, forceEl, skipMouseEvent }) - return mouse._upEvents(fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + return mouse._upEvents(fromElViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) }, /** @@ -381,34 +381,34 @@ const create = (state, keyboard, focused) => { * if (notDetached(el1)) * sendClick(el3) */ - click (fromViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { - debug('mouse.click', { fromViewport, forceEl }) + click (fromElViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + debug('mouse.click', { fromElViewport, forceEl }) - const mouseDownEvents = mouse.down(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseDownEvents = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault - const mouseUpEvents = mouse.up(fromViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseUpEvents = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) const skipClickEvent = $elements.isDetachedEl(mouseDownEvents.pointerdownProps.el) - const mouseClickEvents = mouse._mouseClickEvents(fromViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend) + const mouseClickEvents = mouse._mouseClickEvents(fromElViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend) return _.extend({}, mouseDownEvents, mouseUpEvents, mouseClickEvents) }, /** - * @param {Coords} fromViewport + * @param {Coords} fromElViewport * @param {HTMLElement} el * @param {HTMLElement} forceEl * @param {Window} win */ - _upEvents (fromViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { + _upEvents (fromElViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { const win = state('window') - let defaultOptions = mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win) + let defaultOptions = mouse._getDefaultMouseOptions(fromElViewport.x, fromElViewport.y, win) const pointerEvtOptions = _.extend({}, defaultOptions, { ...defaultPointerDownUpOptions, @@ -420,7 +420,7 @@ const create = (state, keyboard, focused) => { detail: 1, }, mouseEvtOptionsExtend) - const el = forceEl || mouse.moveToCoords(fromViewport) + const el = forceEl || mouse.moveToCoords(fromElViewport) let pointerupProps = sendPointerup(el, pointerEvtOptions) @@ -442,12 +442,12 @@ const create = (state, keyboard, focused) => { }, - _mouseClickEvents (fromViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend = {}) { - const el = forceEl || mouse.moveToCoords(fromViewport) + _mouseClickEvents (fromElViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend = {}) { + const el = forceEl || mouse.moveToCoords(fromElViewport) const win = $dom.getWindowByElement(el) - const defaultOptions = mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win) + const defaultOptions = mouse._getDefaultMouseOptions(fromElViewport.x, fromElViewport.y, win) const clickEventOptions = _.extend({}, defaultOptions, { buttons: 0, @@ -467,11 +467,11 @@ const create = (state, keyboard, focused) => { return { clickProps } }, - _contextmenuEvent (fromViewport, forceEl, mouseEvtOptionsExtend) { - const el = forceEl || mouse.moveToCoords(fromViewport) + _contextmenuEvent (fromElViewport, forceEl, mouseEvtOptionsExtend) { + const el = forceEl || mouse.moveToCoords(fromElViewport) const win = $dom.getWindowByElement(el) - const defaultOptions = mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win) + const defaultOptions = mouse._getDefaultMouseOptions(fromElViewport.x, fromElViewport.y, win) const mouseEvtOptions = _.extend({}, defaultOptions, { button: 2, @@ -485,9 +485,9 @@ const create = (state, keyboard, focused) => { return { contextmenuProps } }, - dblclick (fromViewport, forceEl, mouseEvtOptionsExtend = {}) { + dblclick (fromElViewport, forceEl, mouseEvtOptionsExtend = {}) { const click = (clickNum) => { - const clickEvents = mouse.click(fromViewport, forceEl, {}, { detail: clickNum }) + const clickEvents = mouse.click(fromElViewport, forceEl, {}, { detail: clickNum }) return clickEvents } @@ -495,10 +495,10 @@ const create = (state, keyboard, focused) => { const clickEvents1 = click(1) const clickEvents2 = click(2) - const el = forceEl || mouse.moveToCoords(fromViewport) + const el = forceEl || mouse.moveToCoords(fromElViewport) const win = $dom.getWindowByElement(el) - const dblclickEvtProps = _.extend(mouse._getDefaultMouseOptions(fromViewport.x, fromViewport.y, win), { + const dblclickEvtProps = _.extend(mouse._getDefaultMouseOptions(fromElViewport.x, fromElViewport.y, win), { buttons: 0, detail: 2, }, mouseEvtOptionsExtend) @@ -508,7 +508,7 @@ const create = (state, keyboard, focused) => { return { clickEvents1, clickEvents2, dblclickProps } }, - rightclick (fromViewport, forceEl) { + rightclick (fromElViewport, forceEl) { const pointerEvtOptionsExtend = { button: 2, buttons: 2, @@ -520,13 +520,13 @@ const create = (state, keyboard, focused) => { which: 3, } - const mouseDownEvents = mouse.down(fromViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseDownEvents = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend) - const contextmenuEvent = mouse._contextmenuEvent(fromViewport, forceEl) + const contextmenuEvent = mouse._contextmenuEvent(fromElViewport, forceEl) const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault - const mouseUpEvents = mouse.up(fromViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) + const mouseUpEvents = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend) const clickEvents = _.extend({}, mouseDownEvents, mouseUpEvents) diff --git a/packages/driver/src/dom/coordinates.js b/packages/driver/src/dom/coordinates.js index b6f9d259be4e..f9984c8e9894 100644 --- a/packages/driver/src/dom/coordinates.js +++ b/packages/driver/src/dom/coordinates.js @@ -72,26 +72,26 @@ const getElementPositioning = ($el) => { scrollLeft: el.scrollLeft, width: rect.width, height: rect.height, - fromViewport: { + fromElViewport: { + doc: win.document, top: rect.top, left: rect.left, right: rect.right, bottom: rect.bottom, topCenter, leftCenter, - doc: win.document, }, - fromWindow: { - top: rect.top + win.pageYOffset, - left: rect.left + win.pageXOffset, - topCenter: topCenter + win.pageYOffset, - leftCenter: leftCenter + win.pageXOffset, + fromElWindow: { + top: rect.top + win.scrollY, + left: rect.left + win.scrollX, + topCenter: topCenter + win.scrollY, + leftCenter: leftCenter + win.scrollX, }, fromAutWindow: { - top: rectFromAut.top + autFrame.pageYOffset, - left: rectFromAut.left + autFrame.pageXOffset, - topCenter: rectFromAutCenter.y + autFrame.pageYOffset, - leftCenter: rectFromAutCenter.x + autFrame.pageXOffset, + top: rectFromAut.top + autFrame.scrollY, + left: rectFromAut.left + autFrame.scrollX, + topCenter: rectFromAutCenter.y + autFrame.scrollY, + leftCenter: rectFromAutCenter.x + autFrame.scrollX, }, } } @@ -192,22 +192,22 @@ const getBottomRightCoordinates = (rect) => { const getElementCoordinatesByPositionRelativeToXY = ($el, x, y) => { const positionProps = getElementPositioning($el) - const { fromViewport, fromWindow } = positionProps + const { fromElViewport, fromElWindow } = positionProps - fromViewport.left += x - fromViewport.top += y + fromElViewport.left += x + fromElViewport.top += y - fromWindow.left += x - fromWindow.top += y + fromElWindow.left += x + fromElWindow.top += y - const viewportTargetCoords = getTopLeftCoordinates(fromViewport) - const windowTargetCoords = getTopLeftCoordinates(fromWindow) + const viewportTargetCoords = getTopLeftCoordinates(fromElViewport) + const windowTargetCoords = getTopLeftCoordinates(fromElWindow) - fromViewport.x = viewportTargetCoords.x - fromViewport.y = viewportTargetCoords.y + fromElViewport.x = viewportTargetCoords.x + fromElViewport.y = viewportTargetCoords.y - fromWindow.x = windowTargetCoords.x - fromWindow.y = windowTargetCoords.y + fromElWindow.x = windowTargetCoords.x + fromElWindow.y = windowTargetCoords.y return positionProps } @@ -221,7 +221,7 @@ const getElementCoordinatesByPosition = ($el, position) => { // but also from the viewport so // whoever is calling us can use it // however they'd like - const { width, height, fromViewport, fromWindow, fromAutWindow } = positionProps + const { width, height, fromElViewport, fromElWindow, fromAutWindow } = positionProps // dynamically call the by transforming the nam=> e // bottom -> getBottomCoordinates @@ -237,8 +237,8 @@ const getElementCoordinatesByPosition = ($el, position) => { const viewportTargetCoords = fn({ width, height, - top: fromViewport.top, - left: fromViewport.left, + top: fromElViewport.top, + left: fromElViewport.left, }) // get the desired x/y coords based on @@ -246,15 +246,15 @@ const getElementCoordinatesByPosition = ($el, position) => { const windowTargetCoords = fn({ width, height, - top: fromWindow.top, - left: fromWindow.left, + top: fromElWindow.top, + left: fromElWindow.left, }) - fromViewport.x = viewportTargetCoords.x - fromViewport.y = viewportTargetCoords.y + fromElViewport.x = viewportTargetCoords.x + fromElViewport.y = viewportTargetCoords.y - fromWindow.x = windowTargetCoords.x - fromWindow.y = windowTargetCoords.y + fromElWindow.x = windowTargetCoords.x + fromElWindow.y = windowTargetCoords.y const autTargetCoords = fn({ width, diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index 84eec0621a57..70b873f60c1a 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -179,7 +179,7 @@ const elAtCenterPoint = function ($el) { const doc = $document.getDocumentFromElement($el.get(0)) const elProps = $coordinates.getElementPositioning($el) - const { topCenter, leftCenter } = elProps.fromViewport + const { topCenter, leftCenter } = elProps.fromElViewport const el = $coordinates.getElementAtPointFromViewport(doc, leftCenter, topCenter) @@ -232,16 +232,16 @@ const elIsOutOfBoundsOfAncestorsOverflow = function ($el, $ancestor = $el.parent // target el is out of bounds if ( // target el is to the right of the ancestor's visible area - (elProps.fromWindow.left > (ancestorProps.width + ancestorProps.fromWindow.left)) || + (elProps.fromElWindow.left > (ancestorProps.width + ancestorProps.fromElWindow.left)) || // target el is to the left of the ancestor's visible area - ((elProps.fromWindow.left + elProps.width) < ancestorProps.fromWindow.left) || + ((elProps.fromElWindow.left + elProps.width) < ancestorProps.fromElWindow.left) || // target el is under the ancestor's visible area - (elProps.fromWindow.top > (ancestorProps.height + ancestorProps.fromWindow.top)) || + (elProps.fromElWindow.top > (ancestorProps.height + ancestorProps.fromElWindow.top)) || // target el is above the ancestor's visible area - ((elProps.fromWindow.top + elProps.height) < ancestorProps.fromWindow.top) + ((elProps.fromElWindow.top + elProps.height) < ancestorProps.fromElWindow.top) ) { return true } diff --git a/packages/driver/test/cypress/integration/commands/actions/check_spec.coffee b/packages/driver/test/cypress/integration/commands/actions/check_spec.coffee index 15bafe51ce51..e0f5ed522588 100644 --- a/packages/driver/test/cypress/integration/commands/actions/check_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/check_spec.coffee @@ -432,8 +432,8 @@ describe "src/cy/commands/actions/check", -> it "passes in coords", -> cy.get("[name=colors][value=blue]").check().then ($input) -> lastLog = @lastLog - { fromWindow }= Cypress.dom.getElementCoordinatesByPosition($input) - expect(lastLog.get("coords")).to.deep.eq(fromWindow) + { fromElWindow }= Cypress.dom.getElementCoordinatesByPosition($input) + expect(lastLog.get("coords")).to.deep.eq(fromElWindow) it "ends command when checkbox is already checked", -> cy.get("[name=colors][value=blue]").check().check().then -> @@ -445,13 +445,13 @@ describe "src/cy/commands/actions/check", -> cy.get("[name=colors][value=blue]").check().then ($input) -> lastLog = @lastLog - { fromWindow }= Cypress.dom.getElementCoordinatesByPosition($input) + { fromElWindow }= Cypress.dom.getElementCoordinatesByPosition($input) console = lastLog.invoke("consoleProps") expect(console.Command).to.eq "check" expect(console["Applied To"]).to.eq lastLog.get("$el").get(0) expect(console.Elements).to.eq 1 expect(console.Coords).to.deep.eq( - _.pick(fromWindow, "x", "y") + _.pick(fromElWindow, "x", "y") ) it "#consoleProps when checkbox is already checked", -> @@ -836,13 +836,13 @@ describe "src/cy/commands/actions/check", -> cy.get("[name=colors][value=blue]").uncheck().then ($input) -> lastLog = @lastLog - { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($input) + { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($input) console = lastLog.invoke("consoleProps") expect(console.Command).to.eq "uncheck" expect(console["Applied To"]).to.eq lastLog.get("$el").get(0) expect(console.Elements).to.eq(1) expect(console.Coords).to.deep.eq( - _.pick(fromWindow, "x", "y") + _.pick(fromElWindow, "x", "y") ) it "#consoleProps when checkbox is already unchecked", -> 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 df25ef80236a..2f6a049c9df9 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -71,7 +71,7 @@ describe('src/cy/commands/actions/click', () => { const $btn = cy.$$('#button') $btn.on('click', (e) => { - const { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) const obj = _.pick(e.originalEvent, 'bubbles', 'cancelable', 'view', 'button', 'buttons', 'which', 'relatedTarget', 'altKey', 'ctrlKey', 'shiftKey', 'metaKey', 'detail', 'type') @@ -91,8 +91,8 @@ describe('src/cy/commands/actions/click', () => { type: 'click', }) - expect(e.clientX).to.be.closeTo(fromViewport.x, 1) - expect(e.clientY).to.be.closeTo(fromViewport.y, 1) + expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) + expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() }) @@ -119,7 +119,7 @@ describe('src/cy/commands/actions/click', () => { $btn.get(0).addEventListener('mousedown', (e) => { // calculate after scrolling - const { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) const obj = _.pick(e, 'bubbles', 'cancelable', 'view', 'button', 'buttons', 'which', 'relatedTarget', 'altKey', 'ctrlKey', 'shiftKey', 'metaKey', 'detail', 'type') @@ -139,8 +139,8 @@ describe('src/cy/commands/actions/click', () => { type: 'mousedown', }) - expect(e.clientX).to.be.closeTo(fromViewport.x, 1) - expect(e.clientY).to.be.closeTo(fromViewport.y, 1) + expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) + expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() }) @@ -154,7 +154,7 @@ describe('src/cy/commands/actions/click', () => { const win = cy.state('window') $btn.get(0).addEventListener('mouseup', (e) => { - const { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) const obj = _.pick(e, 'bubbles', 'cancelable', 'view', 'button', 'buttons', 'which', 'relatedTarget', 'altKey', 'ctrlKey', 'shiftKey', 'metaKey', 'detail', 'type') @@ -174,8 +174,8 @@ describe('src/cy/commands/actions/click', () => { type: 'mouseup', }) - expect(e.clientX).to.be.closeTo(fromViewport.x, 1) - expect(e.clientY).to.be.closeTo(fromViewport.y, 1) + expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) + expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() }) @@ -220,10 +220,10 @@ describe('src/cy/commands/actions/click', () => { const win = cy.state('window') $btn.get(0).addEventListener('click', (e) => { - const { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) - expect(win.pageXOffset).to.be.gt(0) - expect(e.clientX).to.be.closeTo(fromViewport.x, 1) + expect(win.scrollX).to.be.gt(0) + expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) done() }) @@ -237,10 +237,10 @@ describe('src/cy/commands/actions/click', () => { const win = cy.state('window') $btn.get(0).addEventListener('click', (e) => { - const { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) - expect(win.pageYOffset).to.be.gt(0) - expect(e.clientY).to.be.closeTo(fromViewport.y, 1) + expect(win.scrollY).to.be.gt(0) + expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() }) @@ -1106,7 +1106,7 @@ describe('src/cy/commands/actions/click', () => { .get('#button-covered-in-nav').click() .then(($btn) => { const rect = $btn.get(0).getBoundingClientRect() - const { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) // this button should be 120 pixels wide expect(rect.width).to.eq(120) @@ -1115,8 +1115,8 @@ describe('src/cy/commands/actions/click', () => { // clientX + clientY are relative to the document expect(scrolled).to.deep.eq(['element', 'element', 'window']) - expect(obj).property('clientX').closeTo(fromViewport.leftCenter, 1) - expect(obj).property('clientY').closeTo(fromViewport.topCenter, 1) + expect(obj).property('clientX').closeTo(fromElViewport.leftCenter, 1) + expect(obj).property('clientY').closeTo(fromElViewport.topCenter, 1) }) }) @@ -1293,14 +1293,14 @@ describe('src/cy/commands/actions/click', () => { it('passes options.animationDistanceThreshold to cy.ensureElementIsNotAnimating', () => { const $btn = cy.$$('button:first') - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) cy.spy(cy, 'ensureElementIsNotAnimating') cy.get('button:first').click({ animationDistanceThreshold: 1000 }).then(() => { const { args } = cy.ensureElementIsNotAnimating.firstCall - expect(args[1]).to.deep.eq([fromWindow, fromWindow]) + expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) expect(args[2]).to.eq(1000) }) @@ -1311,14 +1311,14 @@ describe('src/cy/commands/actions/click', () => { const $btn = cy.$$('button:first') - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) cy.spy(cy, 'ensureElementIsNotAnimating') cy.get('button:first').click().then(() => { const { args } = cy.ensureElementIsNotAnimating.firstCall - expect(args[1]).to.deep.eq([fromWindow, fromWindow]) + expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) expect(args[2]).to.eq(animationDistanceThreshold) }) @@ -2092,9 +2092,9 @@ describe('src/cy/commands/actions/click', () => { const { lastLog } = this $btn.blur() // blur which removes focus styles which would change coords - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) - expect(lastLog.get('coords')).to.deep.eq(fromWindow) + expect(lastLog.get('coords')).to.deep.eq(fromElWindow) }) }) @@ -2132,13 +2132,13 @@ describe('src/cy/commands/actions/click', () => { const rect = $btn.get(0).getBoundingClientRect() const consoleProps = lastLog.invoke('consoleProps') - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) // this button should be 60 pixels wide expect(rect.width).to.eq(60) - expect(consoleProps.Coords.x).to.be.closeTo(fromWindow.x, 1) // ensure we are within 1 - expect(consoleProps.Coords.y).to.be.closeTo(fromWindow.y, 1) // ensure we are within 1 + expect(consoleProps.Coords.x).to.be.closeTo(fromElWindow.x, 1) // ensure we are within 1 + expect(consoleProps.Coords.y).to.be.closeTo(fromElWindow.y, 1) // ensure we are within 1 expect(consoleProps).to.containSubset({ 'Command': 'click', @@ -2902,13 +2902,13 @@ describe('src/cy/commands/actions/click', () => { const rect = $btn.get(0).getBoundingClientRect() const consoleProps = lastLog.invoke('consoleProps') - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) // this button should be 60 pixels wide expect(rect.width).to.eq(60) - expect(consoleProps.Coords.x).to.be.closeTo(fromWindow.x, 1) // ensure we are within 1 - expect(consoleProps.Coords.y).to.be.closeTo(fromWindow.y, 1) // ensure we are within 1 + expect(consoleProps.Coords.x).to.be.closeTo(fromElWindow.x, 1) // ensure we are within 1 + expect(consoleProps.Coords.y).to.be.closeTo(fromElWindow.y, 1) // ensure we are within 1 expect(consoleProps).to.containSubset({ 'Command': 'dblclick', @@ -3346,13 +3346,13 @@ describe('src/cy/commands/actions/click', () => { const rect = $btn.get(0).getBoundingClientRect() const consoleProps = lastLog.invoke('consoleProps') - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) // this button should be 60 pixels wide expect(rect.width).to.eq(60) - expect(consoleProps.Coords.x).to.be.closeTo(fromWindow.x, 1) // ensure we are within 1 - expect(consoleProps.Coords.y).to.be.closeTo(fromWindow.y, 1) // ensure we are within 1 + expect(consoleProps.Coords.x).to.be.closeTo(fromElWindow.x, 1) // ensure we are within 1 + expect(consoleProps.Coords.y).to.be.closeTo(fromElWindow.y, 1) // ensure we are within 1 expect(consoleProps).to.containSubset({ 'Command': 'rightclick', diff --git a/packages/driver/test/cypress/integration/commands/actions/scroll_spec.coffee b/packages/driver/test/cypress/integration/commands/actions/scroll_spec.coffee index 52d1cb85b082..256ca891f032 100644 --- a/packages/driver/test/cypress/integration/commands/actions/scroll_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/scroll_spec.coffee @@ -47,7 +47,7 @@ describe "src/cy/commands/actions/scroll", -> it "can use window", -> cy.window().scrollTo("10px").then (win) -> - expect(win.pageXOffset).to.eq(10) + expect(win.scrollX).to.eq(10) it "can handle window w/length > 1 as a subject", -> cy.visit('/fixtures/dom.html') @@ -479,31 +479,31 @@ describe "src/cy/commands/actions/scroll", -> expect($div).to.match div it "scrolls x axis of window to element", -> - expect(@win.pageYOffset).to.eq(0) - expect(@win.pageXOffset).to.eq(0) + expect(@win.scrollY).to.eq(0) + expect(@win.scrollX).to.eq(0) cy.get("#scroll-into-view-win-horizontal div").scrollIntoView() cy.window().then (win) -> - expect(win.pageYOffset).to.eq(0) - expect(win.pageXOffset).not.to.eq(0) + expect(win.scrollY).to.eq(0) + expect(win.scrollX).not.to.eq(0) it "scrolls y axis of window to element", -> - expect(@win.pageYOffset).to.eq(0) - expect(@win.pageXOffset).to.eq(0) + expect(@win.scrollY).to.eq(0) + expect(@win.scrollX).to.eq(0) cy.get("#scroll-into-view-win-vertical div").scrollIntoView() cy.window().then (win) -> - expect(win.pageYOffset).not.to.eq(0) - expect(win.pageXOffset).to.eq(200) + expect(win.scrollY).not.to.eq(0) + expect(win.scrollX).to.eq(200) it "scrolls both axes of window to element", -> - expect(@win.pageYOffset).to.eq(0) - expect(@win.pageXOffset).to.eq(0) + expect(@win.scrollY).to.eq(0) + expect(@win.scrollX).to.eq(0) cy.get("#scroll-into-view-win-both div").scrollIntoView() cy.window().then (win) -> - expect(win.pageYOffset).not.to.eq(0) - expect(win.pageXOffset).not.to.eq(0) + expect(win.scrollY).not.to.eq(0) + expect(win.scrollX).not.to.eq(0) it "scrolls x axis of container to element", -> expect(@scrollHoriz.get(0).scrollTop).to.eq(0) diff --git a/packages/driver/test/cypress/integration/commands/actions/select_spec.coffee b/packages/driver/test/cypress/integration/commands/actions/select_spec.coffee index 1988a2bf6dc0..dbfcadd7cae1 100644 --- a/packages/driver/test/cypress/integration/commands/actions/select_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/select_spec.coffee @@ -374,13 +374,13 @@ describe "src/cy/commands/actions/select", -> it "#consoleProps", -> cy.get("#select-maps").select("de_dust2").then ($select) -> - { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($select) + { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($select) console = @lastLog.invoke("consoleProps") expect(console.Command).to.eq("select") expect(console.Selected).to.deep.eq ["de_dust2"] expect(console["Applied To"]).to.eq $select.get(0) - expect(console.Coords.x).to.be.closeTo(fromWindow.x, 10) - expect(console.Coords.y).to.be.closeTo(fromWindow.y, 10) + expect(console.Coords.x).to.be.closeTo(fromElWindow.x, 10) + expect(console.Coords.y).to.be.closeTo(fromElWindow.y, 10) it "logs only one select event", -> types = [] diff --git a/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee b/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee index 0f8d3e87ef8e..342f4e6ad48c 100644 --- a/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee @@ -18,7 +18,7 @@ describe "src/cy/commands/actions/trigger", -> $btn = cy.$$("#button") $btn.on "mouseover", (e) => - { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) obj = _.pick(e.originalEvent, "bubbles", "cancelable", "target", "type") expect(obj).to.deep.eq { @@ -28,8 +28,8 @@ describe "src/cy/commands/actions/trigger", -> type: "mouseover" } - expect(e.clientX).to.be.closeTo(fromViewport.x, 1) - expect(e.clientY).to.be.closeTo(fromViewport.y, 1) + expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) + expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() cy.get("#button").trigger("mouseover") @@ -90,10 +90,10 @@ describe "src/cy/commands/actions/trigger", -> win = cy.state("window") $btn.on "mouseover", (e) => - { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) - expect(win.pageXOffset).to.be.gt(0) - expect(e.clientX).to.be.closeTo(fromViewport.x, 1) + expect(win.scrollX).to.be.gt(0) + expect(e.clientX).to.be.closeTo(fromElViewport.x, 1) done() cy.get("#scrolledBtn").trigger("mouseover") @@ -104,10 +104,10 @@ describe "src/cy/commands/actions/trigger", -> win = cy.state("window") $btn.on "mouseover", (e) => - { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) - expect(win.pageXOffset).to.be.gt(0) - expect(e.clientY).to.be.closeTo(fromViewport.y, 1) + expect(win.scrollX).to.be.gt(0) + expect(e.clientY).to.be.closeTo(fromElViewport.y, 1) done() cy.get("#scrolledBtn").trigger("mouseover") @@ -118,10 +118,10 @@ describe "src/cy/commands/actions/trigger", -> win = cy.state("window") $btn.on "mouseover", (e) => - { fromViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) + { fromElViewport } = Cypress.dom.getElementCoordinatesByPosition($btn) - expect(e.pageX).to.be.closeTo(win.pageXOffset + e.clientX, 1) - expect(e.pageY).to.be.closeTo(win.pageYOffset + e.clientY, 1) + expect(e.pageX).to.be.closeTo(win.scrollX + e.clientX, 1) + expect(e.pageY).to.be.closeTo(win.scrollY + e.clientY, 1) done() cy.get("#scrolledBtn").trigger("mouseover") @@ -452,14 +452,14 @@ describe "src/cy/commands/actions/trigger", -> it "passes options.animationDistanceThreshold to cy.ensureElementIsNotAnimating", -> $btn = cy.$$("button:first") - { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) cy.spy(cy, "ensureElementIsNotAnimating") cy.get("button:first").trigger("tap", {animationDistanceThreshold: 1000}).then -> args = cy.ensureElementIsNotAnimating.firstCall.args - expect(args[1]).to.deep.eq([fromWindow, fromWindow]) + expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) expect(args[2]).to.eq(1000) it "passes config.animationDistanceThreshold to cy.ensureElementIsNotAnimating", -> @@ -467,14 +467,14 @@ describe "src/cy/commands/actions/trigger", -> $btn = cy.$$("button:first") - { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) cy.spy(cy, "ensureElementIsNotAnimating") cy.get("button:first").trigger("mouseover").then -> args = cy.ensureElementIsNotAnimating.firstCall.args - expect(args[1]).to.deep.eq([fromWindow, fromWindow]) + expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) expect(args[2]).to.eq(animationDistanceThreshold) describe "assertion verification", -> @@ -787,17 +787,17 @@ describe "src/cy/commands/actions/trigger", -> cy.get("button:first").trigger("mouseover").then ($btn) -> lastLog = @lastLog - { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) - expect(lastLog.get("coords")).to.deep.eq(fromWindow, "x", "y") + { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) + expect(lastLog.get("coords")).to.deep.eq(fromElWindow, "x", "y") it "#consoleProps", -> cy.get("button:first").trigger("mouseover").then ($button) => consoleProps = @lastLog.invoke("consoleProps") - { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($button) + { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($button) logCoords = @lastLog.get("coords") eventOptions = consoleProps["Event options"] - expect(logCoords.x).to.be.closeTo(fromWindow.x, 1) ## ensure we are within 1 - expect(logCoords.y).to.be.closeTo(fromWindow.y, 1) ## ensure we are within 1 + expect(logCoords.x).to.be.closeTo(fromElWindow.x, 1) ## ensure we are within 1 + expect(logCoords.y).to.be.closeTo(fromElWindow.y, 1) ## ensure we are within 1 expect(consoleProps.Command).to.eq "trigger" expect(eventOptions.bubbles).to.be.true expect(eventOptions.cancelable).to.be.true diff --git a/packages/driver/test/cypress/integration/commands/actions/type_spec.js b/packages/driver/test/cypress/integration/commands/actions/type_spec.js index d5df66e08fe9..b86ed25b71ef 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -319,14 +319,14 @@ describe('src/cy/commands/actions/type', () => { it('passes options.animationDistanceThreshold to cy.ensureElementIsNotAnimating', () => { const $txt = cy.$$(':text:first') - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($txt) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($txt) cy.spy(cy, 'ensureElementIsNotAnimating') cy.get(':text:first').type('foo', { animationDistanceThreshold: 1000 }).then(() => { const { args } = cy.ensureElementIsNotAnimating.firstCall - expect(args[1]).to.deep.eq([fromWindow, fromWindow]) + expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) expect(args[2]).to.eq(1000) }) @@ -337,14 +337,14 @@ describe('src/cy/commands/actions/type', () => { const $txt = cy.$$(':text:first') - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($txt) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($txt) cy.spy(cy, 'ensureElementIsNotAnimating') cy.get(':text:first').type('foo').then(() => { const { args } = cy.ensureElementIsNotAnimating.firstCall - expect(args[1]).to.deep.eq([fromWindow, fromWindow]) + expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) expect(args[2]).to.eq(animationDistanceThreshold) }) @@ -4093,15 +4093,15 @@ describe('src/cy/commands/actions/type', () => { context('#consoleProps', () => { it('has all of the regular options', () => { cy.get('input:first').type('foobar').then(function ($input) { - const { fromWindow } = Cypress.dom.getElementCoordinatesByPosition($input) + const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($input) const console = this.lastLog.invoke('consoleProps') expect(console.Command).to.eq('type') expect(console.Typed).to.eq('foobar') expect(console['Applied To']).to.eq($input.get(0)) - expect(console.Coords.x).to.be.closeTo(fromWindow.x, 1) + expect(console.Coords.x).to.be.closeTo(fromElWindow.x, 1) - expect(console.Coords.y).to.be.closeTo(fromWindow.y, 1) + expect(console.Coords.y).to.be.closeTo(fromElWindow.y, 1) }) }) diff --git a/packages/driver/test/cypress/integration/dom/coordinates_spec.coffee b/packages/driver/test/cypress/integration/dom/coordinates_spec.coffee index a974aae98541..6884a8881619 100644 --- a/packages/driver/test/cypress/integration/dom/coordinates_spec.coffee +++ b/packages/driver/test/cypress/integration/dom/coordinates_spec.coffee @@ -21,14 +21,14 @@ describe "src/dom/coordinates", -> it "returns the leftCenter and topCenter normalized", -> win = Cypress.dom.getWindowByElement(@$button.get(0)) - pageYOffset = Object.getOwnPropertyDescriptor(win, "pageYOffset") - pageXOffset = Object.getOwnPropertyDescriptor(win, "pageXOffset") + scrollY = Object.getOwnPropertyDescriptor(win, "scrollY") + scrollX = Object.getOwnPropertyDescriptor(win, "scrollX") - Object.defineProperty(win, "pageYOffset", { + Object.defineProperty(win, "scrollY", { value: 10 }) - Object.defineProperty(win, "pageXOffset", { + Object.defineProperty(win, "scrollX", { value: 20 }) @@ -39,16 +39,16 @@ describe "src/dom/coordinates", -> height: 40 }]) - { fromViewport, fromWindow } = Cypress.dom.getElementPositioning(@$button) + { fromElViewport, fromElWindow } = Cypress.dom.getElementPositioning(@$button) - expect(fromViewport.topCenter).to.eq(120) - expect(fromViewport.leftCenter).to.eq(85) + expect(fromElViewport.topCenter).to.eq(120) + expect(fromElViewport.leftCenter).to.eq(85) - expect(fromWindow.topCenter).to.eq(130) - expect(fromWindow.leftCenter).to.eq(105) + expect(fromElWindow.topCenter).to.eq(130) + expect(fromElWindow.leftCenter).to.eq(105) - Object.defineProperty(win, "pageYOffset", pageYOffset) - Object.defineProperty(win, "pageXOffset", pageXOffset) + Object.defineProperty(win, "scrollY", scrollY) + Object.defineProperty(win, "scrollX", scrollX) context ".getCoordsByPosition", -> it "rounds down x and y values to object", -> @@ -67,13 +67,13 @@ describe "src/dom/coordinates", -> context ".getElementCoordinatesByPosition", -> beforeEach -> - @fromWindowPos = (pos) => + @fromElWindowPos = (pos) => Cypress.dom.getElementCoordinatesByPosition(@$button, pos) - .fromWindow + .fromElWindow describe "topLeft", -> it "returns top left x/y including padding + border", -> - obj = @fromWindowPos("topLeft") + obj = @fromElWindowPos("topLeft") ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(60) @@ -81,7 +81,7 @@ describe "src/dom/coordinates", -> describe "top", -> it "returns top center x/y including padding + border", -> - obj = @fromWindowPos("top") + obj = @fromElWindowPos("top") ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(110) @@ -89,7 +89,7 @@ describe "src/dom/coordinates", -> describe "topRight", -> it "returns top right x/y including padding + border", -> - obj = @fromWindowPos("topRight") + obj = @fromElWindowPos("topRight") ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(159) @@ -97,7 +97,7 @@ describe "src/dom/coordinates", -> describe "left", -> it "returns center left x/y including padding + border", -> - obj = @fromWindowPos("left") + obj = @fromElWindowPos("left") ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(60) @@ -105,7 +105,7 @@ describe "src/dom/coordinates", -> describe "center", -> it "returns center x/y including padding + border", -> - obj = @fromWindowPos() + obj = @fromElWindowPos() ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(110) @@ -117,7 +117,7 @@ describe "src/dom/coordinates", -> ## calculation would be wrong. using getBoundingClientRect passes this test @$button.css({transform: "rotate(90deg)"}) - obj = @fromWindowPos() + obj = @fromElWindowPos() ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(110) @@ -125,7 +125,7 @@ describe "src/dom/coordinates", -> describe "right", -> it "returns center right x/y including padding + border", -> - obj = @fromWindowPos("right") + obj = @fromElWindowPos("right") ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(159) @@ -133,7 +133,7 @@ describe "src/dom/coordinates", -> describe "bottomLeft", -> it "returns bottom left x/y including padding + border", -> - obj = @fromWindowPos("bottomLeft") + obj = @fromElWindowPos("bottomLeft") ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(60) @@ -141,7 +141,7 @@ describe "src/dom/coordinates", -> context "bottom", -> it "returns bottom center x/y including padding + border", -> - obj = @fromWindowPos("bottom") + obj = @fromElWindowPos("bottom") ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(110) @@ -149,7 +149,7 @@ describe "src/dom/coordinates", -> context "bottomRight", -> it "returns bottom right x/y including padding + border", -> - obj = @fromWindowPos("bottomRight") + obj = @fromElWindowPos("bottomRight") ## padding is added to the line-height but width includes the padding expect(obj.x).to.eq(159) @@ -184,6 +184,6 @@ describe "src/dom/coordinates", -> ] ).as 'getClientRects' - obj = Cypress.dom.getElementCoordinatesByPosition($el, 'center').fromViewport + obj = Cypress.dom.getElementCoordinatesByPosition($el, 'center').fromElViewport expect({x: obj.x, y: obj.y}).to.deep.eq({x:125, y:120}) From de38df739fbab601214d0d107b3c12d73137599d Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Fri, 11 Oct 2019 18:04:48 -0400 Subject: [PATCH 34/36] handle calculating the fromElWindow coordinates inline --- packages/driver/src/cy/mouse.js | 17 +++++++++-------- packages/driver/src/dom/elements.js | 9 --------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index b556ab0831ac..808549e36f0a 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -638,20 +638,21 @@ const formatReasonNotFired = (reason) => { } const toCoordsEventOptions = (x, y, win) => { - // these are the coords from the element's window, ignoring scroll position - const fromWindowCoords = $elements.getFromWindowCoords(x, y, win) + // these are the coords from the element's window, + // ignoring scroll position + const { scrollX, scrollY } = win return { + x, + y, clientX: x, clientY: y, screenX: x, screenY: y, - x, - y, - pageX: fromWindowCoords.x, - pageY: fromWindowCoords.y, - layerX: fromWindowCoords.x, - layerY: fromWindowCoords.y, + pageX: x + scrollX, + pageY: x + scrollY, + layerX: x + scrollX, + layerY: x + scrollY, } } diff --git a/packages/driver/src/dom/elements.js b/packages/driver/src/dom/elements.js index e6c6d2bae680..3228dce8fd06 100644 --- a/packages/driver/src/dom/elements.js +++ b/packages/driver/src/dom/elements.js @@ -634,13 +634,6 @@ const isScrollable = ($el) => { return false } -const getFromWindowCoords = (x, y, win) => { - return { - x: win.scrollX + x, - y: win.scrollY + y, - } -} - const isDescendent = ($el1, $el2) => { if (!$el2) { return false @@ -990,8 +983,6 @@ _.extend(module.exports, { getElements, - getFromWindowCoords, - getFirstFocusableEl, getActiveElByDocument, From 5c8a625b0b6b085082632a65c27da3bc8d330ce0 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Fri, 11 Oct 2019 18:05:30 -0400 Subject: [PATCH 35/36] extract out function for clarity, receive consistent arg names --- packages/driver/src/cy/mouse.js | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index 808549e36f0a..ae5e5313313b 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -39,6 +39,21 @@ const getMouseCoords = (state) => { return state('mouseCoords') } +const shouldFireMouseMoveEvents = (targetEl, lastHoveredEl, fromElViewport, coords) => { + // not the same element, fire mouse move events + if (lastHoveredEl !== targetEl) { + return true + } + + const xy = (obj) => { + return _.pick(obj, 'x', 'y') + } + + // if we have the same element, but the xy coords are different + // then fire mouse move events... + return !_.isEqual(xy(fromElViewport), xy(coords)) +} + const create = (state, keyboard, focused) => { const mouse = { _getDefaultMouseOptions (x, y, win) { @@ -60,19 +75,23 @@ const create = (state, keyboard, focused) => { * @param {Coords} coords * @param {HTMLElement} forceEl */ - move (coords, forceEl) { - debug('mouse.move', coords) + move (fromElViewport, forceEl) { + debug('mouse.move', fromElViewport) const lastHoveredEl = getLastHoveredEl(state) - const targetEl = forceEl || mouse.getElAtCoords(coords) + const targetEl = forceEl || mouse.getElAtCoords(fromElViewport) - // if coords are same AND we're already hovered on the element, don't send move events - if (_.isEqual({ x: coords.x, y: coords.y }, getMouseCoords(state)) && lastHoveredEl === targetEl) return { el: targetEl } + // if the element is already hovered and our coords for firing the events + // already match our existing state coords, then bail early and don't fire + // any mouse move events + if (!shouldFireMouseMoveEvents(targetEl, lastHoveredEl, fromElViewport, getMouseCoords(state))) { + return { el: targetEl } + } - const events = mouse._moveEvents(targetEl, coords) + const events = mouse._moveEvents(targetEl, fromElViewport) - const resultEl = forceEl || mouse.getElAtCoords(coords) + const resultEl = forceEl || mouse.getElAtCoords(fromElViewport) return { el: resultEl, fromEl: lastHoveredEl, events } }, From a18dd32b2aa5c036991698f259ec738d1d8e02f7 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Fri, 11 Oct 2019 18:05:42 -0400 Subject: [PATCH 36/36] cleanup, add lots of comments --- packages/driver/src/dom/coordinates.js | 35 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/driver/src/dom/coordinates.js b/packages/driver/src/dom/coordinates.js index f9984c8e9894..c855a5ba7d20 100644 --- a/packages/driver/src/dom/coordinates.js +++ b/packages/driver/src/dom/coordinates.js @@ -7,31 +7,42 @@ const getElementAtPointFromViewport = (doc, x, y) => { const isAutIframe = (win) => !$elements.getNativeProp(win.parent, 'frameElement') +/** + * @param {JQuery} $el + */ const getElementPositioning = ($el) => { - /** - * @type {HTMLElement} - */ + let autFrame + const el = $el[0] const win = $window.getWindowByElement(el) - let autFrame // properties except for width / height // are relative to the top left of the viewport - // we use the first of getClientRects in order to account for inline elements - // that span multiple lines. Which would cause us to click in the center and thus miss - // This should be the same as using getBoundingClientRect() - // for elements with a single rect - // const rect = el.getBoundingClientRect() + // we use the first of getClientRects in order to account for inline + // elements that span multiple lines. Which would cause us to click + // click in the center and thus miss... + // + // however we have a fallback to getBoundingClientRect() such as + // when the element is hidden or detached from the DOM. getClientRects() + // returns a zero length DOMRectList in that case, which becomes undefined. + // so we fallback to getBoundingClientRect() so that we get an actual DOMRect + // with all properties 0'd out const rect = el.getClientRects()[0] || el.getBoundingClientRect() - const getRectFromAutIframe = (rect, el) => { + // we want to return the coordinates from the autWindow to the element + // which handles a situation in which the element is inside of a nested + // iframe. we use these "absolute" coordinates from the autWindow to draw + // things like the red hitbox - since the hitbox layer is placed on the + // autWindow instead of the window the element is actually within + const getRectFromAutIframe = (rect) => { let x = 0 //rect.left let y = 0 //rect.top - let curWindow = el.ownerDocument.defaultView + let curWindow = win let frame + // walk up from a nested iframe so we continually add the x + y values while (!isAutIframe(curWindow) && curWindow.parent !== curWindow) { frame = $elements.getNativeProp(curWindow, 'frameElement') @@ -57,7 +68,7 @@ const getElementPositioning = ($el) => { } } - const rectFromAut = getRectFromAutIframe(rect, el) + const rectFromAut = getRectFromAutIframe(rect) const rectFromAutCenter = getCenterCoordinates(rectFromAut) // add the center coordinates