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 001/370] 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 002/370] 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 003/370] 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 004/370] 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 011/370] re-run build From e1fb2ebede2a3812e70b4572be97602940df4836 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Mon, 29 Jul 2019 21:11:00 -0400 Subject: [PATCH 012/370] extract simulated type from native events --- packages/driver/src/config/jquery.coffee | 58 - packages/driver/src/config/jquery.js | 76 ++ packages/driver/src/config/lodash.coffee | 13 - packages/driver/src/config/lodash.d.ts | 43 + packages/driver/src/config/lodash.js | 15 + packages/driver/src/cy/actionability.coffee | 28 +- .../driver/src/cy/commands/actions/type.js | 579 +++++++++ packages/driver/src/cy/ensures.coffee | 7 +- packages/driver/src/cy/keyboard.ts | 1140 +++++++++++++++++ packages/driver/src/cy/retries.coffee | 4 +- packages/driver/src/cypress.coffee | 2 +- .../driver/src/cypress/UsKeyboardLayout.js | 389 ++++++ packages/driver/src/cypress/cy.coffee | 15 +- .../driver/src/cypress/error_messages.coffee | 21 +- .../driver/src/cypress/setter_getter.d.ts | 4 + packages/driver/src/cypress/utils.coffee | 4 + .../src/dom/{document.js => document.ts} | 9 +- .../src/dom/{elements.js => elements.ts} | 305 +++-- packages/driver/src/dom/index.js | 11 +- packages/driver/src/dom/jquery.js | 1 + .../src/dom/{selection.js => selection.ts} | 342 +++-- packages/driver/src/dom/types.d.ts | 52 + packages/driver/test/cypress/.eslintrc | 3 + .../commands/actions/click_spec.js | 105 +- .../test/cypress/support/matchers/index.d.ts | 19 + .../test/cypress/support/matchers/index.js | 155 +++ packages/driver/ts/import-coffee.d.ts | 4 + packages/driver/tsconfig.json | 55 + packages/launcher/test/unit/detect_spec.ts | 2 +- packages/launcher/test/unit/launcher_spec.ts | 6 +- packages/launcher/tslint.json | 6 - packages/network/index.ts | 10 + packages/runner/ts/import-coffee.d.ts | 4 + packages/server/test/unit/project_spec.coffee | 28 +- 34 files changed, 3129 insertions(+), 386 deletions(-) delete mode 100644 packages/driver/src/config/jquery.coffee create mode 100644 packages/driver/src/config/jquery.js delete mode 100644 packages/driver/src/config/lodash.coffee create mode 100644 packages/driver/src/config/lodash.d.ts create mode 100644 packages/driver/src/config/lodash.js create mode 100644 packages/driver/src/cy/commands/actions/type.js create mode 100644 packages/driver/src/cy/keyboard.ts create mode 100644 packages/driver/src/cypress/UsKeyboardLayout.js create mode 100644 packages/driver/src/cypress/setter_getter.d.ts rename packages/driver/src/dom/{document.js => document.ts} (69%) rename packages/driver/src/dom/{elements.js => elements.ts} (76%) rename packages/driver/src/dom/{selection.js => selection.ts} (70%) create mode 100644 packages/driver/src/dom/types.d.ts create mode 100644 packages/driver/test/cypress/support/matchers/index.d.ts create mode 100644 packages/driver/test/cypress/support/matchers/index.js create mode 100644 packages/driver/ts/import-coffee.d.ts create mode 100644 packages/driver/tsconfig.json delete mode 100644 packages/launcher/tslint.json create mode 100644 packages/network/index.ts create mode 100644 packages/runner/ts/import-coffee.d.ts diff --git a/packages/driver/src/config/jquery.coffee b/packages/driver/src/config/jquery.coffee deleted file mode 100644 index 57837ea76ebd..000000000000 --- a/packages/driver/src/config/jquery.coffee +++ /dev/null @@ -1,58 +0,0 @@ -$ = require("jquery") -_ = require('lodash') -require("jquery.scrollto") - -$dom = require("../dom") - -## force jquery to have the same visible -## and hidden logic as cypress - -## this prevents `is` from calling into the native .matches method -## which would prevent our `focus` code from ever being called during -## is(:focus). -## see https://github.com/jquery/sizzle/wiki#sizzlematchesselector-domelement-element-string-selector- - -## this is to help to interpretor make optimizations around try/catch -tryCatchFinally = ({tryFn, catchFn, finallyFn}) -> - try - tryFn() - catch e - catchFn(e) - finally - finallyFn() - -matchesSelector = $.find.matchesSelector -$.find.matchesSelector = (elem, expr) -> - isUsingFocus = _.includes(expr, ':focus') - if isUsingFocus - supportMatchesSelector = $.find.support.matchesSelector - $.find.support.matchesSelector = false - - args = arguments - _this = @ - - return tryCatchFinally({ - tryFn: -> - matchesSelector.apply(_this, args) - catchFn: (e) -> - throw e - finallyFn: -> - if isUsingFocus - $.find.support.matchesSelector = supportMatchesSelector - }) - - -## see difference between 'filters' and 'pseudos' -## https://api.jquery.com/filter/ and https://api.jquery.com/category/selectors/ - -$.expr.pseudos.focus = $dom.isFocused -$.expr.filters.focus = $dom.isFocused -$.expr.pseudos.focused = $dom.isFocused -$.expr.filters.visible = $dom.isVisible -$.expr.filters.hidden = $dom.isHidden - -$.expr.cacheLength = 1 - -$.ajaxSetup({ - cache: false -}) diff --git a/packages/driver/src/config/jquery.js b/packages/driver/src/config/jquery.js new file mode 100644 index 000000000000..5d097dcb9696 --- /dev/null +++ b/packages/driver/src/config/jquery.js @@ -0,0 +1,76 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ + +const $ = require('jquery') + +require('jquery.scrollto') +const _ = require('lodash') + +const $dom = require('../dom') +// const $elements = require('../dom/elements') + +//# force jquery to have the same visible +//# and hidden logic as cypress + +//# this prevents `is` from calling into the native .matches method +//# which would prevent our `focus` code from ever being called during +//# is(:focus). +//# see https://github.com/jquery/sizzle/wiki#sizzlematchesselector-domelement-element-string-selector- + +//# this is to help to interpretor make optimizations around try/catch +const tryCatchFinally = function ({ tryFn, catchFn, finallyFn }) { + try { + return tryFn() + } catch (e) { + return catchFn(e) + } finally { + finallyFn() + } +} + +const { matchesSelector } = $.find + +$.find.matchesSelector = function (elem, expr, ...otherArgs) { + let supportMatchesSelector + const isUsingFocus = _.includes(expr, ':focus') + + if (isUsingFocus) { + supportMatchesSelector = $.find.support.matchesSelector + $.find.support.matchesSelector = false + } + + const args = [elem, expr, ...otherArgs] + const _this = this + + return tryCatchFinally({ + tryFn () { + return matchesSelector.apply(_this, args) + }, + catchFn (e) { + throw e + }, + finallyFn () { + if (isUsingFocus) { + return $.find.support.matchesSelector = supportMatchesSelector + } + }, + }) +} + +//# see difference between 'filters' and 'pseudos' +//# https://api.jquery.com/filter/ and https://api.jquery.com/category/selectors/ + +$.expr.pseudos.focus = $dom.isFocused +$.expr.filters.focus = $dom.isFocused +$.expr.pseudos.focused = $dom.isFocused +$.expr.filters.visible = $dom.isVisible +$.expr.filters.hidden = $dom.isHidden + +$.expr.cacheLength = 1 + +$.ajaxSetup({ + cache: false, +}) diff --git a/packages/driver/src/config/lodash.coffee b/packages/driver/src/config/lodash.coffee deleted file mode 100644 index d8974fdc2f38..000000000000 --- a/packages/driver/src/config/lodash.coffee +++ /dev/null @@ -1,13 +0,0 @@ -_ = require("lodash") - -inflection = require("@cypress/underscore.inflection")(_) - -## only export exactly what we need, nothing more! -_.mixin({ - clean: require("underscore.string/clean") - count: require("underscore.string/count") - isBlank: require("underscore.string/isBlank") - toBoolean: require("underscore.string/toBoolean") - capitalize: require("underscore.string/capitalize") ## its mo' better the lodash version - ordinalize: inflection.ordinalize -}) diff --git a/packages/driver/src/config/lodash.d.ts b/packages/driver/src/config/lodash.d.ts new file mode 100644 index 000000000000..b875b3040dba --- /dev/null +++ b/packages/driver/src/config/lodash.d.ts @@ -0,0 +1,43 @@ +// const _ = require('lodash') +import _ from 'lodash' +import underscoreInflection from '@cypress/underscore.inflection' + +import clean from 'underscore.string/clean' +import count from 'underscore.string/count' +import isBlank from 'underscore.string/isBlank' +import toBoolean from 'underscore.string/toBoolean' +import capitalize from 'underscore.string/capitalize' + +const inflection = underscoreInflection(_) + +// only export exactly what we need, nothing more! +_.mixin({ + clean, + count, + isBlank, + toBoolean, + capitalize, // its mo' better the lodash version + ordinalize: inflection.ordinalize, +}) + +declare module 'lodash' { + export interface LoDashExplicitWrapper { + clean(...args): LoDashExplicitWrapper + count(...args): LoDashExplicitWrapper + isBlank(...args): LoDashExplicitWrapper + toBoolean(...args): LoDashExplicitWrapper + capitalize(...args): LoDashExplicitWrapper + ordinalize(...args): LoDashExplicitWrapper + } + + export interface LodashStatic { + clean(...args): LoDashExplicitWrapper + count(...args): LoDashExplicitWrapper + isBlank(...args): LoDashExplicitWrapper + toBoolean(...args): LoDashExplicitWrapper + capitalize(...args): LoDashExplicitWrapper + ordinalize(...args): LoDashExplicitWrapper + } +} + +export default _ diff --git a/packages/driver/src/config/lodash.js b/packages/driver/src/config/lodash.js new file mode 100644 index 000000000000..aa4201697046 --- /dev/null +++ b/packages/driver/src/config/lodash.js @@ -0,0 +1,15 @@ +const _ = require('lodash') + +const inflection = require('@cypress/underscore.inflection')(_) + +//# only export exactly what we need, nothing more! +_.mixin({ + clean: require('underscore.string/clean'), + count: require('underscore.string/count'), + isBlank: require('underscore.string/isBlank'), + toBoolean: require('underscore.string/toBoolean'), + capitalize: require('underscore.string/capitalize'), //# its mo' better the lodash version + ordinalize: inflection.ordinalize, +}) + +module.exports = _ diff --git a/packages/driver/src/cy/actionability.coffee b/packages/driver/src/cy/actionability.coffee index 0b06c35306b2..9ea7593ac05f 100644 --- a/packages/driver/src/cy/actionability.coffee +++ b/packages/driver/src/cy/actionability.coffee @@ -209,6 +209,18 @@ ensureNotAnimating = (cy, $el, coordsHistory, animationDistanceThreshold) -> cy.ensureElementIsNotAnimating($el, coordsHistory, animationDistanceThreshold) verify = (cy, $el, options, callbacks) -> + + _.defaults(options, { + ensure: { + position: true, + visibility: true, + receivability: true, + notAnimatingOrCovered: true, + notReadonly: false, + custom: false + } + }) + win = $dom.getWindowByElement($el.get(0)) { _log, force, position } = options @@ -220,7 +232,7 @@ verify = (cy, $el, options, callbacks) -> ## if we have a position we must validate ## this ahead of time else bail early - if position + if position and options.ensure.position try cy.ensureValidPosition(position, _log) catch err @@ -232,6 +244,9 @@ verify = (cy, $el, options, callbacks) -> runAllChecks = -> if force isnt true + ## ensure its 'receivable' + if (options.ensure.receivability) then cy.ensureReceivability($el, _log) + ## scroll the element into view $el.get(0).scrollIntoView() @@ -239,17 +254,20 @@ verify = (cy, $el, options, callbacks) -> onScroll($el, "element") ## ensure its visible - cy.ensureVisibility($el, _log) + if (options.ensure.visibility) then cy.ensureVisibility($el, _log) + + if options.ensure.notReadonly + cy.ensureNotReadonly($el, _log) - ## ensure its 'receivable' (not disabled, readonly) - cy.ensureReceivability($el, _log) + if _.isFunction(options.custom) + options.custom($el, _log) ## now go get all the coords for this element coords = getCoordinatesForEl(cy, $el, options) ## if force is true OR waitForAnimations is false ## then do not perform these additional ensures... - if (force isnt true) and (options.waitForAnimations isnt false) + if (options.ensure.notAnimatingOrCovered) and (force isnt true) and (options.waitForAnimations isnt false) ## store the coords that were absolute ## from the window or from the viewport for sticky elements ## (see https://github.com/cypress-io/cypress/pull/1478) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js new file mode 100644 index 000000000000..7ca2d21404d0 --- /dev/null +++ b/packages/driver/src/cy/commands/actions/type.js @@ -0,0 +1,579 @@ +const _ = require('lodash') +const Promise = require('bluebird') + +const $dom = require('../../../dom') +const $elements = require('../../../dom/elements') +const $selection = require('../../../dom/selection') +const $utils = require('../../../cypress/utils') +const $actionability = require('../../actionability') +const Debug = require('debug') +const debug = Debug('driver:command:type') + +// const dateRegex = /^\d{4}-\d{2}-\d{2}/ +// const monthRegex = /^\d{4}-(0\d|1[0-2])/ +// const weekRegex = /^\d{4}-W(0[1-9]|[1-4]\d|5[0-3])/ +// const timeRegex = /^([0-1]\d|2[0-3]):[0-5]\d(:[0-5]\d)?(\.[0-9]{1,3})?/ +// const dateTimeRegex = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}/ + +module.exports = function (Commands, Cypress, cy, state, config) { + const { keyboard } = cy.internal + const { Keyboard } = Cypress + + function type (subject, chars, options = {}) { + // debugger + // + debug('type:', chars) + let updateTable + + options = _.clone(options) + //# allow the el we're typing into to be + //# changed by options -- used by cy.clear() + _.defaults(options, { + $el: subject, + log: true, + verify: true, + force: false, + simulated: !!Cypress.config('simulatedOnly'), + delay: 10, + release: true, + waitForAnimations: config('waitForAnimations'), + animationDistanceThreshold: config('animationDistanceThreshold'), + }) + + if (options.log) { + //# figure out the options which actually change the behavior of clicks + const deltaOptions = $utils.filterOutOptions(options) + + const table = {} + + const getRow = (id, key, which) => { + return table[id] || (function () { + let obj + + table[id] = (obj = {}) + const modifiers = Keyboard.modifiersToString(Keyboard.getActiveModifiers(state)) + + if (modifiers) { + obj.modifiers = modifiers + } + + if (key) { + obj.typed = key + if (which) { + obj.which = which + } + } + + return obj + })() + } + + updateTable = function (id, key, column, which, value) { + const row = getRow(id, key, which) + + row[column] = value || 'preventedDefault' + } + + //# transform table object into object with zero based index as keys + const getTableData = () => { + return _.reduce(_.values(table), (memo, value, index) => { + memo[index + 1] = value + + return memo + } + , {}) + } + + options._log = Cypress.log({ + message: [chars, deltaOptions], + $el: options.$el, + consoleProps () { + return { + 'Typed': chars, + 'Applied To': $dom.getElements(options.$el), + 'Options': deltaOptions, + 'table': { + //# mouse events tables will take up slots 1 and 2 if they're present + //# this preserves the order of the tables + 3: () => { + return { + name: 'Keyboard Events', + data: getTableData(), + columns: ['typed', 'which', 'keydown', 'keypress', 'textInput', 'input', 'keyup', 'change', 'modifiers'], + } + }, + }, + } + }, + }) + + options._log.snapshot('before', { next: 'after' }) + } + + if (options.$el.length > 1) { + + $utils.throwErrByPath('type.multiple_elements', { + onFail: options._log, + args: { num: options.$el.length }, + }) + } + + chars = `${chars}` + + const charsToType = `${chars}` + + // _setCharsNeedingType(nextChars) + + // options.chars = `${charsToType}` + + const win = state('window') + + const getDefaultButtons = (form) => { + return form.find('input, button').filter((__, el) => { + const $el = $dom.wrap(el) + + return ( + ($dom.isSelector($el, 'input') && $dom.isType($el, 'submit')) || + ($dom.isSelector($el, 'button') && !$dom.isType($el, 'button')) + ) + }) + } + + const type = function () { + const simulateSubmitHandler = function () { + const form = options.$el.parents('form') + + if (!form.length) { + return + } + + const multipleInputsAndNoSubmitElements = function (form) { + const inputs = form.find('input') + const submits = getDefaultButtons(form) + + return inputs.length > 1 && submits.length === 0 + } + + //# throw an error here if there are multiple form parents + + //# bail if we have multiple inputs and no submit elements + if (multipleInputsAndNoSubmitElements(form)) { + return + } + + const clickedDefaultButton = function (button) { + //# find the 'default button' as per HTML spec and click it natively + //# do not issue mousedown / mouseup since this is supposed to be synthentic + if (button.length) { + button.get(0).click() + + return true + } + + return false + } + + const getDefaultButton = (form) => { + return getDefaultButtons(form).first() + } + + const defaultButtonisDisabled = (button) => { + return button.prop('disabled') + } + + const defaultButton = getDefaultButton(form) + + //# bail if the default button is in a 'disabled' state + if (defaultButtonisDisabled(defaultButton)) { + return + } + + //# issue the click event to the 'default button' of the form + //# we need this to be synchronous so not going through our + //# own click command + //# as of now, at least in Chrome, causing the click event + //# on the button will indeed trigger the form submit event + //# so we dont need to fire it manually anymore! + if (!clickedDefaultButton(defaultButton)) { + //# if we werent able to click the default button + //# then synchronously fire the submit event + //# currently this is sync but if we use a waterfall + //# promise in the submit command it will break again + //# consider changing type to a Promise and juggle logging + return cy.now('submit', form, { log: false, $el: form }) + } + } + + const dispatchChangeEvent = function (el, id) { + const change = document.createEvent('HTMLEvents') + + change.initEvent('change', true, false) + + const dispatched = el.dispatchEvent(change) + + if (id && updateTable) { + return updateTable(id, null, 'change', null, dispatched) + } + } + + const needSingleValueChange = (el) => { + return $elements.isNeedSingleValueChangeInputElement(el) + } + + //# see comment in updateValue below + let typed = '' + + const isContentEditable = $elements.isContentEditable(options.$el.get(0)) + const isTextarea = $elements.isTextarea(options.$el.get(0)) + + return keyboard.type({ + $el: options.$el, + chars: charsToType, + delay: options.delay, + release: options.release, + window: win, + force: options.force, + simulated: options.simulated, + + updateValue (el, key, charsToType) { + // in these cases, the value must only be set after all + // the characters are input because attemping to set + // a partial/invalid value results in the value being + // set to an empty string + if (needSingleValueChange(el)) { + typed += key + if (typed === charsToType) { + return $elements.setNativeProp(el, 'value', charsToType) + } + } else { + return $selection.replaceSelectionContents(el, key) + } + }, + + // onFocusChange (el, chars) { + // const lastIndexToType = validateTyping(el, chars) + // const [charsToType, nextChars] = _splitChars( + // `${chars}`, + // lastIndexToType + // ) + + // _setCharsNeedingType(nextChars) + + // return charsToType + // }, + + onAfterType () { + if (options.release === true) { + state('keyboardModifiers', null) + } + // if (charsNeedingType) { + // const lastIndexToType = validateTyping(el, charsNeedingType) + // const [charsToType, nextChars] = _splitChars( + // charsNeedingType, + // lastIndexToType + // ) + + // _setCharsNeedingType(nextChars) + + // return charsToType + // } + + // return false + }, + + onBeforeType (totalKeys) { + //# for the total number of keys we're about to + //# type, ensure we raise the timeout to account + //# for the delay being added to each keystroke + return cy.timeout(totalKeys * options.delay, true, 'type') + }, + + onBeforeSpecialCharAction (id, key) { + //# don't apply any special char actions such as + //# inserting new lines on {enter} or moving the + //# caret / range on left or right movements + // if (isTypeableButNotAnInput) { + // return false + // } + }, + + // onBeforeEvent (id, key, column, which) { + // //# if we are an element which isnt text like but we have + // //# a tabindex then it can receive keyboard events but + // //# should not fire input or textInput and should not fire + // //# change events + // if (inputEvents.includes(column) && isTypeableButNotAnInput) { + // return false + // } + // }, + + onEvent (...args) { + if (updateTable) { + return updateTable(...args) + } + }, + + //# fires only when the 'value' + //# of input/text/contenteditable + //# changes + onValueChange (originalText, el) { + debug('onValueChange', originalText, el) + //# contenteditable should never be called here. + //# only inputs and textareas can have change events + let changeEvent = state('changeEvent') + + if (changeEvent) { + if (!changeEvent(null, true)) { + state('changeEvent', null) + } + + return + } + + return state('changeEvent', (id, readOnly) => { + const changed = + $elements.getNativeProp(el, 'value') !== originalText + + if (!readOnly) { + if (changed) { + dispatchChangeEvent(el, id) + } + + state('changeEvent', null) + } + + return changed + }) + }, + + onEnterPressed (id) { + //# dont dispatch change events or handle + //# submit event if we've pressed enter into + //# a textarea or contenteditable + let changeEvent = state('changeEvent') + + if (isTextarea || isContentEditable) { + return + } + + //# if our value has changed since our + //# element was activated we need to + //# fire a change event immediately + if (changeEvent) { + changeEvent(id) + } + + //# handle submit event handler here + return simulateSubmitHandler() + }, + + onNoMatchingSpecialChars (chars, allChars) { + if (chars === 'tab') { + return $utils.throwErrByPath('type.tab', { onFail: options._log }) + } + + return $utils.throwErrByPath('type.invalid', { + onFail: options._log, + args: { chars: `{${chars}}`, allChars }, + }) + }, + }) + } + + const handleFocused = function () { + + //# if it's the body, don't need to worry about focus + const isBody = options.$el.is('body') + + if (isBody) { + return type() + } + + if (!$elements.isFocusableWhenNotDisabled(options.$el)) { + const node = $dom.stringify(options.$el) + + $utils.throwErrByPath('type.not_on_typeable_element', { + onFail: options._log, + args: { node }, + }) + } + + let elToCheckCurrentlyFocused + //# if the subject is already the focused element, start typing + //# we handle contenteditable children by getting the host contenteditable, + //# and seeing if that is focused + //# Checking first if element is focusable accounts for focusable els inside + //# of contenteditables + let $focused = cy.getFocused() + + $focused = $focused && $focused[0] + + if ($elements.isFocusable(options.$el)) { + elToCheckCurrentlyFocused = options.$el[0] + } else if ($elements.isContentEditable(options.$el[0])) { + elToCheckCurrentlyFocused = $selection.getHostContenteditable(options.$el[0]) + } + + options.ensure = { + position: true, + visibility: true, + receivability: true, + notAnimatingOrCovered: true, + notReadonly: true, + } + + if (elToCheckCurrentlyFocused && (elToCheckCurrentlyFocused === $focused)) { + options.ensure = { + receivability: true, + notReadonly: true, + } + } + + return $actionability.verify(cy, options.$el, options, { + onScroll ($el, type) { + return Cypress.action('cy:scrolled', $el, type) + }, + + onReady ($elToClick) { + const $focused = cy.getFocused() + + //# if we dont have a focused element + //# or if we do and its not ourselves + //# then issue the click + if ( + !$focused || + ($focused && $focused.get(0) !== $elToClick.get(0)) + ) { + //# click the element first to simulate focus + //# and typical user behavior in case the window + //# is out of focus + return cy.now('click', $elToClick, { + $el: $elToClick, + log: false, + verify: false, + _log: options._log, + force: true, //# force the click, avoid waiting + timeout: options.timeout, + interval: options.interval, + }) + .then(() => { + + return type() + + // BEOW DOES NOT APPLY + // cannot just call .focus, since children of contenteditable will not receive cursor + // with .focus() + + // focusCursor calls focus on first focusable + // then moves cursor to end if in textarea, input, or contenteditable + // $selection.focusCursor($elToFocus[0]) + }) + } + + return type() + }, + }) + } + + return handleFocused().then(() => { + cy.timeout($actionability.delay, true, 'type') + + return Promise.delay($actionability.delay, 'type').then(() => { + //# command which consume cy.type may + //# want to handle verification themselves + let verifyAssertions + + if (options.verify === false) { + return options.$el + } + + return (verifyAssertions = () => { + return cy.verifyUpcomingAssertions(options.$el, options, { + onRetry: verifyAssertions, + }) + })() + }) + }) + } + + function clear (subject, options = {}) { + //# what about other types of inputs besides just text? + //# what about the new HTML5 ones? + _.defaults(options, { + log: true, + force: false, + }) + + //# blow up if any member of the subject + //# isnt a textarea or text-like + const clear = function (el) { + const $el = $dom.wrap(el) + + if (options.log) { + //# figure out the options which actually change the behavior of clicks + const deltaOptions = $utils.filterOutOptions(options) + + options._log = Cypress.log({ + message: deltaOptions, + $el, + consoleProps () { + return { + 'Applied To': $dom.getElements($el), + 'Elements': $el.length, + 'Options': deltaOptions, + } + }, + }) + } + + const node = $dom.stringify($el) + + if (!$dom.isTextLike($el.get(0))) { + const word = $utils.plural(subject, 'contains', 'is') + + $utils.throwErrByPath('clear.invalid_element', { + onFail: options._log, + args: { word, node }, + }) + } + + return cy + .now('type', $el, '{selectall}{del}', { + $el, + log: false, + verify: false, //# handle verification ourselves + _log: options._log, + force: options.force, + timeout: options.timeout, + interval: options.interval, + }) + .then(() => { + if (options._log) { + options._log.snapshot().end() + } + + return null + }) + } + + return Promise.resolve(subject.toArray()) + .each(clear) + .then(() => { + let verifyAssertions + + return (verifyAssertions = () => { + return cy.verifyUpcomingAssertions(subject, options, { + onRetry: verifyAssertions, + }) + })() + }) + } + + return Commands.addAll( + { prevSubject: 'element' }, + { + type, + clear, + } + ) +} diff --git a/packages/driver/src/cy/ensures.coffee b/packages/driver/src/cy/ensures.coffee index 6c69c1491c80..1f72173f875b 100644 --- a/packages/driver/src/cy/ensures.coffee +++ b/packages/driver/src/cy/ensures.coffee @@ -112,6 +112,9 @@ create = (state, expect) -> args: { cmd, node } }) + ensureNotReadonly = (subject, onFail) -> + cmd = state("current").get("name") + # readonly can only be applied to input/textarea # not on checkboxes, radios, etc.. if $dom.isTextLike(subject) and subject.prop("readonly") @@ -127,7 +130,7 @@ create = (state, expect) -> # We overwrite the filter(":visible") in jquery # packages/driver/src/config/jquery.coffee#L51 - # So that this effectively calls our logic + # So that this effectively calls our logic # for $dom.isVisible aka !$dom.isHidden if not (subject.length is subject.filter(":visible").length) reason = $dom.getReasonIsHidden(subject) @@ -338,6 +341,8 @@ create = (state, expect) -> ensureValidPosition ensureScrollability + + ensureNotReadonly } module.exports = { diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts new file mode 100644 index 000000000000..b6f7b88c923f --- /dev/null +++ b/packages/driver/src/cy/keyboard.ts @@ -0,0 +1,1140 @@ +import _ from 'lodash' +import moment from 'moment' +import Promise from 'bluebird' +import Debug from 'debug' +import * as $dom from '../dom' +import * as $elements from '../dom/elements' +import * as $document from '../dom/document' +import * as $selection from '../dom/selection' +import $window from '../dom/window' +import $utils from '../cypress/utils.coffee' +import { keyboardMappings as USKeyboard } from '../cypress/UsKeyboardLayout' +import { HTMLTextLikeElement, HTMLTextLikeInputElement } from '../dom/types' + +const debug = Debug('driver:keyboard') + +export interface keyboardModifiers { + alt: boolean + ctrl: boolean + meta: boolean + shift: boolean +} + +export interface KeyboardState { + keyboardModifiers?: keyboardModifiers +} + +export interface ProxyState { + (arg: K): T[K] | undefined + (arg: K, arg2: T[K] | null): void +} + +export type State = ProxyState + +interface KeyDetailsPartial extends Partial { + key: string +} + +type SimulatedDefault = ( + el: HTMLTextLikeElement, + key: KeyDetails, + options: any +) => void + +interface KeyDetails { + key: string + text: string + code: string + keyCode: number + location: number + shiftKey?: string + shiftText?: string + shiftKeyCode?: number + simulatedDefault?: SimulatedDefault + simulatedDefaultOnly?: boolean + events: { + [key in KeyEventType]?: boolean; + } +} +// const $Cypress = require('../cypress') + +const dateRegex = /^\d{4}-\d{2}-\d{2}/ +const monthRegex = /^\d{4}-(0\d|1[0-2])/ +const weekRegex = /^\d{4}-W(0[1-9]|[1-4]\d|5[0-3])/ +const timeRegex = /^([0-1]\d|2[0-3]):[0-5]\d(:[0-5]\d)?(\.[0-9]{1,3})?/ +const dateTimeRegex = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}/ +const charsBetweenCurlyBracesRe = /({.+?})/ +// const clearRegex = /^{selectall}{del}/i +// const isSingleDigitRe = /^\d$/ +// const isStartingDigitRe = /^\d/ + +const initialModifiers = { + alt: false, + ctrl: false, + meta: false, + shift: false, +} + +/** + * @example {meta: true, ctrl: false, shift:false, alt: true} => 5 + */ +const getModifiersValue = (modifiers: keyboardModifiers) => { + return _.map(modifiers, (value, key) => { + return value && modifierValueMap[key] + }).reduce((a, b) => { + return a + b + }, 0) +} + +// const keyToStandard = (key) => { +// return keyStandardMap[key] || key +// } + +const modifierValueMap = { + alt: 1, + ctrl: 2, + meta: 4, + shift: 8, +} + +export type KeyEventType = + | 'keydown' + | 'keyup' + | 'keypress' + | 'input' + | 'textInput' + +const toModifiersEventOptions = (modifiers: keyboardModifiers) => { + return { + altKey: modifiers.alt, + ctrlKey: modifiers.ctrl, + metaKey: modifiers.meta, + shiftKey: modifiers.shift, + } +} + +const fromModifierEventOptions = (eventOptions: { + [key: string]: string +}): keyboardModifiers => { + const modifiers = _.pickBy( + { + alt: eventOptions.altKey, + ctrl: eventOptions.ctrlKey, + meta: eventOptions.metaKey, + shift: eventOptions.shiftKey, + }, + _.identity + ) + + return _.defaults(modifiers, { + alt: false, + ctrl: false, + meta: false, + shift: false, + }) +} + +const getActiveModifiers = (state: State) => { + return _.clone(state('keyboardModifiers')) || _.clone(initialModifiers) +} + +const modifiersToString = (modifiers: keyboardModifiers) => { + return _.keys(_.pickBy(modifiers, (val) => { + return val + })).join(', ') +} + +const joinKeyArrayToString = (keyArr: KeyDetails[]) => { + return _.map(keyArr, (keyDetails) => { + if (keyDetails.text) return keyDetails.key + + return `{${keyDetails.key}}` + }).join('') +} + +// const isSpecialChar = (chars) => { +// return !!kb.specialChars[chars] +// } + +type modifierKeyDetails = KeyDetails & { + key: keyof typeof keyToModifierMap +} + +const isModifier = (details: KeyDetails): details is modifierKeyDetails => { + return !!keyToModifierMap[details.key] +} + +const countNumIndividualKeyStrokes = (keys: KeyDetails[]) => { + return _.countBy(keys, isModifier)['false'] +} + +const findKeyDetailsOrLowercase = (key):KeyDetailsPartial => { + const keymap = getKeymap() + const foundKey = keymap[key] + + if (foundKey) return foundKey + + return _.mapKeys(keymap, (val, key) => { + return _.toLower(key) + })[_.toLower(key)] +} + +const getKeyDetails = (onKeyNotFound) => { + return (key: string) => { + const foundKey = findKeyDetailsOrLowercase(key) + + if (foundKey) { + const details = _.defaults({}, foundKey, { + key: '', + keyCode: 0, + code: '', + text: '', + location: 0, + events: {}, + }) + + if (details.key.length === 1) { + details.text = details.key + } + + return details + } + + onKeyNotFound(key, _.keys(getKeymap()).join(', ')) + + throw Error(`Not a valid key: ${key}`) + } +} + +const hasModifierBesidesShift = (modifiers: keyboardModifiers) => { + return _.some(_.omit(modifiers, ['shift'])) +} + +// type shouldBe = (fn:K, a:T): re +// const shouldBeNumber = (a) => (_.isNumber(a) ? a : throwError()) +// const shouldBe = (fn: (t1)=>t1 is number, a)=>fn(a)?a:throwError() + +/** + * @example '{foo}' => 'foo' + */ +const parseCharsBetweenCurlyBraces = (chars: string) => { + return /{(.+?)}/.exec(chars)![1] +} + +const shouldIgnoreEvent = < + T extends KeyEventType, + K extends { [key in T]?: boolean } +>( + eventName: T, + options: K + ) => { + return options[eventName] === false +} + +const shouldUpdateValue = (el: HTMLElement, key: KeyDetails) => { + if (!key.text) return true + + const bounds = $selection.getSelectionBounds(el) + const noneSelected = bounds.start === bounds.end + + if ($elements.isInput(el) || $elements.isTextarea(el)) { + if ($elements.isReadOnlyInputOrTextarea(el)) { + return false + } + + if (noneSelected) { + // const ml = $elements.getNativeProp(el, 'maxLength') + const ml = el.maxLength + + //# maxlength is -1 by default when omitted + //# but could also be null or undefined :-/ + //# only care if we are trying to type a key + if (ml === 0 || ml > 0) { + //# 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)) { + return false + } + } + } + } + + return true +} + +const getKeymap = () => { + return { + ...keyboardMappings, + ...modifierChars, + ...USKeyboard, + } +} +const validateTyping = ( + el: HTMLElement, + chars: string, + onFail: Function, + skipCheckUntilIndex?: number +) => { + if (skipCheckUntilIndex) { + return { skipCheckUntilIndex: skipCheckUntilIndex-- } + } + + debug('validateTyping:', chars, el) + + // debug('validateTyping', el, chars) + const $el = $dom.wrap(el) + const numElements = $el.length + const isBody = $el.is('body') + const isTextLike = $dom.isTextLike(el) + let isDate = false + let isTime = false + let isMonth = false + let isWeek = false + let isDateTime = false + + if ($elements.isInput(el)) { + isDate = $dom.isType(el, 'date') + isTime = $dom.isType(el, 'time') + isMonth = $dom.isType(el, 'month') + isWeek = $dom.isType(el, 'week') + isDateTime = + $dom.isType(el, 'datetime') || $dom.isType(el, 'datetime-local') + } + + const isFocusable = $elements.isFocusable($el) + const isEmptyChars = _.isEmpty(chars) + const clearChars = '{selectall}{delete}' + const isClearChars = _.startsWith(chars.toLowerCase(), clearChars) + + //# TODO: tabindex can't be -1 + //# TODO: can't be readonly + + if (isBody) { + return {} + } + + if (!isFocusable && !isTextLike) { + const node = $dom.stringify($el) + + $utils.throwErrByPath('type.not_on_typeable_element', { + onFail, + args: { node }, + }) + } + + if (!isFocusable && isTextLike) { + const node = $dom.stringify($el) + + $utils.throwErrByPath('type.not_actionable_textlike', { + onFail, + args: { node }, + }) + } + + if (numElements > 1) { + $utils.throwErrByPath('type.multiple_elements', { + onFail, + args: { num: numElements }, + }) + } + + if (!(_.isString(chars) || _.isFinite(chars))) { + $utils.throwErrByPath('type.wrong_type', { + onFail, + args: { chars }, + }) + } + + if (isEmptyChars) { + $utils.throwErrByPath('type.empty_string', { onFail }) + } + + if (isClearChars) { + skipCheckUntilIndex = 2 // {selectAll}{del} is two keys + + return { skipCheckUntilIndex, isClearChars: true } + } + + if (isDate) { + let dateChars + + if ( + _.isString(chars) && + (dateChars = dateRegex.exec(chars)) !== null && + moment(dateChars[0]).isValid() + ) { + skipCheckUntilIndex = _getEndIndex(chars, dateChars[0]) + + return { skipCheckUntilIndex } + } + + $utils.throwErrByPath('type.invalid_date', { + onFail, + // set matched date or entire char string + args: { chars: dateChars ? dateChars[0] : chars }, + }) + } + + if (isMonth) { + let monthChars + + if (_.isString(chars) && (monthChars = monthRegex.exec(chars)) !== null) { + skipCheckUntilIndex = _getEndIndex(chars, monthChars[0]) + + return { skipCheckUntilIndex } + } + + $utils.throwErrByPath('type.invalid_month', { + onFail, + args: { chars }, + }) + } + + if (isWeek) { + let weekChars + + if (_.isString(chars) && (weekChars = weekRegex.exec(chars)) !== null) { + skipCheckUntilIndex = _getEndIndex(chars, weekChars[0]) + + return { skipCheckUntilIndex } + } + + $utils.throwErrByPath('type.invalid_week', { + onFail, + args: { chars }, + }) + } + + if (isTime) { + let timeChars + + if (_.isString(chars) && (timeChars = timeRegex.exec(chars)) !== null) { + skipCheckUntilIndex = _getEndIndex(chars, timeChars[0]) + + return { skipCheckUntilIndex } + } + + $utils.throwErrByPath('type.invalid_time', { + onFail, + args: { chars }, + }) + } + + if (isDateTime) { + let dateTimeChars + + if ( + _.isString(chars) && + (dateTimeChars = dateTimeRegex.exec(chars)) !== null + ) { + skipCheckUntilIndex = _getEndIndex(chars, dateTimeChars[0]) + + return { skipCheckUntilIndex } + } + + $utils.throwErrByPath('type.invalid_dateTime', { + onFail, + args: { chars }, + }) + } + + return {} +} + +function _getEndIndex (str, substr) { + return str.indexOf(substr) + substr.length +} + +// function _splitChars(chars, index) { +// return [chars.slice(0, index), chars.slice(index)] +// } + +// Simulated default actions for select few keys. +const simulatedDefaultKeyMap: { [key: string]: SimulatedDefault } = { + Enter: (el, key, options) => { + if ($elements.isContentEditable(el) || $elements.isTextarea(el)) { + $selection.replaceSelectionContents(el, '\n') + key.events.input = true + } else { + key.events.textInput = false + key.events.input = false + } + + options.onEnterPressed() + }, + Delete: (el, key) => { + if ($selection.isCollapsed(el)) { + //# if there's no text selected, delete the prev char + //# if deleted char, send the input event + key.events.input = $selection.deleteRightOfCursor(el) + + return + } + + //# text is selected, so delete the selection + //# contents and send the input event + $selection.deleteSelectionContents(el) + key.events.input = true + }, + Backspace: (el, key) => { + if ($selection.isCollapsed(el)) { + //# if there's no text selected, delete the prev char + //# if deleted char, send the input event + key.events.input = $selection.deleteLeftOfCursor(el) + + return + } + + //# text is selected, so delete the selection + //# contents and send the input event + $selection.deleteSelectionContents(el) + key.events.input = true + }, + ArrowLeft: (el) => { + return $selection.moveCursorLeft(el) + }, + ArrowRight: (el) => { + return $selection.moveCursorRight(el) + }, + + ArrowUp: (el) => { + return $selection.moveCursorUp(el) + }, + + ArrowDown: (el) => { + return $selection.moveCursorDown(el) + }, +} + +const modifierChars = { + alt: USKeyboard.Alt, + option: USKeyboard.Alt, + + ctrl: USKeyboard.Control, + control: USKeyboard.Control, + + meta: USKeyboard.Meta, + command: USKeyboard.Meta, + cmd: USKeyboard.Meta, + + shift: USKeyboard.Shift, +} + +const keyboardMappings: { [key: string]: KeyDetailsPartial } = { + selectall: { + key: 'selectAll', + simulatedDefault: (el) => { + const doc = $document.getDocumentFromElement(el) + + return $selection.selectAll(doc) + }, + simulatedDefaultOnly: true, + }, + movetostart: { + key: 'moveToStart', + simulatedDefault: (el) => { + const doc = $document.getDocumentFromElement(el) + + return $selection.moveSelectionToStart(doc) + }, + simulatedDefaultOnly: true, + }, + movetoend: { + key: 'moveToEnd', + simulatedDefault: (el) => { + const doc = $document.getDocumentFromElement(el) + + return $selection.moveSelectionToEnd(doc) + }, + simulatedDefaultOnly: true, + }, + + del: USKeyboard.Delete, + backspace: USKeyboard.Backspace, + esc: USKeyboard.Escape, + enter: USKeyboard.Enter, + rightarrow: USKeyboard.ArrowRight, + leftarrow: USKeyboard.ArrowLeft, + uparrow: USKeyboard.ArrowUp, + downarrow: USKeyboard.ArrowDown, + '{': USKeyboard.BracketLeft, +} + +const keyToModifierMap = { + Alt: 'alt', + Control: 'ctrl', + Meta: 'meta', + Shift: 'shift', +} + +export interface typeOptions { + $el: JQuery + chars: string + force?: boolean + simulated?: boolean + release?: boolean + _log?: any + delay?: number + onError?: Function + onEvent?: Function + onBeforeEvent?: Function + onFocusChange?: Function + onBeforeType?: Function + onAfterType?: Function + onValueChange?: Function + onEnterPressed?: Function + onNoMatchingSpecialChars?: Function + onBeforeSpecialCharAction?: Function +} + +export default class Keyboard { + public foo = 'foo' + + constructor (private state: State) {} + + type (opts: typeOptions) { + const options = _.defaults({}, opts, { + delay: 0, + force: false, + simulated: false, + onError: _.noop, + onEvent: _.noop, + onBeforeEvent: _.noop, + onFocusChange: _.noop, + onBeforeType: _.noop, + onAfterType: _.noop, + onValueChange: _.noop, + onEnterPressed: _.noop, + onNoMatchingSpecialChars: _.noop, + onBeforeSpecialCharAction: _.noop, + }) + + if (options.force) { + options.simulated = true + } + + debug('type:', options.chars, options) + + const el = options.$el.get(0) + const doc = $document.getDocumentFromElement(el) + + const keys = _.flatMap( + options.chars.split(charsBetweenCurlyBracesRe), + (chars) => { + if (charsBetweenCurlyBracesRe.test(chars)) { + //# allow special chars and modifiers to be case-insensitive + return parseCharsBetweenCurlyBraces(chars) //.toLowerCase() + } + + // ignore empty strings + return _.filter(_.split(chars, '')) + } + ) + + const keyDetailsArr = _.map( + keys, + getKeyDetails(options.onNoMatchingSpecialChars) + ) + + const numKeys = countNumIndividualKeyStrokes(keyDetailsArr) + + options.onBeforeType(numKeys) + + // # should make each keystroke async to mimic + //# how keystrokes come into javascript naturally + + // let prevElement = $elements.getActiveElByDocument(doc) + + const getActiveEl = (doc: Document) => { + if (options.force) { + return options.$el.get(0) + } + + const activeEl = $elements.getActiveElByDocument(doc) + + if (activeEl === null) { + return doc.body + } + + return activeEl + } + + let _skipCheckUntilIndex: number | undefined = 0 + + const typeKeyFns = _.map( + keyDetailsArr, + (key: KeyDetails, i: number) => { + return () => { + debug('typing key:', key.key) + + const activeEl = getActiveEl(doc) + + if (!_skipCheckUntilIndex) { + const { skipCheckUntilIndex, isClearChars } = validateTyping( + activeEl, + joinKeyArrayToString(keyDetailsArr.slice(i)), + options._log, + _skipCheckUntilIndex + ) + + if (skipCheckUntilIndex) { + + debug('skipCheckUntilIndex:', skipCheckUntilIndex) + } + + _skipCheckUntilIndex = skipCheckUntilIndex + if ( + _skipCheckUntilIndex + && $elements.isNeedSingleValueChangeInputElement(el) + ) { + const originalText = el.value + + debug('skip validate until:', _skipCheckUntilIndex) + const keysType = keyDetailsArr.slice(0, _skipCheckUntilIndex) + + _.each(keysType, (key) => { + key.simulatedDefaultOnly = true + key.simulatedDefault = _.noop + }) + + _.last(keysType)!.simulatedDefault = () => { + options.onValueChange(originalText, el) + + const valToSet = isClearChars ? '' : joinKeyArrayToString(keysType) + + debug('setting element value', valToSet, el) + + return $elements.setNativeProp( + el as HTMLTextLikeInputElement, + 'value', + valToSet + ) + } + } + } + + if (key.simulatedDefaultOnly && key.simulatedDefault) { + key.simulatedDefault(activeEl, key, options) + + return null + } + + this.typeSimulatedKey(activeEl, key, options) + debug('returning null') + + return null + + } + } + ) + + const modifierKeys = _.filter(keyDetailsArr, (key) => { + return isModifier(key) + }) + + if (options.simulated && !options.delay) { + _.each(typeKeyFns, (fn) => { + return fn() + }) + + if (options.release !== false) { + _.each(modifierKeys, (key) => { + return this.simulatedKeyup(getActiveEl(doc), key, options) + }) + } + + options.onAfterType() + + return + } + + return Promise.each(typeKeyFns, (fn) => { + return Promise.try(() => { + return fn() + }).delay(options.delay) + }) + .then(() => { + if (options.release !== false) { + return Promise.map(modifierKeys, (key) => { + return this.simulatedKeyup(getActiveEl(doc), key, options) + }) + } + + return [] + }) + .then(() => { + options.onAfterType() + }) + } + + fireSimulatedEvent ( + el: HTMLElement, + eventType: KeyEventType, + keyDetails: KeyDetails, + opts: { + onBeforeEvent?: (...args) => boolean + onEvent?: (...args) => boolean + id: string + } + ) { + debug('fireSimulatedEvent', eventType, keyDetails) + const options = _.defaults(opts, { + onBeforeEvent: _.noop, + onEvent: _.noop, + }) + + const win = $window.getWindowByElement(el) + const text = keyDetails.text + let charCode: number | undefined + let keyCode: number | undefined + let which: number | undefined + let data: string | undefined + let location: number | undefined = keyDetails.location || 0 + let key: string | undefined + let code: string | undefined = keyDetails.code + let eventConstructor = 'KeyboardEvent' + let cancelable = true + + let addModifiers = true + + switch (eventType) { + case 'keydown': + case 'keyup': { + keyCode = keyDetails.keyCode + which = keyDetails.keyCode + key = keyDetails.key + charCode = 0 + break + } + + case 'keypress': { + const charCodeAt = keyDetails.text.charCodeAt(0) + + charCode = charCodeAt + keyCode = charCodeAt + which = charCodeAt + key = keyDetails.key + break + } + + case 'textInput': // lowercase in IE11 + eventConstructor = 'TextEvent' + addModifiers = false + charCode = 0 + keyCode = 0 + which = 0 + location = undefined + data = text + break + + case 'input': + eventConstructor = 'InputEvent' + addModifiers = false + data = text + location = undefined + cancelable = false + break + default: { + throw new Error(`Invalid event: ${eventType}`) + } + } + + let eventOptions: EventInit & { + view?: Window + data?: string + repeat?: boolean + } = {} + + if (addModifiers) { + const modifierEventOptions = toModifiersEventOptions(getActiveModifiers(this.state)) + + eventOptions = { + ...eventOptions, + ...modifierEventOptions, + repeat: false, + } + } + + eventOptions = { + ...eventOptions, + ..._.omitBy( + { + bubbles: true, + cancelable, + key, + code, + charCode, + location, + keyCode, + which, + data, + detail: 0, + view: win, + }, + _.isUndefined + ), + } + + let event: Event + + debug('event options:', eventType, eventOptions) + if (eventConstructor === 'TextEvent') { + event = document.createEvent('TextEvent') + // @ts-ignore + event.initTextEvent( + eventType, + eventOptions.bubbles, + eventOptions.cancelable, + eventOptions.view, + eventOptions.data, + 1 + // eventOptions.locale + ) + /*1: IE11 Input method param*/ + // event.initEvent(eventType) + + // or is IE + } else { + event = new win[eventConstructor](eventType, eventOptions) + } + + const dispatched = el.dispatchEvent(event) + + debug(`dispatched [${eventType}] on ${el}`) + options.onEvent(options.id, key, eventType, which, dispatched) + + return dispatched + } + + // foo('a')(1) + + // const maybeFireSimulatedEvent:Curry = (shouldFire:boolean | undefined, fireSimulatedEvent) => { + + getModifierKeyDetails (key: KeyDetails) { + const modifiers = getActiveModifiers(this.state) + const details = { ...key, modifiers: getModifiersValue(modifiers) } + + if (modifiers.shift && details.shiftKey) { + details.key = details.shiftKey + } + + if (modifiers.shift && details.shiftKeyCode) { + details.keyCode = details.shiftKeyCode + } + + if (modifiers.shift && details.shiftText) { + details.text = details.shiftText + } + + // If any modifier besides shift is pressed, no text. + if (hasModifierBesidesShift(modifiers)) { + details.text = '' + } + + return details + } + + flagModifier (key: modifierKeyDetails, setTo = true) { + debug('handleModifier', key.key) + const modifier = keyToModifierMap[key.key] + + //# do nothing if already activated + if (!!getActiveModifiers(this.state)[modifier] === setTo) { + return false + } + + const _activeModifiers = getActiveModifiers(this.state) + + _activeModifiers[modifier] = setTo + + this.state('keyboardModifiers', _activeModifiers) + + return true + } + + simulatedKeydown (el: HTMLElement, _key: KeyDetails, options: any) { + if (isModifier(_key)) { + const didFlag = this.flagModifier(_key) + + if (!didFlag) { + return null + } + + _key.events.keyup = false + } + + const key = this.getModifierKeyDetails(_key) + + if (!key.text) { + key.events.input = false + key.events.keypress = false + key.events.textInput = false + } + + let elToType + + options.id = _.uniqueId('char') + + debug( + 'typeSimulatedKey options:', + _.pick(options, ['keydown', 'keypress', 'textInput', 'input', 'id']) + ) + + if ( + shouldIgnoreEvent('keydown', key.events) || + this.fireSimulatedEvent(el, 'keydown', key, options) + ) { + elToType = this.getActiveEl(options) + + if (key.key === 'Enter' && $elements.isInput(elToType)) { + key.events.textInput = false + } + + if ($elements.isReadOnlyInputOrTextarea(elToType)) { + key.events.textInput = false + } + + if ( + shouldIgnoreEvent('keypress', key.events) || + this.fireSimulatedEvent(elToType, 'keypress', key, options) + ) { + if ( + shouldIgnoreEvent('textInput', key.events) || + this.fireSimulatedEvent(elToType, 'textInput', key, options) + ) { + return this.performSimulatedDefault(elToType, key, options) + } + } + } + } + + typeSimulatedKey (el: HTMLElement, key: KeyDetails, options) { + debug('typeSimulatedKey', key.key, el) + _.defaults(options, { + prevText: null, + }) + + const isFocusable = $elements.isFocusable($dom.wrap(el)) + const isTextLike = $elements.isTextLike(el) + + const isTypeableButNotTextLike = !isTextLike && isFocusable + + if (isTypeableButNotTextLike) { + key.events.input = false + key.events.textInput = false + } + + this.simulatedKeydown(el, key, options) + const elToKeyup = this.getActiveEl(options) + + this.simulatedKeyup(elToKeyup, key, options) + } + + simulatedKeyup (el: HTMLElement, _key: KeyDetails, options: any) { + if (shouldIgnoreEvent('keyup', _key.events)) { + debug('simulatedKeyup: ignoring event') + delete _key.events.keyup + + return + } + + if (isModifier(_key)) { + this.flagModifier(_key, false) + } + + const key = this.getModifierKeyDetails(_key) + + this.fireSimulatedEvent(el, 'keyup', key, options) + } + + getSimulatedDefaultForKey (key: KeyDetails) { + debug('getSimulatedDefaultForKey', key.key) + if (key.simulatedDefault) return key.simulatedDefault + + let nonShiftModifierPressed = hasModifierBesidesShift(getActiveModifiers(this.state)) + + debug({ nonShiftModifierPressed, key }) + if (!nonShiftModifierPressed && simulatedDefaultKeyMap[key.key]) { + return simulatedDefaultKeyMap[key.key] + } + + return (el: HTMLElement) => { + debug('replaceSelectionContents') + + if (!shouldUpdateValue(el, key)) { + debug('skip typing key', false) + key.events.input = false + + return + } + + // noop if not in a text-editable + const ret = $selection.replaceSelectionContents(el, key.text) + + debug('replaceSelectionContents:', key.text, ret) + } + } + + getActiveEl (options) { + const el = options.$el.get(0) + + if (options.force) { + return el + } + + const doc = $document.getDocumentFromElement(el) + + let activeEl = $elements.getActiveElByDocument(doc) + + // TODO: validate activeElement isn't null (aka body is active) + if (activeEl === null) { + activeEl = doc.body + } + + return activeEl + } + + performSimulatedDefault (el: HTMLElement, key: KeyDetails, options: any) { + debug('performSimulatedDefault', key.key) + const simulatedDefault = this.getSimulatedDefaultForKey(key) + + // const isBody = $elements.isBody(el) + // const isFocusable = $elements.isFocusable($dom.wrap(el)) + // const isTextLike = $elements.isTextLike(el) + + // const isTypeableButNotTextLike = !isTextLike && isFocusable + + if ($elements.isTextLike(el)) { + if ($elements.isInput(el) || $elements.isTextarea(el)) { + const curText = $elements.getNativeProp(el, 'value') + + simulatedDefault(el, key, options) + if (key.events.input !== false) { + options.onValueChange(curText, el) + } else { + // key.events.textInput = false + } + } else { + // el is contenteditable + simulatedDefault(el, key, options) + } + + shouldIgnoreEvent('input', key.events) || + this.fireSimulatedEvent(el, 'input', key, options) + + return + } + + return simulatedDefault(el, key, options) + } + + static toModifiersEventOptions = toModifiersEventOptions + static getActiveModifiers = getActiveModifiers + static modifierChars = modifierChars + static modifiersToString = modifiersToString + static fromModifierEventOptions = fromModifierEventOptions + static validateTyping = validateTyping + static getKeymap = getKeymap + static keyboardMappings = keyboardMappings +} diff --git a/packages/driver/src/cy/retries.coffee b/packages/driver/src/cy/retries.coffee index 33f6fe346f29..f6ea583e9b76 100644 --- a/packages/driver/src/cy/retries.coffee +++ b/packages/driver/src/cy/retries.coffee @@ -6,6 +6,9 @@ $utils = require("../cypress/utils") create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) -> return { retry: (fn, options, log) -> + if options.error + if !options.error.message.includes('coordsHistory must be') + console.error(options.error) ## remove the runnables timeout because we are now in retry ## mode and should be handling timing out ourselves and dont ## want to accidentally time out via mocha @@ -52,7 +55,6 @@ create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) - _.get(err, 'displayMessage') or _.get(err, 'message') or err - $utils.throwErrByPath "miscellaneous.retry_timed_out", { onFail: (options.onFail or log) args: { error: getErrMessage(options.error) } diff --git a/packages/driver/src/cypress.coffee b/packages/driver/src/cypress.coffee index 6c6d36eb52ce..40398abf656c 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("./cy/keyboard") +$Keyboard = require("./cy/keyboard").default $Log = require("./cypress/log") $Location = require("./cypress/location") $LocalStorage = require("./cypress/local_storage") diff --git a/packages/driver/src/cypress/UsKeyboardLayout.js b/packages/driver/src/cypress/UsKeyboardLayout.js new file mode 100644 index 000000000000..aef6799280e6 --- /dev/null +++ b/packages/driver/src/cypress/UsKeyboardLayout.js @@ -0,0 +1,389 @@ +module.exports = { + keyboardMappings: { + '0': { keyCode: 48, key: '0', code: 'Digit0' }, + '1': { keyCode: 49, key: '1', code: 'Digit1' }, + '2': { keyCode: 50, key: '2', code: 'Digit2' }, + '3': { keyCode: 51, key: '3', code: 'Digit3' }, + '4': { keyCode: 52, key: '4', code: 'Digit4' }, + '5': { keyCode: 53, key: '5', code: 'Digit5' }, + '6': { keyCode: 54, key: '6', code: 'Digit6' }, + '7': { keyCode: 55, key: '7', code: 'Digit7' }, + '8': { keyCode: 56, key: '8', code: 'Digit8' }, + '9': { keyCode: 57, key: '9', code: 'Digit9' }, + Power: { key: 'Power', code: 'Power' }, + Eject: { key: 'Eject', code: 'Eject' }, + Abort: { keyCode: 3, code: 'Abort', key: 'Cancel' }, + Help: { keyCode: 6, code: 'Help', key: 'Help' }, + Backspace: { keyCode: 8, code: 'Backspace', key: 'Backspace' }, + Tab: { keyCode: 9, code: 'Tab', key: 'Tab' }, + Numpad5: { + keyCode: 12, + shiftKeyCode: 101, + key: 'Clear', + code: 'Numpad5', + shiftKey: '5', + location: 3, + }, + NumpadEnter: { + keyCode: 13, + code: 'NumpadEnter', + key: 'Enter', + text: '\r', + location: 3, + }, + Enter: { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' }, + '\r': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' }, + '\n': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' }, + ShiftLeft: { keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1 }, + ShiftRight: { keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2 }, + ControlLeft: { + keyCode: 17, + code: 'ControlLeft', + key: 'Control', + location: 1, + }, + ControlRight: { + keyCode: 17, + code: 'ControlRight', + key: 'Control', + location: 2, + }, + AltLeft: { keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1 }, + AltRight: { keyCode: 18, code: 'AltRight', key: 'Alt', location: 2 }, + Pause: { keyCode: 19, code: 'Pause', key: 'Pause' }, + CapsLock: { keyCode: 20, code: 'CapsLock', key: 'CapsLock' }, + Escape: { keyCode: 27, code: 'Escape', key: 'Escape' }, + Convert: { keyCode: 28, code: 'Convert', key: 'Convert' }, + NonConvert: { keyCode: 29, code: 'NonConvert', key: 'NonConvert' }, + Space: { keyCode: 32, code: 'Space', key: ' ' }, + Numpad9: { + keyCode: 33, + shiftKeyCode: 105, + key: 'PageUp', + code: 'Numpad9', + shiftKey: '9', + location: 3, + }, + PageUp: { keyCode: 33, code: 'PageUp', key: 'PageUp' }, + Numpad3: { + keyCode: 34, + shiftKeyCode: 99, + key: 'PageDown', + code: 'Numpad3', + shiftKey: '3', + location: 3, + }, + PageDown: { keyCode: 34, code: 'PageDown', key: 'PageDown' }, + End: { keyCode: 35, code: 'End', key: 'End' }, + Numpad1: { + keyCode: 35, + shiftKeyCode: 97, + key: 'End', + code: 'Numpad1', + shiftKey: '1', + location: 3, + }, + Home: { keyCode: 36, code: 'Home', key: 'Home' }, + Numpad7: { + keyCode: 36, + shiftKeyCode: 103, + key: 'Home', + code: 'Numpad7', + shiftKey: '7', + location: 3, + }, + ArrowLeft: { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' }, + Numpad4: { + keyCode: 37, + shiftKeyCode: 100, + key: 'ArrowLeft', + code: 'Numpad4', + shiftKey: '4', + location: 3, + }, + Numpad8: { + keyCode: 38, + shiftKeyCode: 104, + key: 'ArrowUp', + code: 'Numpad8', + shiftKey: '8', + location: 3, + }, + ArrowUp: { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' }, + ArrowRight: { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' }, + Numpad6: { + keyCode: 39, + shiftKeyCode: 102, + key: 'ArrowRight', + code: 'Numpad6', + shiftKey: '6', + location: 3, + }, + Numpad2: { + keyCode: 40, + shiftKeyCode: 98, + key: 'ArrowDown', + code: 'Numpad2', + shiftKey: '2', + location: 3, + }, + ArrowDown: { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' }, + Select: { keyCode: 41, code: 'Select', key: 'Select' }, + Open: { keyCode: 43, code: 'Open', key: 'Execute' }, + PrintScreen: { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' }, + Insert: { keyCode: 45, code: 'Insert', key: 'Insert' }, + Numpad0: { + keyCode: 45, + shiftKeyCode: 96, + key: 'Insert', + code: 'Numpad0', + shiftKey: '0', + location: 3, + }, + Delete: { keyCode: 46, code: 'Delete', key: 'Delete' }, + NumpadDecimal: { + keyCode: 46, + shiftKeyCode: 110, + code: 'NumpadDecimal', + key: '\u0000', + shiftKey: '.', + location: 3, + }, + Digit0: { keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0' }, + Digit1: { keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1' }, + Digit2: { keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2' }, + Digit3: { keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3' }, + Digit4: { keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4' }, + Digit5: { keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5' }, + Digit6: { keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6' }, + Digit7: { keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7' }, + Digit8: { keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8' }, + Digit9: { keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9' }, + KeyA: { keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a' }, + KeyB: { keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b' }, + KeyC: { keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c' }, + KeyD: { keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd' }, + KeyE: { keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e' }, + KeyF: { keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f' }, + KeyG: { keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g' }, + KeyH: { keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h' }, + KeyI: { keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i' }, + KeyJ: { keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j' }, + KeyK: { keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k' }, + KeyL: { keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l' }, + KeyM: { keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm' }, + KeyN: { keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n' }, + KeyO: { keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o' }, + KeyP: { keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p' }, + KeyQ: { keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q' }, + KeyR: { keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r' }, + KeyS: { keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's' }, + KeyT: { keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't' }, + KeyU: { keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u' }, + KeyV: { keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v' }, + KeyW: { keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w' }, + KeyX: { keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x' }, + KeyY: { keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y' }, + KeyZ: { keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z' }, + MetaLeft: { keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1 }, + MetaRight: { keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2 }, + ContextMenu: { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' }, + NumpadMultiply: { + keyCode: 106, + code: 'NumpadMultiply', + key: '*', + location: 3, + }, + NumpadAdd: { keyCode: 107, code: 'NumpadAdd', key: '+', location: 3 }, + NumpadSubtract: { + keyCode: 109, + code: 'NumpadSubtract', + key: '-', + location: 3, + }, + NumpadDivide: { keyCode: 111, code: 'NumpadDivide', key: '/', location: 3 }, + F1: { keyCode: 112, code: 'F1', key: 'F1' }, + F2: { keyCode: 113, code: 'F2', key: 'F2' }, + F3: { keyCode: 114, code: 'F3', key: 'F3' }, + F4: { keyCode: 115, code: 'F4', key: 'F4' }, + F5: { keyCode: 116, code: 'F5', key: 'F5' }, + F6: { keyCode: 117, code: 'F6', key: 'F6' }, + F7: { keyCode: 118, code: 'F7', key: 'F7' }, + F8: { keyCode: 119, code: 'F8', key: 'F8' }, + F9: { keyCode: 120, code: 'F9', key: 'F9' }, + F10: { keyCode: 121, code: 'F10', key: 'F10' }, + F11: { keyCode: 122, code: 'F11', key: 'F11' }, + F12: { keyCode: 123, code: 'F12', key: 'F12' }, + F13: { keyCode: 124, code: 'F13', key: 'F13' }, + F14: { keyCode: 125, code: 'F14', key: 'F14' }, + F15: { keyCode: 126, code: 'F15', key: 'F15' }, + F16: { keyCode: 127, code: 'F16', key: 'F16' }, + F17: { keyCode: 128, code: 'F17', key: 'F17' }, + F18: { keyCode: 129, code: 'F18', key: 'F18' }, + F19: { keyCode: 130, code: 'F19', key: 'F19' }, + F20: { keyCode: 131, code: 'F20', key: 'F20' }, + F21: { keyCode: 132, code: 'F21', key: 'F21' }, + F22: { keyCode: 133, code: 'F22', key: 'F22' }, + F23: { keyCode: 134, code: 'F23', key: 'F23' }, + F24: { keyCode: 135, code: 'F24', key: 'F24' }, + NumLock: { keyCode: 144, code: 'NumLock', key: 'NumLock' }, + ScrollLock: { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' }, + AudioVolumeMute: { + keyCode: 173, + code: 'AudioVolumeMute', + key: 'AudioVolumeMute', + }, + AudioVolumeDown: { + keyCode: 174, + code: 'AudioVolumeDown', + key: 'AudioVolumeDown', + }, + AudioVolumeUp: { + keyCode: 175, + code: 'AudioVolumeUp', + key: 'AudioVolumeUp', + }, + MediaTrackNext: { + keyCode: 176, + code: 'MediaTrackNext', + key: 'MediaTrackNext', + }, + MediaTrackPrevious: { + keyCode: 177, + code: 'MediaTrackPrevious', + key: 'MediaTrackPrevious', + }, + MediaStop: { keyCode: 178, code: 'MediaStop', key: 'MediaStop' }, + MediaPlayPause: { + keyCode: 179, + code: 'MediaPlayPause', + key: 'MediaPlayPause', + }, + Semicolon: { keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';' }, + Equal: { keyCode: 187, code: 'Equal', shiftKey: '+', key: '=' }, + NumpadEqual: { keyCode: 187, code: 'NumpadEqual', key: '=', location: 3 }, + Comma: { keyCode: 188, code: 'Comma', shiftKey: '<', key: ',' }, + Minus: { keyCode: 189, code: 'Minus', shiftKey: '_', key: '-' }, + Period: { keyCode: 190, code: 'Period', shiftKey: '>', key: '.' }, + Slash: { keyCode: 191, code: 'Slash', shiftKey: '?', key: '/' }, + Backquote: { keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`' }, + BracketLeft: { keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '[' }, + Backslash: { keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\' }, + BracketRight: { + keyCode: 221, + code: 'BracketRight', + shiftKey: '}', + key: ']', + }, + Quote: { keyCode: 222, code: 'Quote', shiftKey: '"', key: '\'' }, + AltGraph: { keyCode: 225, code: 'AltGraph', key: 'AltGraph' }, + Props: { keyCode: 247, code: 'Props', key: 'CrSel' }, + Cancel: { keyCode: 3, key: 'Cancel', code: 'Abort' }, + Clear: { keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3 }, + Shift: { keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1 }, + Control: { keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1 }, + Alt: { keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1 }, + Accept: { keyCode: 30, key: 'Accept' }, + ModeChange: { keyCode: 31, key: 'ModeChange' }, + ' ': { keyCode: 32, key: ' ', code: 'Space' }, + Print: { keyCode: 42, key: 'Print' }, + Execute: { keyCode: 43, key: 'Execute', code: 'Open' }, + '\u0000': { + keyCode: 46, + key: '\u0000', + code: 'NumpadDecimal', + location: 3, + }, + a: { keyCode: 65, key: 'a', code: 'KeyA' }, + b: { keyCode: 66, key: 'b', code: 'KeyB' }, + c: { keyCode: 67, key: 'c', code: 'KeyC' }, + d: { keyCode: 68, key: 'd', code: 'KeyD' }, + e: { keyCode: 69, key: 'e', code: 'KeyE' }, + f: { keyCode: 70, key: 'f', code: 'KeyF' }, + g: { keyCode: 71, key: 'g', code: 'KeyG' }, + h: { keyCode: 72, key: 'h', code: 'KeyH' }, + i: { keyCode: 73, key: 'i', code: 'KeyI' }, + j: { keyCode: 74, key: 'j', code: 'KeyJ' }, + k: { keyCode: 75, key: 'k', code: 'KeyK' }, + l: { keyCode: 76, key: 'l', code: 'KeyL' }, + m: { keyCode: 77, key: 'm', code: 'KeyM' }, + n: { keyCode: 78, key: 'n', code: 'KeyN' }, + o: { keyCode: 79, key: 'o', code: 'KeyO' }, + p: { keyCode: 80, key: 'p', code: 'KeyP' }, + q: { keyCode: 81, key: 'q', code: 'KeyQ' }, + r: { keyCode: 82, key: 'r', code: 'KeyR' }, + s: { keyCode: 83, key: 's', code: 'KeyS' }, + t: { keyCode: 84, key: 't', code: 'KeyT' }, + u: { keyCode: 85, key: 'u', code: 'KeyU' }, + v: { keyCode: 86, key: 'v', code: 'KeyV' }, + w: { keyCode: 87, key: 'w', code: 'KeyW' }, + x: { keyCode: 88, key: 'x', code: 'KeyX' }, + y: { keyCode: 89, key: 'y', code: 'KeyY' }, + z: { keyCode: 90, key: 'z', code: 'KeyZ' }, + Meta: { keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1 }, + '*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 }, + '+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 }, + '-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 }, + '/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 }, + ';': { keyCode: 186, key: ';', code: 'Semicolon' }, + '=': { keyCode: 187, key: '=', code: 'Equal' }, + ',': { keyCode: 188, key: ',', code: 'Comma' }, + '.': { keyCode: 190, key: '.', code: 'Period' }, + '`': { keyCode: 192, key: '`', code: 'Backquote' }, + '[': { keyCode: 219, key: '[', code: 'BracketLeft' }, + '\\': { keyCode: 220, key: '\\', code: 'Backslash' }, + ']': { keyCode: 221, key: ']', code: 'BracketRight' }, + '\'': { keyCode: 222, key: '\'', code: 'Quote' }, + Attn: { keyCode: 246, key: 'Attn' }, + CrSel: { keyCode: 247, key: 'CrSel', code: 'Props' }, + ExSel: { keyCode: 248, key: 'ExSel' }, + EraseEof: { keyCode: 249, key: 'EraseEof' }, + Play: { keyCode: 250, key: 'Play' }, + ZoomOut: { keyCode: 251, key: 'ZoomOut' }, + ')': { keyCode: 48, key: ')', code: 'Digit0' }, + '!': { keyCode: 49, key: '!', code: 'Digit1' }, + '@': { keyCode: 50, key: '@', code: 'Digit2' }, + '#': { keyCode: 51, key: '#', code: 'Digit3' }, + $: { keyCode: 52, key: '$', code: 'Digit4' }, + '%': { keyCode: 53, key: '%', code: 'Digit5' }, + '^': { keyCode: 54, key: '^', code: 'Digit6' }, + '&': { keyCode: 55, key: '&', code: 'Digit7' }, + '(': { keyCode: 57, key: '(', code: 'Digit9' }, + A: { keyCode: 65, key: 'A', code: 'KeyA' }, + B: { keyCode: 66, key: 'B', code: 'KeyB' }, + C: { keyCode: 67, key: 'C', code: 'KeyC' }, + D: { keyCode: 68, key: 'D', code: 'KeyD' }, + E: { keyCode: 69, key: 'E', code: 'KeyE' }, + F: { keyCode: 70, key: 'F', code: 'KeyF' }, + G: { keyCode: 71, key: 'G', code: 'KeyG' }, + H: { keyCode: 72, key: 'H', code: 'KeyH' }, + I: { keyCode: 73, key: 'I', code: 'KeyI' }, + J: { keyCode: 74, key: 'J', code: 'KeyJ' }, + K: { keyCode: 75, key: 'K', code: 'KeyK' }, + L: { keyCode: 76, key: 'L', code: 'KeyL' }, + M: { keyCode: 77, key: 'M', code: 'KeyM' }, + N: { keyCode: 78, key: 'N', code: 'KeyN' }, + O: { keyCode: 79, key: 'O', code: 'KeyO' }, + P: { keyCode: 80, key: 'P', code: 'KeyP' }, + Q: { keyCode: 81, key: 'Q', code: 'KeyQ' }, + R: { keyCode: 82, key: 'R', code: 'KeyR' }, + S: { keyCode: 83, key: 'S', code: 'KeyS' }, + T: { keyCode: 84, key: 'T', code: 'KeyT' }, + U: { keyCode: 85, key: 'U', code: 'KeyU' }, + V: { keyCode: 86, key: 'V', code: 'KeyV' }, + W: { keyCode: 87, key: 'W', code: 'KeyW' }, + X: { keyCode: 88, key: 'X', code: 'KeyX' }, + Y: { keyCode: 89, key: 'Y', code: 'KeyY' }, + Z: { keyCode: 90, key: 'Z', code: 'KeyZ' }, + ':': { keyCode: 186, key: ':', code: 'Semicolon' }, + '<': { keyCode: 188, key: '<', code: 'Comma' }, + _: { keyCode: 189, key: '_', code: 'Minus' }, + '>': { keyCode: 190, key: '>', code: 'Period' }, + '?': { keyCode: 191, key: '?', code: 'Slash' }, + '~': { keyCode: 192, key: '~', code: 'Backquote' }, + '{': { keyCode: 219, key: '{', code: 'BracketLeft' }, + '|': { keyCode: 220, key: '|', code: 'Backslash' }, + '}': { keyCode: 221, key: '}', code: 'BracketRight' }, + '"': { keyCode: 222, key: '"', code: 'Quote' }, + }, +} diff --git a/packages/driver/src/cypress/cy.coffee b/packages/driver/src/cypress/cy.coffee index 5e990e851e95..57578aa8fd8e 100644 --- a/packages/driver/src/cypress/cy.coffee +++ b/packages/driver/src/cypress/cy.coffee @@ -14,7 +14,7 @@ $Errors = require("../cy/errors") $Ensures = require("../cy/ensures") $Focused = require("../cy/focused") $Mouse = require("../cy/mouse") -$Keyboard = require("../cy/keyboard") +$Keyboard = require("../cy/keyboard").default $Location = require("../cy/location") $Assertions = require("../cy/assertions") $Listeners = require("../cy/listeners") @@ -82,7 +82,7 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> jquery = $jQuery.create(state) location = $Location.create(state) focused = $Focused.create(state) - keyboard = $Keyboard.create(state) + keyboard = new $Keyboard(state) mouse = $Mouse.create(state, focused) timers = $Timers.create() @@ -156,19 +156,19 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> }) contentWindow.HTMLElement.prototype.focus = (focusOption) -> - focused.interceptFocus(this, contentWindow, focusOption) + focused.interceptFocus(@, contentWindow, focusOption) contentWindow.HTMLElement.prototype.blur = -> - focused.interceptBlur(this) + focused.interceptBlur(@) contentWindow.SVGElement.prototype.focus = (focusOption) -> - focused.interceptFocus(this, contentWindow, focusOption) + focused.interceptFocus(@, contentWindow, focusOption) contentWindow.SVGElement.prototype.blur = -> - focused.interceptBlur(this) + focused.interceptBlur(@) contentWindow.HTMLInputElement.prototype.select = -> - $selection.interceptSelect.call(this) + $selection.interceptSelect.call(@) contentWindow.document.hasFocus = -> focused.documentHasFocus.call(@) @@ -680,6 +680,7 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> ensureElDoesNotHaveCSS: ensures.ensureElDoesNotHaveCSS ensureVisibility: ensures.ensureVisibility ensureDescendents: ensures.ensureDescendents + ensureNotReadonly: ensures.ensureNotReadonly ensureReceivability: ensures.ensureReceivability ensureValidPosition: ensures.ensureValidPosition ensureScrollability: ensures.ensureScrollability diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index fe980c77429f..93f8eac0dd42 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -209,6 +209,15 @@ module.exports = { https://on.cypress.io/element-cannot-be-interacted-with """ + readonly: """ + #{cmd('{{cmd}}')} failed because this element is readonly: + + {{node}} + + Fix this problem, or use {force: true} to disable error checking. + + https://on.cypress.io/element-cannot-be-interacted-with + """ invalid_position_argument: "Invalid position argument: '{{position}}'. Position may only be {{validPositions}}." not_scrollable: """ #{cmd('{{cmd}}')} failed because this element is not scrollable:\n @@ -915,6 +924,7 @@ module.exports = { invalid_month: "Typing into a month input with #{cmd('type')} requires a valid month with the format 'yyyy-MM'. You passed: {{chars}}" invalid_week: "Typing into a week input with #{cmd('type')} requires a valid week with the format 'yyyy-Www', where W is the literal character 'W' and ww is the week number (00-53). You passed: {{chars}}" invalid_time: "Typing into a time input with #{cmd('type')} requires a valid time with the format 'HH:mm', 'HH:mm:ss' or 'HH:mm:ss.SSS', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: {{chars}}" + invalid_dateTime: "Typing into a datetime input with #{cmd('type')} requires a valid datetime with the format 'yyyy-MM-ddThh:mm', for example '2017-06-01T08:30'. You passed: {{chars}}" multiple_elements: "#{cmd('type')} can only be called on a single element. Your subject contained {{num}} elements." not_on_typeable_element: """ #{cmd('type')} failed because it requires a valid typeable element. @@ -923,7 +933,16 @@ module.exports = { > {{node}} - Cypress considers the 'body', 'textarea', any 'element' with a 'tabindex' or 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid typeable elements. + Cypress considers the 'body', 'textarea', any 'element' with a 'tabindex' or 'contenteditable' attribute, any focusable element, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid typeable elements. + """ + not_actionable_textlike: """ + #{cmd('type')} failed because it targeted a disabled element. + + The element typed into was: + + > {{node}} + + You should ensure the element does not have an attribute named 'disabled' before typing into it. """ tab: "{tab} isn't a supported character sequence. You'll want to use the command #{cmd('tab')}, which is not ready yet, but when it is done that's what you'll use." wrong_type: "#{cmd('type')} can only accept a String or Number. You passed in: '{{chars}}'" diff --git a/packages/driver/src/cypress/setter_getter.d.ts b/packages/driver/src/cypress/setter_getter.d.ts new file mode 100644 index 000000000000..25ed79548e19 --- /dev/null +++ b/packages/driver/src/cypress/setter_getter.d.ts @@ -0,0 +1,4 @@ +type SetterGetter = { + (key: K): T + (key: K, value: T): T +} diff --git a/packages/driver/src/cypress/utils.coffee b/packages/driver/src/cypress/utils.coffee index 1baf13b13d9d..dd2a60307793 100644 --- a/packages/driver/src/cypress/utils.coffee +++ b/packages/driver/src/cypress/utils.coffee @@ -4,6 +4,8 @@ methods = require("methods") moment = require("moment") Promise = require("bluebird") +UsKeyboardLayout = require('../cypress/UsKeyboardLayout') + $jquery = require("../dom/jquery") $Location = require("./location") $errorMessages = require("./error_messages") @@ -371,6 +373,8 @@ module.exports = { run(0) + keyboardMappings: UsKeyboardLayout.keyboardMappings + memoize: (func, cacheInstance = new Map()) -> memoized = (args...) -> key = args[0] diff --git a/packages/driver/src/dom/document.js b/packages/driver/src/dom/document.ts similarity index 69% rename from packages/driver/src/dom/document.js rename to packages/driver/src/dom/document.ts index 7ba4b493e5e7..54f8575086fa 100644 --- a/packages/driver/src/dom/document.js +++ b/packages/driver/src/dom/document.ts @@ -2,7 +2,8 @@ const $jquery = require('./jquery') const docNode = window.Node.DOCUMENT_NODE -const isDocument = (obj) => { +//TODO: make this not allow jquery +const isDocument = (obj:HTMLElement | Document): obj is Document => { try { if ($jquery.isJquery(obj)) { obj = obj[0] @@ -19,15 +20,15 @@ const hasActiveWindow = (doc) => { return !!doc.defaultView } -const getDocumentFromElement = (el) => { +const getDocumentFromElement = (el:HTMLElement):Document => { if (isDocument(el)) { return el } - return el.ownerDocument + return el.ownerDocument as Document } -module.exports = { +export { isDocument, hasActiveWindow, diff --git a/packages/driver/src/dom/elements.js b/packages/driver/src/dom/elements.ts similarity index 76% rename from packages/driver/src/dom/elements.js rename to packages/driver/src/dom/elements.ts index 6ffa2ace842f..b5946d065811 100644 --- a/packages/driver/src/dom/elements.js +++ b/packages/driver/src/dom/elements.ts @@ -1,9 +1,11 @@ -const _ = require('lodash') -const $ = require('jquery') -const $jquery = require('./jquery') -const $window = require('./window') -const $document = require('./document') -const $utils = require('../cypress/utils') +// NOT patched jquery +import $ from 'jquery' +import * as $jquery from './jquery' +import * as $window from './window' +import * as $document from './document' +import $utils from '../cypress/utils.coffee' + +import _ from '../config/lodash' const fixedOrStickyRe = /(fixed|sticky)/ @@ -18,15 +20,66 @@ const focusable = [ '[tabindex]', '[contentEditable]', ] -const inputTypeNeedSingleValueChangeRe = /^(date|time|month|week)$/ +const focusableWhenNotDisabled = [ + 'a[href]', + 'area[href]', + 'input', + 'select', + 'textarea', + 'button', + 'iframe', + '[tabindex]', + '[contentEditable]', +] + +// const isTextInputable = (el: HTMLElement) => { +// if (isTextLike(el)) { +// return _.some([':not([readonly])'].map((sel) => $jquery.wrap(el).is(sel))) +// } + +// return false + +// } + +// const textinputable = ['input'] + +//'body,a[href],button,select,[tabindex],input,textarea,[contenteditable]' + +const inputTypeNeedSingleValueChangeRe = /^(date|time|week|month)$/ const canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/ +declare global { + interface Window { + Element: typeof Element + HTMLElement: typeof HTMLElement + HTMLInputElement: typeof HTMLInputElement + HTMLSelectElement: typeof HTMLSelectElement + HTMLButtonElement: typeof HTMLButtonElement + HTMLOptionElement: typeof HTMLOptionElement + HTMLTextAreaElement: typeof HTMLTextAreaElement + Selection: typeof Selection + SVGElement: typeof SVGElement + EventTarget: typeof EventTarget + Document: typeof Document + } + + interface Selection { + modify: Function + } +} + // rules for native methods and props // if a setter or getter or function then add a native method // if a traversal, don't -const descriptor = (klass, prop) => { - return Object.getOwnPropertyDescriptor(window[klass].prototype, prop) +const descriptor = (klass: T, prop: K) => { + const descriptor = Object.getOwnPropertyDescriptor(window[klass].prototype, prop) + + if (descriptor === undefined) { + throw new Error(`Error, could not get property descriptor for ${klass} ${prop}. This should never happen`) + } + + return descriptor } const _getValue = function () { @@ -79,6 +132,8 @@ const _getSelectionStart = function () { if (isTextarea(this)) { return descriptor('HTMLTextAreaElement', 'selectionStart').get } + + throw new Error('this should never happen, cannot get selectionStart') } const _getSelectionEnd = function () { @@ -89,6 +144,8 @@ const _getSelectionEnd = function () { if (isTextarea(this)) { return descriptor('HTMLTextAreaElement', 'selectionEnd').get } + + throw new Error('this should never happen, cannot get selectionEnd') } const _nativeFocus = function () { @@ -149,6 +206,8 @@ const _setType = function () { if (isButton(this)) { return descriptor('HTMLButtonElement', 'type').set } + + throw new Error('this should never happen, cannot set type') } const _getType = function () { @@ -159,6 +218,8 @@ const _getType = function () { if (isButton(this)) { return descriptor('HTMLButtonElement', 'type').get } + + throw new Error('this should never happen, cannot get type') } const nativeGetters = { @@ -170,7 +231,7 @@ const nativeGetters = { type: _getType, activeElement: descriptor('Document', 'activeElement').get, body: descriptor('Document', 'body').get, - frameElement: Object.getOwnPropertyDescriptor(window, 'frameElement').get, + frameElement: Object.getOwnPropertyDescriptor(window, 'frameElement')!.get, } const nativeSetters = { @@ -195,9 +256,9 @@ const nativeMethods = { select: _nativeSelect, } -const tryCallNativeMethod = (...args) => { +const tryCallNativeMethod = (obj, fn, ...args) => { try { - return callNativeMethod(...args) + return callNativeMethod(obj, fn, ...args) } catch (err) { return } @@ -221,8 +282,10 @@ const callNativeMethod = function (obj, fn, ...args) { return retFn } -const getNativeProp = function (obj, prop) { - const nativeProp = nativeGetters[prop] +// const b = getNativeProp({foo:{hello:'world'}}, 'foo') + +const getNativeProp = function (obj: T, prop: K): T[K] { + const nativeProp = nativeGetters[prop as string] if (!nativeProp) { const props = _.keys(nativeGetters).join(', ') @@ -241,8 +304,8 @@ const getNativeProp = function (obj, prop) { return retProp } -const setNativeProp = function (obj, prop, val) { - const nativeProp = nativeSetters[prop] +const setNativeProp = function (obj: T, prop: K, val) { + const nativeProp = nativeSetters[prop as string] if (!nativeProp) { const fns = _.keys(nativeSetters).join(', ') @@ -259,7 +322,11 @@ const setNativeProp = function (obj, prop, val) { return retProp } -const isNeedSingleValueChangeInputElement = (el) => { +export interface HTMLSingleValueChangeInputElement extends HTMLInputElement { + type: 'date' | 'time' | 'week' | 'month' +} + +const isNeedSingleValueChangeInputElement = (el: HTMLElement): el is HTMLSingleValueChangeInputElement => { if (!isInput(el)) { return false } @@ -267,7 +334,8 @@ const isNeedSingleValueChangeInputElement = (el) => { return inputTypeNeedSingleValueChangeRe.test(el.type) } -const canSetSelectionRangeElement = (el) => { +const canSetSelectionRangeElement = (el): el is HTMLElementCanSetSelectionRange => { + //TODO: If IE, all inputs can set selection range return isTextarea(el) || (isInput(el) && canSetSelectionRangeElementRe.test(getNativeProp(el, 'type'))) } @@ -281,35 +349,27 @@ const getTagName = (el) => { // should be true for elements: // - with [contenteditable] // - with document.designMode = 'on' -const isContentEditable = (el) => { +const isContentEditable = (el: any): el is HTMLContentEditableElement => { return getNativeProp(el, 'isContentEditable') } -const isTextarea = (el) => { +const isTextarea = (el): el is HTMLTextAreaElement => { return getTagName(el) === 'textarea' } -const isInput = (el) => { +const isInput = (el): el is HTMLInputElement => { return getTagName(el) === 'input' } -const isButton = (el) => { +const isButton = (el): el is HTMLButtonElement => { return getTagName(el) === 'button' } -const isSelect = (el) => { +const isSelect = (el): el is HTMLSelectElement => { return getTagName(el) === 'select' } -const isOption = (el) => { - return getTagName(el) === 'option' -} - -const isOptgroup = (el) => { - return getTagName(el) === 'optgroup' -} - -const isBody = (el) => { +const isBody = (el): el is HTMLBodyElement => { return getTagName(el) === 'body' } @@ -321,7 +381,7 @@ const isHTML = (el) => { return getTagName(el) === 'html' } -const isSvg = function (el) { +const isSvg = function (el): el is SVGElement { try { return 'ownerSVGElement' in el } catch (error) { @@ -329,10 +389,18 @@ const isSvg = function (el) { } } +const isOption = (el) => { + return getTagName(el) === 'option' +} + +const isOptgroup = (el) => { + return getTagName(el) === 'optgroup' +} + // active element is the default if its null // or its equal to document.body const activeElementIsDefault = (activeElement, body) => { - return (!activeElement) || (activeElement === body) + return !activeElement || activeElement === body } const isFocused = (el) => { @@ -351,7 +419,17 @@ const isFocused = (el) => { } } -const isElement = function (obj) { +const getActiveElByDocument = (doc: Document): HTMLElement | null => { + const activeElement = getNativeProp(doc, 'activeElement') + + if (isFocused(activeElement)) { + return activeElement as HTMLElement + } + + return null +} + +const isElement = function (obj): obj is HTMLElement | JQuery { try { if ($jquery.isJquery(obj)) { obj = obj[0] @@ -363,14 +441,24 @@ const isElement = function (obj) { } } -const isFocusable = ($el) => { - return _.some(focusable, (sel) => { - return $el.is(sel) - }) +/** + * The element can be activeElement, recieve focus events, and also recieve keyboard events + */ +const isFocusable = ($el: JQuery) => { + return _.some(focusable, (sel) => $el.is(sel)) || (isElement($el[0]) && getTagName($el[0]) === 'html' && isContentEditable($el[0])) } -const isType = function ($el, type) { - const el = [].concat($jquery.unwrap($el))[0] +/** + * The element can be activeElement, recieve focus events, and also recieve keyboard events + * OR, it is a disabled element that would have been focusable + */ +const isFocusableWhenNotDisabled = ($el: JQuery) => { + return _.some(focusableWhenNotDisabled, (sel) => $el.is(sel)) || (isElement($el[0]) && getTagName($el[0]) === 'html' && isContentEditable($el[0])) +} + +const isType = function (el: HTMLInputElement | HTMLInputElement[] | JQuery, type) { + el = ([] as HTMLInputElement[]).concat($jquery.unwrap(el))[0] + // NOTE: use DOMElement.type instead of getAttribute('type') since // will have type="text", and behaves like text type const elType = (getNativeProp(el, 'type') || '').toLowerCase() @@ -383,7 +471,7 @@ const isType = function ($el, type) { } const isScrollOrAuto = (prop) => { - return (prop === 'scroll') || (prop === 'auto') + return prop === 'scroll' || prop === 'auto' } const isAncestor = ($el, $maybeAncestor) => { @@ -407,7 +495,7 @@ const getFirstCommonAncestor = (el1, el2) => { const getAllParents = (el) => { let curEl = el.parentNode - const allParents = [] + const allParents:any[] = [] while (curEl) { allParents.push(curEl) @@ -415,15 +503,28 @@ const getAllParents = (el) => { } return allParents +} +const isSelector = ($el: JQuery, selector) => { + return $el.is(selector) } const isChild = ($el, $maybeChild) => { return $el.children().index($maybeChild) >= 0 } -const isSelector = ($el, selector) => { - return $el.is(selector) +const isDisabled = ($el:JQuery) => { + return $el.prop('disabled') +} + +const isReadOnlyInputOrTextarea = ( + el: HTMLInputElement | HTMLTextAreaElement +) => { + return el.readOnly +} + +const isReadOnlyInput = ($el: JQuery) => { + return $el.prop('readonly') } const isDetached = ($el) => { @@ -463,7 +564,7 @@ const isAttached = function ($el) { // is technically bound to a differnet document // but c'mon const isIn = (el) => { - return $.contains(doc, el) + return $.contains((doc as unknown) as Element, el) } // make sure the document is currently @@ -494,15 +595,48 @@ const isSame = function ($el1, $el2) { return el1 && el2 && _.isEqual(el1, el2) } -const isTextLike = function ($el) { +export interface HTMLContentEditableElement extends HTMLElement {} + +export interface HTMLTextLikeInputElement extends HTMLInputElement { + type: + | 'text' + | 'password' + | 'email' + | 'number' + | 'date' + | 'week' + | 'month' + | 'time' + | 'datetime' + | 'datetime-local' + | 'search' + | 'url' + | 'tel' + setSelectionRange: HTMLInputElement['setSelectionRange'] +} + +export interface HTMLElementCanSetSelectionRange extends HTMLElement { + setSelectionRange: HTMLInputElement['setSelectionRange'] +} + +export type HTMLTextLikeElement = HTMLTextAreaElement | HTMLTextLikeInputElement | HTMLContentEditableElement + +const isTextLike = function (el: HTMLElement): el is HTMLTextLikeElement { + const $el = $jquery.wrap(el) const sel = (selector) => { return isSelector($el, selector) } const type = (type) => { - return isType($el, type) + if (isInput(el)) { + return isType(el, type) + } + + return false } - const isContentEditableElement = isContentEditable($el.get(0)) + const isContentEditableElement = isContentEditable(el) + + if (isContentEditableElement) return true return _.some([ isContentEditableElement, @@ -608,7 +742,7 @@ const isDescendent = ($el1, $el2) => { return false } - return !!(($el1.get(0) === $el2.get(0)) || $el1.has($el2).length) + return !!($el1.get(0) === $el2.get(0) || $el1.has($el2).length) } const findParent = (el, fn) => { @@ -637,7 +771,7 @@ const findParent = (el, fn) => { // 2. check to figure out the element listed at those coordinates // 3. if this element is ourself or our descendants, click whatever was returned // 4. else throw an error because something is covering us up -const getFirstFocusableEl = ($el) => { +const getFirstFocusableEl = ($el: JQuery) => { if (isFocusable($el)) { return $el } @@ -655,14 +789,6 @@ 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 @@ -759,7 +885,6 @@ const getElements = ($el) => { } return els - } const getContainsSelector = (text, filter = '') => { @@ -830,8 +955,15 @@ const stringify = (el, form = 'long') => { const $el = $jquery.wrap(el) const long = () => { - const str = $el.clone().empty().prop('outerHTML') - const text = _.chain($el.text()).clean().truncate({ length: 10 }).value() + const str = $el + .clone() + .empty() + .prop('outerHTML') + + const text = (_.chain($el.text()) as any) + .clean() + .truncate({ length: 10 }) + .value() const children = $el.children().length if (children) { @@ -877,92 +1009,53 @@ const stringify = (el, form = 'long') => { }) } -module.exports = { +export { isElement, - isSelector, - isScrollOrAuto, - isFocusable, - + isFocusableWhenNotDisabled, + isDisabled, + isReadOnlyInput, + isReadOnlyInputOrTextarea, isAttached, - isDetached, - isAttachedEl, - isDetachedEl, - isAncestor, - isChild, - isScrollable, - isTextLike, - isDescendent, - isContentEditable, - isSame, - isOption, - isOptgroup, - isBody, - isHTML, - isInput, - isIframe, - isTextarea, - isType, - isFocused, - isInputAllowingImplicitFormSubmission, - isNeedSingleValueChangeInputElement, - canSetSelectionRangeElement, - stringify, - getNativeProp, - setNativeProp, - callNativeMethod, - tryCallNativeMethod, - findParent, - getElements, - getFromDocCoords, - getFirstFocusableEl, - getActiveElByDocument, - getContainsSelector, - getFirstDeepestElement, - getFirstCommonAncestor, - getFirstParentWithTagName, - getFirstFixedOrStickyPositionParent, - getFirstStickyPositionParent, - getFirstScrollableParent, } diff --git a/packages/driver/src/dom/index.js b/packages/driver/src/dom/index.js index 6b4fab4d345e..700ef6ee67c6 100644 --- a/packages/driver/src/dom/index.js +++ b/packages/driver/src/dom/index.js @@ -6,12 +6,12 @@ const $visibility = require('./visibility') const $coordinates = require('./coordinates') const { isWindow, getWindowByElement } = $window -const { isDocument } = $document +const { isDocument, getDocumentFromElement } = $document const { wrap, unwrap, isJquery, query } = $jquery const { isVisible, isHidden, getReasonIsHidden } = $visibility const { isType, isFocusable, isElement, isScrollable, isFocused, stringify, getElements, getContainsSelector, getFirstDeepestElement, isDetached, isAttached, isTextLike, isSelector, isDescendent, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent } = $elements const { getCoordsByPosition, getElementPositioning, getElementCoordinatesByPosition, getElementAtPointFromViewport, getElementCoordinatesByPositionRelativeToXY } = $coordinates - +const { getHostContenteditable, getSelectionBounds } = require('./selection') const isDom = (obj) => { return isElement(obj) || isWindow(obj) || isDocument(obj) } @@ -22,6 +22,7 @@ const isDom = (obj) => { // purposes or for overriding. Everything else // can be tucked away behind these interfaces. module.exports = { + wrap, query, @@ -88,4 +89,10 @@ module.exports = { getElementCoordinatesByPositionRelativeToXY, + getHostContenteditable, + + getSelectionBounds, + + getDocumentFromElement, + } diff --git a/packages/driver/src/dom/jquery.js b/packages/driver/src/dom/jquery.js index 13336cbd8a17..eb5209bf2be7 100644 --- a/packages/driver/src/dom/jquery.js +++ b/packages/driver/src/dom/jquery.js @@ -7,6 +7,7 @@ const wrap = (obj) => { } const query = (selector, context) => { + // @ts-ignore return new $.fn.init(selector, context) } diff --git a/packages/driver/src/dom/selection.js b/packages/driver/src/dom/selection.ts similarity index 70% rename from packages/driver/src/dom/selection.js rename to packages/driver/src/dom/selection.ts index c9c4fa4bf283..ff8d2e44fad4 100644 --- a/packages/driver/src/dom/selection.js +++ b/packages/driver/src/dom/selection.ts @@ -1,15 +1,6 @@ -// 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 $document = require('./document') const $elements = require('./elements') - -const INTERNAL_STATE = '__Cypress_state__' +const $dom = require('../dom') const _getSelectionBoundsFromTextarea = (el) => { return { @@ -19,6 +10,7 @@ const _getSelectionBoundsFromTextarea = (el) => { } const _getSelectionBoundsFromInput = function (el) { + if ($elements.canSetSelectionRangeElement(el)) { return { start: $elements.getNativeProp(el, 'selectionStart'), @@ -26,48 +18,43 @@ const _getSelectionBoundsFromInput = function (el) { } } - const internalState = el[INTERNAL_STATE] - - if (internalState) { - return { - start: internalState.start, - end: internalState.end, - } - } + const doc = $document.getDocumentFromElement(el) + const range = _getSelectionRange(doc) return { - start: 0, - end: 0, + start: range.startOffset, + end: range.endOffset, } + } -const _getSelectionBoundsFromContentEditable = function (el) { - const doc = $document.getDocumentFromElement(el) +const _getSelectionRange = (doc) => { + const sel = doc.getSelection() - if (doc.getSelection) { - //# global selection object - const sel = doc.getSelection() + //# selection has at least one range (most always 1; only 0 at page load) + if (sel.rangeCount) { + //# get the first (usually only) range obj + return sel.getRangeAt(0) + } - //# selection has at least one range (most always 1; only 0 at page load) - if (sel.rangeCount) { - //# get the first (usually only) range obj - const range = sel.getRangeAt(0) + return doc.createRange() +} - //# if div[contenteditable] > text - const hostContenteditable = getHostContenteditable(range.commonAncestorContainer) +const _getSelectionBoundsFromContentEditable = function (el) { + const doc = $document.getDocumentFromElement(el) + const range = _getSelectionRange(doc) + const hostContenteditable = getHostContenteditable(range.commonAncestorContainer) - if (hostContenteditable === el) { - return { - start: range.startOffset, - end: range.endOffset, - } - } + if (hostContenteditable === el) { + return { + start: range.startOffset, + end: range.endOffset, } } return { - start: null, - end: null, + start: 0, + end: 0, } } @@ -77,7 +64,7 @@ const _replaceSelectionContentsContentEditable = function (el, text) { const doc = $document.getDocumentFromElement(el) //# NOTE: insertText will also handle '\n', and render newlines - $elements.callNativeMethod(doc, 'execCommand', 'insertText', true, text) + return $elements.callNativeMethod(doc, 'execCommand', 'insertText', true, text) } //# Keeping around native implementation @@ -147,20 +134,35 @@ const getHostContenteditable = function (el) { return curEl } -const _getInnerLastChild = function (el) { - while (el.lastChild) { - el = el.lastChild - } +// const _getInnerLastChild = function (el) { +// while (el.lastChild) { +// el = el.lastChild +// } - return el -} +// return el +// } +/** + * + * @param {HTMLElement} el + * @returns {Selection} + */ const _getSelectionByEl = function (el) { const doc = $document.getDocumentFromElement(el) return doc.getSelection() } +// const _getSelectionRangeByEl = function (el) { +// const sel = _getSelectionByEl(el) + +// if (sel.rangeCount > 0) { +// return sel.getRangeAt(0) +// } + +// throw new Error('No selection in document') +// } + const deleteSelectionContents = function (el) { if ($elements.isContentEditable(el)) { const doc = $document.getDocumentFromElement(el) @@ -175,22 +177,16 @@ const deleteSelectionContents = function (el) { } const setSelectionRange = function (el, start, end) { - - if ($elements.canSetSelectionRangeElement(el)) { - $elements.callNativeMethod(el, 'setSelectionRange', start, end) - - return - } + $elements.callNativeMethod(el, 'setSelectionRange', start, end) //# NOTE: Some input elements have mobile implementations //# and thus may not always have a cursor, so calling setSelectionRange will throw. //# we are assuming desktop here, so we store our own internal state. - el[INTERNAL_STATE] = { - start, - end, - } - + // el[INTERNAL_STATE] = { + // start, + // end, + // } } const deleteRightOfCursor = function (el) { @@ -212,7 +208,13 @@ const deleteRightOfCursor = function (el) { if ($elements.isContentEditable(el)) { const selection = _getSelectionByEl(el) - $elements.callNativeMethod(selection, 'modify', 'extend', 'forward', 'character') + $elements.callNativeMethod( + selection, + 'modify', + 'extend', + 'forward', + 'character' + ) if ($elements.getNativeProp(selection, 'isCollapsed')) { //# there's nothing to delete @@ -224,6 +226,8 @@ const deleteRightOfCursor = function (el) { //# successful delete return true } + + return false } const deleteLeftOfCursor = function (el) { @@ -246,7 +250,13 @@ const deleteLeftOfCursor = function (el) { //# there is no 'backwardDelete' command for execCommand, so use the Selection API const selection = _getSelectionByEl(el) - $elements.callNativeMethod(selection, 'modify', 'extend', 'backward', 'character') + $elements.callNativeMethod( + selection, + 'modify', + 'extend', + 'backward', + 'character' + ) if (selection.isCollapsed) { //# there's nothing to delete @@ -259,6 +269,8 @@ const deleteLeftOfCursor = function (el) { //# successful delete return true } + + return false } const _collapseInputOrTextArea = (el, toIndex) => { @@ -266,7 +278,7 @@ const _collapseInputOrTextArea = (el, toIndex) => { } const moveCursorLeft = function (el) { - if ($elements.isTextarea(el) || $elements.isInput(el)) { + if ($elements.canSetSelectionRangeElement(el)) { const { start, end } = getSelectionBounds(el) if (start !== end) { @@ -280,23 +292,19 @@ const moveCursorLeft = function (el) { return setSelectionRange(el, start - 1, start - 1) } - if ($elements.isContentEditable(el)) { - const selection = _getSelectionByEl(el) + // if ($elements.isContentEditable(el)) { + const selection = _getSelectionByEl(el) - return $elements.callNativeMethod(selection, 'modify', 'move', 'backward', 'character') - } + return $elements.callNativeMethod( + selection, + 'modify', + 'move', + 'backward', + 'character' + ) + // } } -// const _getSelectionRangeByEl = function (el) { -// const sel = _getSelectionByEl(el) - -// if (sel.rangeCount > 0) { -// return sel.getRangeAt(0) -// } - -// throw new Error('No selection in document') -// } - //# Keeping around native implementation //# for same reasons as listed below //# @@ -310,7 +318,7 @@ const moveCursorLeft = function (el) { // range.setEnd(range.startContainer, newOffset) const moveCursorRight = function (el) { - if ($elements.isTextarea(el) || $elements.isInput(el)) { + if ($elements.canSetSelectionRangeElement(el)) { const { start, end } = getSelectionBounds(el) if (start !== end) { @@ -322,11 +330,15 @@ const moveCursorRight = function (el) { return setSelectionRange(el, start + 1, end + 1) } - if ($elements.isContentEditable(el)) { - const selection = _getSelectionByEl(el) + const selection = _getSelectionByEl(el) - return $elements.callNativeMethod(selection, 'modify', 'move', 'forward', 'character') - } + return $elements.callNativeMethod( + selection, + 'modify', + 'move', + 'forward', + 'character' + ) } const moveCursorUp = (el) => { @@ -339,9 +351,9 @@ const moveCursorDown = (el) => { const _moveCursorUpOrDown = function (el, up) { if ($elements.isInput(el)) { - //# on an input, instead of moving the cursor - //# we want to perform the native browser action - //# which is to increment the step/interval + //# on an input, instead of moving the cursor + //# we want to perform the native browser action + //# which is to increment the step/interval if ($elements.isType(el, 'number')) { if (up) { if (typeof el.stepUp === 'function') { @@ -360,10 +372,13 @@ const _moveCursorUpOrDown = function (el, up) { if ($elements.isTextarea(el) || $elements.isContentEditable(el)) { const selection = _getSelectionByEl(el) - return $elements.callNativeMethod(selection, 'modify', + return $elements.callNativeMethod( + selection, + 'modify', 'move', up ? 'backward' : 'forward', - 'line') + 'line' + ) } } @@ -399,18 +414,22 @@ const isCollapsed = function (el) { } } -const selectAll = function (el) { - if ($elements.isTextarea(el) || $elements.isInput(el)) { +const selectAll = function (doc) { + const el = _getActive(doc) + + if ($elements.canSetSelectionRangeElement(el)) { setSelectionRange(el, 0, $elements.getNativeProp(el, 'value').length) return } - if ($elements.isContentEditable(el)) { - const doc = $document.getDocumentFromElement(el) - - return $elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null) - } + return $elements.callNativeMethod( + doc, + 'execCommand', + 'selectAll', + false, + null + ) } //# Keeping around native implementation //# for same reasons as listed below @@ -426,61 +445,91 @@ const selectAll = function (el) { const getSelectionBounds = function (el) { //# this function works for input, textareas, and contentEditables - switch (false) { - case !$elements.isInput(el): + switch (true) { + case !!$elements.isInput(el): return _getSelectionBoundsFromInput(el) - case !$elements.isTextarea(el): + case !!$elements.isTextarea(el): return _getSelectionBoundsFromTextarea(el) - case !$elements.isContentEditable(el): + case !!$elements.isContentEditable(el): return _getSelectionBoundsFromContentEditable(el) default: return { - start: null, - end: null, + start: 0, + end: 0, } } } -const moveSelectionToEnd = function (el) { - let length - - if ($elements.isInput(el) || $elements.isTextarea(el)) { - ({ length } = $elements.getNativeProp(el, 'value')) +const moveSelectionToEnd = (doc) => { + return _moveSelectionTo(false, doc) +} - return setSelectionRange(el, length, length) +const moveSelectionToStart = (doc) => { + return _moveSelectionTo(true, doc) +} - } +const _moveSelectionTo = function (toStart, doc) { + const el = _getActive(doc) - if ($elements.isContentEditable(el)) { - //# NOTE: can't use execCommand API here because we would have - //# to selectAll and then collapse so we use the Selection API - const doc = $document.getDocumentFromElement(el) - const range = $elements.callNativeMethod(doc, 'createRange') - const hostContenteditable = getHostContenteditable(el) - let lastTextNode = _getInnerLastChild(hostContenteditable) + if ($elements.canSetSelectionRangeElement(el)) { + let cursorPosition - if (lastTextNode.tagName === 'BR') { - lastTextNode = lastTextNode.parentNode + if (toStart) { + cursorPosition = 0 + } else { + cursorPosition = $elements.getNativeProp(el, 'value').length } - range.setStart(lastTextNode, lastTextNode.length) - range.setEnd(lastTextNode, lastTextNode.length) + setSelectionRange(el, cursorPosition, cursorPosition) - const sel = $elements.callNativeMethod(doc, 'getSelection') + return + } - $elements.callNativeMethod(sel, 'removeAllRanges') + $elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null) + const selection = doc.getSelection() - return $elements.callNativeMethod(sel, 'addRange', range) - } + // collapsing the range doesn't work on input/textareas, since the range contains more than the input element + // However, IE can always* set selection range, so only modern browsers (with the selection API) will need this + const direction = toStart ? 'backward' : 'forward' + + selection.modify('move', direction, 'line') + + // if (selection.rangeCount > 0) { + // const range = selection.getRangeAt(0) + // range.collapse(toStart) + // doc.getSelection().removeAllRanges() + // selection.addRange(range) + // } + // } } +// if $elements.isInput(el) || $elements.isTextarea(el) +// length = $elements.getNativeProp(el, "value").length +// setSelectionRange(el, length, length) + +// else if $elements.isContentEditable(el) +// ## NOTE: can't use execCommand API here because we would have +// ## to selectAll and then collapse so we use the Selection API +// # doc = $document.getDocumentFromElement(el) +// # range = $elements.callNativeMethod(doc, "createRange") +// # hostContenteditable = _getHostContenteditable(el) +// # lastTextNode = _getInnerLastChild(hostContenteditable) + +// # if lastTextNode.tagName is "BR" +// # lastTextNode = lastTextNode.parentNode + +// # range.setStart(lastTextNode, lastTextNode.length) +// # range.setEnd(lastTextNode, lastTextNode.length) + +// # sel = $elements.callNativeMethod(doc, "getSelection") +// # $elements.callNativeMethod(sel, "removeAllRanges") +// # $elements.callNativeMethod(sel, "addRange", range) + //# TODO: think about renaming this -const replaceSelectionContents = function (el, key) { - if ($elements.isContentEditable(el)) { - return _replaceSelectionContentsContentEditable(el, key) - } +const replaceSelectionContents = function (el:HTMLElement, key:string) { - if ($elements.isInput(el) || $elements.isTextarea(el)) { + if ($elements.canSetSelectionRangeElement(el)) { + // if ($elements.isRead) const { start, end } = getSelectionBounds(el) const value = $elements.getNativeProp(el, 'value') || '' @@ -490,12 +539,15 @@ const replaceSelectionContents = function (el, key) { return setSelectionRange(el, start + key.length, start + key.length) } + + return _replaceSelectionContentsContentEditable(el, key) + } const getCaretPosition = function (el) { const bounds = getSelectionBounds(el) - if ((bounds.start == null)) { + if (bounds.start == null) { //# no selection return null } @@ -507,12 +559,39 @@ const getCaretPosition = function (el) { return null } -const interceptSelect = function () { - if ($elements.isInput(this) && !$elements.canSetSelectionRangeElement(this)) { - setSelectionRange(this, 0, $elements.getNativeProp(this, 'value').length) +const _getActive = function (doc) { + // TODO: remove this state access + // eslint-disable-next-line + const activeEl = $elements.getNativeProp(doc, 'activeElement') + + return activeEl +} + +// const getFirstFocusable = (el) => { +// if ($elements.isContentEditable(el)) { +// const hostContenteditable = _getHostContenteditable(el) + +// return hostContenteditable +// } + +// return el +// } + +const focusCursor = function (el, doc) { + + const elToFocus = $elements.getFirstFocusableEl($dom.wrap(el)).get(0) + + const prevFocused = _getActive(doc) + + elToFocus.focus() + + if ($elements.isInput(elToFocus) || $elements.isTextarea(elToFocus)) { + moveSelectionToEnd(doc) } - return $elements.callNativeMethod(this, 'select') + if ($elements.isContentEditable(elToFocus) && prevFocused !== elToFocus) { + moveSelectionToEnd(doc) + } } //# Selection API implementation of insert newline. @@ -590,13 +669,14 @@ const interceptSelect = function () { // el = el.firstChild // return el -module.exports = { +export { getSelectionBounds, deleteRightOfCursor, deleteLeftOfCursor, selectAll, deleteSelectionContents, moveSelectionToEnd, + moveSelectionToStart, getCaretPosition, getHostContenteditable, moveCursorLeft, @@ -607,5 +687,5 @@ module.exports = { moveCursorToLineEnd, replaceSelectionContents, isCollapsed, - interceptSelect, + focusCursor, } diff --git a/packages/driver/src/dom/types.d.ts b/packages/driver/src/dom/types.d.ts new file mode 100644 index 000000000000..a155af433c79 --- /dev/null +++ b/packages/driver/src/dom/types.d.ts @@ -0,0 +1,52 @@ +declare global { + interface Window { + Element: typeof Element + HTMLElement: typeof HTMLElement + HTMLInputElement: typeof HTMLInputElement + HTMLSelectElement: typeof HTMLSelectElement + HTMLButtonElement: typeof HTMLButtonElement + HTMLOptionElement: typeof HTMLOptionElement + HTMLTextAreaElement: typeof HTMLTextAreaElement + Selection: typeof Selection + SVGElement: typeof SVGElement + EventTarget: typeof EventTarget + Document: typeof Document + } + + interface Selection { + modify: Function + } +} + +export interface HTMLSingleValueChangeInputElement extends HTMLInputElement { + type: 'date' | 'time' | 'week' | 'month' + } + +export interface HTMLContentEditableElement extends HTMLElement {} + +export interface HTMLTextLikeInputElement extends HTMLInputElement { + type: + | 'text' + | 'password' + | 'email' + | 'number' + | 'date' + | 'week' + | 'month' + | 'time' + | 'datetime' + | 'datetime-local' + | 'search' + | 'url' + | 'tel' + setSelectionRange: HTMLInputElement['setSelectionRange'] +} + +export interface HTMLElementCanSetSelectionRange extends HTMLElement { + setSelectionRange: HTMLInputElement['setSelectionRange'] +} + +export type HTMLTextLikeElement = + | HTMLTextAreaElement + | HTMLTextLikeInputElement + | HTMLContentEditableElement diff --git a/packages/driver/test/cypress/.eslintrc b/packages/driver/test/cypress/.eslintrc index 5b988562725d..d96e95f4ae5e 100644 --- a/packages/driver/test/cypress/.eslintrc +++ b/packages/driver/test/cypress/.eslintrc @@ -4,5 +4,8 @@ ], "env": { "cypress/globals": true + }, + "globals": { + "chai": false } } 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..947f01b7e2f1 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -423,6 +423,49 @@ describe('src/cy/commands/actions/click', () => { cy.getAll('div', 'pointerover pointerenter pointerdown mousedown pointerup mouseup click').each(shouldBeCalled) }) + 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', () => { const $btn = cy.$$('button:first') @@ -599,9 +642,7 @@ describe('src/cy/commands/actions/click', () => { cy.get('#three-buttons button').click({ multiple: true }).then(() => { const calls = cy.timeout.getCalls() - const num = _.filter(calls, (call) => { - return _.isEqual(call.args, [50, true, 'click']) - }) + const num = _.filter(calls, (call) => _.isEqual(call.args, [50, true, 'click'])) expect(num.length).to.eq(count) }) @@ -1523,9 +1564,25 @@ describe('src/cy/commands/actions/click', () => { .focused().should('have.id', 'button-covered-in-span') }) + it('focus window', () => { + const stub = cy.stub() + .callsFake(() => { + // debugger + }) + // const win = cy.state('window') + const win = cy.$$('*') + + cy.$$(cy.state('window')).on('focus', cy.stub().as('win')) + + cy.$$(cy.state('document')).on('focus', cy.stub().as('doc')) + + win.on('focus', stub) + + cy.get('li').first().then((el) => el.focus().focus().focus()) + }) + it('will not fire focus events when nothing can receive focus', () => { const onFocus = cy.stub() - const win = cy.state('window') const $body = cy.$$('body') const $div = cy.$$('#nested-find') @@ -1630,9 +1687,7 @@ describe('src/cy/commands/actions/click', () => { cy.on('fail', (err) => { const { lastLog, logs } = this - const logsArr = logs.map((log) => { - return log.get().consoleProps() - }) + const logsArr = logs.map((log) => log.get().consoleProps()) expect(logsArr).to.have.length(4) expect(lastLog.get('error')).to.eq(err) @@ -1906,9 +1961,7 @@ describe('src/cy/commands/actions/click', () => { const clicks = [] // append two buttons - const button = () => { - return $('') - } + const button = () => $('') cy.$$('body').append(button()).append(button()) @@ -2014,9 +2067,7 @@ describe('src/cy/commands/actions/click', () => { }) it('#consoleProps groups MouseDown', () => { - cy.$$('input:first').mousedown(() => { - return false - }) + cy.$$('input:first').mousedown(() => false) cy.get('input:first').click().then(function () { @@ -2092,9 +2143,7 @@ describe('src/cy/commands/actions/click', () => { }) it('#consoleProps groups MouseUp', () => { - cy.$$('input:first').mouseup(() => { - return false - }) + cy.$$('input:first').mouseup(() => false) cy.get('input:first').click().then(function () { expect(this.lastLog.invoke('consoleProps').table[2]().data).to.containSubset([ @@ -2133,9 +2182,7 @@ describe('src/cy/commands/actions/click', () => { }) it('#consoleProps groups Click', () => { - cy.$$('input:first').click(() => { - return false - }) + cy.$$('input:first').click(() => false) cy.get('input:first').click().then(function () { expect(this.lastLog.invoke('consoleProps').table[2]().data).to.containSubset([ @@ -2242,9 +2289,7 @@ describe('src/cy/commands/actions/click', () => { }) it('#consoleProps groups have activated modifiers', () => { - cy.$$('input:first').click(() => { - return false - }) + cy.$$('input:first').click(() => false) cy.get('input:first').type('{ctrl}{shift}', { release: false }).click().then(function () { expect(this.lastLog.invoke('consoleProps').table[2]().data).to.containSubset([ @@ -2583,9 +2628,7 @@ describe('src/cy/commands/actions/click', () => { cy.get('#three-buttons button').dblclick().then(() => { const calls = cy.timeout.getCalls() - const num = _.filter(calls, (call) => { - return _.isEqual(call.args, [50, true, 'dblclick']) - }) + const num = _.filter(calls, (call) => _.isEqual(call.args, [50, true, 'dblclick'])) expect(num.length).to.eq(count) }) @@ -2709,9 +2752,7 @@ describe('src/cy/commands/actions/click', () => { const dblclicks = [] // append two buttons - const $button = () => { - return $('
@@ -349,7 +356,7 @@ 5 - 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 4ef2f9e67da8..15bafe51ce51 100644 --- a/packages/driver/test/cypress/integration/commands/actions/check_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/check_spec.coffee @@ -75,6 +75,11 @@ describe "src/cy/commands/actions/check", -> done("should not fire change event") cy.get(checkbox).check() + + ## readonly should only be limited to inputs, not checkboxes + it "can check readonly checkboxes", -> + cy.get('#readonly-checkbox').check().then ($checkbox) -> + expect($checkbox).to.be.checked it "does not require visibility with force: true", -> checkbox = ":checkbox[name='birds']" 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 947f01b7e2f1..7981589c3cd2 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -802,11 +802,19 @@ describe('src/cy/commands/actions/click', () => { }) describe('actionability', () => { - it('can click on inline elements that wrap lines', () => { cy.get('#overflow-link').find('.wrapped').click() }) + // readonly should only limit typing, not clicking + it('can click on readonly inputs', () => { + cy.get('#readonly-attr').click() + }) + + it('can click on readonly submit inputs', () => { + cy.get('#readonly-submit').click() + }) + it('can click elements which are hidden until scrolled within parent container', () => { cy.get('#overflow-auto-container').contains('quux').click() }) diff --git a/packages/driver/test/cypress/integration/commands/actions/focus_spec.coffee b/packages/driver/test/cypress/integration/commands/actions/focus_spec.coffee index a50da7112212..dcf6c0b1490b 100644 --- a/packages/driver/test/cypress/integration/commands/actions/focus_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/focus_spec.coffee @@ -46,7 +46,7 @@ describe "src/cy/commands/actions/focus", -> .get("input:first").focus() .get("#focus input").focus() .then -> - expect(blurred).to.be.true + expect(blurred).to.be.true it "matches cy.focused()", -> button = cy.$$("#button") @@ -153,6 +153,14 @@ describe "src/cy/commands/actions/focus", -> cy.get("[data-cy=rect]").focus().then -> expect(onFocus).to.be.calledOnce + it "can focus on readonly inputs", -> + onFocus = cy.stub() + + cy.$$("#readonly-attr").focus(onFocus) + + cy.get("#readonly-attr").focus().then -> + expect(onFocus).to.be.calledOnce + describe "assertion verification", -> beforeEach -> cy.on "log:added", (attrs, log) => @@ -290,6 +298,7 @@ describe "src/cy/commands/actions/focus", -> cy.focus() + ## TODO: dont skip this it.skip "slurps up failed promises", (done) -> cy.timeout(1000) 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 d52c37dfda40..1988a2bf6dc0 100644 --- a/packages/driver/test/cypress/integration/commands/actions/select_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/select_spec.coffee @@ -66,6 +66,11 @@ describe "src/cy/commands/actions/select", -> cy.get("select[name=movies]").select(["The Human Condition", "There Will Be Blood"]).then ($select) -> expect($select.val()).to.deep.eq ["thc", "twbb"] + ## readonly should only be limited to inputs, not checkboxes + it "can select a readonly select", -> + cy.get("select[name=hunter]").select("gon").then ($select) -> + expect($select.val()).to.eq("gon-val") + it "clears previous values when providing an array", -> ## make sure we have a previous value select = cy.$$("select[name=movies]").val(["2001"]) @@ -75,17 +80,16 @@ describe "src/cy/commands/actions/select", -> expect($select.val()).to.deep.eq ["apoc", "br"] it "lists the select as the focused element", -> - select = cy.$$("select:first") + select = cy.$$("#select-maps") - cy.get("select:first").select("de_train").focused().then ($focused) -> + cy.get("#select-maps").select("de_train").focused().then ($focused) -> expect($focused.get(0)).to.eq select.get(0) it "causes previous input to receive blur", (done) -> cy.$$("input:text:first").blur -> done() - cy - .get("input:text:first").type("foo") - .get("select:first").select("de_train") + cy.get("input:text:first").type("foo") + cy.get("#select-maps").select("de_train") it "can forcibly click even when being covered by another element", (done) -> select = $("").attr("id", "select-covered-in-span").prependTo(cy.$$("body")) @@ -107,19 +111,19 @@ describe "src/cy/commands/actions/select", -> cy.get("#select-covered-in-span").select("foobar", {timeout: 1000, interval: 60}) it "can forcibly click even when element is invisible", (done) -> - select = cy.$$("select:first").hide() + select = cy.$$("#select-maps").hide() select.click -> done() - cy.get("select:first").select("de_dust2", {force: true}) + cy.get("#select-maps").select("de_dust2", {force: true}) it "retries until ") cy.on "command:retry", _.once => - cy.$$("select:first").append option + cy.$$("#select-maps").append option - cy.get("select:first").select("foo") + cy.get("#select-maps").select("foo") it "retries until ') - .attr('id', 'input-covered-in-span') - .css({ - width: 50, - }) - .prependTo(cy.$$('body')) + .attr('id', 'input-covered-in-span') + .css({ + width: 50, + }) + .prependTo(cy.$$('body')) $('span on input') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) let clicked = false @@ -284,8 +284,8 @@ describe('src/cy/commands/actions/type', () => { }) cy.stub(cy, 'ensureElementIsNotAnimating') - .throws(new Error('animating!')) - .onThirdCall().returns() + .throws(new Error('animating!')) + .onThirdCall().returns() cy.get(':text:first').type('foo').then(() => { // - retry animation coords @@ -499,12 +499,12 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('#tabindex').type('f{leftarrow}{rightarrow}{enter}') - .then(() => { - expect(keydowns).to.have.length(4) - expect(keypresses).to.have.length(2) + .then(() => { + expect(keydowns).to.have.length(4) + expect(keypresses).to.have.length(2) - expect(keyups).to.have.length(4) - }) + expect(keyups).to.have.length(4) + }) }) }) @@ -513,10 +513,10 @@ describe('src/cy/commands/actions/type', () => { cy.spy(cy, 'timeout') cy.get(':text:first') - .type('foo{enter}bar{leftarrow}', { delay: 5 }) - .then(() => { - expect(cy.timeout).to.be.calledWith(5 * 8, true, 'type') - }) + .type('foo{enter}bar{leftarrow}', { delay: 5 }) + .then(() => { + expect(cy.timeout).to.be.calledWith(5 * 8, true, 'type') + }) }) it('can cancel additional keystrokes', (done) => { @@ -675,48 +675,48 @@ describe('src/cy/commands/actions/type', () => { it('fires events for each key stroke') it('does fire input event when value changes', () => { - let fired = false + const onInput = cy.stub() - cy.$$(':text:first').on('input', () => { - fired = true - }) + cy.$$(':text:first').on('input', onInput) - fired = false cy.get(':text:first') - .invoke('val', 'bar') - .type('{selectAll}{rightarrow}{backspace}') - .then(() => { - expect(fired).to.eq(true) - }) + .invoke('val', 'bar') + .type('{selectAll}{rightarrow}{backspace}') + .then(() => { + expect(onInput).calledOnce + }) + .then(() => { + onInput.reset() + }) - fired = false cy.get(':text:first') - .invoke('val', 'bar') - .type('{selectAll}{leftarrow}{del}') - .then(() => { - expect(fired).to.eq(true) - }) + .invoke('val', 'bar') + .type('{selectAll}{leftarrow}{del}') + .then(() => { + expect(onInput).calledOnce + }) + .then(() => { + onInput.reset() + }) - cy.$$('[contenteditable]:first').on('input', () => { - fired = true - }) + cy.$$('[contenteditable]:first').on('input', onInput) - fired = false cy.get('[contenteditable]:first') - .invoke('html', 'foobar') - .type('{selectAll}{rightarrow}{backspace}') - .then(() => { - expect(fired).to.eq(true) - }) - - fired = false + .invoke('html', 'foobar') + .type('{selectAll}{rightarrow}{backspace}') + .then(() => { + expect(onInput).calledOnce + }) + .then(() => { + onInput.reset() + }) cy.get('[contenteditable]:first') - .invoke('html', 'foobar') - .type('{selectAll}{leftarrow}{del}') - .then(() => { - expect(fired).to.eq(true) - }) + .invoke('html', 'foobar') + .type('{selectAll}{leftarrow}{del}') + .then(() => { + expect(onInput).calledOnce + }) }) it('does not fire input event when value does not change', () => { @@ -726,62 +726,55 @@ describe('src/cy/commands/actions/type', () => { fired = true }) - fired = false cy.get(':text:first') - .invoke('val', 'bar') - .type('{selectAll}{rightarrow}{del}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('val', 'bar') + .type('{selectAll}{rightarrow}{del}') + .then(() => { + expect(fired).to.eq(false) + }) - fired = false cy.get(':text:first') - .invoke('val', 'bar') - .type('{selectAll}{leftarrow}{backspace}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('val', 'bar') + .type('{selectAll}{leftarrow}{backspace}') + .then(() => { + expect(fired).to.eq(false) + }) cy.$$('textarea:first').on('input', () => { fired = true }) - fired = false cy.get('textarea:first') - .invoke('val', 'bar') - .type('{selectAll}{rightarrow}{del}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('val', 'bar') + .type('{selectAll}{rightarrow}{del}') + .then(() => { + expect(fired).to.eq(false) + }) - fired = false cy.get('textarea:first') - .invoke('val', 'bar') - .type('{selectAll}{leftarrow}{backspace}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('val', 'bar') + .type('{selectAll}{leftarrow}{backspace}') + .then(() => { + expect(fired).to.eq(false) + }) cy.$$('[contenteditable]:first').on('input', () => { fired = true }) - fired = false cy.get('[contenteditable]:first') - .invoke('html', 'foobar') - .type('{selectAll}{rightarrow}{del}') - .then(() => { - expect(fired).to.eq(false) - }) - - fired = false + .invoke('html', 'foobar') + .type('{movetoend}') + .then(($el) => { + expect(fired).to.eq(false) + }) cy.get('[contenteditable]:first') - .invoke('html', 'foobar') - .type('{selectAll}{leftarrow}{backspace}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('html', 'foobar') + .type('{selectAll}{leftarrow}{backspace}') + .then(() => { + expect(fired).to.eq(false) + }) }) }) @@ -792,10 +785,10 @@ describe('src/cy/commands/actions/type', () => { $input.attr('maxlength', 5) cy.get(':text:first') - .type('1234567890') - .then((input) => { - expect(input).to.have.value('12345') - }) + .type('1234567890') + .then((input) => { + expect(input).to.have.value('12345') + }) }) it('ignores an invalid maxlength attribute', () => { @@ -804,10 +797,10 @@ describe('src/cy/commands/actions/type', () => { $input.attr('maxlength', 'five') cy.get(':text:first') - .type('1234567890') - .then((input) => { - expect(input).to.have.value('1234567890') - }) + .type('1234567890') + .then((input) => { + expect(input).to.have.value('1234567890') + }) }) it('handles special characters', () => { @@ -816,10 +809,10 @@ describe('src/cy/commands/actions/type', () => { $input.attr('maxlength', 5) cy.get(':text:first') - .type('12{selectall}') - .then((input) => { - expect(input).to.have.value('12') - }) + .type('12{selectall}') + .then((input) => { + expect(input).to.have.value('12') + }) }) it('maxlength=0 events', () => { @@ -832,21 +825,21 @@ describe('src/cy/commands/actions/type', () => { } cy - .$$(':text:first') - .attr('maxlength', 0) - .on('keydown', push('keydown')) - .on('keypress', push('keypress')) - .on('textInput', push('textInput')) - .on('input', push('input')) - .on('keyup', push('keyup')) + .$$(':text:first') + .attr('maxlength', 0) + .on('keydown', push('keydown')) + .on('keypress', push('keypress')) + .on('textInput', push('textInput')) + .on('input', push('input')) + .on('keyup', push('keyup')) cy.get(':text:first') - .type('1') - .then(() => { - expect(events).to.deep.eq([ - 'keydown', 'keypress', 'textInput', 'keyup', - ]) - }) + .type('1') + .then(() => { + expect(events).to.deep.eq([ + 'keydown', 'keypress', 'textInput', 'keyup', + ]) + }) }) it('maxlength=1 events', () => { @@ -859,22 +852,22 @@ describe('src/cy/commands/actions/type', () => { } cy - .$$(':text:first') - .attr('maxlength', 1) - .on('keydown', push('keydown')) - .on('keypress', push('keypress')) - .on('textInput', push('textInput')) - .on('input', push('input')) - .on('keyup', push('keyup')) + .$$(':text:first') + .attr('maxlength', 1) + .on('keydown', push('keydown')) + .on('keypress', push('keypress')) + .on('textInput', push('textInput')) + .on('input', push('input')) + .on('keyup', push('keyup')) cy.get(':text:first') - .type('12') - .then(() => { - expect(events).to.deep.eq([ - 'keydown', 'keypress', 'textInput', 'input', 'keyup', - 'keydown', 'keypress', 'textInput', 'keyup', - ]) - }) + .type('12') + .then(() => { + expect(events).to.deep.eq([ + 'keydown', 'keypress', 'textInput', 'input', 'keyup', + 'keydown', 'keypress', 'textInput', 'keyup', + ]) + }) }) }) @@ -1139,8 +1132,36 @@ describe('src/cy/commands/actions/type', () => { it('can type negative numbers', () => { cy.get('#number-without-value') - .type('-123.12') - .should('have.value', '-123.12') + .type('-123.12') + .should('have.value', '-123.12') + }) + + it('can type {del}', () => { + cy.get('#number-with-value') + .type('{selectAll}{del}') + .should('have.value', '') + }) + + it('can type {selectAll}{del}', () => { + const sentInput = cy.stub() + + cy.get('#number-with-value') + .then(($el) => $el.on('input', sentInput)) + .type('{selectAll}{del}') + .should('have.value', '') + .then(() => expect(sentInput).calledOnce) + + }) + + it('can type {selectAll}{del} without sending input event', () => { + + const sentInput = cy.stub() + + cy.get('#number-without-value') + .then(($el) => $el.on('input', sentInput)) + .type('{selectAll}{del}') + .should('have.value', '') + .then(() => expect(sentInput).not.called) }) it('type=number blurs consistently', () => { @@ -1151,10 +1172,10 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('#number-without-value') - .type('200').blur() - .then(() => { - expect(blurred).to.eq(1) - }) + .type('200').blur() + .then(() => { + expect(blurred).to.eq(1) + }) }) }) @@ -1205,10 +1226,10 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('#email-without-value') - .type('foo@bar.com').blur() - .then(() => { - expect(blurred).to.eq(1) - }) + .type('foo@bar.com').blur() + .then(() => { + expect(blurred).to.eq(1) + }) }) }) @@ -1250,18 +1271,18 @@ describe('src/cy/commands/actions/type', () => { } cy - .$$('#password-without-value') - .val('secret') - .click(select) - .keyup((e) => { - switch (e.key) { - case 'g': - return select(e) - case 'n': - return e.target.setSelectionRange(0, 1) - default: - } - }) + .$$('#password-without-value') + .val('secret') + .click(select) + .keyup((e) => { + switch (e.key) { + case 'g': + return select(e) + case 'n': + return e.target.setSelectionRange(0, 1) + default: + } + }) cy.get('#password-without-value').type('agent').then(($input) => { expect($input).to.have.value('tn') @@ -1392,92 +1413,108 @@ describe('src/cy/commands/actions/type', () => { }) }) + it('inserts text with only one input event', () => { + const onInput = cy.stub() + const onTextInput = cy.stub() + + cy.get('#input-types [contenteditable]') + + .invoke('text', 'foo') + .then(($el) => $el.on('input', onInput)) + .then(($el) => $el.on('input', onTextInput)) + .type('\n').then(($text) => { + expect(trimInnerText($text)).eq('foo') + }) + .then(() => expect(onInput).calledOnce) + .then(() => expect(onTextInput).calledOnce) + }) + it('can type into [contenteditable] with existing
', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '
foo
' cy.get('[contenteditable]:first') - .type('bar').then(($div) => { - expect(trimInnerText($div)).to.eql('foobar') - expect($div.get(0).textContent).to.eql('foobar') + .type('bar').then(($div) => { + expect(trimInnerText($div)).to.eql('foobar') + expect($div.get(0).textContent).to.eql('foobar') - expect($div.get(0).innerHTML).to.eql('
foobar
') - }) + expect($div.get(0).innerHTML).to.eql('
foobar
') + }) }) it('can type into [contenteditable] with existing

', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '

foo

' cy.get('[contenteditable]:first') - .type('bar').then(($div) => { - expect(trimInnerText($div)).to.eql('foobar') - expect($div.get(0).textContent).to.eql('foobar') + .type('bar').then(($div) => { + expect(trimInnerText($div)).to.eql('foobar') + expect($div.get(0).textContent).to.eql('foobar') - expect($div.get(0).innerHTML).to.eql('

foobar

') - }) + expect($div.get(0).innerHTML).to.eql('

foobar

') + }) }) it('collapses selection to start on {leftarrow}', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '
bar
' cy.get('[contenteditable]:first') - .type('{selectall}{leftarrow}foo').then(($div) => { - expect(trimInnerText($div)).to.eql('foobar') - }) + .type('{selectall}{leftarrow}foo').then(($div) => { + expect(trimInnerText($div)).to.eql('foobar') + }) }) it('collapses selection to end on {rightarrow}', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '
bar
' cy.get('[contenteditable]:first') - .type('{selectall}{leftarrow}foo{selectall}{rightarrow}baz').then(($div) => { - expect(trimInnerText($div)).to.eql('foobarbaz') - }) + .type('{selectall}{leftarrow}foo{selectall}{rightarrow}baz').then(($div) => { + expect(trimInnerText($div)).to.eql('foobarbaz') + }) }) it('can remove a placeholder
', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '

' cy.get('[contenteditable]:first') - .type('foobar').then(($div) => { - expect($div.get(0).innerHTML).to.eql('
foobar
') - }) + .type('foobar').then(($div) => { + expect($div.get(0).innerHTML).to.eql('
foobar
') + }) }) it('can type into an iframe with designmode = \'on\'', () => { // append a new iframe to the body cy.$$('') - .appendTo(cy.$$('body')) + .appendTo(cy.$$('body')) // wait for iframe to load let loaded = false cy.get('#generic-iframe') - .then(($iframe) => { - $iframe.load(() => { - loaded = true + .then(($iframe) => { + $iframe.load(() => { + loaded = true + }) + }).scrollIntoView() + .should(() => { + expect(loaded).to.eq(true) }) - }).scrollIntoView() - .should(() => { - expect(loaded).to.eq(true) - }) // type text into iframe cy.get('#generic-iframe') - .then(($iframe) => { - $iframe[0].contentDocument.designMode = 'on' - const iframe = $iframe.contents() + .then(($iframe) => { + $iframe[0].contentDocument.designMode = 'on' + const iframe = $iframe.contents() - cy.wrap(iframe.find('html')).first() - .type('{selectall}{del} foo bar baz{enter}ac{leftarrow}b') - }) + cy.wrap(iframe.find('html')).first() + .type('{selectall}{del} foo bar baz{enter}ac{leftarrow}b') + }) // assert that text was typed cy.get('#generic-iframe') - .then(($iframe) => { - const iframeText = $iframe[0].contentDocument.body.innerText + .then(($iframe) => { + const iframeText = $iframe[0].contentDocument.body.innerText - expect(iframeText).to.include('foo bar baz\nabc') - }) + expect(iframeText).to.include('foo bar baz\nabc') + }) }) }) @@ -1508,11 +1545,11 @@ describe('src/cy/commands/actions/type', () => { context('parseSpecialCharSequences: false', () => { it('types special character sequences literally', (done) => { cy.get(':text:first').invoke('val', 'foo') - .type('{{}{backspace}', { parseSpecialCharSequences: false }).then(($input) => { - expect($input).to.have.value('foo{{}{backspace}') + .type('{{}{backspace}', { parseSpecialCharSequences: false }).then(($input) => { + expect($input).to.have.value('foo{{}{backspace}') - done() - }) + done() + }) }) }) @@ -1633,11 +1670,11 @@ describe('src/cy/commands/actions/type', () => { it('can backspace a selection range of characters', () => { // select the 'ar' characters cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(1, 3) - }).get(':text:first').type('{backspace}').then(($input) => { - expect($input).to.have.value('b') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(1, 3) + }).get(':text:first').type('{backspace}').then(($input) => { + expect($input).to.have.value('b') + }) }) it('sets which and keyCode to 8 and does not fire keypress events', (done) => { @@ -1691,11 +1728,11 @@ describe('src/cy/commands/actions/type', () => { it('can delete a selection range of characters', () => { // select the 'ar' characters cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(1, 3) - }).get(':text:first').type('{del}').then(($input) => { - expect($input).to.have.value('b') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(1, 3) + }).get(':text:first').type('{del}').then(($input) => { + expect($input).to.have.value('b') + }) }) it('sets which and keyCode to 46 and does not fire keypress events', (done) => { @@ -1724,16 +1761,17 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('does fire input event when value changes', (done) => { - cy.$$(':text:first').on('input', (e) => { - done() - }) + it('{del} does fire input event when value changes', () => { + const onInput = cy.stub() + + cy.$$(':text:first').on('input', onInput) // select the 'a' characters cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(0, 1) - }).get(':text:first').type('{del}') + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(0, 1) + }).get(':text:first').type('{del}') + .then(() => expect(onInput).calledOnce) }) it('does not fire input event when value does not change', (done) => { @@ -1777,21 +1815,21 @@ describe('src/cy/commands/actions/type', () => { it('sets the cursor to the left bounds', () => { // select the 'a' character cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(1, 2) - }).get(':text:first').type('{leftarrow}n').then(($input) => { - expect($input).to.have.value('bnar') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(1, 2) + }).get(':text:first').type('{leftarrow}n').then(($input) => { + expect($input).to.have.value('bnar') + }) }) it('sets the cursor to the very beginning', () => { // select the 'a' character cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(0, 1) - }).get(':text:first').type('{leftarrow}n').then(($input) => { - expect($input).to.have.value('nbar') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(0, 1) + }).get(':text:first').type('{leftarrow}n').then(($input) => { + expect($input).to.have.value('nbar') + }) }) it('sets which and keyCode to 37 and does not fire keypress events', (done) => { @@ -1866,20 +1904,20 @@ describe('src/cy/commands/actions/type', () => { it('sets the cursor to the rights bounds', () => { // select the 'a' character cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(1, 2) - }).get(':text:first').type('{rightarrow}n').then(($input) => { - expect($input).to.have.value('banr') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(1, 2) + }).get(':text:first').type('{rightarrow}n').then(($input) => { + expect($input).to.have.value('banr') + }) }) it('sets the cursor to the very beginning', () => { cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.select() - }).get(':text:first').type('{leftarrow}n').then(($input) => { - expect($input).to.have.value('nbar') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + return $input.select() + }).get(':text:first').type('{leftarrow}n').then(($input) => { + expect($input).to.have.value('nbar') + }) }) it('sets which and keyCode to 39 and does not fire keypress events', (done) => { @@ -1990,7 +2028,7 @@ describe('src/cy/commands/actions/type', () => { cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz' cy.get('textarea:first') - .type('{home}11{uparrow}{home}22{uparrow}{home}33').should('have.value', '33foo\n22bar\n11baz') + .type('{home}11{uparrow}{home}22{uparrow}{home}33').should('have.value', '33foo\n22bar\n11baz') }) it('should move cursor to the start of each line in contenteditable', () => { @@ -2000,9 +2038,9 @@ describe('src/cy/commands/actions/type', () => { '
baz
' cy.get('[contenteditable]:first') - .type('{home}11{uparrow}{home}22{uparrow}{home}33').then(($div) => { - expect(trimInnerText($div)).to.eql('33foo\n22bar\n11baz') - }) + .type('{home}11{uparrow}{home}22{uparrow}{home}33').then(($div) => { + expect(trimInnerText($div)).to.eql('33foo\n22bar\n11baz') + }) }) }) @@ -2061,7 +2099,7 @@ describe('src/cy/commands/actions/type', () => { cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz' cy.get('textarea:first') - .type('{end}11{uparrow}{end}22{uparrow}{end}33').should('have.value', 'foo33\nbar22\nbaz11') + .type('{end}11{uparrow}{end}22{uparrow}{end}33').should('have.value', 'foo33\nbar22\nbaz11') }) it('should move cursor to the end of each line in contenteditable', () => { @@ -2071,9 +2109,9 @@ describe('src/cy/commands/actions/type', () => { '
baz
' cy.get('[contenteditable]:first') - .type('{end}11{uparrow}{end}22{uparrow}{end}33').then(($div) => { - expect(trimInnerText($div)).to.eql('foo33\nbar22\nbaz11') - }) + .type('{end}11{uparrow}{end}22{uparrow}{end}33').then(($div) => { + expect(trimInnerText($div)).to.eql('foo33\nbar22\nbaz11') + }) }) }) @@ -2127,9 +2165,9 @@ describe('src/cy/commands/actions/type', () => { '
baz
' cy.get('[contenteditable]:first') - .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33').then(($div) => { - expect(trimInnerText($div)).to.eql('foo22\nb11ar\nbaz33') - }) + .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33').then(($div) => { + expect(trimInnerText($div)).to.eql('foo22\nb11ar\nbaz33') + }) }) it('uparrow ignores current selection', () => { @@ -2150,23 +2188,23 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('[contenteditable]:first') - .type('{uparrow}11').then(($div) => { - expect(trimInnerText($div)).to.eql('11foo\nbar\nbaz') - }) + .type('{uparrow}11').then(($div) => { + expect(trimInnerText($div)).to.eql('11foo\nbar\nbaz') + }) }) it('up and down arrow on textarea', () => { cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz' cy.get('textarea:first') - .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33').should('have.value', 'foo22\nb11ar\nbaz33') + .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33').should('have.value', 'foo22\nb11ar\nbaz33') }) it('increments input[type=number]', () => { cy.get('input[type="number"]:first') - .invoke('val', '12.34') - .type('{uparrow}{uparrow}') - .should('have.value', '14') + .invoke('val', '12.34') + .type('{uparrow}{uparrow}') + .should('have.value', '14') }) }) @@ -2217,14 +2255,14 @@ describe('src/cy/commands/actions/type', () => { cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz' cy.get('textarea:first') - .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33{leftarrow}{downarrow}44').should('have.value', 'foo22\nb11ar\nbaz3344') + .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33{leftarrow}{downarrow}44').should('have.value', 'foo22\nb11ar\nbaz3344') }) it('decrements input[type=\'number\']', () => { cy.get('input[type="number"]:first') - .invoke('val', '12.34') - .type('{downarrow}{downarrow}') - .should('have.value', '11') + .invoke('val', '12.34') + .type('{downarrow}{downarrow}') + .should('have.value', '11') }) it('downarrow ignores current selection', () => { @@ -2245,9 +2283,9 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('[contenteditable]:first') - .type('{downarrow}22').then(($div) => { - expect(trimInnerText($div)).to.eql('foo\n22bar\nbaz') - }) + .type('{downarrow}22').then(($div) => { + expect(trimInnerText($div)).to.eql('foo\n22bar\nbaz') + }) }) }) @@ -2339,24 +2377,24 @@ describe('src/cy/commands/actions/type', () => { it('inserts new line into [contenteditable] ', () => { cy.get('#input-types [contenteditable]:first').invoke('text', 'foo') - .type('bar{enter}baz{enter}{enter}{enter}quux').then(function ($div) { - const conditionalNewLines = '\n\n'.repeat(this.multiplierNumNewLines) + .type('bar{enter}baz{enter}{enter}{enter}quux').then(function ($div) { + const conditionalNewLines = '\n\n'.repeat(this.multiplierNumNewLines) - expect(trimInnerText($div)).to.eql(`foobar\nbaz${conditionalNewLines}\nquux`) - expect($div.get(0).textContent).to.eql('foobarbazquux') + expect(trimInnerText($div)).to.eql(`foobar\nbaz${conditionalNewLines}\nquux`) + expect($div.get(0).textContent).to.eql('foobarbazquux') - expect($div.get(0).innerHTML).to.eql('foobar
baz


quux
') - }) + expect($div.get(0).innerHTML).to.eql('foobar
baz


quux
') + }) }) it('inserts new line into [contenteditable] from midline', () => { cy.get('#input-types [contenteditable]:first').invoke('text', 'foo') - .type('bar{leftarrow}{enter}baz{leftarrow}{enter}quux').then(($div) => { - expect(trimInnerText($div)).to.eql('fooba\nba\nquuxzr') - expect($div.get(0).textContent).to.eql('foobabaquuxzr') + .type('bar{leftarrow}{enter}baz{leftarrow}{enter}quux').then(($div) => { + expect(trimInnerText($div)).to.eql('fooba\nba\nquuxzr') + expect($div.get(0).textContent).to.eql('foobabaquuxzr') - expect($div.get(0).innerHTML).to.eql('fooba
ba
quuxzr
') - }) + expect($div.get(0).innerHTML).to.eql('fooba
ba
quuxzr
') + }) }) }) @@ -2574,22 +2612,22 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{command}{control}') - .type('ok') - .then(() => { - expect(events[2].metaKey).to.be.false - expect(events[2].ctrlKey).to.be.false - expect(events[2].which).to.equal(79) + .get('input:text:first') + .type('{command}{control}') + .type('ok') + .then(() => { + expect(events[2].metaKey).to.be.false + expect(events[2].ctrlKey).to.be.false + expect(events[2].which).to.equal(79) - expect(events[3].metaKey).to.be.false - expect(events[3].ctrlKey).to.be.false - expect(events[3].which).to.equal(75) + expect(events[3].metaKey).to.be.false + expect(events[3].ctrlKey).to.be.false + expect(events[3].which).to.equal(75) - $input.off('keydown') + $input.off('keydown') - done() - }) + done() + }) }) it('does not maintain modifiers for subsequent click commands', (done) => { @@ -2611,24 +2649,24 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{cmd}{option}') - .get('button:first').click().then(() => { - expect(mouseDownEvent.metaKey).to.be.false - expect(mouseDownEvent.altKey).to.be.false + .get('input:text:first') + .type('{cmd}{option}') + .get('button:first').click().then(() => { + expect(mouseDownEvent.metaKey).to.be.false + expect(mouseDownEvent.altKey).to.be.false - expect(mouseUpEvent.metaKey).to.be.false - expect(mouseUpEvent.altKey).to.be.false + expect(mouseUpEvent.metaKey).to.be.false + expect(mouseUpEvent.altKey).to.be.false - expect(clickEvent.metaKey).to.be.false - expect(clickEvent.altKey).to.be.false + expect(clickEvent.metaKey).to.be.false + expect(clickEvent.altKey).to.be.false - $button.off('mousedown') - $button.off('mouseup') - $button.off('click') + $button.off('mousedown') + $button.off('mouseup') + $button.off('click') - done() - }) + done() + }) }) it('sends keyup event for activated modifiers when typing is finished', (done) => { @@ -2640,22 +2678,22 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{alt}{ctrl}{meta}{shift}ok') - .then(() => { + .get('input:text:first') + .type('{alt}{ctrl}{meta}{shift}ok') + .then(() => { // first keyups should be for the chars typed, "ok" - expect(events[0].which).to.equal(79) - expect(events[1].which).to.equal(75) + expect(events[0].which).to.equal(79) + expect(events[1].which).to.equal(75) - expect(events[2].which).to.equal(18) - expect(events[3].which).to.equal(17) - expect(events[4].which).to.equal(91) - expect(events[5].which).to.equal(16) + expect(events[2].which).to.equal(18) + expect(events[3].which).to.equal(17) + expect(events[4].which).to.equal(91) + expect(events[5].which).to.equal(16) - $input.off('keyup') + $input.off('keyup') - done() - }) + done() + }) }) }) @@ -2670,20 +2708,20 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{command}{control}', { release: false }) - .type('ok') - .then(() => { - expect(events[2].metaKey).to.be.true - expect(events[2].ctrlKey).to.be.true - expect(events[2].which).to.equal(79) + .get('input:text:first') + .type('{command}{control}', { release: false }) + .type('ok') + .then(() => { + expect(events[2].metaKey).to.be.true + expect(events[2].ctrlKey).to.be.true + expect(events[2].which).to.equal(79) + + expect(events[3].metaKey).to.be.true + expect(events[3].ctrlKey).to.be.true + expect(events[3].which).to.equal(75) - expect(events[3].metaKey).to.be.true - expect(events[3].ctrlKey).to.be.true - expect(events[3].which).to.equal(75) - - done() - }) + done() + }) }) it('maintains modifiers for subsequent click commands', (done) => { @@ -2705,20 +2743,20 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{meta}{alt}', { release: false }) - .get('button:first').click().then(() => { - expect(mouseDownEvent.metaKey).to.be.true - expect(mouseDownEvent.altKey).to.be.true + .get('input:text:first') + .type('{meta}{alt}', { release: false }) + .get('button:first').click().then(() => { + expect(mouseDownEvent.metaKey).to.be.true + expect(mouseDownEvent.altKey).to.be.true - expect(mouseUpEvent.metaKey).to.be.true - expect(mouseUpEvent.altKey).to.be.true + expect(mouseUpEvent.metaKey).to.be.true + expect(mouseUpEvent.altKey).to.be.true - expect(clickEvent.metaKey).to.be.true - expect(clickEvent.altKey).to.be.true + expect(clickEvent.metaKey).to.be.true + expect(clickEvent.altKey).to.be.true - done() - }) + done() + }) }) it('resets modifiers before next test', () => { @@ -2733,14 +2771,14 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('a', { release: false }) - .then(() => { - expect(events[0].metaKey).to.be.false - expect(events[0].ctrlKey).to.be.false + .get('input:text:first') + .type('a', { release: false }) + .then(() => { + expect(events[0].metaKey).to.be.false + expect(events[0].ctrlKey).to.be.false - expect(events[0].altKey).to.be.false - }) + expect(events[0].altKey).to.be.false + }) }) }) @@ -2821,15 +2859,15 @@ describe('src/cy/commands/actions/type', () => { const input = $('').attr('id', 'input-covered-in-span').prependTo(cy.$$('body')) $('span on input') - .css({ - position: 'absolute', - left: input.offset().left, - top: input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: input.offset().left, + top: input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) cy.on('command:retry', (options) => { expect(options.timeout).to.eq(1000) @@ -2899,11 +2937,11 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').type('foo').then(() => { - expect(changed).to.eq(0) - }).get('button:first').click().then(() => { - expect(changed).to.eq(1) - }) + .get(':text:first').type('foo').then(() => { + expect(changed).to.eq(0) + }).get('button:first').click().then(() => { + expect(changed).to.eq(1) + }) }) it('fires when element loses focus due to another action (type)', () => { @@ -2914,11 +2952,11 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').type('foo').then(() => { - expect(changed).to.eq(0) - }).get('textarea:first').type('bar').then(() => { - expect(changed).to.eq(1) - }) + .get(':text:first').type('foo').then(() => { + expect(changed).to.eq(0) + }).get('textarea:first').type('bar').then(() => { + expect(changed).to.eq(1) + }) }) it('fires when element is directly blurred', () => { @@ -2929,9 +2967,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').type('foo').blur().then(() => { - expect(changed).to.eq(1) - }) + .get(':text:first').type('foo').blur().then(() => { + expect(changed).to.eq(1) + }) }) it('fires when element is tabbed away from')//, -> @@ -3003,9 +3041,9 @@ describe('src/cy/commands/actions/type', () => { return $el }).blur() - .then(() => { - expect(changed).to.eq(1) - }) + .then(() => { + expect(changed).to.eq(1) + }) }) it('does not fire if {enter} is preventedDefault', () => { @@ -3046,9 +3084,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').type('foo').then(() => { - expect(changed).to.eq(0) - }) + .get(':text:first').type('foo').then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire change event if value hasnt actually changed', () => { @@ -3059,9 +3097,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').invoke('val', 'foo').type('{backspace}{backspace}oo{enter}').blur().then(() => { - expect(changed).to.eq(0) - }) + .get(':text:first').invoke('val', 'foo').type('{backspace}{backspace}oo{enter}').blur().then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire if mousedown is preventedDefault which prevents element from losing focus', () => { @@ -3076,10 +3114,10 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').invoke('val', 'foo').type('bar') - .get('textarea:first').click().then(() => { - expect(changed).to.eq(0) - }) + .get(':text:first').invoke('val', 'foo').type('bar') + .get('textarea:first').click().then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire hitting {enter} inside of a textarea', () => { @@ -3090,9 +3128,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('textarea:first').type('foo{enter}bar').then(() => { - expect(changed).to.eq(0) - }) + .get('textarea:first').type('foo{enter}bar').then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire hitting {enter} inside of [contenteditable]', () => { @@ -3103,9 +3141,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('[contenteditable]:first').type('foo{enter}bar').then(() => { - expect(changed).to.eq(0) - }) + .get('[contenteditable]:first').type('foo{enter}bar').then(() => { + expect(changed).to.eq(0) + }) }) // [contenteditable] does not fire ANY change events ever. @@ -3117,10 +3155,10 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('[contenteditable]:first').type('foo') - .get('button:first').click().then(() => { - expect(changed).to.eq(0) - }) + .get('[contenteditable]:first').type('foo') + .get('button:first').click().then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire on .clear() without blur', () => { @@ -3131,16 +3169,16 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('input:first').invoke('val', 'foo') - .clear() - .then(($el) => { - expect(changed).to.eq(0) + .clear() + .then(($el) => { + expect(changed).to.eq(0) - return $el - }).type('foo') - .blur() - .then(() => { - expect(changed).to.eq(0) - }) + return $el + }).type('foo') + .blur() + .then(() => { + expect(changed).to.eq(0) + }) }) it('fires change for single value change inputs', () => { @@ -3151,11 +3189,11 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('input[type="date"]:first') - .type('1959-09-13') - .blur() - .then(() => { - expect(changed).to.eql(1) - }) + .type('1959-09-13') + .blur() + .then(() => { + expect(changed).to.eql(1) + }) }) it('does not fire change for non-change single value input', () => { @@ -3166,12 +3204,12 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('input[type="date"]:first') - .invoke('val', '1959-09-13') - .type('1959-09-13') - .blur() - .then(() => { - expect(changed).to.eql(0) - }) + .invoke('val', '1959-09-13') + .type('1959-09-13') + .blur() + .then(() => { + expect(changed).to.eql(0) + }) }) it('does not fire change for type\'d change that restores value', () => { @@ -3182,14 +3220,14 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('input:first') - .invoke('val', 'foo') - .type('{backspace}o') - .invoke('val', 'bar') - .type('{backspace}r') - .blur() - .then(() => { - expect(changed).to.eql(0) - }) + .invoke('val', 'foo') + .type('{backspace}o') + .invoke('val', 'bar') + .type('{backspace}r') + .blur() + .then(() => { + expect(changed).to.eql(0) + }) }) }) @@ -3295,7 +3333,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('input:first').get(0))) - .to.deep.eq({ start: 4, end: 4 }) + .to.deep.eq({ start: 4, end: 4 }) }) }) @@ -3304,7 +3342,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('input:first').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3313,7 +3351,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('input:first').get(0))) - .to.deep.eq({ start: 0, end: 0 }) + .to.deep.eq({ start: 0, end: 0 }) }) }) @@ -3322,7 +3360,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3335,7 +3373,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3344,7 +3382,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) - .to.deep.eq({ start: 1, end: 1 }) + .to.deep.eq({ start: 1, end: 1 }) }) }) @@ -3359,7 +3397,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$(':text:first').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3368,7 +3406,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('#comments').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3382,10 +3420,10 @@ describe('src/cy/commands/actions/type', () => { cy.get('[contenteditable]:first') // move cursor to beginning of div - .type('{selectall}{leftarrow}') - .type(`${'{rightarrow}'.repeat(14)}[_I_]`).then(($el) => { - expect(trimInnerText($el)).to.eql('start\nmiddle\ne[_I_]nd') - }) + .type('{selectall}{leftarrow}') + .type(`${'{rightarrow}'.repeat(14)}[_I_]`).then(($el) => { + expect(trimInnerText($el)).to.eql('start\nmiddle\ne[_I_]nd') + }) }) it('can wrap cursor to prev line in [contenteditable] with {leftarrow}', () => { @@ -3410,10 +3448,10 @@ describe('src/cy/commands/actions/type', () => { const newLines = '\n\n\n'.repeat(this.multiplierNumNewLines) cy.get('[contenteditable]:first') - .type('{selectall}{leftarrow}') - .type(`foobar${'{rightarrow}'.repeat(6)}[_I_]`).then(() => { - expect(trimInnerText($el)).to.eql(`foobar${newLines}\nen[_I_]d`) - }) + .type('{selectall}{leftarrow}') + .type(`foobar${'{rightarrow}'.repeat(6)}[_I_]`).then(() => { + expect(trimInnerText($el)).to.eql(`foobar${newLines}\nen[_I_]d`) + }) }) it('can use {rightarrow} and nested elements', () => { @@ -3423,10 +3461,10 @@ describe('src/cy/commands/actions/type', () => { el.innerHTML = '
start
' cy.get('[contenteditable]:first') - .type('{selectall}{leftarrow}') - .type(`${'{rightarrow}'.repeat(3)}[_I_]`).then(() => { - expect(trimInnerText($el)).to.eql('sta[_I_]rt') - }) + .type('{selectall}{leftarrow}') + .type(`${'{rightarrow}'.repeat(3)}[_I_]`).then(() => { + expect(trimInnerText($el)).to.eql('sta[_I_]rt') + }) }) it('enter and \\n should act the same for [contenteditable]', () => { @@ -3444,27 +3482,29 @@ describe('src/cy/commands/actions/type', () => { const expected = '{\n foo: 1\n bar: 2\n baz: 3\n}' cy.get('[contenteditable]:first') - .invoke('html', '

') - .type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}') - .should(($el) => { - expectMatchInnerText($el, expected) - }).clear() - .type('{{}\n foo: 1\n bar: 2\n baz: 3\n}') - .should(($el) => { - expectMatchInnerText($el, expected) - }) + .invoke('html', '

') + .type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}') + .should(($el) => { + expectMatchInnerText($el, expected) + }) + .clear() + .blur() + .type('{{}\n foo: 1\n bar: 2\n baz: 3\n}') + .should(($el) => { + expectMatchInnerText($el, expected) + }) }) it('enter and \\n should act the same for textarea', () => { const expected = '{\n foo: 1\n bar: 2\n baz: 3\n}' cy.get('textarea:first') - .clear() - .type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}') - .should('have.prop', 'value', expected) - .clear() - .type('{{}\n foo: 1\n bar: 2\n baz: 3\n}') - .should('have.prop', 'value', expected) + .clear() + .type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}') + .should('have.prop', 'value', expected) + .clear() + .type('{{}\n foo: 1\n bar: 2\n baz: 3\n}') + .should('have.prop', 'value', expected) }) }) @@ -3744,7 +3784,7 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('#multiple-inputs-and-reset-and-submit-buttons input:first') - .type('foo{enter}') + .type('foo{enter}') cy.then(() => { expect(submit).calledOnce @@ -3986,11 +4026,11 @@ describe('src/cy/commands/actions/type', () => { } cy - .get('#comments').type('foobarbaz').then(($txt) => { - expectToHaveValueAndCoords() - }).get('#comments').clear().type('onetwothree').then(() => { - expectToHaveValueAndCoords() - }) + .get('#comments').type('foobarbaz').then(($txt) => { + expectToHaveValueAndCoords() + }).get('#comments').clear().type('onetwothree').then(() => { + expectToHaveValueAndCoords() + }) }) it('clones textarea value when textarea is focused first', () => { @@ -4005,11 +4045,11 @@ describe('src/cy/commands/actions/type', () => { } cy - .get('#comments').focus().type('foobarbaz').then(($txt) => { - expectToHaveValueAndNoCoords() - }).get('#comments').clear().type('onetwothree').then(() => { - expectToHaveValueAndNoCoords() - }) + .get('#comments').focus().type('foobarbaz').then(($txt) => { + expectToHaveValueAndNoCoords() + }).get('#comments').clear().type('onetwothree').then(() => { + expectToHaveValueAndNoCoords() + }) }) it('logs only one type event', () => { @@ -4102,31 +4142,31 @@ describe('src/cy/commands/actions/type', () => { it('has a table of keys', () => { cy.get(':text:first').type('{cmd}{option}foo{enter}b{leftarrow}{del}{enter}') - .then(function () { - const table = this.lastLog.invoke('consoleProps').table[3]() + .then(function () { + const table = this.lastLog.invoke('consoleProps').table[3]() - // eslint-disable-next-line + // eslint-disable-next-line console.table(table.data, table.columns) - expect(table.columns).to.deep.eq([ - 'typed', 'which', 'keydown', 'keypress', 'textInput', 'input', 'keyup', 'change', 'modifiers', - ]) - - expect(table.name).to.eq('Keyboard Events') - const expectedTable = { - 1: { typed: '', which: 91, keydown: true, modifiers: 'meta' }, - 2: { typed: '', which: 18, keydown: true, modifiers: 'alt, meta' }, - 3: { typed: 'f', which: 70, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 4: { typed: 'o', which: 79, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 5: { typed: 'o', which: 79, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 6: { typed: '{enter}', which: 13, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 7: { typed: 'b', which: 66, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 8: { typed: '{leftArrow}', which: 37, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 9: { typed: '{del}', which: 46, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 10: { typed: '{enter}', which: 13, keydown: true, keyup: true, modifiers: 'alt, meta' }, - } - - expect(table.data).to.deep.eq(expectedTable) - }) + expect(table.columns).to.deep.eq([ + 'typed', 'which', 'keydown', 'keypress', 'textInput', 'input', 'keyup', 'change', 'modifiers', + ]) + + expect(table.name).to.eq('Keyboard Events') + const expectedTable = { + 1: { typed: '', which: 91, keydown: true, modifiers: 'meta' }, + 2: { typed: '', which: 18, keydown: true, modifiers: 'alt, meta' }, + 3: { typed: 'f', which: 70, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 4: { typed: 'o', which: 79, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 5: { typed: 'o', which: 79, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 6: { typed: '{enter}', which: 13, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 7: { typed: 'b', which: 66, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 8: { typed: '{leftArrow}', which: 37, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 9: { typed: '{del}', which: 46, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 10: { typed: '{enter}', which: 13, keydown: true, keyup: true, modifiers: 'alt, meta' }, + } + + expect(table.data).to.deep.eq(expectedTable) + }) }) // table.data.forEach (item, i) -> @@ -4298,19 +4338,19 @@ describe('src/cy/commands/actions/type', () => { it('throws when input cannot be clicked', function (done) { const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) $('span on button') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) @@ -4341,48 +4381,45 @@ https://on.cypress.io/type`) cy.get(':text:first').type('foo{bar}') }) - it('throws when attemping to type tab', function () { - const onFail = (err) => { + it('throws when attemping to type tab', function (done) { + cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) expect(err.message).to.eq('{tab} isn\'t a supported character sequence. You\'ll want to use the command cy.tab(), which is not ready yet, but when it is done that\'s what you\'ll use.') - } - - cy.on('fail', onFail) + done() + }) cy.get(':text:first').type('foo{tab}') - cy.then(() => expect(onFail).calledOnce) }) - it('throws on an empty string', function () { - const onFail = (err) => { + it('throws on an empty string', function (done) { + + cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) expect(err.message).to.eq('cy.type() cannot accept an empty String. You need to actually type something.') - } - - cy.on('fail', onFail) + done() + }) cy.get(':text:first').type('') - cy.then(() => expect(onFail).calledOnce) }) it('allows typing spaces', () => { cy - .get(':text:first').type(' ') - .should('have.value', ' ') + .get(':text:first').type(' ') + .should('have.value', ' ') }) it('allows typing special characters', () => { cy - .get(':text:first').type('{esc}') - .should('have.value', '') + .get(':text:first').type('{esc}') + .should('have.value', '') }) _.each(['toString', 'toLocaleString', 'hasOwnProperty', 'valueOf', 'undefined', 'null', 'true', 'false', 'True', 'False'], (val) => { it(`allows typing reserved Javscript word (${val})`, () => { cy - .get(':text:first').type(val) - .should('have.value', val) + .get(':text:first').type(val) + .should('have.value', val) }) }) @@ -4392,23 +4429,23 @@ https://on.cypress.io/type`) '', '$USER'], (val) => { it(`allows typing some naughtly strings (${val})`, () => { cy - .get(':text:first').type(val) - .should('have.value', val) + .get(':text:first').type(val) + .should('have.value', val) }) }) }) it('allows typing special characters', () => { cy - .get(':text:first').type('{esc}') - .should('have.value', '') + .get(':text:first').type('{esc}') + .should('have.value', '') }) it('can type into input with invalid type attribute', () => { cy.get(':text:first') - .invoke('attr', 'type', 'asdf') - .type('foobar') - .should('have.value', 'foobar') + .invoke('attr', 'type', 'asdf') + .type('foobar') + .should('have.value', 'foobar') }) describe('throws when trying to type', () => { @@ -4554,8 +4591,8 @@ https://on.cypress.io/type`) context('[type=tel]', () => { it('can edit tel', () => { cy.get('#by-name > input[type="tel"]') - .type('1234567890') - .should('have.prop', 'value', '1234567890') + .type('1234567890') + .should('have.prop', 'value', '1234567890') }) }) @@ -4705,19 +4742,19 @@ https://on.cypress.io/type`) it('can forcibly click even when being covered by another element', () => { const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) $('span on input') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) let clicked = false @@ -4951,19 +4988,19 @@ https://on.cypress.io/type`) it('throws when input cannot be cleared', function (done) { const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) $('span on input') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) diff --git a/packages/runner/src/lib/logger.js b/packages/runner/src/lib/logger.js index 0f6262ca56ad..d39f0c711ccf 100644 --- a/packages/runner/src/lib/logger.js +++ b/packages/runner/src/lib/logger.js @@ -101,7 +101,6 @@ export default { _getTable (consoleProps) { const table = _.result(consoleProps, 'table') - debugger if (!table) return From 87ba79ac49d5c9840a6eb48adc812e8e3ba3c472 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 13 Aug 2019 14:51:30 -0400 Subject: [PATCH 021/370] fix elements util, cleanup debugging code --- packages/driver/src/cy/keyboard.ts | 6 ++++-- packages/driver/src/cy/retries.coffee | 4 +--- packages/driver/src/dom/elements.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index 8c2e64f3e124..bfba1a961e5f 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -195,8 +195,8 @@ const findKeyDetailsOrLowercase = (key: string): KeyDetailsPartial => { const getTextLength = (str) => _.toArray(str).length const getKeyDetails = (onKeyNotFound) => { - return (key: string) => { - let foundKey + return (key: string): KeyDetails => { + let foundKey: KeyDetailsPartial if (getTextLength(key) === 1) { foundKey = USKeyboard[key] @@ -695,6 +695,7 @@ export default class Keyboard { ) _skipCheckUntilIndex = skipCheckUntilIndex + if ( _skipCheckUntilIndex && $elements.isNeedSingleValueChangeInputElement(el) @@ -723,6 +724,7 @@ export default class Keyboard { ) } } + } else { debug('skipping validation due to *skipCheckUntilIndex*', _skipCheckUntilIndex) } diff --git a/packages/driver/src/cy/retries.coffee b/packages/driver/src/cy/retries.coffee index f6ea583e9b76..33f6fe346f29 100644 --- a/packages/driver/src/cy/retries.coffee +++ b/packages/driver/src/cy/retries.coffee @@ -6,9 +6,6 @@ $utils = require("../cypress/utils") create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) -> return { retry: (fn, options, log) -> - if options.error - if !options.error.message.includes('coordsHistory must be') - console.error(options.error) ## remove the runnables timeout because we are now in retry ## mode and should be handling timing out ourselves and dont ## want to accidentally time out via mocha @@ -55,6 +52,7 @@ create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) - _.get(err, 'displayMessage') or _.get(err, 'message') or err + $utils.throwErrByPath "miscellaneous.retry_timed_out", { onFail: (options.onFail or log) args: { error: getErrMessage(options.error) } diff --git a/packages/driver/src/dom/elements.ts b/packages/driver/src/dom/elements.ts index 4356c20e0033..30c0dbbb8341 100644 --- a/packages/driver/src/dom/elements.ts +++ b/packages/driver/src/dom/elements.ts @@ -45,7 +45,7 @@ const focusableWhenNotDisabled = [ //'body,a[href],button,select,[tabindex],input,textarea,[contenteditable]' -const inputTypeNeedSingleValueChangeRe = /^(date|time|week|month)$/ +const inputTypeNeedSingleValueChangeRe = /^(date|time|week|month|datetime-local)$/ const canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/ declare global { From 2f1889a8d22f6c4995e6816dda575692863ec9a2 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 13 Aug 2019 15:46:41 -0400 Subject: [PATCH 022/370] fix linting rules --- .eslintrc.json | 6 +- .../driver/src/cy/commands/actions/type.js | 64 +- packages/driver/src/cy/keyboard.ts | 22 +- packages/driver/src/dom/elements.ts | 12 +- .../integration/commands/actions/type_spec.js | 1156 ++++++++--------- 5 files changed, 632 insertions(+), 628 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 6ed2769f678d..21452fedca9d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,7 +9,11 @@ "indent": "off", "@typescript-eslint/indent": [ "error", - 2 + 2, + { + "SwitchCase": 1, + "MemberExpression": 0 + } ], "@typescript-eslint/no-unused-vars": [ "error", diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index eac8c19ff0ad..ed2caefcc5da 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -479,18 +479,18 @@ module.exports = function (Commands, Cypress, cy, state, config) { timeout: options.timeout, interval: options.interval, }) - .then(() => { + .then(() => { - return type() + return type() - // BEOW DOES NOT APPLY - // cannot just call .focus, since children of contenteditable will not receive cursor - // with .focus() + // BEOW DOES NOT APPLY + // cannot just call .focus, since children of contenteditable will not receive cursor + // with .focus() - // focusCursor calls focus on first focusable - // then moves cursor to end if in textarea, input, or contenteditable + // focusCursor calls focus on first focusable + // then moves cursor to end if in textarea, input, or contenteditable // $selection.focusCursor($elToFocus[0]) - }) + }) } return type() @@ -561,35 +561,35 @@ module.exports = function (Commands, Cypress, cy, state, config) { } return cy - .now('type', $el, '{selectall}{del}', { - $el, - log: false, - verify: false, //# handle verification ourselves - _log: options._log, - force: options.force, - timeout: options.timeout, - interval: options.interval, - }) - .then(() => { - if (options._log) { - options._log.snapshot().end() - } + .now('type', $el, '{selectall}{del}', { + $el, + log: false, + verify: false, //# handle verification ourselves + _log: options._log, + force: options.force, + timeout: options.timeout, + interval: options.interval, + }) + .then(() => { + if (options._log) { + options._log.snapshot().end() + } - return null - }) + return null + }) } return Promise.resolve(subject.toArray()) - .each(clear) - .then(() => { - let verifyAssertions + .each(clear) + .then(() => { + let verifyAssertions - return (verifyAssertions = () => { - return cy.verifyUpcomingAssertions(subject, options, { - onRetry: verifyAssertions, - }) - })() - }) + return (verifyAssertions = () => { + return cy.verifyUpcomingAssertions(subject, options, { + onRetry: verifyAssertions, + }) + })() + }) } return Commands.addAll( diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index bfba1a961e5f..807ebba7bb87 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -769,18 +769,18 @@ export default class Keyboard { return fn() }).delay(options.delay) }) - .then(() => { - if (options.release !== false) { - return Promise.map(modifierKeys, (key) => { - return this.simulatedKeyup(getActiveEl(doc), key, options) - }) - } + .then(() => { + if (options.release !== false) { + return Promise.map(modifierKeys, (key) => { + return this.simulatedKeyup(getActiveEl(doc), key, options) + }) + } - return [] - }) - .then(() => { - options.onAfterType() - }) + return [] + }) + .then(() => { + options.onAfterType() + }) } fireSimulatedEvent ( diff --git a/packages/driver/src/dom/elements.ts b/packages/driver/src/dom/elements.ts index 30c0dbbb8341..897bd2c83586 100644 --- a/packages/driver/src/dom/elements.ts +++ b/packages/driver/src/dom/elements.ts @@ -985,14 +985,14 @@ const stringify = (el, form = 'long') => { const long = () => { const str = $el - .clone() - .empty() - .prop('outerHTML') + .clone() + .empty() + .prop('outerHTML') const text = (_.chain($el.text()) as any) - .clean() - .truncate({ length: 10 }) - .value() + .clean() + .truncate({ length: 10 }) + .value() const children = $el.children().length if (children) { 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 d9ec1bd2ace8..201d7d1a6929 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -12,31 +12,31 @@ const trimInnerText = ($el) => { describe('src/cy/commands/actions/type', () => { before(() => { cy - .visit('/fixtures/dom.html') - .then(function (win) { - const el = cy.$$('[contenteditable]:first').get(0) - - // by default... the last new line by itself - // will only ever count as a single new line... - // but new lines above it will count as 2 new lines... - // so by adding "3" new lines, the last counts as 1 - // and the first 2 count as 2... - el.innerHTML = '

'.repeat(3) - - // browsers changed their implementation - // of the number of newlines that

- // create. newer versions of chrome set 2 new lines - // per set - whereas older ones create only 1 new line. - // so we grab the current sets for the assertion later - // 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 - }) + .visit('/fixtures/dom.html') + .then(function (win) { + const el = cy.$$('[contenteditable]:first').get(0) + + // by default... the last new line by itself + // will only ever count as a single new line... + // but new lines above it will count as 2 new lines... + // so by adding "3" new lines, the last counts as 1 + // and the first 2 count as 2... + el.innerHTML = '

'.repeat(3) + + // browsers changed their implementation + // of the number of newlines that

+ // create. newer versions of chrome set 2 new lines + // per set - whereas older ones create only 1 new line. + // so we grab the current sets for the assertion later + // 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 + }) }) beforeEach(() => { @@ -68,20 +68,20 @@ describe('src/cy/commands/actions/type', () => { it('appends subsequent type commands', () => { cy - .get('input:first').type('123').type('456') - .should('have.value', '123456') + .get('input:first').type('123').type('456') + .should('have.value', '123456') }) it('appends subsequent commands when value is changed in between', () => { cy - .get('input:first') - .type('123') - .then(($input) => { - $input[0].value += '-' - - return $input - }).type('456') - .should('have.value', '123-456') + .get('input:first') + .type('123') + .then(($input) => { + $input[0].value += '-' + + return $input + }).type('456') + .should('have.value', '123-456') }) it('can type numbers', () => { @@ -115,19 +115,19 @@ describe('src/cy/commands/actions/type', () => { cy.get('input:text:first').type('foo') cy.get('input:text:last').type('bar') - .then(() => { - expect(blurred).to.be.true - }) + .then(() => { + expect(blurred).to.be.true + }) }) it('can type into contenteditable', () => { const oldText = cy.$$('#contenteditable').get(0).innerText cy.get('#contenteditable') - .type(' foo') - .then(($div) => { - expect($div.get(0).innerText).to.eq((`${oldText} foo`)) - }) + .type(' foo') + .then(($div) => { + expect($div.get(0).innerText).to.eq((`${oldText} foo`)) + }) }) it('delays 50ms before resolving', () => { @@ -187,22 +187,22 @@ describe('src/cy/commands/actions/type', () => { it('can forcibly click even when being covered by another element', () => { const $input = $('') - .attr('id', 'input-covered-in-span') - .css({ - width: 50, - }) - .prependTo(cy.$$('body')) + .attr('id', 'input-covered-in-span') + .css({ + width: 50, + }) + .prependTo(cy.$$('body')) $('span on input') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) let clicked = false @@ -284,8 +284,8 @@ describe('src/cy/commands/actions/type', () => { }) cy.stub(cy, 'ensureElementIsNotAnimating') - .throws(new Error('animating!')) - .onThirdCall().returns() + .throws(new Error('animating!')) + .onThirdCall().returns() cy.get(':text:first').type('foo').then(() => { // - retry animation coords @@ -499,12 +499,12 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('#tabindex').type('f{leftarrow}{rightarrow}{enter}') - .then(() => { - expect(keydowns).to.have.length(4) - expect(keypresses).to.have.length(2) + .then(() => { + expect(keydowns).to.have.length(4) + expect(keypresses).to.have.length(2) - expect(keyups).to.have.length(4) - }) + expect(keyups).to.have.length(4) + }) }) }) @@ -513,10 +513,10 @@ describe('src/cy/commands/actions/type', () => { cy.spy(cy, 'timeout') cy.get(':text:first') - .type('foo{enter}bar{leftarrow}', { delay: 5 }) - .then(() => { - expect(cy.timeout).to.be.calledWith(5 * 8, true, 'type') - }) + .type('foo{enter}bar{leftarrow}', { delay: 5 }) + .then(() => { + expect(cy.timeout).to.be.calledWith(5 * 8, true, 'type') + }) }) it('can cancel additional keystrokes', (done) => { @@ -680,43 +680,43 @@ describe('src/cy/commands/actions/type', () => { cy.$$(':text:first').on('input', onInput) cy.get(':text:first') - .invoke('val', 'bar') - .type('{selectAll}{rightarrow}{backspace}') - .then(() => { - expect(onInput).calledOnce - }) - .then(() => { - onInput.reset() - }) + .invoke('val', 'bar') + .type('{selectAll}{rightarrow}{backspace}') + .then(() => { + expect(onInput).calledOnce + }) + .then(() => { + onInput.reset() + }) cy.get(':text:first') - .invoke('val', 'bar') - .type('{selectAll}{leftarrow}{del}') - .then(() => { - expect(onInput).calledOnce - }) - .then(() => { - onInput.reset() - }) + .invoke('val', 'bar') + .type('{selectAll}{leftarrow}{del}') + .then(() => { + expect(onInput).calledOnce + }) + .then(() => { + onInput.reset() + }) cy.$$('[contenteditable]:first').on('input', onInput) cy.get('[contenteditable]:first') - .invoke('html', 'foobar') - .type('{selectAll}{rightarrow}{backspace}') - .then(() => { - expect(onInput).calledOnce - }) - .then(() => { - onInput.reset() - }) + .invoke('html', 'foobar') + .type('{selectAll}{rightarrow}{backspace}') + .then(() => { + expect(onInput).calledOnce + }) + .then(() => { + onInput.reset() + }) cy.get('[contenteditable]:first') - .invoke('html', 'foobar') - .type('{selectAll}{leftarrow}{del}') - .then(() => { - expect(onInput).calledOnce - }) + .invoke('html', 'foobar') + .type('{selectAll}{leftarrow}{del}') + .then(() => { + expect(onInput).calledOnce + }) }) it('does not fire input event when value does not change', () => { @@ -727,54 +727,54 @@ describe('src/cy/commands/actions/type', () => { }) cy.get(':text:first') - .invoke('val', 'bar') - .type('{selectAll}{rightarrow}{del}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('val', 'bar') + .type('{selectAll}{rightarrow}{del}') + .then(() => { + expect(fired).to.eq(false) + }) cy.get(':text:first') - .invoke('val', 'bar') - .type('{selectAll}{leftarrow}{backspace}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('val', 'bar') + .type('{selectAll}{leftarrow}{backspace}') + .then(() => { + expect(fired).to.eq(false) + }) cy.$$('textarea:first').on('input', () => { fired = true }) cy.get('textarea:first') - .invoke('val', 'bar') - .type('{selectAll}{rightarrow}{del}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('val', 'bar') + .type('{selectAll}{rightarrow}{del}') + .then(() => { + expect(fired).to.eq(false) + }) cy.get('textarea:first') - .invoke('val', 'bar') - .type('{selectAll}{leftarrow}{backspace}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('val', 'bar') + .type('{selectAll}{leftarrow}{backspace}') + .then(() => { + expect(fired).to.eq(false) + }) cy.$$('[contenteditable]:first').on('input', () => { fired = true }) cy.get('[contenteditable]:first') - .invoke('html', 'foobar') - .type('{movetoend}') - .then(($el) => { - expect(fired).to.eq(false) - }) + .invoke('html', 'foobar') + .type('{movetoend}') + .then(($el) => { + expect(fired).to.eq(false) + }) cy.get('[contenteditable]:first') - .invoke('html', 'foobar') - .type('{selectAll}{leftarrow}{backspace}') - .then(() => { - expect(fired).to.eq(false) - }) + .invoke('html', 'foobar') + .type('{selectAll}{leftarrow}{backspace}') + .then(() => { + expect(fired).to.eq(false) + }) }) }) @@ -785,10 +785,10 @@ describe('src/cy/commands/actions/type', () => { $input.attr('maxlength', 5) cy.get(':text:first') - .type('1234567890') - .then((input) => { - expect(input).to.have.value('12345') - }) + .type('1234567890') + .then((input) => { + expect(input).to.have.value('12345') + }) }) it('ignores an invalid maxlength attribute', () => { @@ -797,10 +797,10 @@ describe('src/cy/commands/actions/type', () => { $input.attr('maxlength', 'five') cy.get(':text:first') - .type('1234567890') - .then((input) => { - expect(input).to.have.value('1234567890') - }) + .type('1234567890') + .then((input) => { + expect(input).to.have.value('1234567890') + }) }) it('handles special characters', () => { @@ -809,10 +809,10 @@ describe('src/cy/commands/actions/type', () => { $input.attr('maxlength', 5) cy.get(':text:first') - .type('12{selectall}') - .then((input) => { - expect(input).to.have.value('12') - }) + .type('12{selectall}') + .then((input) => { + expect(input).to.have.value('12') + }) }) it('maxlength=0 events', () => { @@ -825,21 +825,21 @@ describe('src/cy/commands/actions/type', () => { } cy - .$$(':text:first') - .attr('maxlength', 0) - .on('keydown', push('keydown')) - .on('keypress', push('keypress')) - .on('textInput', push('textInput')) - .on('input', push('input')) - .on('keyup', push('keyup')) + .$$(':text:first') + .attr('maxlength', 0) + .on('keydown', push('keydown')) + .on('keypress', push('keypress')) + .on('textInput', push('textInput')) + .on('input', push('input')) + .on('keyup', push('keyup')) cy.get(':text:first') - .type('1') - .then(() => { - expect(events).to.deep.eq([ - 'keydown', 'keypress', 'textInput', 'keyup', - ]) - }) + .type('1') + .then(() => { + expect(events).to.deep.eq([ + 'keydown', 'keypress', 'textInput', 'keyup', + ]) + }) }) it('maxlength=1 events', () => { @@ -852,22 +852,22 @@ describe('src/cy/commands/actions/type', () => { } cy - .$$(':text:first') - .attr('maxlength', 1) - .on('keydown', push('keydown')) - .on('keypress', push('keypress')) - .on('textInput', push('textInput')) - .on('input', push('input')) - .on('keyup', push('keyup')) + .$$(':text:first') + .attr('maxlength', 1) + .on('keydown', push('keydown')) + .on('keypress', push('keypress')) + .on('textInput', push('textInput')) + .on('input', push('input')) + .on('keyup', push('keyup')) cy.get(':text:first') - .type('12') - .then(() => { - expect(events).to.deep.eq([ - 'keydown', 'keypress', 'textInput', 'input', 'keyup', - 'keydown', 'keypress', 'textInput', 'keyup', - ]) - }) + .type('12') + .then(() => { + expect(events).to.deep.eq([ + 'keydown', 'keypress', 'textInput', 'input', 'keyup', + 'keydown', 'keypress', 'textInput', 'keyup', + ]) + }) }) }) @@ -1132,24 +1132,24 @@ describe('src/cy/commands/actions/type', () => { it('can type negative numbers', () => { cy.get('#number-without-value') - .type('-123.12') - .should('have.value', '-123.12') + .type('-123.12') + .should('have.value', '-123.12') }) it('can type {del}', () => { cy.get('#number-with-value') - .type('{selectAll}{del}') - .should('have.value', '') + .type('{selectAll}{del}') + .should('have.value', '') }) it('can type {selectAll}{del}', () => { const sentInput = cy.stub() cy.get('#number-with-value') - .then(($el) => $el.on('input', sentInput)) - .type('{selectAll}{del}') - .should('have.value', '') - .then(() => expect(sentInput).calledOnce) + .then(($el) => $el.on('input', sentInput)) + .type('{selectAll}{del}') + .should('have.value', '') + .then(() => expect(sentInput).calledOnce) }) @@ -1158,10 +1158,10 @@ describe('src/cy/commands/actions/type', () => { const sentInput = cy.stub() cy.get('#number-without-value') - .then(($el) => $el.on('input', sentInput)) - .type('{selectAll}{del}') - .should('have.value', '') - .then(() => expect(sentInput).not.called) + .then(($el) => $el.on('input', sentInput)) + .type('{selectAll}{del}') + .should('have.value', '') + .then(() => expect(sentInput).not.called) }) it('type=number blurs consistently', () => { @@ -1172,10 +1172,10 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('#number-without-value') - .type('200').blur() - .then(() => { - expect(blurred).to.eq(1) - }) + .type('200').blur() + .then(() => { + expect(blurred).to.eq(1) + }) }) }) @@ -1226,10 +1226,10 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('#email-without-value') - .type('foo@bar.com').blur() - .then(() => { - expect(blurred).to.eq(1) - }) + .type('foo@bar.com').blur() + .then(() => { + expect(blurred).to.eq(1) + }) }) }) @@ -1271,18 +1271,18 @@ describe('src/cy/commands/actions/type', () => { } cy - .$$('#password-without-value') - .val('secret') - .click(select) - .keyup((e) => { - switch (e.key) { - case 'g': - return select(e) - case 'n': - return e.target.setSelectionRange(0, 1) - default: - } - }) + .$$('#password-without-value') + .val('secret') + .click(select) + .keyup((e) => { + switch (e.key) { + case 'g': + return select(e) + case 'n': + return e.target.setSelectionRange(0, 1) + default: + } + }) cy.get('#password-without-value').type('agent').then(($input) => { expect($input).to.have.value('tn') @@ -1419,102 +1419,102 @@ describe('src/cy/commands/actions/type', () => { cy.get('#input-types [contenteditable]') - .invoke('text', 'foo') - .then(($el) => $el.on('input', onInput)) - .then(($el) => $el.on('input', onTextInput)) - .type('\n').then(($text) => { - expect(trimInnerText($text)).eq('foo') - }) - .then(() => expect(onInput).calledOnce) - .then(() => expect(onTextInput).calledOnce) + .invoke('text', 'foo') + .then(($el) => $el.on('input', onInput)) + .then(($el) => $el.on('input', onTextInput)) + .type('\n').then(($text) => { + expect(trimInnerText($text)).eq('foo') + }) + .then(() => expect(onInput).calledOnce) + .then(() => expect(onTextInput).calledOnce) }) it('can type into [contenteditable] with existing
', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '
foo
' cy.get('[contenteditable]:first') - .type('bar').then(($div) => { - expect(trimInnerText($div)).to.eql('foobar') - expect($div.get(0).textContent).to.eql('foobar') + .type('bar').then(($div) => { + expect(trimInnerText($div)).to.eql('foobar') + expect($div.get(0).textContent).to.eql('foobar') - expect($div.get(0).innerHTML).to.eql('
foobar
') - }) + expect($div.get(0).innerHTML).to.eql('
foobar
') + }) }) it('can type into [contenteditable] with existing

', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '

foo

' cy.get('[contenteditable]:first') - .type('bar').then(($div) => { - expect(trimInnerText($div)).to.eql('foobar') - expect($div.get(0).textContent).to.eql('foobar') + .type('bar').then(($div) => { + expect(trimInnerText($div)).to.eql('foobar') + expect($div.get(0).textContent).to.eql('foobar') - expect($div.get(0).innerHTML).to.eql('

foobar

') - }) + expect($div.get(0).innerHTML).to.eql('

foobar

') + }) }) it('collapses selection to start on {leftarrow}', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '
bar
' cy.get('[contenteditable]:first') - .type('{selectall}{leftarrow}foo').then(($div) => { - expect(trimInnerText($div)).to.eql('foobar') - }) + .type('{selectall}{leftarrow}foo').then(($div) => { + expect(trimInnerText($div)).to.eql('foobar') + }) }) it('collapses selection to end on {rightarrow}', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '
bar
' cy.get('[contenteditable]:first') - .type('{selectall}{leftarrow}foo{selectall}{rightarrow}baz').then(($div) => { - expect(trimInnerText($div)).to.eql('foobarbaz') - }) + .type('{selectall}{leftarrow}foo{selectall}{rightarrow}baz').then(($div) => { + expect(trimInnerText($div)).to.eql('foobarbaz') + }) }) it('can remove a placeholder
', () => { cy.$$('[contenteditable]:first').get(0).innerHTML = '

' cy.get('[contenteditable]:first') - .type('foobar').then(($div) => { - expect($div.get(0).innerHTML).to.eql('
foobar
') - }) + .type('foobar').then(($div) => { + expect($div.get(0).innerHTML).to.eql('
foobar
') + }) }) it('can type into an iframe with designmode = \'on\'', () => { // append a new iframe to the body cy.$$('') - .appendTo(cy.$$('body')) + .appendTo(cy.$$('body')) // wait for iframe to load let loaded = false cy.get('#generic-iframe') - .then(($iframe) => { - $iframe.load(() => { - loaded = true - }) - }).scrollIntoView() - .should(() => { - expect(loaded).to.eq(true) + .then(($iframe) => { + $iframe.load(() => { + loaded = true }) + }).scrollIntoView() + .should(() => { + expect(loaded).to.eq(true) + }) // type text into iframe cy.get('#generic-iframe') - .then(($iframe) => { - $iframe[0].contentDocument.designMode = 'on' - const iframe = $iframe.contents() + .then(($iframe) => { + $iframe[0].contentDocument.designMode = 'on' + const iframe = $iframe.contents() - cy.wrap(iframe.find('html')).first() - .type('{selectall}{del} foo bar baz{enter}ac{leftarrow}b') - }) + cy.wrap(iframe.find('html')).first() + .type('{selectall}{del} foo bar baz{enter}ac{leftarrow}b') + }) // assert that text was typed cy.get('#generic-iframe') - .then(($iframe) => { - const iframeText = $iframe[0].contentDocument.body.innerText + .then(($iframe) => { + const iframeText = $iframe[0].contentDocument.body.innerText - expect(iframeText).to.include('foo bar baz\nabc') - }) + expect(iframeText).to.include('foo bar baz\nabc') + }) }) }) @@ -1545,11 +1545,11 @@ describe('src/cy/commands/actions/type', () => { context('parseSpecialCharSequences: false', () => { it('types special character sequences literally', (done) => { cy.get(':text:first').invoke('val', 'foo') - .type('{{}{backspace}', { parseSpecialCharSequences: false }).then(($input) => { - expect($input).to.have.value('foo{{}{backspace}') + .type('{{}{backspace}', { parseSpecialCharSequences: false }).then(($input) => { + expect($input).to.have.value('foo{{}{backspace}') - done() - }) + done() + }) }) }) @@ -1670,11 +1670,11 @@ describe('src/cy/commands/actions/type', () => { it('can backspace a selection range of characters', () => { // select the 'ar' characters cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(1, 3) - }).get(':text:first').type('{backspace}').then(($input) => { - expect($input).to.have.value('b') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(1, 3) + }).get(':text:first').type('{backspace}').then(($input) => { + expect($input).to.have.value('b') + }) }) it('sets which and keyCode to 8 and does not fire keypress events', (done) => { @@ -1728,11 +1728,11 @@ describe('src/cy/commands/actions/type', () => { it('can delete a selection range of characters', () => { // select the 'ar' characters cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(1, 3) - }).get(':text:first').type('{del}').then(($input) => { - expect($input).to.have.value('b') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(1, 3) + }).get(':text:first').type('{del}').then(($input) => { + expect($input).to.have.value('b') + }) }) it('sets which and keyCode to 46 and does not fire keypress events', (done) => { @@ -1768,10 +1768,10 @@ describe('src/cy/commands/actions/type', () => { // select the 'a' characters cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(0, 1) - }).get(':text:first').type('{del}') - .then(() => expect(onInput).calledOnce) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(0, 1) + }).get(':text:first').type('{del}') + .then(() => expect(onInput).calledOnce) }) it('does not fire input event when value does not change', (done) => { @@ -1815,21 +1815,21 @@ describe('src/cy/commands/actions/type', () => { it('sets the cursor to the left bounds', () => { // select the 'a' character cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(1, 2) - }).get(':text:first').type('{leftarrow}n').then(($input) => { - expect($input).to.have.value('bnar') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(1, 2) + }).get(':text:first').type('{leftarrow}n').then(($input) => { + expect($input).to.have.value('bnar') + }) }) it('sets the cursor to the very beginning', () => { // select the 'a' character cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(0, 1) - }).get(':text:first').type('{leftarrow}n').then(($input) => { - expect($input).to.have.value('nbar') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(0, 1) + }).get(':text:first').type('{leftarrow}n').then(($input) => { + expect($input).to.have.value('nbar') + }) }) it('sets which and keyCode to 37 and does not fire keypress events', (done) => { @@ -1904,20 +1904,20 @@ describe('src/cy/commands/actions/type', () => { it('sets the cursor to the rights bounds', () => { // select the 'a' character cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - $input.get(0).setSelectionRange(1, 2) - }).get(':text:first').type('{rightarrow}n').then(($input) => { - expect($input).to.have.value('banr') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + $input.get(0).setSelectionRange(1, 2) + }).get(':text:first').type('{rightarrow}n').then(($input) => { + expect($input).to.have.value('banr') + }) }) it('sets the cursor to the very beginning', () => { cy - .get(':text:first').invoke('val', 'bar').focus().then(($input) => { - return $input.select() - }).get(':text:first').type('{leftarrow}n').then(($input) => { - expect($input).to.have.value('nbar') - }) + .get(':text:first').invoke('val', 'bar').focus().then(($input) => { + return $input.select() + }).get(':text:first').type('{leftarrow}n').then(($input) => { + expect($input).to.have.value('nbar') + }) }) it('sets which and keyCode to 39 and does not fire keypress events', (done) => { @@ -2028,7 +2028,7 @@ describe('src/cy/commands/actions/type', () => { cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz' cy.get('textarea:first') - .type('{home}11{uparrow}{home}22{uparrow}{home}33').should('have.value', '33foo\n22bar\n11baz') + .type('{home}11{uparrow}{home}22{uparrow}{home}33').should('have.value', '33foo\n22bar\n11baz') }) it('should move cursor to the start of each line in contenteditable', () => { @@ -2038,9 +2038,9 @@ describe('src/cy/commands/actions/type', () => { '
baz
' cy.get('[contenteditable]:first') - .type('{home}11{uparrow}{home}22{uparrow}{home}33').then(($div) => { - expect(trimInnerText($div)).to.eql('33foo\n22bar\n11baz') - }) + .type('{home}11{uparrow}{home}22{uparrow}{home}33').then(($div) => { + expect(trimInnerText($div)).to.eql('33foo\n22bar\n11baz') + }) }) }) @@ -2099,7 +2099,7 @@ describe('src/cy/commands/actions/type', () => { cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz' cy.get('textarea:first') - .type('{end}11{uparrow}{end}22{uparrow}{end}33').should('have.value', 'foo33\nbar22\nbaz11') + .type('{end}11{uparrow}{end}22{uparrow}{end}33').should('have.value', 'foo33\nbar22\nbaz11') }) it('should move cursor to the end of each line in contenteditable', () => { @@ -2109,9 +2109,9 @@ describe('src/cy/commands/actions/type', () => { '
baz
' cy.get('[contenteditable]:first') - .type('{end}11{uparrow}{end}22{uparrow}{end}33').then(($div) => { - expect(trimInnerText($div)).to.eql('foo33\nbar22\nbaz11') - }) + .type('{end}11{uparrow}{end}22{uparrow}{end}33').then(($div) => { + expect(trimInnerText($div)).to.eql('foo33\nbar22\nbaz11') + }) }) }) @@ -2165,9 +2165,9 @@ describe('src/cy/commands/actions/type', () => { '
baz
' cy.get('[contenteditable]:first') - .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33').then(($div) => { - expect(trimInnerText($div)).to.eql('foo22\nb11ar\nbaz33') - }) + .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33').then(($div) => { + expect(trimInnerText($div)).to.eql('foo22\nb11ar\nbaz33') + }) }) it('uparrow ignores current selection', () => { @@ -2188,23 +2188,23 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('[contenteditable]:first') - .type('{uparrow}11').then(($div) => { - expect(trimInnerText($div)).to.eql('11foo\nbar\nbaz') - }) + .type('{uparrow}11').then(($div) => { + expect(trimInnerText($div)).to.eql('11foo\nbar\nbaz') + }) }) it('up and down arrow on textarea', () => { cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz' cy.get('textarea:first') - .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33').should('have.value', 'foo22\nb11ar\nbaz33') + .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33').should('have.value', 'foo22\nb11ar\nbaz33') }) it('increments input[type=number]', () => { cy.get('input[type="number"]:first') - .invoke('val', '12.34') - .type('{uparrow}{uparrow}') - .should('have.value', '14') + .invoke('val', '12.34') + .type('{uparrow}{uparrow}') + .should('have.value', '14') }) }) @@ -2255,14 +2255,14 @@ describe('src/cy/commands/actions/type', () => { cy.$$('textarea:first').get(0).value = 'foo\nbar\nbaz' cy.get('textarea:first') - .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33{leftarrow}{downarrow}44').should('have.value', 'foo22\nb11ar\nbaz3344') + .type('{leftarrow}{leftarrow}{uparrow}11{uparrow}22{downarrow}{downarrow}33{leftarrow}{downarrow}44').should('have.value', 'foo22\nb11ar\nbaz3344') }) it('decrements input[type=\'number\']', () => { cy.get('input[type="number"]:first') - .invoke('val', '12.34') - .type('{downarrow}{downarrow}') - .should('have.value', '11') + .invoke('val', '12.34') + .type('{downarrow}{downarrow}') + .should('have.value', '11') }) it('downarrow ignores current selection', () => { @@ -2283,9 +2283,9 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('[contenteditable]:first') - .type('{downarrow}22').then(($div) => { - expect(trimInnerText($div)).to.eql('foo\n22bar\nbaz') - }) + .type('{downarrow}22').then(($div) => { + expect(trimInnerText($div)).to.eql('foo\n22bar\nbaz') + }) }) }) @@ -2377,24 +2377,24 @@ describe('src/cy/commands/actions/type', () => { it('inserts new line into [contenteditable] ', () => { cy.get('#input-types [contenteditable]:first').invoke('text', 'foo') - .type('bar{enter}baz{enter}{enter}{enter}quux').then(function ($div) { - const conditionalNewLines = '\n\n'.repeat(this.multiplierNumNewLines) + .type('bar{enter}baz{enter}{enter}{enter}quux').then(function ($div) { + const conditionalNewLines = '\n\n'.repeat(this.multiplierNumNewLines) - expect(trimInnerText($div)).to.eql(`foobar\nbaz${conditionalNewLines}\nquux`) - expect($div.get(0).textContent).to.eql('foobarbazquux') + expect(trimInnerText($div)).to.eql(`foobar\nbaz${conditionalNewLines}\nquux`) + expect($div.get(0).textContent).to.eql('foobarbazquux') - expect($div.get(0).innerHTML).to.eql('foobar
baz


quux
') - }) + expect($div.get(0).innerHTML).to.eql('foobar
baz


quux
') + }) }) it('inserts new line into [contenteditable] from midline', () => { cy.get('#input-types [contenteditable]:first').invoke('text', 'foo') - .type('bar{leftarrow}{enter}baz{leftarrow}{enter}quux').then(($div) => { - expect(trimInnerText($div)).to.eql('fooba\nba\nquuxzr') - expect($div.get(0).textContent).to.eql('foobabaquuxzr') + .type('bar{leftarrow}{enter}baz{leftarrow}{enter}quux').then(($div) => { + expect(trimInnerText($div)).to.eql('fooba\nba\nquuxzr') + expect($div.get(0).textContent).to.eql('foobabaquuxzr') - expect($div.get(0).innerHTML).to.eql('fooba
ba
quuxzr
') - }) + expect($div.get(0).innerHTML).to.eql('fooba
ba
quuxzr
') + }) }) }) @@ -2612,22 +2612,22 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{command}{control}') - .type('ok') - .then(() => { - expect(events[2].metaKey).to.be.false - expect(events[2].ctrlKey).to.be.false - expect(events[2].which).to.equal(79) + .get('input:text:first') + .type('{command}{control}') + .type('ok') + .then(() => { + expect(events[2].metaKey).to.be.false + expect(events[2].ctrlKey).to.be.false + expect(events[2].which).to.equal(79) - expect(events[3].metaKey).to.be.false - expect(events[3].ctrlKey).to.be.false - expect(events[3].which).to.equal(75) + expect(events[3].metaKey).to.be.false + expect(events[3].ctrlKey).to.be.false + expect(events[3].which).to.equal(75) - $input.off('keydown') + $input.off('keydown') - done() - }) + done() + }) }) it('does not maintain modifiers for subsequent click commands', (done) => { @@ -2649,24 +2649,24 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{cmd}{option}') - .get('button:first').click().then(() => { - expect(mouseDownEvent.metaKey).to.be.false - expect(mouseDownEvent.altKey).to.be.false + .get('input:text:first') + .type('{cmd}{option}') + .get('button:first').click().then(() => { + expect(mouseDownEvent.metaKey).to.be.false + expect(mouseDownEvent.altKey).to.be.false - expect(mouseUpEvent.metaKey).to.be.false - expect(mouseUpEvent.altKey).to.be.false + expect(mouseUpEvent.metaKey).to.be.false + expect(mouseUpEvent.altKey).to.be.false - expect(clickEvent.metaKey).to.be.false - expect(clickEvent.altKey).to.be.false + expect(clickEvent.metaKey).to.be.false + expect(clickEvent.altKey).to.be.false - $button.off('mousedown') - $button.off('mouseup') - $button.off('click') + $button.off('mousedown') + $button.off('mouseup') + $button.off('click') - done() - }) + done() + }) }) it('sends keyup event for activated modifiers when typing is finished', (done) => { @@ -2678,22 +2678,22 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{alt}{ctrl}{meta}{shift}ok') - .then(() => { + .get('input:text:first') + .type('{alt}{ctrl}{meta}{shift}ok') + .then(() => { // first keyups should be for the chars typed, "ok" - expect(events[0].which).to.equal(79) - expect(events[1].which).to.equal(75) + expect(events[0].which).to.equal(79) + expect(events[1].which).to.equal(75) - expect(events[2].which).to.equal(18) - expect(events[3].which).to.equal(17) - expect(events[4].which).to.equal(91) - expect(events[5].which).to.equal(16) + expect(events[2].which).to.equal(18) + expect(events[3].which).to.equal(17) + expect(events[4].which).to.equal(91) + expect(events[5].which).to.equal(16) - $input.off('keyup') + $input.off('keyup') - done() - }) + done() + }) }) }) @@ -2708,20 +2708,20 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{command}{control}', { release: false }) - .type('ok') - .then(() => { - expect(events[2].metaKey).to.be.true - expect(events[2].ctrlKey).to.be.true - expect(events[2].which).to.equal(79) - - expect(events[3].metaKey).to.be.true - expect(events[3].ctrlKey).to.be.true - expect(events[3].which).to.equal(75) + .get('input:text:first') + .type('{command}{control}', { release: false }) + .type('ok') + .then(() => { + expect(events[2].metaKey).to.be.true + expect(events[2].ctrlKey).to.be.true + expect(events[2].which).to.equal(79) - done() - }) + expect(events[3].metaKey).to.be.true + expect(events[3].ctrlKey).to.be.true + expect(events[3].which).to.equal(75) + + done() + }) }) it('maintains modifiers for subsequent click commands', (done) => { @@ -2743,20 +2743,20 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('{meta}{alt}', { release: false }) - .get('button:first').click().then(() => { - expect(mouseDownEvent.metaKey).to.be.true - expect(mouseDownEvent.altKey).to.be.true + .get('input:text:first') + .type('{meta}{alt}', { release: false }) + .get('button:first').click().then(() => { + expect(mouseDownEvent.metaKey).to.be.true + expect(mouseDownEvent.altKey).to.be.true - expect(mouseUpEvent.metaKey).to.be.true - expect(mouseUpEvent.altKey).to.be.true + expect(mouseUpEvent.metaKey).to.be.true + expect(mouseUpEvent.altKey).to.be.true - expect(clickEvent.metaKey).to.be.true - expect(clickEvent.altKey).to.be.true + expect(clickEvent.metaKey).to.be.true + expect(clickEvent.altKey).to.be.true - done() - }) + done() + }) }) it('resets modifiers before next test', () => { @@ -2771,14 +2771,14 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('input:text:first') - .type('a', { release: false }) - .then(() => { - expect(events[0].metaKey).to.be.false - expect(events[0].ctrlKey).to.be.false + .get('input:text:first') + .type('a', { release: false }) + .then(() => { + expect(events[0].metaKey).to.be.false + expect(events[0].ctrlKey).to.be.false - expect(events[0].altKey).to.be.false - }) + expect(events[0].altKey).to.be.false + }) }) }) @@ -2859,15 +2859,15 @@ describe('src/cy/commands/actions/type', () => { const input = $('').attr('id', 'input-covered-in-span').prependTo(cy.$$('body')) $('span on input') - .css({ - position: 'absolute', - left: input.offset().left, - top: input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: input.offset().left, + top: input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) cy.on('command:retry', (options) => { expect(options.timeout).to.eq(1000) @@ -2937,11 +2937,11 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').type('foo').then(() => { - expect(changed).to.eq(0) - }).get('button:first').click().then(() => { - expect(changed).to.eq(1) - }) + .get(':text:first').type('foo').then(() => { + expect(changed).to.eq(0) + }).get('button:first').click().then(() => { + expect(changed).to.eq(1) + }) }) it('fires when element loses focus due to another action (type)', () => { @@ -2952,11 +2952,11 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').type('foo').then(() => { - expect(changed).to.eq(0) - }).get('textarea:first').type('bar').then(() => { - expect(changed).to.eq(1) - }) + .get(':text:first').type('foo').then(() => { + expect(changed).to.eq(0) + }).get('textarea:first').type('bar').then(() => { + expect(changed).to.eq(1) + }) }) it('fires when element is directly blurred', () => { @@ -2967,9 +2967,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').type('foo').blur().then(() => { - expect(changed).to.eq(1) - }) + .get(':text:first').type('foo').blur().then(() => { + expect(changed).to.eq(1) + }) }) it('fires when element is tabbed away from')//, -> @@ -3041,9 +3041,9 @@ describe('src/cy/commands/actions/type', () => { return $el }).blur() - .then(() => { - expect(changed).to.eq(1) - }) + .then(() => { + expect(changed).to.eq(1) + }) }) it('does not fire if {enter} is preventedDefault', () => { @@ -3084,9 +3084,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').type('foo').then(() => { - expect(changed).to.eq(0) - }) + .get(':text:first').type('foo').then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire change event if value hasnt actually changed', () => { @@ -3097,9 +3097,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').invoke('val', 'foo').type('{backspace}{backspace}oo{enter}').blur().then(() => { - expect(changed).to.eq(0) - }) + .get(':text:first').invoke('val', 'foo').type('{backspace}{backspace}oo{enter}').blur().then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire if mousedown is preventedDefault which prevents element from losing focus', () => { @@ -3114,10 +3114,10 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get(':text:first').invoke('val', 'foo').type('bar') - .get('textarea:first').click().then(() => { - expect(changed).to.eq(0) - }) + .get(':text:first').invoke('val', 'foo').type('bar') + .get('textarea:first').click().then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire hitting {enter} inside of a textarea', () => { @@ -3128,9 +3128,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('textarea:first').type('foo{enter}bar').then(() => { - expect(changed).to.eq(0) - }) + .get('textarea:first').type('foo{enter}bar').then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire hitting {enter} inside of [contenteditable]', () => { @@ -3141,9 +3141,9 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('[contenteditable]:first').type('foo{enter}bar').then(() => { - expect(changed).to.eq(0) - }) + .get('[contenteditable]:first').type('foo{enter}bar').then(() => { + expect(changed).to.eq(0) + }) }) // [contenteditable] does not fire ANY change events ever. @@ -3155,10 +3155,10 @@ describe('src/cy/commands/actions/type', () => { }) cy - .get('[contenteditable]:first').type('foo') - .get('button:first').click().then(() => { - expect(changed).to.eq(0) - }) + .get('[contenteditable]:first').type('foo') + .get('button:first').click().then(() => { + expect(changed).to.eq(0) + }) }) it('does not fire on .clear() without blur', () => { @@ -3169,16 +3169,16 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('input:first').invoke('val', 'foo') - .clear() - .then(($el) => { - expect(changed).to.eq(0) + .clear() + .then(($el) => { + expect(changed).to.eq(0) - return $el - }).type('foo') - .blur() - .then(() => { - expect(changed).to.eq(0) - }) + return $el + }).type('foo') + .blur() + .then(() => { + expect(changed).to.eq(0) + }) }) it('fires change for single value change inputs', () => { @@ -3189,11 +3189,11 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('input[type="date"]:first') - .type('1959-09-13') - .blur() - .then(() => { - expect(changed).to.eql(1) - }) + .type('1959-09-13') + .blur() + .then(() => { + expect(changed).to.eql(1) + }) }) it('does not fire change for non-change single value input', () => { @@ -3204,12 +3204,12 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('input[type="date"]:first') - .invoke('val', '1959-09-13') - .type('1959-09-13') - .blur() - .then(() => { - expect(changed).to.eql(0) - }) + .invoke('val', '1959-09-13') + .type('1959-09-13') + .blur() + .then(() => { + expect(changed).to.eql(0) + }) }) it('does not fire change for type\'d change that restores value', () => { @@ -3220,14 +3220,14 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('input:first') - .invoke('val', 'foo') - .type('{backspace}o') - .invoke('val', 'bar') - .type('{backspace}r') - .blur() - .then(() => { - expect(changed).to.eql(0) - }) + .invoke('val', 'foo') + .type('{backspace}o') + .invoke('val', 'bar') + .type('{backspace}r') + .blur() + .then(() => { + expect(changed).to.eql(0) + }) }) }) @@ -3333,7 +3333,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('input:first').get(0))) - .to.deep.eq({ start: 4, end: 4 }) + .to.deep.eq({ start: 4, end: 4 }) }) }) @@ -3342,7 +3342,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('input:first').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3351,7 +3351,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('input:first').get(0))) - .to.deep.eq({ start: 0, end: 0 }) + .to.deep.eq({ start: 0, end: 0 }) }) }) @@ -3360,7 +3360,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3373,7 +3373,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3382,7 +3382,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('[contenteditable]:first').get(0))) - .to.deep.eq({ start: 1, end: 1 }) + .to.deep.eq({ start: 1, end: 1 }) }) }) @@ -3397,7 +3397,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$(':text:first').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3406,7 +3406,7 @@ describe('src/cy/commands/actions/type', () => { cy.window().then((win) => { expect(Cypress.dom.getSelectionBounds(Cypress.$('#comments').get(0))) - .to.deep.eq({ start: 6, end: 6 }) + .to.deep.eq({ start: 6, end: 6 }) }) }) @@ -3420,10 +3420,10 @@ describe('src/cy/commands/actions/type', () => { cy.get('[contenteditable]:first') // move cursor to beginning of div - .type('{selectall}{leftarrow}') - .type(`${'{rightarrow}'.repeat(14)}[_I_]`).then(($el) => { - expect(trimInnerText($el)).to.eql('start\nmiddle\ne[_I_]nd') - }) + .type('{selectall}{leftarrow}') + .type(`${'{rightarrow}'.repeat(14)}[_I_]`).then(($el) => { + expect(trimInnerText($el)).to.eql('start\nmiddle\ne[_I_]nd') + }) }) it('can wrap cursor to prev line in [contenteditable] with {leftarrow}', () => { @@ -3448,10 +3448,10 @@ describe('src/cy/commands/actions/type', () => { const newLines = '\n\n\n'.repeat(this.multiplierNumNewLines) cy.get('[contenteditable]:first') - .type('{selectall}{leftarrow}') - .type(`foobar${'{rightarrow}'.repeat(6)}[_I_]`).then(() => { - expect(trimInnerText($el)).to.eql(`foobar${newLines}\nen[_I_]d`) - }) + .type('{selectall}{leftarrow}') + .type(`foobar${'{rightarrow}'.repeat(6)}[_I_]`).then(() => { + expect(trimInnerText($el)).to.eql(`foobar${newLines}\nen[_I_]d`) + }) }) it('can use {rightarrow} and nested elements', () => { @@ -3461,10 +3461,10 @@ describe('src/cy/commands/actions/type', () => { el.innerHTML = '
start
' cy.get('[contenteditable]:first') - .type('{selectall}{leftarrow}') - .type(`${'{rightarrow}'.repeat(3)}[_I_]`).then(() => { - expect(trimInnerText($el)).to.eql('sta[_I_]rt') - }) + .type('{selectall}{leftarrow}') + .type(`${'{rightarrow}'.repeat(3)}[_I_]`).then(() => { + expect(trimInnerText($el)).to.eql('sta[_I_]rt') + }) }) it('enter and \\n should act the same for [contenteditable]', () => { @@ -3482,29 +3482,29 @@ describe('src/cy/commands/actions/type', () => { const expected = '{\n foo: 1\n bar: 2\n baz: 3\n}' cy.get('[contenteditable]:first') - .invoke('html', '

') - .type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}') - .should(($el) => { - expectMatchInnerText($el, expected) - }) - .clear() - .blur() - .type('{{}\n foo: 1\n bar: 2\n baz: 3\n}') - .should(($el) => { - expectMatchInnerText($el, expected) - }) + .invoke('html', '

') + .type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}') + .should(($el) => { + expectMatchInnerText($el, expected) + }) + .clear() + .blur() + .type('{{}\n foo: 1\n bar: 2\n baz: 3\n}') + .should(($el) => { + expectMatchInnerText($el, expected) + }) }) it('enter and \\n should act the same for textarea', () => { const expected = '{\n foo: 1\n bar: 2\n baz: 3\n}' cy.get('textarea:first') - .clear() - .type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}') - .should('have.prop', 'value', expected) - .clear() - .type('{{}\n foo: 1\n bar: 2\n baz: 3\n}') - .should('have.prop', 'value', expected) + .clear() + .type('{{}{enter} foo: 1{enter} bar: 2{enter} baz: 3{enter}}') + .should('have.prop', 'value', expected) + .clear() + .type('{{}\n foo: 1\n bar: 2\n baz: 3\n}') + .should('have.prop', 'value', expected) }) }) @@ -3784,7 +3784,7 @@ describe('src/cy/commands/actions/type', () => { }) cy.get('#multiple-inputs-and-reset-and-submit-buttons input:first') - .type('foo{enter}') + .type('foo{enter}') cy.then(() => { expect(submit).calledOnce @@ -4026,11 +4026,11 @@ describe('src/cy/commands/actions/type', () => { } cy - .get('#comments').type('foobarbaz').then(($txt) => { - expectToHaveValueAndCoords() - }).get('#comments').clear().type('onetwothree').then(() => { - expectToHaveValueAndCoords() - }) + .get('#comments').type('foobarbaz').then(($txt) => { + expectToHaveValueAndCoords() + }).get('#comments').clear().type('onetwothree').then(() => { + expectToHaveValueAndCoords() + }) }) it('clones textarea value when textarea is focused first', () => { @@ -4045,11 +4045,11 @@ describe('src/cy/commands/actions/type', () => { } cy - .get('#comments').focus().type('foobarbaz').then(($txt) => { - expectToHaveValueAndNoCoords() - }).get('#comments').clear().type('onetwothree').then(() => { - expectToHaveValueAndNoCoords() - }) + .get('#comments').focus().type('foobarbaz').then(($txt) => { + expectToHaveValueAndNoCoords() + }).get('#comments').clear().type('onetwothree').then(() => { + expectToHaveValueAndNoCoords() + }) }) it('logs only one type event', () => { @@ -4142,31 +4142,31 @@ describe('src/cy/commands/actions/type', () => { it('has a table of keys', () => { cy.get(':text:first').type('{cmd}{option}foo{enter}b{leftarrow}{del}{enter}') - .then(function () { - const table = this.lastLog.invoke('consoleProps').table[3]() + .then(function () { + const table = this.lastLog.invoke('consoleProps').table[3]() - // eslint-disable-next-line + // eslint-disable-next-line console.table(table.data, table.columns) - expect(table.columns).to.deep.eq([ - 'typed', 'which', 'keydown', 'keypress', 'textInput', 'input', 'keyup', 'change', 'modifiers', - ]) - - expect(table.name).to.eq('Keyboard Events') - const expectedTable = { - 1: { typed: '', which: 91, keydown: true, modifiers: 'meta' }, - 2: { typed: '', which: 18, keydown: true, modifiers: 'alt, meta' }, - 3: { typed: 'f', which: 70, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 4: { typed: 'o', which: 79, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 5: { typed: 'o', which: 79, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 6: { typed: '{enter}', which: 13, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 7: { typed: 'b', which: 66, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 8: { typed: '{leftArrow}', which: 37, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 9: { typed: '{del}', which: 46, keydown: true, keyup: true, modifiers: 'alt, meta' }, - 10: { typed: '{enter}', which: 13, keydown: true, keyup: true, modifiers: 'alt, meta' }, - } - - expect(table.data).to.deep.eq(expectedTable) - }) + expect(table.columns).to.deep.eq([ + 'typed', 'which', 'keydown', 'keypress', 'textInput', 'input', 'keyup', 'change', 'modifiers', + ]) + + expect(table.name).to.eq('Keyboard Events') + const expectedTable = { + 1: { typed: '', which: 91, keydown: true, modifiers: 'meta' }, + 2: { typed: '', which: 18, keydown: true, modifiers: 'alt, meta' }, + 3: { typed: 'f', which: 70, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 4: { typed: 'o', which: 79, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 5: { typed: 'o', which: 79, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 6: { typed: '{enter}', which: 13, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 7: { typed: 'b', which: 66, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 8: { typed: '{leftArrow}', which: 37, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 9: { typed: '{del}', which: 46, keydown: true, keyup: true, modifiers: 'alt, meta' }, + 10: { typed: '{enter}', which: 13, keydown: true, keyup: true, modifiers: 'alt, meta' }, + } + + expect(table.data).to.deep.eq(expectedTable) + }) }) // table.data.forEach (item, i) -> @@ -4338,19 +4338,19 @@ describe('src/cy/commands/actions/type', () => { it('throws when input cannot be clicked', function (done) { const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) $('span on button') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) @@ -4404,22 +4404,22 @@ https://on.cypress.io/type`) it('allows typing spaces', () => { cy - .get(':text:first').type(' ') - .should('have.value', ' ') + .get(':text:first').type(' ') + .should('have.value', ' ') }) it('allows typing special characters', () => { cy - .get(':text:first').type('{esc}') - .should('have.value', '') + .get(':text:first').type('{esc}') + .should('have.value', '') }) _.each(['toString', 'toLocaleString', 'hasOwnProperty', 'valueOf', 'undefined', 'null', 'true', 'false', 'True', 'False'], (val) => { it(`allows typing reserved Javscript word (${val})`, () => { cy - .get(':text:first').type(val) - .should('have.value', val) + .get(':text:first').type(val) + .should('have.value', val) }) }) @@ -4429,23 +4429,23 @@ https://on.cypress.io/type`) '', '$USER'], (val) => { it(`allows typing some naughtly strings (${val})`, () => { cy - .get(':text:first').type(val) - .should('have.value', val) + .get(':text:first').type(val) + .should('have.value', val) }) }) }) it('allows typing special characters', () => { cy - .get(':text:first').type('{esc}') - .should('have.value', '') + .get(':text:first').type('{esc}') + .should('have.value', '') }) it('can type into input with invalid type attribute', () => { cy.get(':text:first') - .invoke('attr', 'type', 'asdf') - .type('foobar') - .should('have.value', 'foobar') + .invoke('attr', 'type', 'asdf') + .type('foobar') + .should('have.value', 'foobar') }) describe('throws when trying to type', () => { @@ -4591,8 +4591,8 @@ https://on.cypress.io/type`) context('[type=tel]', () => { it('can edit tel', () => { cy.get('#by-name > input[type="tel"]') - .type('1234567890') - .should('have.prop', 'value', '1234567890') + .type('1234567890') + .should('have.prop', 'value', '1234567890') }) }) @@ -4742,19 +4742,19 @@ https://on.cypress.io/type`) it('can forcibly click even when being covered by another element', () => { const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) $('span on input') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) let clicked = false @@ -4988,19 +4988,19 @@ https://on.cypress.io/type`) it('throws when input cannot be cleared', function (done) { const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) $('span on input') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) cy.on('fail', (err) => { expect(this.logs.length).to.eq(2) From f5fe2d583684489690da5cc55f9772924d31f192 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Wed, 14 Aug 2019 15:21:15 -0400 Subject: [PATCH 023/370] fix type_spec in electron by using plain Event contructor --- packages/driver/src/cy/keyboard.js | 754 ------------------ packages/driver/src/cy/keyboard.ts | 6 +- .../driver/src/cypress/UsKeyboardLayout.js | 17 +- .../integration/e2e/keyboard_spec.coffee | 6 +- 4 files changed, 23 insertions(+), 760 deletions(-) delete mode 100644 packages/driver/src/cy/keyboard.js diff --git a/packages/driver/src/cy/keyboard.js b/packages/driver/src/cy/keyboard.js deleted file mode 100644 index 26d489917075..000000000000 --- a/packages/driver/src/cy/keyboard.js +++ /dev/null @@ -1,754 +0,0 @@ -const _ = require('lodash') -const Promise = require('bluebird') -const $elements = require('../dom/elements') -const $selection = require('../dom/selection') - -const isSingleDigitRe = /^\d$/ -const isStartingDigitRe = /^\d/ -const charsBetweenCurlyBracesRe = /({.+?})/ - -// Keyboard event map -// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values -const keyStandardMap = { - // Cypress keyboard key : Standard value - '{backspace}': 'Backspace', - '{insert}': 'Insert', - '{del}': 'Delete', - '{downarrow}': 'ArrowDown', - '{enter}': 'Enter', - '{esc}': 'Escape', - '{leftarrow}': 'ArrowLeft', - '{rightarrow}': 'ArrowRight', - '{uparrow}': 'ArrowUp', - '{home}': 'Home', - '{end}': 'End', - '{alt}': 'Alt', - '{ctrl}': 'Control', - '{meta}': 'Meta', - '{shift}': 'Shift', - '{pageup}': 'PageUp', - '{pagedown}': 'PageDown', -} - -const initialModifiers = { - alt: false, - ctrl: false, - meta: false, - shift: false, -} - -const create = (state) => { - const kb = { - getActiveModifiers () { - return _.clone(state('keyboardModifiers')) || _.clone(initialModifiers) - }, - - 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 kb.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 - } - - // text is selected, so delete the selection - // contents and send the input event - $selection.deleteSelectionContents(el) - options.input = true - - }) - }, - - // 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 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) - }, - - // "{tab}": (el, rng) -> - - '{{}' (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 kb.ensureKey(el, null, options, () => { - 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 kb.ensureKey(el, null, options, () => { - 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 kb.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 kb.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, - parseSpecialCharSequences: true, - onEvent () {}, - onBeforeEvent () {}, - onBeforeType () {}, - onValueChange () {}, - onEnterPressed () {}, - onNoMatchingSpecialChars () {}, - onBeforeSpecialCharAction () {}, - }) - - const el = options.$el.get(0) - - let keys = options.chars - - if (options.parseSpecialCharSequences) { - keys = options.chars.split(charsBetweenCurlyBracesRe).map((chars) => { - if (charsBetweenCurlyBracesRe.test(chars)) { - // allow special chars and modifiers to be case-insensitive - return chars.toLowerCase() - } - - return chars - }) - } - - options.onBeforeType(kb.countNumIndividualKeyStrokes(keys)) - - // 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(el, options.window) - } - }) - }, - - 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 (kb.isModifier(chars)) { - return memo - } - - return memo + chars.length - - } - , 0) - }, - - typeChars (el, chars, options) { - options = _.clone(options) - - switch (false) { - case !kb.isSpecialChar(chars): { - return Promise - .resolve(kb.handleSpecialChars(el, chars, options)) - .delay(options.delay) - } - case !kb.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(kb.modifierChars)).join(', ') - - return Promise - .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) - - return kb.charCodeMap[code] != null ? kb.charCodeMap[code] : code - }, - - getAsciiCode (key) { - const code = key.charCodeAt(0) - - return code - }, - - expectedValueDoesNotMatchCurrentValue (expected, rng) { - return expected !== rng.all() - }, - - moveCaretToEnd (rng) { - const len = rng.length() - - return rng.bounds([len, len]) - }, - - simulateKey (el, eventType, key, options) { - // bail if we've said not to fire this specific event - // in our options - - let charCode - let keyCode - let which - - if (options[eventType] === false) { - return true - } - - key = options.key != null ? options.key : key - - let keys = true - let otherKeys = true - - const event = new Event(eventType, { - bubbles: true, - cancelable: eventType !== 'input', - }) - - switch (eventType) { - case 'keydown': case 'keyup': { - keyCode = options.charCode != null ? options.charCode : kb.getKeyCode(key.toUpperCase()) - - charCode = 0 - which = keyCode - break - } - case 'keypress': { - const asciiCode = options.charCode != null ? options.charCode : kb.getAsciiCode(key) - - charCode = asciiCode - keyCode = asciiCode - which = asciiCode - break - } - case 'textInput': { - charCode = 0 - keyCode = 0 - which = 0 - otherKeys = false - _.extend(event, { - data: key, - }) - - break - } - - case 'input': { - keys = false - otherKeys = false - break - } - - default: null - } - - if (otherKeys) { - _.extend(event, { - location: 0, - repeat: false, - }) - - kb.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 = kb.keyToStandard(options.setKey || key) - - _.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] - - // 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) - - args.push(dispatched) - - options.onEvent.apply(this, args) - - return dispatched - }, - - typeKey (el, key, options) { - return kb.ensureKey(el, key, options, () => { - - 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 (!isDigit && (isDigitsInText || !isValidCharacter || (selectionStart !== 0))) { - options.prevChar = key - - 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 - } - } - - return options.updateValue(el, key) - }) - }, - - ensureKey (el, key, options, fn) { - _.defaults(options, { - prevText: null, - }) - - 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 - - if (!$elements.isContentEditable(el)) { - prevText = $elements.getNativeProp(el, 'value') - } - - fn.call(this) - - if ((options.prevText === null) && !$elements.isContentEditable(el)) { - options.prevText = prevText - options.onValueChange(options.prevText, el) - } - } - - return kb.simulateKey(el, 'input', key, options) - } - - if (kb.simulateKey(el, 'keydown', key, options)) { - if (kb.simulateKey(el, 'keypress', key, options)) { - if (kb.simulateKey(el, 'textInput', key, options)) { - - let ml - - if ($elements.isInput(el) || $elements.isTextarea(el)) { - ml = el.maxLength - } - - // 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() - } - } - } - } - - return kb.simulateKey(el, 'keyup', key, options) - }, - - isSpecialChar (chars) { - let needle - - return (needle = chars, _.keys(kb.specialChars).includes(needle)) - }, - - handleSpecialChars (el, chars, options) { - options.key = chars - - return kb.specialChars[chars].call(this, el, options) - }, - - isModifier (chars) { - let needle - - return (needle = chars, _.keys(kb.modifierChars).includes(needle)) - }, - - handleModifier (el, chars, options) { - const modifier = kb.modifierChars[chars] - - const activeModifiers = kb.getActiveModifiers() - - // do nothing if already activated - if (activeModifiers[modifier]) { - return - } - - activeModifiers[modifier] = true - state('keyboardModifiers', activeModifiers) - - return kb.simulateModifier(el, 'keydown', modifier, options) - }, - - simulateModifier (el, eventType, modifier, options) { - return kb.simulateKey(el, eventType, null, _.extend(options, { - charCode: kb.modifierCodeMap[modifier], - id: _.uniqueId('char'), - key: `<${modifier}>`, - })) - }, - - mixinModifiers (event) { - const activeModifiers = kb.getActiveModifiers() - - return _.extend(event, { - altKey: activeModifiers.alt, - ctrlKey: activeModifiers.ctrl, - metaKey: activeModifiers.meta, - shiftKey: activeModifiers.shift, - }) - }, - - getActiveModifiersArray () { - return _.reduce(kb.getActiveModifiers(), (memo, isActivated, modifier) => { - if (isActivated) { - memo.push(modifier) - } - - return memo - } - , []) - }, - - resetModifiers (el, window) { - return (() => { - const result = [] - - const activeModifiers = kb.getActiveModifiers() - - for (let modifier in activeModifiers) { - const isActivated = activeModifiers[modifier] - - activeModifiers[modifier] = false - state('keyboardModifiers', activeModifiers) - if (isActivated) { - result.push(kb.simulateModifier(el, 'keyup', modifier, { - window, - onBeforeEvent () {}, - onEvent () {}, - })) - } else { - result.push(undefined) - } - } - - return result - })() - }, - } - - return kb -} - -module.exports = { create } diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index 807ebba7bb87..2b9f5b567d78 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -911,7 +911,11 @@ export default class Keyboard { // or is IE } else { - event = new win[eventConstructor](eventType, eventOptions) + // For some reason we can't set certain props on Keyboard Events in chrome < 63. + // So we'll use the plain Event constructor + // event = new win[eventConstructor](eventType, eventOptions) + event = new win['Event'](eventType, eventOptions) + _.extend(event, eventOptions) } const dispatched = el.dispatchEvent(event) diff --git a/packages/driver/src/cypress/UsKeyboardLayout.js b/packages/driver/src/cypress/UsKeyboardLayout.js index 2259821c0523..35316ce7dde7 100644 --- a/packages/driver/src/cypress/UsKeyboardLayout.js +++ b/packages/driver/src/cypress/UsKeyboardLayout.js @@ -321,10 +321,19 @@ module.exports = { y: { keyCode: 89, key: 'y', code: 'KeyY' }, z: { keyCode: 90, key: 'z', code: 'KeyZ' }, Meta: { keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1 }, - '*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 }, - '+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 }, - '-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 }, - '/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 }, + + '*': { keyCode: 56, code: 'Digit8', key: '*' }, + // '*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 }, + + '+': { keyCode: 187, code: 'Equal', key: '+' }, + // '+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 }, + + '-': { keyCode: 189, code: 'Minus', key: '-' }, + // '-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 }, + + '/': { keyCode: 191, code: 'Slash', key: '/' }, + // '/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 }, + ';': { keyCode: 186, key: ';', code: 'Semicolon' }, '=': { keyCode: 187, key: '=', code: 'Equal' }, ',': { keyCode: 188, key: ',', code: 'Comma' }, diff --git a/packages/driver/test/cypress/integration/e2e/keyboard_spec.coffee b/packages/driver/test/cypress/integration/e2e/keyboard_spec.coffee index 23c1695d40e0..81bd17735c02 100644 --- a/packages/driver/test/cypress/integration/e2e/keyboard_spec.coffee +++ b/packages/driver/test/cypress/integration/e2e/keyboard_spec.coffee @@ -27,7 +27,11 @@ describe "keyboard", -> characters = [ ['.', 46, 190], ['/', 47, 191], - ['{enter}', 13, 13] + ['{enter}', 13, 13], + ['*', 42, 56], + ['+', 43, 187], + ['-', 45, 189], + ] characters.forEach ([char, asciiCode, keyCode]) -> 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 024/370] 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 025/370] 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 357ef2943e1a0f115b77eb2cace1a06943145e39 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 15 Aug 2019 13:51:48 -0400 Subject: [PATCH 026/370] use webpack-preprocessor (required for typescript files) --- packages/desktop-gui/webpack.config.ts | 4 +- packages/driver/package.json | 1 + packages/driver/test/cypress/plugins/index.js | 7 + packages/reporter/webpack.config.ts | 4 +- packages/runner/webpack.config.ts | 4 +- packages/web-config/webpack.config.base.ts | 317 +++++++++--------- 6 files changed, 176 insertions(+), 161 deletions(-) diff --git a/packages/desktop-gui/webpack.config.ts b/packages/desktop-gui/webpack.config.ts index b99ac09363ef..b992daaab1d1 100644 --- a/packages/desktop-gui/webpack.config.ts +++ b/packages/desktop-gui/webpack.config.ts @@ -1,6 +1,8 @@ -import commonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' +import getCommonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' import path from 'path' +const commonConfig = getCommonConfig() + const config: typeof commonConfig = { ...commonConfig, entry: { diff --git a/packages/driver/package.json b/packages/driver/package.json index 91cf2f75e0db..00f4b306610c 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -16,6 +16,7 @@ "@cypress/sinon-chai": "1.1.0", "@cypress/underscore.inflection": "1.0.1", "@cypress/unique-selector": "0.4.2", + "@cypress/webpack-preprocessor": "4.1.0", "@cypress/what-is-circular": "1.0.1", "angular": "1.7.7", "backbone": "1.4.0", diff --git a/packages/driver/test/cypress/plugins/index.js b/packages/driver/test/cypress/plugins/index.js index 317be19c1ef1..72fd81d488bc 100644 --- a/packages/driver/test/cypress/plugins/index.js +++ b/packages/driver/test/cypress/plugins/index.js @@ -1,9 +1,16 @@ +require('@packages/ts/register') + const _ = require('lodash') const path = require('path') const fs = require('fs-extra') const Promise = require('bluebird') +const webpack = require('@cypress/webpack-preprocessor') + +const webpackOptions = require('@packages/runner/webpack.config.ts').default module.exports = (on) => { + on('file:preprocessor', webpack({ webpackOptions })) + on('task', { 'return:arg' (arg) { return arg diff --git a/packages/reporter/webpack.config.ts b/packages/reporter/webpack.config.ts index bb422a5eff2c..2989417c863d 100644 --- a/packages/reporter/webpack.config.ts +++ b/packages/reporter/webpack.config.ts @@ -1,6 +1,8 @@ -import commonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' +import getCommonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' import path from 'path' +const commonConfig = getCommonConfig() + const config: typeof commonConfig = { ...commonConfig, entry: { diff --git a/packages/runner/webpack.config.ts b/packages/runner/webpack.config.ts index 6805f6888e62..f07501754026 100644 --- a/packages/runner/webpack.config.ts +++ b/packages/runner/webpack.config.ts @@ -1,6 +1,8 @@ -import commonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' +import getCommonConfig, { HtmlWebpackPlugin } from '@packages/web-config/webpack.config.base' import path from 'path' +const commonConfig = getCommonConfig() + const config: typeof commonConfig = { ...commonConfig, entry: { diff --git a/packages/web-config/webpack.config.base.ts b/packages/web-config/webpack.config.base.ts index 75781733bbf9..44af9b8e6a95 100644 --- a/packages/web-config/webpack.config.base.ts +++ b/packages/web-config/webpack.config.base.ts @@ -22,178 +22,179 @@ if (liveReloadEnabled && watchModeEnabled) console.log(chalk.gray(`\nLive Reload process.env.NODE_ENV = env -const config: webpack.Configuration = { - mode: 'none', - node: { - fs: 'empty', - child_process: 'empty', - net: 'empty', - tls: 'empty', - module: 'empty', - }, - resolve: { - extensions: ['.ts', '.js', '.jsx', '.tsx', '.coffee', '.scss', '.json'], - }, - - stats: { - errors: true, - warningsFilter: /node_modules\/mocha\/lib\/mocha.js/, - warnings: true, - all: false, - builtAt: true, - colors: true, - modules: true, - maxModules: 20, - excludeModules: /main.scss/, - timings: true, - }, - - module: { - rules: [ - { - test: /\.coffee/, - exclude: /node_modules/, - use: { - loader: require.resolve('coffee-loader'), - }, - }, - { - test: /\.(ts|js|jsx|tsx)$/, - exclude: /node_modules/, - use: { - loader: require.resolve('babel-loader'), - options: { - plugins: [ - // "istanbul", - [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }], - [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }], - ], - presets: [ - require.resolve('@babel/preset-env'), - require.resolve('@babel/preset-react'), - require.resolve('@babel/preset-typescript'), - ], - babelrc: false, +const getCommonConfig = (): webpack.Configuration => { + return { + mode: 'none', + node: { + fs: 'empty', + child_process: 'empty', + net: 'empty', + tls: 'empty', + module: 'empty', + }, + resolve: { + extensions: ['.ts', '.js', '.jsx', '.tsx', '.coffee', '.scss', '.json'], + }, + + stats: { + errors: true, + warningsFilter: /node_modules\/mocha\/lib\/mocha.js/, + warnings: true, + all: false, + builtAt: true, + colors: true, + modules: true, + maxModules: 20, + excludeModules: /main.scss/, + timings: true, + }, + + module: { + rules: [ + { + test: /\.coffee/, + exclude: /node_modules/, + use: { + loader: require.resolve('coffee-loader'), }, }, - }, - { - test: /\.s?css$/, - exclude: /node_modules/, - use: [ - { loader: MiniCSSExtractWebpackPlugin.loader }, - ], - }, - { - test: /\.s?css$/, - exclude: /node_modules/, - enforce: 'pre', - use: [ - { - loader: require.resolve('css-loader'), - options: { - // sourceMap: true, - modules: false, - }, - }, // translates CSS into CommonJS - { - loader: require.resolve('postcss-loader'), + { + test: /\.(ts|js|jsx|tsx)$/, + exclude: /node_modules/, + use: { + loader: require.resolve('babel-loader'), options: { plugins: [ - require('autoprefixer')({ overrideBrowserslist: ['last 2 versions'], cascade: false }), + // "istanbul", + [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }], + [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }], + ], + presets: [ + require.resolve('@babel/preset-env'), + require.resolve('@babel/preset-react'), + require.resolve('@babel/preset-typescript'), ], + babelrc: false, }, }, - { - loader: require.resolve('resolve-url-loader'), - }, - { - loader: require.resolve('sass-loader'), - options: { - sourceMap: true, - importer (...args) { - args[0] = args[0].replace(/\\/g, '/') - args[1] = args[1].replace(/\\/g, '/') - - return sassGlobImporter.apply(this, args) + }, + { + test: /\.s?css$/, + exclude: /node_modules/, + use: [ + { loader: MiniCSSExtractWebpackPlugin.loader }, + ], + }, + { + test: /\.s?css$/, + exclude: /node_modules/, + enforce: 'pre', + use: [ + { + loader: require.resolve('css-loader'), + options: { + // sourceMap: true, + modules: false, + }, + }, // translates CSS into CommonJS + { + loader: require.resolve('postcss-loader'), + options: { + plugins: [ + require('autoprefixer')({ overrideBrowserslist: ['last 2 versions'], cascade: false }), + ], }, }, - }, // compiles Sass to CSS, using Node Sass by default - ], - }, - { - test: /\.(eot|svg|ttf|woff|woff2)$/, - use: [ - { - loader: require.resolve('file-loader'), - options: { - name: './fonts/[name].[ext]', + { + loader: require.resolve('resolve-url-loader'), }, - }, - ], - }, - { - test: /\.(png)$/, - use: [ - { - loader: require.resolve('file-loader'), - options: { - name: './img/[name].[ext]', + { + loader: require.resolve('sass-loader'), + options: { + sourceMap: true, + importer (...args) { + args[0] = args[0].replace(/\\/g, '/') + args[1] = args[1].replace(/\\/g, '/') + + return sassGlobImporter.apply(this, args) + }, + }, + }, // compiles Sass to CSS, using Node Sass by default + ], + }, + { + test: /\.(eot|svg|ttf|woff|woff2)$/, + use: [ + { + loader: require.resolve('file-loader'), + options: { + name: './fonts/[name].[ext]', + }, }, - }, - ], - }, - ], - }, - - optimization: { - usedExports: true, - providedExports: true, - sideEffects: true, - namedChunks: true, - namedModules: true, - removeAvailableModules: true, - mergeDuplicateChunks: true, - flagIncludedChunks: true, - removeEmptyChunks: true, - }, - - plugins: [ - new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }), - new MiniCSSExtractWebpackPlugin(), - - // Enable source maps / eval maps - // 'EvalDevtoolModulePlugin' is used in development - // because it is fast and maps to filenames while showing compiled source - // 'SourceMapDevToolPlugin' is used in production for the same reasons as 'eval', but it - // shows full source and does not cause crossorigin errors like 'eval' (in Chromium < 63) - // files will be mapped like: `cypress://../driver/cy/commands/click.coffee` - - // other sourcemap options: - // [new webpack.SourceMapDevToolPlugin({ - // moduleFilenameTemplate: 'cypress://[namespace]/[resource-path]', - // fallbackModuleFilenameTemplate: 'cypress://[namespace]/[resourcePath]?[hash]' - // })] : - - ...(env === 'production' ? - [ - new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), - ] : - [ + ], + }, + { + test: /\.(png)$/, + use: [ + { + loader: require.resolve('file-loader'), + options: { + name: './img/[name].[ext]', + }, + }, + ], + }, + ], + }, + + optimization: { + usedExports: true, + providedExports: true, + sideEffects: true, + namedChunks: true, + namedModules: true, + removeAvailableModules: true, + mergeDuplicateChunks: true, + flagIncludedChunks: true, + removeEmptyChunks: true, + }, + + plugins: [ + new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }), + new MiniCSSExtractWebpackPlugin(), + + // Enable source maps / eval maps + // 'EvalDevtoolModulePlugin' is used in development + // because it is fast and maps to filenames while showing compiled source + // 'SourceMapDevToolPlugin' is used in production for the same reasons as 'eval', but it + // shows full source and does not cause crossorigin errors like 'eval' (in Chromium < 63) + // files will be mapped like: `cypress://../driver/cy/commands/click.coffee` + + // other sourcemap options: + // [new webpack.SourceMapDevToolPlugin({ + // moduleFilenameTemplate: 'cypress://[namespace]/[resource-path]', + // fallbackModuleFilenameTemplate: 'cypress://[namespace]/[resourcePath]?[hash]' + // })] : + + ...(env === 'production' ? + [ + new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), + ] : + [ // @ts-ignore - new webpack.EvalDevToolModulePlugin({ - moduleFilenameTemplate: 'cypress://[namespace]/[resource-path]', - fallbackModuleFilenameTemplate: 'cypress://[namespace]/[resourcePath]?[hash]', - }), - ] - ), - ...(liveReloadEnabled ? [new LiveReloadPlugin({ appendScriptTag: 'true', port: 0, hostname: 'localhost' })] : []), - ], - - cache: true, + new webpack.EvalDevToolModulePlugin({ + moduleFilenameTemplate: 'cypress://[namespace]/[resource-path]', + fallbackModuleFilenameTemplate: 'cypress://[namespace]/[resourcePath]?[hash]', + }), + ] + ), + ...(liveReloadEnabled ? [new LiveReloadPlugin({ appendScriptTag: 'true', port: 0, hostname: 'localhost' })] : []), + ], + cache: true, + } } -export default config +export default getCommonConfig export { HtmlWebpackPlugin } From 7311aa4a43eb339c333b4160ddc9833caff29acd Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 15 Aug 2019 14:22:19 -0400 Subject: [PATCH 027/370] fix invalid connectors_spec --- .../test/cypress/integration/commands/connectors_spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/test/cypress/integration/commands/connectors_spec.coffee b/packages/driver/test/cypress/integration/commands/connectors_spec.coffee index c1547b34c33d..735b12f571b2 100644 --- a/packages/driver/test/cypress/integration/commands/connectors_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/connectors_spec.coffee @@ -480,8 +480,8 @@ describe "src/cy/commands/connectors", -> memo + num , 0 math: { - sum: => - @obj.sum.apply(@obj, arguments) + sum: (args...) => + @obj.sum.apply(@obj, args) } } From 478c493f0536d5eabf8e8fbf5620131ad26777a4 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 15 Aug 2019 14:49:54 -0400 Subject: [PATCH 028/370] chore(lint): fix linting, since changed rules --- .../driver/src/cypress/setter_getter.d.ts | 4 +-- packages/driver/src/dom/document.ts | 4 +-- packages/driver/src/dom/types.d.ts | 30 +++++++++---------- .../test/cypress/support/matchers/index.d.ts | 16 +++++----- packages/network/lib/agent.ts | 2 +- packages/runner/src/header/header.jsx | 2 +- packages/server/lib/util/path_helpers.js | 1 + packages/server/lib/util/settings.js | 1 + packages/server/lib/util/terminal.js | 1 + packages/server/lib/util/validation.js | 1 + packages/ts/index.d.ts | 4 +-- 11 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/driver/src/cypress/setter_getter.d.ts b/packages/driver/src/cypress/setter_getter.d.ts index 25ed79548e19..82fee6810c92 100644 --- a/packages/driver/src/cypress/setter_getter.d.ts +++ b/packages/driver/src/cypress/setter_getter.d.ts @@ -1,4 +1,4 @@ type SetterGetter = { - (key: K): T - (key: K, value: T): T + (key: K): T + (key: K, value: T): T } diff --git a/packages/driver/src/dom/document.ts b/packages/driver/src/dom/document.ts index 54f8575086fa..083983037402 100644 --- a/packages/driver/src/dom/document.ts +++ b/packages/driver/src/dom/document.ts @@ -3,7 +3,7 @@ const $jquery = require('./jquery') const docNode = window.Node.DOCUMENT_NODE //TODO: make this not allow jquery -const isDocument = (obj:HTMLElement | Document): obj is Document => { +const isDocument = (obj: HTMLElement | Document): obj is Document => { try { if ($jquery.isJquery(obj)) { obj = obj[0] @@ -20,7 +20,7 @@ const hasActiveWindow = (doc) => { return !!doc.defaultView } -const getDocumentFromElement = (el:HTMLElement):Document => { +const getDocumentFromElement = (el: HTMLElement): Document => { if (isDocument(el)) { return el } diff --git a/packages/driver/src/dom/types.d.ts b/packages/driver/src/dom/types.d.ts index a155af433c79..39418e5203f0 100644 --- a/packages/driver/src/dom/types.d.ts +++ b/packages/driver/src/dom/types.d.ts @@ -19,26 +19,26 @@ declare global { } export interface HTMLSingleValueChangeInputElement extends HTMLInputElement { - type: 'date' | 'time' | 'week' | 'month' - } + type: 'date' | 'time' | 'week' | 'month' +} export interface HTMLContentEditableElement extends HTMLElement {} export interface HTMLTextLikeInputElement extends HTMLInputElement { type: - | 'text' - | 'password' - | 'email' - | 'number' - | 'date' - | 'week' - | 'month' - | 'time' - | 'datetime' - | 'datetime-local' - | 'search' - | 'url' - | 'tel' + | 'text' + | 'password' + | 'email' + | 'number' + | 'date' + | 'week' + | 'month' + | 'time' + | 'datetime' + | 'datetime-local' + | 'search' + | 'url' + | 'tel' setSelectionRange: HTMLInputElement['setSelectionRange'] } diff --git a/packages/driver/test/cypress/support/matchers/index.d.ts b/packages/driver/test/cypress/support/matchers/index.d.ts index 35c38c08092b..1888df5ac41f 100644 --- a/packages/driver/test/cypress/support/matchers/index.d.ts +++ b/packages/driver/test/cypress/support/matchers/index.d.ts @@ -1,14 +1,14 @@ /// declare global { - namespace Chai { - interface Assertion { - matchEql(expected: any): Assertion - } - interface Assert { - matchEql(val: any, exp: any, msg?: string): void - } - } + namespace Chai { + interface Assertion { + matchEql(expected: any): Assertion + } + interface Assert { + matchEql(val: any, exp: any, msg?: string): void + } + } } declare function matchEql(chai: any, utils: any): void diff --git a/packages/network/lib/agent.ts b/packages/network/lib/agent.ts index e16c92f6e1fb..9da79c4d9991 100644 --- a/packages/network/lib/agent.ts +++ b/packages/network/lib/agent.ts @@ -17,7 +17,7 @@ interface RequestOptionsWithProxy extends http.RequestOptions { } type FamilyCache = { - [host: string] : 4 | 6 + [host: string]: 4 | 6 } export function buildConnectReqHead (hostname: string, port: string, proxy: url.Url) { diff --git a/packages/runner/src/header/header.jsx b/packages/runner/src/header/header.jsx index 684449485349..8fa419335f80 100644 --- a/packages/runner/src/header/header.jsx +++ b/packages/runner/src/header/header.jsx @@ -59,7 +59,7 @@ export default class Header extends Component {

The viewport determines the width and height of your application. By default the viewport will be {state.defaults.width}px by {state.defaults.height}px unless specified by a cy.viewport command.

Additionally you can override the default viewport dimensions by specifying these values in your cypress.json.

{/* eslint-disable indent */}
-{`{
+                {`{
   "viewportWidth": ${state.defaults.width},
   "viewportHeight": ${state.defaults.height}
 }`}
diff --git a/packages/server/lib/util/path_helpers.js b/packages/server/lib/util/path_helpers.js
index 702c3270168e..72ca220425be 100644
--- a/packages/server/lib/util/path_helpers.js
+++ b/packages/server/lib/util/path_helpers.js
@@ -1,4 +1,5 @@
 /* eslint-disable
+    @typescript-eslint/no-unused-vars,
     no-unused-vars,
 */
 // TODO: This file was created by bulk-decaffeinate.
diff --git a/packages/server/lib/util/settings.js b/packages/server/lib/util/settings.js
index c2ed642e35ea..4b18c6f1b303 100644
--- a/packages/server/lib/util/settings.js
+++ b/packages/server/lib/util/settings.js
@@ -2,6 +2,7 @@
     brace-style,
     no-cond-assign,
     no-unused-vars,
+    @typescript-eslint/no-unused-vars,
 */
 // TODO: This file was created by bulk-decaffeinate.
 // Fix any style issues and re-enable lint.
diff --git a/packages/server/lib/util/terminal.js b/packages/server/lib/util/terminal.js
index a105fbbf201c..21c2276dc404 100644
--- a/packages/server/lib/util/terminal.js
+++ b/packages/server/lib/util/terminal.js
@@ -4,6 +4,7 @@
     no-cond-assign,
     no-console,
     no-unused-vars,
+    @typescript-eslint/no-unused-vars,
 */
 // TODO: This file was created by bulk-decaffeinate.
 // Fix any style issues and re-enable lint.
diff --git a/packages/server/lib/util/validation.js b/packages/server/lib/util/validation.js
index f4f0aa45f00d..6ce5fbaa57ed 100644
--- a/packages/server/lib/util/validation.js
+++ b/packages/server/lib/util/validation.js
@@ -1,5 +1,6 @@
 /* eslint-disable
     no-unused-vars,
+    @typescript-eslint/no-unused-vars,
 */
 // TODO: This file was created by bulk-decaffeinate.
 // Fix any style issues and re-enable lint.
diff --git a/packages/ts/index.d.ts b/packages/ts/index.d.ts
index 65d38a99e2e6..9f8a630dd159 100644
--- a/packages/ts/index.d.ts
+++ b/packages/ts/index.d.ts
@@ -27,7 +27,7 @@ declare module 'http' {
   }
 
   interface ClientRequest {
-    _header: { [key: string]:string }
+    _header: { [key: string]: string }
     _implicitHeader: () => void
     output: string[]
     agent: Agent
@@ -76,7 +76,7 @@ declare type Optional = T | void
 
 declare module 'plist' {
   interface Plist {
-    parse: (s:string) => any
+    parse: (s: string) => any
   }
   const plist: Plist
   export = plist

From 24c53fc72899fb68ab8313c1511bf6f3034f5e73 Mon Sep 17 00:00:00 2001
From: Ben Kucera <14625260+Bkucera@users.noreply.github.com>
Date: Fri, 16 Aug 2019 13:03:44 -0400
Subject: [PATCH 029/370] apply firefox-ie branch on top of develop

---
 .eslintignore                                 |   2 +-
 packages/desktop-gui/src/app/nav.scss         |   3 +
 packages/desktop-gui/src/lib/utils.js         |   6 +-
 .../desktop-gui/src/project-nav/browsers.jsx  |   6 +-
 packages/driver/src/cy/chai.coffee            |   1 +
 packages/driver/src/cy/errors.coffee          |   1 +
 packages/driver/src/cypress.coffee            | 342 +++++++++---------
 packages/driver/src/cypress/browser.coffee    |  35 ++
 packages/driver/src/cypress/cy.coffee         |   1 +
 .../driver/src/cypress/error_messages.coffee  |   3 +
 packages/driver/src/cypress/log.coffee        |   1 +
 packages/driver/src/cypress/server.coffee     |  11 +-
 .../driver/src/cypress/setter_getter.coffee   |  12 +-
 packages/driver/src/cypress/utils.coffee      |   7 +-
 .../driver/test/cypress/fixtures/dom.html     |   7 +
 .../commands/actions/focus_spec.coffee        |   1 +
 .../commands/actions/scroll_spec.coffee       |   2 +-
 .../commands/actions/trigger_spec.coffee      |  11 +-
 .../integration/commands/commands_spec.coffee |   2 +-
 .../commands/navigation_spec.coffee           |   6 +
 .../integration/commands/querying_spec.coffee |   2 +-
 .../integration/commands/very_simple_spec.js  |  27 ++
 .../integration/cypress/browser_spec.coffee   |  49 +++
 .../integration/cypress/utils_spec.coffee     |  11 +-
 .../integration/e2e/redirects_spec.coffee     |   2 +
 .../integration/issues/573_spec.coffee        |   4 +-
 .../test/cypress/support/defaults.coffee      |   3 +
 .../driver/test/unit/cypress/chai_spec.coffee |   2 +-
 packages/extension/README.md                  |   8 +-
 packages/extension/app/background.coffee      |  99 +++--
 packages/extension/app/browser-polyfill.js    | 116 ++++++
 packages/extension/app/manifest.json          |  11 +-
 .../test/integration/background_spec.coffee   | 159 ++++----
 packages/gateway/package.json                 |  25 ++
 packages/gateway/server/firefox.js            |  38 ++
 packages/gateway/server/ie.js                 |  70 ++++
 packages/gateway/server/iframe.html           |  30 ++
 packages/gateway/server/index.html            |  79 ++++
 packages/gateway/server/proxy.js              |  36 ++
 packages/gateway/server/static.js             |  14 +
 packages/https-proxy/https.js                 |  16 +-
 .../test/helpers/https_server.coffee          |   1 +
 .../https-proxy/test/helpers/proxy.coffee     |   3 +
 packages/launcher/lib/browsers.ts             |  31 ++
 packages/launcher/lib/darwin/index.ts         |  26 +-
 packages/launcher/lib/darwin/util.ts          |   9 +-
 packages/launcher/lib/types.ts                |   4 +-
 packages/launcher/lib/windows/index.ts        |  33 ++
 .../reporter/src/runnables/runnables.scss     |  10 +-
 packages/runner/src/app/app.scss              |   1 +
 packages/runner/src/app/app.spec.jsx          |   1 -
 .../src/errors/automation-disconnected.jsx    |   2 +-
 .../errors/automation-disconnected.spec.jsx   |   2 +-
 packages/runner/src/errors/no-automation.jsx  |   5 +
 packages/runner/src/header/header.jsx         |   2 +
 packages/runner/src/header/header.scss        |  11 +-
 packages/runner/src/iframe/iframe.scss        |   3 +-
 packages/runner/src/iframe/iframes.jsx        |   4 +-
 packages/runner/src/lib/event-manager.js      |  18 +-
 packages/runner/static/index.html             |   7 +-
 packages/server/IEDriverServer.exe            | Bin 0 -> 10105856 bytes
 packages/server/lib/browsers/chrome.coffee    |  22 +-
 packages/server/lib/browsers/electron.coffee  |  11 +-
 packages/server/lib/browsers/firefox-util.js  |  59 +++
 packages/server/lib/browsers/firefox.coffee   | 172 +++++++++
 packages/server/lib/browsers/ie.js            | 155 ++++++++
 packages/server/lib/browsers/index.coffee     |   7 +
 packages/server/lib/browsers/utils.coffee     |  18 +
 packages/server/lib/file_server.coffee        |   1 -
 packages/server/lib/open_project.coffee       |   2 +-
 packages/server/lib/pac_server.js             |  39 ++
 packages/server/lib/project.coffee            |  16 +-
 packages/server/lib/server.coffee             |  46 ++-
 packages/server/lib/util/base_server.js       |  34 ++
 packages/server/package.json                  |   7 +
 .../test/e2e/1_async_timeouts_spec.coffee     |   1 +
 .../1_caught_uncaught_hook_errors_spec.coffee |   1 +
 .../1_commands_outside_of_test_spec.coffee    |   1 +
 packages/server/test/e2e/2_config_spec.coffee |   1 +
 .../server/test/e2e/2_cookies_spec.coffee     |   1 +
 .../test/e2e/2_form_submissions_spec.coffee   |   3 +
 .../server/test/e2e/3_issue_149_spec.coffee   |   1 +
 .../server/test/e2e/3_issue_173_spec.coffee   |   1 +
 .../server/test/e2e/3_issue_674_spec.coffee   |   1 +
 .../test/e2e/3_js_error_handling_spec.coffee  |   1 +
 .../server/test/e2e/4_promises_spec.coffee    |   1 +
 .../test/unit/browsers/chrome_spec.coffee     |   5 +-
 .../test/unit/browsers/firefox_spec.coffee    | 139 +++++++
 .../server/test/unit/browsers/ie_spec.coffee  | 142 ++++++++
 packages/server/test/unit/pac_spec.js         |  59 +++
 90 files changed, 1985 insertions(+), 396 deletions(-)
 create mode 100644 packages/driver/src/cypress/browser.coffee
 create mode 100644 packages/driver/test/cypress/integration/commands/very_simple_spec.js
 create mode 100644 packages/driver/test/cypress/integration/cypress/browser_spec.coffee
 create mode 100644 packages/extension/app/browser-polyfill.js
 create mode 100644 packages/gateway/package.json
 create mode 100644 packages/gateway/server/firefox.js
 create mode 100644 packages/gateway/server/ie.js
 create mode 100644 packages/gateway/server/iframe.html
 create mode 100644 packages/gateway/server/index.html
 create mode 100644 packages/gateway/server/proxy.js
 create mode 100644 packages/gateway/server/static.js
 create mode 100644 packages/server/IEDriverServer.exe
 create mode 100644 packages/server/lib/browsers/firefox-util.js
 create mode 100644 packages/server/lib/browsers/firefox.coffee
 create mode 100644 packages/server/lib/browsers/ie.js
 create mode 100644 packages/server/lib/pac_server.js
 create mode 100644 packages/server/lib/util/base_server.js
 create mode 100644 packages/server/test/unit/browsers/firefox_spec.coffee
 create mode 100644 packages/server/test/unit/browsers/ie_spec.coffee
 create mode 100644 packages/server/test/unit/pac_spec.js

diff --git a/.eslintignore b/.eslintignore
index c50ce0052813..7761ef8cdccf 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -20,7 +20,7 @@ packages/extension/test/helpers/background.js
 packages/server/lib/scaffold/plugins/index.js
 packages/server/lib/scaffold/support/index.js
 packages/server/lib/scaffold/support/commands.js
-
+packages/extension/app/browser-polyfill.js
 packages/launcher/lib/**/*.js
 
 **/package-lock.json
diff --git a/packages/desktop-gui/src/app/nav.scss b/packages/desktop-gui/src/app/nav.scss
index 737502001356..8502d5bfb367 100644
--- a/packages/desktop-gui/src/app/nav.scss
+++ b/packages/desktop-gui/src/app/nav.scss
@@ -151,6 +151,9 @@
   .canary > i { color: #e4b721; }
   .chromium > i { color: #829ac3; }
   .electron > i { color: #777; }
+  .firefox > i { color: #ff7d08; }
+  .firefox-developer-edition > i { color: #006ad9; }
+  .firefox-nightly > i { color: #a752f7; }
 
   .dropdown-menu {
     left: auto;
diff --git a/packages/desktop-gui/src/lib/utils.js b/packages/desktop-gui/src/lib/utils.js
index 95ac38990d1b..a57572cccb5d 100644
--- a/packages/desktop-gui/src/lib/utils.js
+++ b/packages/desktop-gui/src/lib/utils.js
@@ -12,8 +12,12 @@ const osIconLookup = {
 
 const browserIconLookup = {
   chrome: 'chrome',
-  Electron: 'chrome',
+  chromium: 'chrome',
+  canary: 'chrome',
+  electron: 'chrome',
   firefox: 'firefox',
+  firefoxDeveloperEdition: 'firefox',
+  firefoxNightly: 'firefox',
   safari: 'safari',
 }
 
diff --git a/packages/desktop-gui/src/project-nav/browsers.jsx b/packages/desktop-gui/src/project-nav/browsers.jsx
index 2f68f7c689b6..897ff0fa3fb4 100644
--- a/packages/desktop-gui/src/project-nav/browsers.jsx
+++ b/packages/desktop-gui/src/project-nav/browsers.jsx
@@ -1,3 +1,4 @@
+import _ from 'lodash'
 import React, { Component } from 'react'
 import { observer } from 'mobx-react'
 import Tooltip from '@cypress/react-tooltip'
@@ -5,6 +6,7 @@ import Dropdown from '../dropdown/dropdown'
 import MarkdownRenderer from '../lib/markdown-renderer'
 
 import projectsApi from '../projects/projects-api'
+import utils from '../lib/utils'
 
 @observer
 export default class Browsers extends Component {
@@ -64,12 +66,12 @@ export default class Browsers extends Component {
       icon = 'check-circle-o green'
       prefixText = 'Running'
     } else {
-      icon = browser.icon
+      icon = utils.browserIcon(browser.name)
       prefixText = ''
     }
 
     return (
-      
+      
         {' '}
         {prefixText}{' '}
         {browser.displayName}{' '}
diff --git a/packages/driver/src/cy/chai.coffee b/packages/driver/src/cy/chai.coffee
index a9f0cb7e7c82..167c03eab180 100644
--- a/packages/driver/src/cy/chai.coffee
+++ b/packages/driver/src/cy/chai.coffee
@@ -118,6 +118,7 @@ chai.use (chai, u) ->
         assert._obj = $dom.stringify(obj, "short")
 
       msg = getMessage.call(@, assert, args)
+      # msg = 'foobarbaz'
 
       ## restore the real obj if we changed it
       if obj isnt assert._obj
diff --git a/packages/driver/src/cy/errors.coffee b/packages/driver/src/cy/errors.coffee
index beb2d1bcbd21..18c8d80c29bb 100644
--- a/packages/driver/src/cy/errors.coffee
+++ b/packages/driver/src/cy/errors.coffee
@@ -28,6 +28,7 @@ create = (state, config, log) ->
 
   createUncaughtException = (type, args) ->
     [msg, source, lineno, colno, err] = args
+    debugger
 
     current = state("current")
 
diff --git a/packages/driver/src/cypress.coffee b/packages/driver/src/cypress.coffee
index 1b4d1bfa6f5e..10b9bafe83ce 100644
--- a/packages/driver/src/cypress.coffee
+++ b/packages/driver/src/cypress.coffee
@@ -25,6 +25,14 @@ $Server = require("./cypress/server")
 $Screenshot = require("./cypress/screenshot")
 $SelectorPlayground = require("./cypress/selector_playground")
 $utils = require("./cypress/utils")
+browserInfo = require("./cypress/browser")
+
+tryCatch = (fn) ->
+  try
+    fn()
+  catch err
+    debugger
+    throw err
 
 proxies = {
   runner: "getStartTime getTestsState getEmissions setNumLogs countByTestState getDisplayPropsForLog getConsolePropsForLogById getSnapshotPropsForLogById getErrorByTestId setStartTime resumeAtTest normalizeAll".split(" ")
@@ -107,7 +115,6 @@ class $Cypress
     @arch = config.arch
     @spec = config.spec
     @version = config.version
-    @browser = config.browser
     @platform = config.platform
 
     ## normalize this into boolean
@@ -133,6 +140,8 @@ class $Cypress
 
     config = _.omit(config, "env", "remote", "resolved", "scaffoldedFiles", "javascripts", "state")
 
+    _.extend(@, browserInfo(config))
+
     @state = $SetterGetter.create({})
     @config = $SetterGetter.create(config)
     @env = $SetterGetter.create(env)
@@ -177,228 +186,229 @@ class $Cypress
     return null
 
   action: (eventName, args...) ->
-    ## normalizes all the various ways
-    ## other objects communicate intent
-    ## and 'action' to Cypress
-    switch eventName
-      when "cypress:stop"
-        @emit("stop")
-
-      when "cypress:config"
-        @emit("config", args[0])
-
-      when "runner:start"
-        ## mocha runner has begun running the tests
-        @emit("run:start")
-
-        return if @_RESUMED_AT_TEST
-
-        if @config("isTextTerminal")
-          @emit("mocha", "start", args[0])
-
-      when "runner:end"
-        ## mocha runner has finished running the tests
-
-        ## end may have been caused by an uncaught error
-        ## that happened inside of a hook.
-        ##
-        ## when this happens mocha aborts the entire run
-        ## and does not do the usual cleanup so that means
-        ## we have to fire the test:after:hooks and
-        ## test:after:run events ourselves
-        @emit("run:end")
+    tryCatch =>
+      ## normalizes all the various ways
+      ## other objects communicate intent
+      ## and 'action' to Cypress
+      switch eventName
+        when "cypress:stop"
+          @emit("stop")
+
+        when "cypress:config"
+          @emit("config", args[0])
+
+        when "runner:start"
+          ## mocha runner has begun running the tests
+          @emit("run:start")
+
+          return if @_RESUMED_AT_TEST
+
+          if @config("isTextTerminal")
+            @emit("mocha", "start", args[0])
+
+        when "runner:end"
+          ## mocha runner has finished running the tests
+
+          ## end may have been caused by an uncaught error
+          ## that happened inside of a hook.
+          ##
+          ## when this happens mocha aborts the entire run
+          ## and does not do the usual cleanup so that means
+          ## we have to fire the test:after:hooks and
+          ## test:after:run events ourselves
+          @emit("run:end")
 
-        if @config("isTextTerminal")
-          @emit("mocha", "end", args[0])
+          if @config("isTextTerminal")
+            @emit("mocha", "end", args[0])
 
-      when "runner:set:runnable"
-        ## when there is a hook / test (runnable) that
-        ## is about to be invoked
-        @cy.setRunnable(args...)
+        when "runner:set:runnable"
+          ## when there is a hook / test (runnable) that
+          ## is about to be invoked
+          @cy.setRunnable(args...)
 
-      when "runner:suite:start"
-        ## mocha runner started processing a suite
-        if @config("isTextTerminal")
-          @emit("mocha", "suite", args...)
+        when "runner:suite:start"
+          ## mocha runner started processing a suite
+          if @config("isTextTerminal")
+            @emit("mocha", "suite", args...)
 
-      when "runner:suite:end"
-        ## mocha runner finished processing a suite
-        if @config("isTextTerminal")
-          @emit("mocha", "suite end", args...)
+        when "runner:suite:end"
+          ## mocha runner finished processing a suite
+          if @config("isTextTerminal")
+            @emit("mocha", "suite end", args...)
 
-      when "runner:hook:start"
-        ## mocha runner started processing a hook
-        if @config("isTextTerminal")
-          @emit("mocha", "hook", args...)
+        when "runner:hook:start"
+          ## mocha runner started processing a hook
+          if @config("isTextTerminal")
+            @emit("mocha", "hook", args...)
 
-      when "runner:hook:end"
-        ## mocha runner finished processing a hook
-        if @config("isTextTerminal")
-          @emit("mocha", "hook end", args...)
+        when "runner:hook:end"
+          ## mocha runner finished processing a hook
+          if @config("isTextTerminal")
+            @emit("mocha", "hook end", args...)
 
-      when "runner:test:start"
-        ## mocha runner started processing a hook
-        if @config("isTextTerminal")
-          @emit("mocha", "test", args...)
+        when "runner:test:start"
+          ## mocha runner started processing a hook
+          if @config("isTextTerminal")
+            @emit("mocha", "test", args...)
 
-      when "runner:test:end"
-        if @config("isTextTerminal")
-          @emit("mocha", "test end", args...)
+        when "runner:test:end"
+          if @config("isTextTerminal")
+            @emit("mocha", "test end", args...)
 
-      when "runner:pass"
-        ## mocha runner calculated a pass
-        if @config("isTextTerminal")
-          @emit("mocha", "pass", args...)
+        when "runner:pass"
+          ## mocha runner calculated a pass
+          if @config("isTextTerminal")
+            @emit("mocha", "pass", args...)
 
-      when "runner:pending"
-        ## mocha runner calculated a pending test
-        if @config("isTextTerminal")
-          @emit("mocha", "pending", args...)
+        when "runner:pending"
+          ## mocha runner calculated a pending test
+          if @config("isTextTerminal")
+            @emit("mocha", "pending", args...)
 
-      when "runner:fail"
-        ## mocha runner calculated a failure
-        if @config("isTextTerminal")
-          @emit("mocha", "fail", args...)
+        when "runner:fail"
+          ## mocha runner calculated a failure
+          if @config("isTextTerminal")
+            @emit("mocha", "fail", args...)
 
-      when "mocha:runnable:run"
-        @runner.onRunnableRun(args...)
+        when "mocha:runnable:run"
+          @runner.onRunnableRun(args...)
 
-      when "runner:test:before:run"
-        ## get back to a clean slate
-        @cy.reset()
+        when "runner:test:before:run"
+          ## get back to a clean slate
+          @cy.reset()
 
-        @emit("test:before:run", args...)
+          @emit("test:before:run", args...)
 
-      when "runner:test:before:run:async"
-        ## TODO: handle timeouts here? or in the runner?
-        @emitThen("test:before:run:async", args...)
+        when "runner:test:before:run:async"
+          ## TODO: handle timeouts here? or in the runner?
+          @emitThen("test:before:run:async", args...)
 
-      when "runner:runnable:after:run:async"
-        @emitThen("runnable:after:run:async", args...)
+        when "runner:runnable:after:run:async"
+          @emitThen("runnable:after:run:async", args...)
 
-      when "runner:test:after:run"
-        @runner.cleanupQueue(@config("numTestsKeptInMemory"))
+        when "runner:test:after:run"
+          @runner.cleanupQueue(@config("numTestsKeptInMemory"))
 
-        ## this event is how the reporter knows how to display
-        ## stats and runnable properties such as errors
-        @emit("test:after:run", args...)
+          ## this event is how the reporter knows how to display
+          ## stats and runnable properties such as errors
+          @emit("test:after:run", args...)
 
-        if @config("isTextTerminal")
-          ## needed for calculating wallClockDuration
-          ## and the timings of after + afterEach hooks
-          @emit("mocha", "test:after:run", args[0])
+          if @config("isTextTerminal")
+            ## needed for calculating wallClockDuration
+            ## and the timings of after + afterEach hooks
+            @emit("mocha", "test:after:run", args[0])
 
-      when "cy:before:all:screenshots"
-        @emit("before:all:screenshots", args...)
+        when "cy:before:all:screenshots"
+          @emit("before:all:screenshots", args...)
 
-      when "cy:before:screenshot"
-        @emit("before:screenshot", args...)
+        when "cy:before:screenshot"
+          @emit("before:screenshot", args...)
 
-      when "cy:after:screenshot"
-        @emit("after:screenshot", args...)
+        when "cy:after:screenshot"
+          @emit("after:screenshot", args...)
 
-      when "cy:after:all:screenshots"
-        @emit("after:all:screenshots", args...)
+        when "cy:after:all:screenshots"
+          @emit("after:all:screenshots", args...)
 
-      when "command:log:added"
-        @runner.addLog(args[0], @config("isInteractive"))
+        when "command:log:added"
+          @runner.addLog(args[0], @config("isInteractive"))
 
-        @emit("log:added", args...)
+          @emit("log:added", args...)
 
-      when "command:log:changed"
-        @runner.addLog(args[0], @config("isInteractive"))
+        when "command:log:changed"
+          @runner.addLog(args[0], @config("isInteractive"))
 
-        @emit("log:changed", args...)
+          @emit("log:changed", args...)
 
-      when "cy:fail"
-        ## comes from cypress errors fail()
-        @emitMap("fail", args...)
+        when "cy:fail"
+          ## comes from cypress errors fail()
+          @emitMap("fail", args...)
 
-      when "cy:stability:changed"
-        @emit("stability:changed", args...)
+        when "cy:stability:changed"
+          @emit("stability:changed", args...)
 
-      when "cy:paused"
-        @emit("paused", args...)
+        when "cy:paused"
+          @emit("paused", args...)
 
-      when "cy:canceled"
-        @emit("canceled")
+        when "cy:canceled"
+          @emit("canceled")
 
-      when "cy:visit:failed"
-        @emit("visit:failed", args[0])
+        when "cy:visit:failed"
+          @emit("visit:failed", args[0])
 
-      when "cy:viewport:changed"
-        @emit("viewport:changed", args...)
+        when "cy:viewport:changed"
+          @emit("viewport:changed", args...)
 
-      when "cy:command:start"
-        @emit("command:start", args...)
+        when "cy:command:start"
+          @emit("command:start", args...)
 
-      when "cy:command:end"
-        @emit("command:end", args...)
+        when "cy:command:end"
+          @emit("command:end", args...)
 
-      when "cy:command:retry"
-        @emit("command:retry", args...)
+        when "cy:command:retry"
+          @emit("command:retry", args...)
 
-      when "cy:command:enqueued"
-        @emit("command:enqueued", args[0])
+        when "cy:command:enqueued"
+          @emit("command:enqueued", args[0])
 
-      when "cy:command:queue:before:end"
-        @emit("command:queue:before:end")
+        when "cy:command:queue:before:end"
+          @emit("command:queue:before:end")
 
-      when "cy:command:queue:end"
-        @emit("command:queue:end")
+        when "cy:command:queue:end"
+          @emit("command:queue:end")
 
-      when "cy:url:changed"
-        @emit("url:changed", args[0])
+        when "cy:url:changed"
+          @emit("url:changed", args[0])
 
-      when "cy:next:subject:prepared"
-        @emit("next:subject:prepared", args...)
+        when "cy:next:subject:prepared"
+          @emit("next:subject:prepared", args...)
 
-      when "cy:collect:run:state"
-        @emitThen("collect:run:state")
+        when "cy:collect:run:state"
+          @emitThen("collect:run:state")
 
-      when "cy:scrolled"
-        @emit("scrolled", args...)
+        when "cy:scrolled"
+          @emit("scrolled", args...)
 
-      when "app:uncaught:exception"
-        @emitMap("uncaught:exception", args...)
+        when "app:uncaught:exception"
+          @emitMap("uncaught:exception", args...)
 
-      when "app:window:alert"
-        @emit("window:alert", args[0])
+        when "app:window:alert"
+          @emit("window:alert", args[0])
 
-      when "app:window:confirm"
-        @emitMap("window:confirm", args[0])
+        when "app:window:confirm"
+          @emitMap("window:confirm", args[0])
 
-      when "app:window:confirmed"
-        @emit("window:confirmed", args...)
+        when "app:window:confirmed"
+          @emit("window:confirmed", args...)
 
-      when "app:page:loading"
-        @emit("page:loading", args[0])
+        when "app:page:loading"
+          @emit("page:loading", args[0])
 
-      when "app:window:before:load"
-        @cy.onBeforeAppWindowLoad(args[0])
+        when "app:window:before:load"
+          @cy.onBeforeAppWindowLoad(args[0])
 
-        @emit("window:before:load", args[0])
+          @emit("window:before:load", args[0])
 
-      when "app:navigation:changed"
-        @emit("navigation:changed", args...)
+        when "app:navigation:changed"
+          @emit("navigation:changed", args...)
 
-      when "app:form:submitted"
-        @emit("form:submitted", args[0])
+        when "app:form:submitted"
+          @emit("form:submitted", args[0])
 
-      when "app:window:load"
-        @emit("window:load", args[0])
+        when "app:window:load"
+          @emit("window:load", args[0])
 
-      when "app:window:before:unload"
-        @emit("window:before:unload", args[0])
+        when "app:window:before:unload"
+          @emit("window:before:unload", args[0])
 
-      when "app:window:unload"
-        @emit("window:unload", args[0])
+        when "app:window:unload"
+          @emit("window:unload", args[0])
 
-      when "app:css:modified"
-        @emit("css:modified", args[0])
+        when "app:css:modified"
+          @emit("css:modified", args[0])
 
-      when "spec:script:error"
-        @emit("script:error", args...)
+        when "spec:script:error"
+          @emit("script:error", args...)
 
   backend: (eventName, args...) ->
     new Promise (resolve, reject) =>
diff --git a/packages/driver/src/cypress/browser.coffee b/packages/driver/src/cypress/browser.coffee
new file mode 100644
index 000000000000..6d8681c41af9
--- /dev/null
+++ b/packages/driver/src/cypress/browser.coffee
@@ -0,0 +1,35 @@
+_ = require("lodash")
+$utils = require("./utils")
+
+browsers = {
+  chrome: "chrome"
+  canary: "chrome"
+  chromium: "chrome"
+  electron: "chrome"
+
+  firefox: "firefox"
+  firefoxDeveloperEdition: "firefox"
+  firefoxNightly: "firefox"
+
+  ie: 'ie'
+}
+
+isBrowser = (method, config, normalize, browserName = "") ->
+  if not _.isString(browserName)
+    $utils.throwErrByPath("browser.invalid_arg", {
+      args: { method, browserName: $utils.stringify(browserName) }
+    })
+
+  browserName = browserName.toLowerCase()
+  currentBrowser = config.browser.name.toLowerCase()
+  if normalize
+    currentBrowser = browsers[currentBrowser]
+
+  return browserName is currentBrowser
+
+module.exports = (config) ->
+  {
+    browser: config.browser
+    isBrowser: _.partial(isBrowser, "isBrowser", config, false)
+    isBrowserType: _.partial(isBrowser, "isBrowserType", config, true)
+  }
diff --git a/packages/driver/src/cypress/cy.coffee b/packages/driver/src/cypress/cy.coffee
index 98ce874a495e..1459fc25b730 100644
--- a/packages/driver/src/cypress/cy.coffee
+++ b/packages/driver/src/cypress/cy.coffee
@@ -728,6 +728,7 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
           ## listeners time to be invoked prior to moving on
           stability.isStable(true, "load")
         catch err
+          debugger
           ## we failed setting the remote window props
           ## which means we're in a cross domain failure
           ## check first to see if you have a callback function
diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee
index 751de97eb105..c0aef4f851e6 100644
--- a/packages/driver/src/cypress/error_messages.coffee
+++ b/packages/driver/src/cypress/error_messages.coffee
@@ -73,6 +73,9 @@ module.exports = {
     timed_out:  "#{cmd('blur')} timed out because your browser did not receive any blur events. This is a known bug in Chrome when it is not the currently focused window."
     wrong_focused_element: "#{cmd('blur')} can only be called on the focused element. Currently the focused element is a: {{node}}"
 
+  browser:
+    invalid_arg: "Cypress.{{method}}() must be passed the name of a browser. You passed: {{browserName}}"
+
   chai:
     length_invalid_argument: "You must provide a valid number to a length assertion. You passed: '{{length}}'"
     match_invalid_argument: "'match' requires its argument be a 'RegExp'. You passed: '{{regExp}}'"
diff --git a/packages/driver/src/cypress/log.coffee b/packages/driver/src/cypress/log.coffee
index a1c3674ad01b..763869fac277 100644
--- a/packages/driver/src/cypress/log.coffee
+++ b/packages/driver/src/cypress/log.coffee
@@ -417,6 +417,7 @@ create = (Cypress, cy, state, config) ->
   delay ?= setDelay(config("logAttrsDelay"))
 
   trigger = (log, event) ->
+    # return
     ## bail if we never fired our initial log event
     return if not log._hasInitiallyLogged
 
diff --git a/packages/driver/src/cypress/server.coffee b/packages/driver/src/cypress/server.coffee
index 20848673695b..684a868c359a 100644
--- a/packages/driver/src/cypress/server.coffee
+++ b/packages/driver/src/cypress/server.coffee
@@ -354,10 +354,13 @@ create = (options = {}) ->
       abort  = XHR.prototype.abort
       srh    = XHR.prototype.setRequestHeader
 
-      restoreFn = ->
-        ## restore the property back on the window
-        _.each {send: send, open: open, abort: abort, setRequestHeader: srh}, (value, key) ->
-          XHR.prototype[key] = value
+      ## TODO: figure out what's causing an error
+      
+      # restoreFn = ->
+      #   ## restore the property back on the window
+      #   _.each {send: send, open: open, abort: abort, setRequestHeader: srh}, (value, key) ->
+      #     debugger
+      #     XHR.prototype[key] = value
 
       XHR.prototype.setRequestHeader = ->
         ## if the XHR leaks into the next test
diff --git a/packages/driver/src/cypress/setter_getter.coffee b/packages/driver/src/cypress/setter_getter.coffee
index f30b8135a292..e69fd85bd8b6 100644
--- a/packages/driver/src/cypress/setter_getter.coffee
+++ b/packages/driver/src/cypress/setter_getter.coffee
@@ -24,7 +24,17 @@ create = (state = {}) ->
       obj[key] = value
       ret = value
 
-    _.extend(state, obj)
+    ## manually iterate through the
+    ## object and set its key/value on
+    ## state because using _.extend()
+    ## can throw in IE11 because of
+    ## internal copyObject function which
+    ## reads in the existing values. those
+    ## existing values may include references
+    ## to values we no longer have access to.
+    # window.debugger(2, true, true)
+    _.each obj, (v, k) ->
+      state[k] = v
 
     return ret
 
diff --git a/packages/driver/src/cypress/utils.coffee b/packages/driver/src/cypress/utils.coffee
index 1baf13b13d9d..71f91a6bbea2 100644
--- a/packages/driver/src/cypress/utils.coffee
+++ b/packages/driver/src/cypress/utils.coffee
@@ -84,13 +84,18 @@ module.exports = {
     ## because the browser has a cached
     ## dynamic stack getter that will
     ## not be evaluated later
-    stack = err.stack
+    stack = err.stack or ""
 
     ## preserve message
     ## and toString
     msg = err.message
     str = err.toString()
 
+    ## Firefox stack does not include toString'd error, so normalize
+    ## things by prepending it
+    if _.includes(stack, str)
+      stack = "#{str}\n#{stack}"
+
     ## append message
     msg += "\n\n" + message
 
diff --git a/packages/driver/test/cypress/fixtures/dom.html b/packages/driver/test/cypress/fixtures/dom.html
index f1a56c0c3ee4..0e78e9fd950a 100644
--- a/packages/driver/test/cypress/fixtures/dom.html
+++ b/packages/driver/test/cypress/fixtures/dom.html
@@ -25,6 +25,13 @@
           overflow: hidden;
         }
 
+        #button {
+          margin: 0;
+          padding: 0;
+          width: 100px;
+          height: 50px;
+        }
+
         .slidein {
           background-color: yellow;
           border: 1px solid red;
diff --git a/packages/driver/test/cypress/integration/commands/actions/focus_spec.coffee b/packages/driver/test/cypress/integration/commands/actions/focus_spec.coffee
index a50da7112212..689f192e1c45 100644
--- a/packages/driver/test/cypress/integration/commands/actions/focus_spec.coffee
+++ b/packages/driver/test/cypress/integration/commands/actions/focus_spec.coffee
@@ -450,6 +450,7 @@ describe "src/cy/commands/actions/focus", ->
         .get("[contenteditable]:first").focus().blur().then ($ce) ->
           expect($ce.get(0)).to.eq ce.get(0)
 
+    ## FIREFOX FIXME: "cy.blur() can only be called when there is a currently focused element."
     it "can blur input[type=time]", ->
       blurred = false
 
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..3e72e0ae8bdc 100644
--- a/packages/driver/test/cypress/integration/commands/actions/scroll_spec.coffee
+++ b/packages/driver/test/cypress/integration/commands/actions/scroll_spec.coffee
@@ -494,7 +494,7 @@ describe "src/cy/commands/actions/scroll", ->
       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(Math.floor(win.pageXOffset)).to.eq(200)
 
     it "scrolls both axes of window to element", ->
       expect(@win.pageYOffset).to.eq(0)
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 9ba929bcd7be..55b2ef8f8fee 100644
--- a/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee
+++ b/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee
@@ -242,12 +242,13 @@ describe "src/cy/commands/actions/trigger", ->
       it "issues event to descendent", ->
         mouseovers = 0
 
-        $btn = $("
+
+	
+
+
\ No newline at end of file
diff --git a/packages/gateway/server/index.html b/packages/gateway/server/index.html
new file mode 100644
index 000000000000..ca4e8cdac365
--- /dev/null
+++ b/packages/gateway/server/index.html
@@ -0,0 +1,79 @@
+
+
+
+
+	
+	
+	
+	Document
+	
+	
+
+
+
+	
+	
+
+
+
\ No newline at end of file
diff --git a/packages/gateway/server/proxy.js b/packages/gateway/server/proxy.js
new file mode 100644
index 000000000000..efd6bec27306
--- /dev/null
+++ b/packages/gateway/server/proxy.js
@@ -0,0 +1,36 @@
+const express = require('express')
+const Promise = require('bluebird')
+const debug = require('debug')('gateway:proxy')
+
+const create = (send) => {
+  const app = express()
+  const server = require('http').createServer(app)
+  const io = require('socket.io')(server)
+  const listenAsync = Promise.promisify(server.listen, { context: server })
+
+  debug('starting proxy server')
+  // @ts-ignore
+  io.set('transports', ['websocket'])
+
+  io.on('connection', function (socket) {
+    debug('socket connected')
+
+    socket.on('command', (payload, cb) => {
+      debug('got command, %O', payload)
+
+      return send(payload)
+      .then((resp) => cb({ response: resp }))
+      .catch((err) => {
+        debug('command error', err)
+        cb({ error: 'error' }) //errors.clone(err) }))
+      })
+    })
+  })
+
+  // @ts-ignore
+  return listenAsync(3000)
+}
+
+module.exports = {
+  create,
+}
diff --git a/packages/gateway/server/static.js b/packages/gateway/server/static.js
new file mode 100644
index 000000000000..c855ab8b57e1
--- /dev/null
+++ b/packages/gateway/server/static.js
@@ -0,0 +1,14 @@
+const express = require('express')
+const app = express()
+const path = require('path')
+
+const start = () => {
+  app.use('/', express.static(`${__dirname}/`))
+  app.use('/node_modules', express.static(path.join(__dirname, '../node_modules')))
+
+  return app.listen(3001)
+}
+
+module.exports = {
+  start,
+}
diff --git a/packages/https-proxy/https.js b/packages/https-proxy/https.js
index 1756a4729b70..37b2bebbbafd 100644
--- a/packages/https-proxy/https.js
+++ b/packages/https-proxy/https.js
@@ -1,3 +1,17 @@
 require('@packages/coffee/register')
 
-module.exports = require('./test/helpers/https_server')
+const Promise = require('bluebird')
+const proxy = require('./test/helpers/proxy')
+const httpServer = require('./test/helpers/http_server')
+const httpsServer = require('./test/helpers/https_server')
+
+// module.exports = require('./test/helpers/https_server').start(1234)
+
+Promise.join(
+  httpServer.start(8888),
+
+  httpsServer.start(8444),
+  httpsServer.start(8445),
+
+  proxy.start(3333),
+)
diff --git a/packages/https-proxy/test/helpers/https_server.coffee b/packages/https-proxy/test/helpers/https_server.coffee
index 58b4e53ac419..ac60aa71973f 100644
--- a/packages/https-proxy/test/helpers/https_server.coffee
+++ b/packages/https-proxy/test/helpers/https_server.coffee
@@ -14,6 +14,7 @@ defaultOnRequest = (req, res) ->
 servers = []
 
 create = (onRequest) ->
+  console.log(onRequest ? defaultOnRequest)
   https.createServer(certs, onRequest ? defaultOnRequest)
 
 module.exports = {
diff --git a/packages/https-proxy/test/helpers/proxy.coffee b/packages/https-proxy/test/helpers/proxy.coffee
index fcc2078793cc..cae07182202c 100644
--- a/packages/https-proxy/test/helpers/proxy.coffee
+++ b/packages/https-proxy/test/helpers/proxy.coffee
@@ -1,5 +1,6 @@
 http       = require("http")
 path       = require("path")
+request    = require("request")
 httpsProxy = require("../../lib/proxy")
 
 prx = null
@@ -13,12 +14,14 @@ pipe = (req, res) ->
   .pipe(res)
 
 onConnect = (req, socket, head, proxy) ->
+  console.log("GOT ON CONNECT ", req.url)
   proxy.connect(req, socket, head, {
     onDirectConnection: (req, socket, head) ->
       ["localhost:8444", "localhost:12344"].includes(req.url)
   })
 
 onRequest = (req, res) ->
+  console.log("GOT ON REQUEST ", req.url, req.headers)
   pipe(req, res)
 
 module.exports = {
diff --git a/packages/launcher/lib/browsers.ts b/packages/launcher/lib/browsers.ts
index b9ae258bae8d..e58496aca3da 100644
--- a/packages/launcher/lib/browsers.ts
+++ b/packages/launcher/lib/browsers.ts
@@ -28,6 +28,37 @@ export const browsers: Browser[] = [
     profile: true,
     binary: 'google-chrome-canary',
   },
+  {
+    name: 'firefox',
+    family: 'firefox',
+    displayName: 'Firefox',
+    versionRegex: /Firefox (\S+)/,
+    profile: true,
+    binary: 'firefox',
+  },
+  {
+    name: 'firefoxDeveloperEdition',
+    family: 'firefox',
+    displayName: 'Firefox Developer Edition',
+    versionRegex: /Firefox Developer Edition (\S+)/,
+    profile: true,
+    binary: 'firefox-developer-edition',
+  },
+  {
+    name: 'firefoxNightly',
+    family: 'firefox',
+    displayName: 'Firefox Nightly',
+    versionRegex: /Firefox Nightly (\S+)/,
+    profile: true,
+    binary: 'firefox-nightly',
+  },
+  // {
+  //   name: 'ie',
+  //   displayName: 'IE',
+  //   versionRegex: /.*/,
+  //   profile: false,
+  //   binary: 'iexplorer',
+  // },
 ]
 
 /** starts a found browser and opens URL if given one */
diff --git a/packages/launcher/lib/darwin/index.ts b/packages/launcher/lib/darwin/index.ts
index 673fcc9dc09a..9c05bae97426 100644
--- a/packages/launcher/lib/darwin/index.ts
+++ b/packages/launcher/lib/darwin/index.ts
@@ -5,20 +5,41 @@ import { log } from '../log'
 import { merge, partial } from 'ramda'
 
 const detectCanary = partial(findApp, [
+  'Google Chrome Canary.app',
   'Contents/MacOS/Google Chrome Canary',
   'com.google.Chrome.canary',
   'KSVersion',
 ])
 const detectChrome = partial(findApp, [
+  'Google Chrome.app',
   'Contents/MacOS/Google Chrome',
   'com.google.Chrome',
   'KSVersion',
 ])
 const detectChromium = partial(findApp, [
-  'Contents/MacOS/Chromium',
+  'Chromium.app',
+  'Contents/DMacOS/Chromium',
   'org.chromium.Chromium',
   'CFBundleShortVersionString',
 ])
+const detectFirefox = partial(findApp, [
+  'Firefox.app',
+  'Contents/MacOS/firefox-bin',
+  'org.mozilla.firefox',
+  'CFBundleShortVersionString',
+])
+const detectFirefoxDeveloperEdition = partial(findApp, [
+  'Firefox Developer Edition.app',
+  'Contents/MacOS/firefox-bin',
+  'org.mozilla.firefoxdeveloperedition',
+  'CFBundleShortVersionString',
+])
+const detectFirefoxNightly = partial(findApp, [
+  'Firefox Nightly.app',
+  'Contents/MacOS/firefox-bin',
+  'org.mozilla.nightly',
+  'CFBundleShortVersionString',
+])
 
 type Detectors = {
   [index: string]: Function
@@ -28,6 +49,9 @@ const browsers: Detectors = {
   chrome: detectChrome,
   canary: detectCanary,
   chromium: detectChromium,
+  firefox: detectFirefox,
+  firefoxDeveloperEdition: detectFirefoxDeveloperEdition,
+  firefoxNightly: detectFirefoxNightly,
 }
 
 export function getVersionString (path: string) {
diff --git a/packages/launcher/lib/darwin/util.ts b/packages/launcher/lib/darwin/util.ts
index 5b900b5e5165..e31a0d315b48 100644
--- a/packages/launcher/lib/darwin/util.ts
+++ b/packages/launcher/lib/darwin/util.ts
@@ -57,16 +57,13 @@ export type AppInfo = {
   version: string
 }
 
-function formApplicationPath (executable: string) {
-  const parts = executable.split('/')
-  const name = parts[parts.length - 1]
-  const appName = `${name}.app`
-
+function formApplicationPath (appName: string) {
   return path.join('/Applications', appName)
 }
 
 /** finds an application and its version */
 export function findApp (
+  appName: string,
   executable: string,
   appId: string,
   versionProperty: string
@@ -87,7 +84,7 @@ export function findApp (
   }
 
   const tryFullApplicationFind = () => {
-    const applicationPath = formApplicationPath(executable)
+    const applicationPath = formApplicationPath(appName)
 
     log('looking for application %s', applicationPath)
 
diff --git a/packages/launcher/lib/types.ts b/packages/launcher/lib/types.ts
index 1aca1ebec540..5a47afc064db 100644
--- a/packages/launcher/lib/types.ts
+++ b/packages/launcher/lib/types.ts
@@ -1,9 +1,9 @@
 import { ChildProcess } from 'child_process'
 import * as Bluebird from 'bluebird'
 
-export type BrowserName = 'chrome' | 'chromium' | 'canary' | string
+export type BrowserName = 'chrome' | 'chromium' | 'canary' | 'firefox' | string
 
-export type BrowserFamily = 'chrome' | 'electron'
+export type BrowserFamily = 'chrome' | 'electron' | 'firefox'
 
 export type PlatformName = 'darwin' | 'linux' | 'win32'
 
diff --git a/packages/launcher/lib/windows/index.ts b/packages/launcher/lib/windows/index.ts
index cc234bdebffa..f8293ca07043 100644
--- a/packages/launcher/lib/windows/index.ts
+++ b/packages/launcher/lib/windows/index.ts
@@ -34,6 +34,30 @@ function formChromeCanaryAppPath () {
   return normalize(exe)
 }
 
+function formFirefoxAppPath () {
+  const exe = 'C:/Program Files (x86)/Mozilla Firefox/firefox.exe'
+
+  return normalize(exe)
+}
+
+function formFirefoxDeveloperEditionAppPath () {
+  const exe = 'C:/Program Files (x86)/Firefox Developer Edition/firefox.exe'
+
+  return normalize(exe)
+}
+
+function formFirefoxNightlyAppPath () {
+  const exe = 'C:/Program Files (x86)/Firefox Nightly/firefox.exe'
+
+  return normalize(exe)
+}
+
+function formIEAppPath () {
+  const exe = 'C:/Program Files (x86)/Internet Explorer/iexplore.exe'
+
+  return normalize(exe)
+}
+
 type NameToPath = (name: string) => string
 
 interface WindowsBrowserPaths {
@@ -41,12 +65,18 @@ interface WindowsBrowserPaths {
   chrome: NameToPath
   canary: NameToPath
   chromium: NameToPath
+  firefox: NameToPath
+  ie: NameToPath
 }
 
 const formPaths: WindowsBrowserPaths = {
   chrome: formFullAppPath,
   canary: formChromeCanaryAppPath,
   chromium: formChromiumAppPath,
+  firefox: formFirefoxAppPath,
+  firefoxDeveloperEdition: formFirefoxDeveloperEditionAppPath,
+  firefoxNightly: formFirefoxNightlyAppPath,
+  ie: formIEAppPath,
 }
 
 function getWindowsBrowser (name: string): Promise {
@@ -118,3 +148,6 @@ export function getVersionString (path: string) {
 export function detect (browser: Browser) {
   return getWindowsBrowser(browser.name)
 }
+
+// Get version of IE:
+// $ reg query "HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer" //v svcVersion
diff --git a/packages/reporter/src/runnables/runnables.scss b/packages/reporter/src/runnables/runnables.scss
index 6a69698f5dff..8b49dab4c9a7 100644
--- a/packages/reporter/src/runnables/runnables.scss
+++ b/packages/reporter/src/runnables/runnables.scss
@@ -1,14 +1,17 @@
+.fa { &:not(.fa-spin) { animation: none; } }
+
 .reporter {
+  min-height: 0; // needed for firefox or else scrolling gets funky
+
   .container {
     box-shadow: 0 1px 2px #aaa;
     flex-grow: 2;
-    overflow: auto;
-    padding-bottom: 40px;
+    overflow-y: auto;
   }
 
   .wrap {
     box-shadow: 0 1px 2px #aaa;
-    overflow: auto;
+    margin-bottom: 40px;
     padding-left: 0;
     width: 100%;
   }
@@ -169,6 +172,7 @@
     &.runnable-passed > .runnable-wrapper {
       .runnable-state {
         @extend .#{$fa-css-prefix}-check;
+
         color: $pass;
       }
     }
diff --git a/packages/runner/src/app/app.scss b/packages/runner/src/app/app.scss
index f448bd32a807..21dbfdae5a18 100644
--- a/packages/runner/src/app/app.scss
+++ b/packages/runner/src/app/app.scss
@@ -9,6 +9,7 @@
   position: absolute;
   top: 0;
   width: 33%;
+  z-index: 1;
 
   .is-reporter-sized & {
     min-width: 0;
diff --git a/packages/runner/src/app/app.spec.jsx b/packages/runner/src/app/app.spec.jsx
index dc8c037804b7..64e055b4fca9 100644
--- a/packages/runner/src/app/app.spec.jsx
+++ b/packages/runner/src/app/app.spec.jsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import { shallow } from 'enzyme'
 import sinon from 'sinon'
-
 import * as reporter from '@packages/reporter'
 import Message from '../message/message'
 import State from '../lib/state'
diff --git a/packages/runner/src/errors/automation-disconnected.jsx b/packages/runner/src/errors/automation-disconnected.jsx
index 0467742198e0..022e456e8f9a 100644
--- a/packages/runner/src/errors/automation-disconnected.jsx
+++ b/packages/runner/src/errors/automation-disconnected.jsx
@@ -3,7 +3,7 @@ import React from 'react'
 export default ({ onReload }) => (
   
-

Whoops, the Cypress Chrome extension has disconnected.

+

Whoops, the Cypress extension has disconnected.

Cypress cannot run tests without this extension.

+
+
+ +
-
-
-
- -
- -
- -
- -
- +
+
+
+ +
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 074/370] 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 075/370] 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 076/370] 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 1977a39d773a1c05cb999b444424147dca27cfba Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Sun, 6 Oct 2019 12:57:52 -0400 Subject: [PATCH 077/370] temp 10/06/19 [skip ci] --- .../driver/src/cy/commands/actions/type.js | 2 +- packages/driver/src/cy/mouse.js | 5 +-- packages/driver/src/cy/retries.coffee | 4 +++ packages/driver/tsconfig.json | 34 +------------------ 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index 094b9b808e16..fe43c0dab607 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -51,7 +51,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { let obj table[id] = (obj = {}) - const modifiers = $Keyboard.modifiersToString(keyboard.getActiveModifiers()) + const modifiers = $Keyboard.modifiersToString($Keyboard.getActiveModifiers(state)) if (modifiers) { obj.modifiers = modifiers diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index 3c904eef841c..47dc1e6080e2 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -2,7 +2,7 @@ const $ = require('jquery') const _ = require('lodash') const $dom = require('../dom') const $elements = require('../dom/elements') -const $Keyboard = require('./keyboard') +const $Keyboard = require('./keyboard').default const $selection = require('../dom/selection') const debug = require('debug')('cypress:driver:mouse') @@ -42,7 +42,8 @@ const getMouseCoords = (state) => { const create = (state, keyboard, focused) => { const mouse = { _getDefaultMouseOptions (x, y, win) { - const _activeModifiers = keyboard.getActiveModifiers() + debug({ keyboard }) + const _activeModifiers = $Keyboard.getActiveModifiers(state) const modifiersEventOptions = $Keyboard.toModifiersEventOptions(_activeModifiers) const coordsEventOptions = toCoordsEventOptions(x, y, win) diff --git a/packages/driver/src/cy/retries.coffee b/packages/driver/src/cy/retries.coffee index 33f6fe346f29..b77eafc85356 100644 --- a/packages/driver/src/cy/retries.coffee +++ b/packages/driver/src/cy/retries.coffee @@ -6,6 +6,10 @@ $utils = require("../cypress/utils") create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) -> return { retry: (fn, options, log) -> + ## FIXME: remove this debugging code + if options.error + if !options.error.message.includes('coordsHistory must be') + console.error(options.error) ## remove the runnables timeout because we are now in retry ## mode and should be handling timing out ourselves and dont ## want to accidentally time out via mocha diff --git a/packages/driver/tsconfig.json b/packages/driver/tsconfig.json index 4c1884efbeec..9255340ab9d0 100644 --- a/packages/driver/tsconfig.json +++ b/packages/driver/tsconfig.json @@ -1,55 +1,23 @@ { "compilerOptions": { - /* Basic Options */ "target": "es5", "module": "commonjs", - /* - * Allow javascript files to be compiled. - * Override this in modules that need JS - */ "allowJs": true, "noImplicitAny": false, "noImplicitThis": false, "strictFunctionTypes": true, "preserveWatchOutput": true, - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - /* Generates corresponding '.d.ts' file. */ - // "declaration": true, - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - /* Generates corresponding '.map' file. */ "sourceMap": true, - /* Import emit helpers from 'tslib'. */ "importHelpers": true, "strictNullChecks": true, - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ "strict": true, "forceConsistentCasingInFileNames": true, - /** - * Skip type checking of all declaration files (*.d.ts). - * TODO: Look into changing this in the future - */ "skipLibCheck": true, - /* Additional Checks */ - /* Report errors on unused locals. */ "noEmit": true, - // "noUnusedLocals": true, - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - /* Report error when not all code paths in function return a value. */ "noImplicitReturns": true, - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ "allowSyntheticDefaultImports": true, "outDir": "dist", "esModuleInterop": true, "noErrorTruncation": true } -} \ No newline at end of file +} 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 078/370] 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 1640ebbe3911fd30c6cb5512161012e0b87ba462 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Mon, 7 Oct 2019 14:33:42 -0400 Subject: [PATCH 079/370] fix after merge from mouse-fixes branch / develop --- .../driver/src/cy/commands/actions/type.js | 2 +- .../integration/commands/actions/type_spec.js | 1136 ++++++++--------- 2 files changed, 569 insertions(+), 569 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index b04fb60b8830..211dcf080497 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -441,7 +441,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { options.ensure = { position: true, visibility: true, - receivability: true, + notDisabled: true, notAnimating: true, notCovered: true, notReadonly: 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 7b3691a694b7..350aff86c9e8 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -4307,840 +4307,840 @@ describe('src/cy/commands/actions/type', () => { // done() // }) }) + }) - it('throws when subject is a collection of elements', function (done) { - cy.get('textarea,:text').then(function ($inputs) { - this.num = $inputs.length + it('throws when subject is a collection of elements', function (done) { + cy.get('textarea,:text').then(function ($inputs) { + this.num = $inputs.length - $inputs - }).type('foo') + $inputs + }).type('foo') - cy.on('fail', (err) => { - expect(err.message).to.include(`cy.type() can only be called on a single element. Your subject contained ${this.num} elements.`) + cy.on('fail', (err) => { + expect(err.message).to.include(`cy.type() can only be called on a single element. Your subject contained ${this.num} elements.`) - done() - }) + done() }) + }) - it('throws when the subject isnt visible', function (done) { - cy.$$('input:text:first').show().hide() - - cy.on('fail', (err) => { - const { lastLog } = this + it('throws when the subject isnt visible', function (done) { + cy.$$('input:text:first').show().hide() - expect(this.logs.length).to.eq(2) - expect(lastLog.get('error')).to.eq(err) - expect(err.message).to.include('cy.type() failed because this element is not visible') + cy.on('fail', (err) => { + const { lastLog } = this - done() - }) + expect(this.logs.length).to.eq(2) + expect(lastLog.get('error')).to.eq(err) + expect(err.message).to.include('cy.type() failed because this element is not visible') - cy.get('input:text:first').type('foo') + done() }) - it('throws when subject is disabled', function (done) { - cy.$$('input:text:first').prop('disabled', true) + cy.get('input:text:first').type('foo') + }) - cy.on('fail', (err) => { - // get + type logs - expect(this.logs.length).eq(2) - expect(err.message).to.include('cy.type() failed because this element is disabled:\n') + it('throws when subject is disabled', function (done) { + cy.$$('input:text:first').prop('disabled', true) - done() - }) + cy.on('fail', (err) => { + // get + type logs + expect(this.logs.length).eq(2) + expect(err.message).to.include('cy.type() failed because this element is disabled:\n') - cy.get('input:text:first').type('foo') + done() }) - it('throws when submitting within nested forms') + cy.get('input:text:first').type('foo') + }) - it('logs once when not dom subject', function (done) { - cy.on('fail', (err) => { - const { lastLog } = this + it('throws when submitting within nested forms') - expect(this.logs.length).to.eq(1) - expect(lastLog.get('error')).to.eq(err) + it('logs once when not dom subject', function (done) { + cy.on('fail', (err) => { + const { lastLog } = this - done() - }) + expect(this.logs.length).to.eq(1) + expect(lastLog.get('error')).to.eq(err) - cy.type('foobar') + done() }) - it('throws when input cannot be clicked', function (done) { - const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) + cy.type('foobar') + }) - $('span on button') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + it('throws when input cannot be clicked', function (done) { + const $input = $('') + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.include('cy.type() failed because this element') - expect(err.message).to.include('is being covered by another element') + $('span on button') + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) - done() - }) + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.include('cy.type() failed because this element') + expect(err.message).to.include('is being covered by another element') - cy.get('#input-covered-in-span').type('foo') + done() }) - it('throws when special characters dont exist', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) + cy.get('#input-covered-in-span').type('foo') + }) - const allChars = _.keys(cy.devices.keyboard.specialChars).concat(_.keys(cy.devices.keyboard.modifierChars)).join(', ') + it('throws when special characters dont exist', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) - expect(err.message).to.eq(`Special character sequence: '{bar}' is not recognized. Available sequences are: ${allChars} + const allChars = _.keys(cy.devices.keyboard.getKeymap()).join(', ') + + expect(err.message).to.eq(`Special character sequence: '{bar}' is not recognized. Available sequences are: ${allChars} If you want to skip parsing special character sequences and type the text exactly as written, pass the option: {parseSpecialCharSequences: false} https://on.cypress.io/type`) - done() - }) - - cy.get(':text:first').type('foo{bar}') + done() }) - it('throws when attemping to type tab', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('{tab} isn\'t a supported character sequence. You\'ll want to use the command cy.tab(), which is not ready yet, but when it is done that\'s what you\'ll use.') - done() - }) + cy.get(':text:first').type('foo{bar}') + }) - cy.get(':text:first').type('foo{tab}') + it('throws when attemping to type tab', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('{tab} isn\'t a supported character sequence. You\'ll want to use the command cy.tab(), which is not ready yet, but when it is done that\'s what you\'ll use.') + done() }) - it('throws on an empty string', function (done) { + cy.get(':text:first').type('foo{tab}') + }) - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('cy.type() cannot accept an empty String. You need to actually type something.') - done() - }) + it('throws on an empty string', function (done) { - cy.get(':text:first').type('') + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('cy.type() cannot accept an empty String. You need to actually type something.') + done() }) - it('allows typing spaces', () => { - cy - .get(':text:first').type(' ') - .should('have.value', ' ') - }) + cy.get(':text:first').type('') + }) - it('allows typing special characters', () => { + it('allows typing spaces', () => { + cy + .get(':text:first').type(' ') + .should('have.value', ' ') + }) + + it('allows typing special characters', () => { + cy + .get(':text:first').type('{esc}') + .should('have.value', '') + }) + + _.each(['toString', 'toLocaleString', 'hasOwnProperty', 'valueOf', + 'undefined', 'null', 'true', 'false', 'True', 'False'], (val) => { + it(`allows typing reserved Javscript word (${val})`, () => { cy - .get(':text:first').type('{esc}') - .should('have.value', '') + .get(':text:first').type(val) + .should('have.value', val) }) + }) - _.each(['toString', 'toLocaleString', 'hasOwnProperty', 'valueOf', - 'undefined', 'null', 'true', 'false', 'True', 'False'], (val) => { - it(`allows typing reserved Javscript word (${val})`, () => { + describe('naughtly strings', () => { + _.each(['Ω≈ç√∫˜µ≤≥÷', '2.2250738585072011e-308', '田中さんにあげて下さい', + '', '⁰⁴⁵₀₁₂', '🐵 🙈 🙉 🙊', + '', '$USER'], (val) => { + it(`allows typing some naughtly strings (${val})`, () => { cy .get(':text:first').type(val) .should('have.value', val) }) }) + }) - describe('naughtly strings', () => { - _.each(['Ω≈ç√∫˜µ≤≥÷', '2.2250738585072011e-308', '田中さんにあげて下さい', - '', '⁰⁴⁵₀₁₂', '🐵 🙈 🙉 🙊', - '', '$USER'], (val) => { - it(`allows typing some naughtly strings (${val})`, () => { - cy - .get(':text:first').type(val) - .should('have.value', val) - }) - }) - }) - - it('allows typing special characters', () => { - cy - .get(':text:first').type('{esc}') - .should('have.value', '') - }) - - it('can type into input with invalid type attribute', () => { - cy.get(':text:first') - .invoke('attr', 'type', 'asdf') - .type('foobar') - .should('have.value', 'foobar') - }) + it('allows typing special characters', () => { + cy + .get(':text:first').type('{esc}') + .should('have.value', '') + }) - describe('throws when trying to type', () => { + it('can type into input with invalid type attribute', () => { + cy.get(':text:first') + .invoke('attr', 'type', 'asdf') + .type('foobar') + .should('have.value', 'foobar') + }) - _.each([NaN, Infinity, [], {}, null, undefined], (val) => { - it(`throws when trying to type: ${val}`, function (done) { - const logs = [] + describe('throws when trying to type', () => { - cy.on('log:added', (attrs, log) => { - return logs.push(log) - }) + _.each([NaN, Infinity, [], {}, null, undefined], (val) => { + it(`throws when trying to type: ${val}`, function (done) { + const logs = [] - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq(`cy.type() can only accept a String or Number. You passed in: '${val}'`) - done() - }) + cy.on('log:added', (attrs, log) => { + return logs.push(log) + }) - cy.get(':text:first').type(val) + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq(`cy.type() can only accept a String or Number. You passed in: '${val}'`) + done() }) + + cy.get(':text:first').type(val) }) }) + }) - it('throws when type is canceled by preventingDefault mousedown') + it('throws when type is canceled by preventingDefault mousedown') - it('throws when element animation exceeds timeout', (done) => { + it('throws when element animation exceeds timeout', (done) => { // force the animation calculation to think we moving at a huge distance ;-) - cy.stub(Cypress.utils, 'getDistanceBetween').returns(100000) + cy.stub(Cypress.utils, 'getDistanceBetween').returns(100000) - let keydowns = 0 - - cy.$$(':text:first').on('keydown', () => { - keydowns += 1 - }) + let keydowns = 0 - cy.on('fail', (err) => { - expect(keydowns).to.eq(0) - expect(err.message).to.include('cy.type() could not be issued because this element is currently animating:\n') + cy.$$(':text:first').on('keydown', () => { + keydowns += 1 + }) - done() - }) + cy.on('fail', (err) => { + expect(keydowns).to.eq(0) + expect(err.message).to.include('cy.type() could not be issued because this element is currently animating:\n') - cy.get(':text:first').type('foo') + done() }) - it('eventually fails the assertion', function (done) { - cy.on('fail', (err) => { - const { lastLog } = this + cy.get(':text:first').type('foo') + }) - expect(err.message).to.include(lastLog.get('error').message) - 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) + it('eventually fails the assertion', function (done) { + cy.on('fail', (err) => { + const { lastLog } = this - done() - }) + expect(err.message).to.include(lastLog.get('error').message) + 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) - cy.get('input:first').type('f').should('have.class', 'typed') + done() }) - it('does not log an additional log on failure', function (done) { - cy.on('fail', () => { - expect(this.logs.length).to.eq(3) + cy.get('input:first').type('f').should('have.class', 'typed') + }) - done() - }) + it('does not log an additional log on failure', function (done) { + cy.on('fail', () => { + expect(this.logs.length).to.eq(3) - cy.get('input:first').type('f').should('have.class', 'typed') + done() }) - context('[type=date]', () => { - it('throws when chars is not a string', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a date input with cy.type() requires a valid date with the format \'yyyy-MM-dd\'. You passed: 1989') + cy.get('input:first').type('f').should('have.class', 'typed') + }) - done() - }) + context('[type=date]', () => { + it('throws when chars is not a string', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a date input with cy.type() requires a valid date with the format \'yyyy-MM-dd\'. You passed: 1989') - cy.get('#date-without-value').type(1989) + done() }) - it('throws when chars is invalid format', function (done) { - cy.on('fail', (err) => { - // debugger - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a date input with cy.type() requires a valid date with the format \'yyyy-MM-dd\'. You passed: 01-01-1989') + cy.get('#date-without-value').type(1989) + }) - done() - }) + it('throws when chars is invalid format', function (done) { + cy.on('fail', (err) => { + // debugger + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a date input with cy.type() requires a valid date with the format \'yyyy-MM-dd\'. You passed: 01-01-1989') - cy.get('#date-without-value').type('01-01-1989') + done() }) - it('throws when chars is invalid date', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a date input with cy.type() requires a valid date with the format \'yyyy-MM-dd\'. You passed: 1989-04-31') + cy.get('#date-without-value').type('01-01-1989') + }) - done() - }) + it('throws when chars is invalid date', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a date input with cy.type() requires a valid date with the format \'yyyy-MM-dd\'. You passed: 1989-04-31') - cy.get('#date-without-value').type('1989-04-31') + done() }) - }) - context('[type=month]', () => { - it('throws when chars is not a string', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a month input with cy.type() requires a valid month with the format \'yyyy-MM\'. You passed: 6') + cy.get('#date-without-value').type('1989-04-31') + }) + }) - done() - }) + context('[type=month]', () => { + it('throws when chars is not a string', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a month input with cy.type() requires a valid month with the format \'yyyy-MM\'. You passed: 6') - cy.get('#month-without-value').type(6) + done() }) - it('throws when chars is invalid format', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a month input with cy.type() requires a valid month with the format \'yyyy-MM\'. You passed: 01/2000') + cy.get('#month-without-value').type(6) + }) - done() - }) + it('throws when chars is invalid format', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a month input with cy.type() requires a valid month with the format \'yyyy-MM\'. You passed: 01/2000') - cy.get('#month-without-value').type('01/2000') + done() }) - it('throws when chars is invalid month', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a month input with cy.type() requires a valid month with the format \'yyyy-MM\'. You passed: 1989-13') + cy.get('#month-without-value').type('01/2000') + }) - done() - }) + it('throws when chars is invalid month', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a month input with cy.type() requires a valid month with the format \'yyyy-MM\'. You passed: 1989-13') - cy.get('#month-without-value').type('1989-13') + done() }) + + cy.get('#month-without-value').type('1989-13') }) + }) - context('[type=tel]', () => { - it('can edit tel', () => { - cy.get('#by-name > input[type="tel"]') - .type('1234567890') - .should('have.prop', 'value', '1234567890') - }) + context('[type=tel]', () => { + it('can edit tel', () => { + cy.get('#by-name > input[type="tel"]') + .type('1234567890') + .should('have.prop', 'value', '1234567890') }) + }) - // it "throws when chars is invalid format", (done) -> - // cy.on "fail", (err) => - // expect(@logs.length).to.eq(2) - // expect(err.message).to.eq("Typing into a week input with cy.type() requires a valid week with the format 'yyyy-Www', where W is the literal character 'W' and ww is the week number (00-53). You passed: 2005/W18") - // done() + // it "throws when chars is invalid format", (done) -> + // cy.on "fail", (err) => + // expect(@logs.length).to.eq(2) + // expect(err.message).to.eq("Typing into a week input with cy.type() requires a valid week with the format 'yyyy-Www', where W is the literal character 'W' and ww is the week number (00-53). You passed: 2005/W18") + // done() - context('[type=week]', () => { - it('throws when chars is not a string', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a week input with cy.type() requires a valid week with the format \'yyyy-Www\', where W is the literal character \'W\' and ww is the week number (00-53). You passed: 23') - - done() - }) + context('[type=week]', () => { + it('throws when chars is not a string', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a week input with cy.type() requires a valid week with the format \'yyyy-Www\', where W is the literal character \'W\' and ww is the week number (00-53). You passed: 23') - cy.get('#week-without-value').type(23) + done() }) - it('throws when chars is invalid format', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a week input with cy.type() requires a valid week with the format \'yyyy-Www\', where W is the literal character \'W\' and ww is the week number (00-53). You passed: 2005/W18') + cy.get('#week-without-value').type(23) + }) - done() - }) + it('throws when chars is invalid format', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a week input with cy.type() requires a valid week with the format \'yyyy-Www\', where W is the literal character \'W\' and ww is the week number (00-53). You passed: 2005/W18') - cy.get('#week-without-value').type('2005/W18') + done() }) - it('throws when chars is invalid week', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.eq('Typing into a week input with cy.type() requires a valid week with the format \'yyyy-Www\', where W is the literal character \'W\' and ww is the week number (00-53). You passed: 1995-W60') + cy.get('#week-without-value').type('2005/W18') + }) - done() - }) + it('throws when chars is invalid week', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.eq('Typing into a week input with cy.type() requires a valid week with the format \'yyyy-Www\', where W is the literal character \'W\' and ww is the week number (00-53). You passed: 1995-W60') - cy.get('#week-without-value').type('1995-W60') + done() }) - }) - context('[type=time]', () => { - it('throws when chars is not a string', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.equal(2) - expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 9999') + cy.get('#week-without-value').type('1995-W60') + }) + }) - done() - }) + context('[type=time]', () => { + it('throws when chars is not a string', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.equal(2) + expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 9999') - cy.get('#time-without-value').type(9999) + done() }) - it('throws when chars is invalid format (1:30)', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.equal(2) - expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 1:30') + cy.get('#time-without-value').type(9999) + }) - done() - }) + it('throws when chars is invalid format (1:30)', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.equal(2) + expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 1:30') - cy.get('#time-without-value').type('1:30') + done() }) - it('throws when chars is invalid format (01:30pm)', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.equal(2) - expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 01:30pm') + cy.get('#time-without-value').type('1:30') + }) - done() - }) + it('throws when chars is invalid format (01:30pm)', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.equal(2) + expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 01:30pm') - cy.get('#time-without-value').type('01:30pm') + done() }) - it('throws when chars is invalid format (01:30:30.3333)', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.equal(2) - expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 01:30:30.3333') + cy.get('#time-without-value').type('01:30pm') + }) - done() - }) + it('throws when chars is invalid format (01:30:30.3333)', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.equal(2) + expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 01:30:30.3333') - cy.get('#time-without-value').type('01:30:30.3333') + done() }) - it('throws when chars is invalid time', function (done) { - cy.on('fail', (err) => { - expect(this.logs.length).to.equal(2) - expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 01:60') + cy.get('#time-without-value').type('01:30:30.3333') + }) - done() - }) + it('throws when chars is invalid time', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.equal(2) + expect(err.message).to.equal('Typing into a time input with cy.type() requires a valid time with the format \'HH:mm\', \'HH:mm:ss\' or \'HH:mm:ss.SSS\', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: 01:60') - cy.get('#time-without-value').type('01:60') + done() }) + + cy.get('#time-without-value').type('01:60') }) }) }) + }) - context('#clear', () => { - it('does not change the subject', () => { - const textarea = cy.$$('textarea') + context('#clear', () => { + it('does not change the subject', () => { + const textarea = cy.$$('textarea') - cy.get('textarea').clear().then(($textarea) => { - expect($textarea).to.match(textarea) - }) + cy.get('textarea').clear().then(($textarea) => { + expect($textarea).to.match(textarea) }) + }) - it('removes the current value', () => { - const textarea = cy.$$('#comments') + it('removes the current value', () => { + const textarea = cy.$$('#comments') - textarea.val('foo bar') + textarea.val('foo bar') - // make sure it really has that value first - expect(textarea).to.have.value('foo bar') + // make sure it really has that value first + expect(textarea).to.have.value('foo bar') - cy.get('#comments').clear().then(($textarea) => { - expect($textarea).to.have.value('') - }) + cy.get('#comments').clear().then(($textarea) => { + expect($textarea).to.have.value('') }) + }) - it('waits until element is no longer disabled', () => { - const textarea = cy.$$('#comments').val('foo bar').prop('disabled', true) + it('waits until element is no longer disabled', () => { + const textarea = cy.$$('#comments').val('foo bar').prop('disabled', true) - let retried = false - let clicks = 0 + let retried = false + let clicks = 0 - textarea.on('click', () => { - clicks += 1 - }) + textarea.on('click', () => { + clicks += 1 + }) - cy.on('command:retry', _.after(3, () => { - textarea.prop('disabled', false) - retried = true - })) + cy.on('command:retry', _.after(3, () => { + textarea.prop('disabled', false) + retried = true + })) - cy.get('#comments').clear().then(() => { - expect(clicks).to.eq(1) + cy.get('#comments').clear().then(() => { + expect(clicks).to.eq(1) - expect(retried).to.be.true - }) + expect(retried).to.be.true }) + }) - it('can forcibly click even when being covered by another element', () => { - const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) - - $('span on input') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + it('can forcibly click even when being covered by another element', () => { + const $input = $('') + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) - let clicked = false + $('span on input') + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) - $input.on('click', () => { - clicked = true - }) + let clicked = false - cy.get('#input-covered-in-span').clear({ force: true }).then(() => { - expect(clicked).to.be.true - }) + $input.on('click', () => { + clicked = true }) - it('passes timeout and interval down to click', (done) => { - const input = $('').attr('id', 'input-covered-in-span').prependTo(cy.$$('body')) + cy.get('#input-covered-in-span').clear({ force: true }).then(() => { + expect(clicked).to.be.true + }) + }) - $('span on input').css({ position: 'absolute', left: input.offset().left, top: input.offset().top, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).prependTo(cy.$$('body')) + it('passes timeout and interval down to click', (done) => { + const input = $('').attr('id', 'input-covered-in-span').prependTo(cy.$$('body')) - cy.on('command:retry', (options) => { - expect(options.timeout).to.eq(1000) - expect(options.interval).to.eq(60) + $('span on input').css({ position: 'absolute', left: input.offset().left, top: input.offset().top, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).prependTo(cy.$$('body')) - done() - }) + cy.on('command:retry', (options) => { + expect(options.timeout).to.eq(1000) + expect(options.interval).to.eq(60) - cy.get('#input-covered-in-span').clear({ timeout: 1000, interval: 60 }) - }) - - context('works on input type', () => { - const inputTypes = [ - 'date', - 'datetime', - 'datetime-local', - 'email', - 'month', - 'number', - 'password', - 'search', - 'tel', - 'text', - 'time', - 'url', - 'week', - ] - - inputTypes.forEach((type) => { - it(type, () => { - cy.get(`#${type}-with-value`).clear().then(($input) => { - expect($input.val()).to.equal('') - }) - }) - }) + done() }) - describe('assertion verification', () => { - beforeEach(function () { - cy.on('log:added', (attrs, log) => { - if (log.get('name') === 'assert') { - this.lastLog = log - } - }) + cy.get('#input-covered-in-span').clear({ timeout: 1000, interval: 60 }) + }) - null - }) + context('works on input type', () => { + const inputTypes = [ + 'date', + 'datetime', + 'datetime-local', + 'email', + 'month', + 'number', + 'password', + 'search', + 'tel', + 'text', + 'time', + 'url', + 'week', + ] - it('eventually passes the assertion', () => { - cy.$$('input:first').keyup(function () { - _.delay(() => { - $(this).addClass('cleared') - } - , 100) + inputTypes.forEach((type) => { + it(type, () => { + cy.get(`#${type}-with-value`).clear().then(($input) => { + expect($input.val()).to.equal('') }) + }) + }) + }) - cy.get('input:first').clear().should('have.class', 'cleared').then(function () { - const { lastLog } = this + describe('assertion verification', () => { + beforeEach(function () { + cy.on('log:added', (attrs, log) => { + if (log.get('name') === 'assert') { + this.lastLog = log + } + }) - expect(lastLog.get('name')).to.eq('assert') - expect(lastLog.get('state')).to.eq('passed') + null + }) - expect(lastLog.get('ended')).to.be.true - }) + it('eventually passes the assertion', () => { + cy.$$('input:first').keyup(function () { + _.delay(() => { + $(this).addClass('cleared') + } + , 100) }) - it('eventually passes the assertion on multiple inputs', () => { - cy.$$('input').keyup(function () { - _.delay(() => { - $(this).addClass('cleared') - } - , 100) - }) + cy.get('input:first').clear().should('have.class', 'cleared').then(function () { + const { lastLog } = this + + expect(lastLog.get('name')).to.eq('assert') + expect(lastLog.get('state')).to.eq('passed') - cy.get('input').invoke('slice', 0, 2).clear().should('have.class', 'cleared') + expect(lastLog.get('ended')).to.be.true }) }) - describe('errors', () => { - beforeEach(function () { - Cypress.config('defaultCommandTimeout', 100) + it('eventually passes the assertion on multiple inputs', () => { + cy.$$('input').keyup(function () { + _.delay(() => { + $(this).addClass('cleared') + } + , 100) + }) - this.logs = [] + cy.get('input').invoke('slice', 0, 2).clear().should('have.class', 'cleared') + }) + }) - cy.on('log:added', (attrs, log) => { - this.lastLog = log + describe('errors', () => { + beforeEach(function () { + Cypress.config('defaultCommandTimeout', 100) - this.logs.push(log) - }) + this.logs = [] + + cy.on('log:added', (attrs, log) => { + this.lastLog = log - null + this.logs.push(log) }) - it('throws when not a dom subject', (done) => { - cy.on('fail', (err) => { - done() - }) + null + }) - cy.noop({}).clear() + it('throws when not a dom subject', (done) => { + cy.on('fail', (err) => { + done() }) - it('throws when subject is not in the document', (done) => { - let cleared = 0 + cy.noop({}).clear() + }) - const input = cy.$$('input:first').val('123').keydown((e) => { - cleared += 1 + it('throws when subject is not in the document', (done) => { + let cleared = 0 - input.remove() - }) + const input = cy.$$('input:first').val('123').keydown((e) => { + cleared += 1 - cy.on('fail', (err) => { - expect(cleared).to.eq(1) - expect(err.message).to.include('cy.clear() failed because this element') + input.remove() + }) - done() - }) + cy.on('fail', (err) => { + expect(cleared).to.eq(1) + expect(err.message).to.include('cy.clear() failed because this element') - cy.get('input:first').clear().clear() + done() }) - it('throws if any subject isnt a textarea or text-like', function (done) { - cy.on('fail', (err) => { - const { lastLog } = this + cy.get('input:first').clear().clear() + }) - expect(this.logs.length).to.eq(3) - expect(lastLog.get('error')).to.eq(err) - expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') - expect(err.message).to.include('The element cleared was:') - expect(err.message).to.include('
...
') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + it('throws if any subject isnt a textarea or text-like', function (done) { + cy.on('fail', (err) => { + const { lastLog } = this - done() - }) + expect(this.logs.length).to.eq(3) + expect(lastLog.get('error')).to.eq(err) + expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') + expect(err.message).to.include('The element cleared was:') + expect(err.message).to.include('
...
') + expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') - cy.get('textarea:first,form#checkboxes').clear() + done() }) - it('throws if any subject isnt a :text', (done) => { - cy.on('fail', (err) => { - expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') - expect(err.message).to.include('The element cleared was:') - expect(err.message).to.include('
...
') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + cy.get('textarea:first,form#checkboxes').clear() + }) - done() - }) + it('throws if any subject isnt a :text', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') + expect(err.message).to.include('The element cleared was:') + expect(err.message).to.include('
...
') + expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') - cy.get('div').clear() + done() }) - it('throws on an input radio', (done) => { - cy.on('fail', (err) => { - expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') - expect(err.message).to.include('The element cleared was:') - expect(err.message).to.include('') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + cy.get('div').clear() + }) - done() - }) + it('throws on an input radio', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') + expect(err.message).to.include('The element cleared was:') + expect(err.message).to.include('') + expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') - cy.get(':radio').clear() + done() }) - it('throws on an input checkbox', (done) => { - cy.on('fail', (err) => { - expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') - expect(err.message).to.include('The element cleared was:') - expect(err.message).to.include('') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + cy.get(':radio').clear() + }) - done() - }) + it('throws on an input checkbox', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') + expect(err.message).to.include('The element cleared was:') + expect(err.message).to.include('') + expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') - cy.get(':checkbox').clear() + done() }) - it('throws when the subject isnt visible', (done) => { - cy.$$('input:text:first').show().hide() + cy.get(':checkbox').clear() + }) - cy.on('fail', (err) => { - expect(err.message).to.include('cy.clear() failed because this element is not visible') + it('throws when the subject isnt visible', (done) => { + cy.$$('input:text:first').show().hide() - done() - }) + cy.on('fail', (err) => { + expect(err.message).to.include('cy.clear() failed because this element is not visible') - cy.get('input:text:first').clear() + done() }) - it('throws when subject is disabled', function (done) { - cy.$$('input:text:first').prop('disabled', true) + cy.get('input:text:first').clear() + }) - cy.on('fail', (err) => { - // get + type logs - expect(this.logs.length).eq(2) - expect(err.message).to.include('cy.clear() failed because this element is disabled:\n') + it('throws when subject is disabled', function (done) { + cy.$$('input:text:first').prop('disabled', true) - done() - }) + cy.on('fail', (err) => { + // get + type logs + expect(this.logs.length).eq(2) + expect(err.message).to.include('cy.clear() failed because this element is disabled:\n') - cy.get('input:text:first').clear() + done() }) - it('logs once when not dom subject', function (done) { - cy.on('fail', (err) => { - const { lastLog } = this + cy.get('input:text:first').clear() + }) - expect(this.logs.length).to.eq(1) - expect(lastLog.get('error')).to.eq(err) + it('logs once when not dom subject', function (done) { + cy.on('fail', (err) => { + const { lastLog } = this - done() - }) + expect(this.logs.length).to.eq(1) + expect(lastLog.get('error')).to.eq(err) - cy.clear() + done() }) - it('throws when input cannot be cleared', function (done) { - const $input = $('') - .attr('id', 'input-covered-in-span') - .prependTo(cy.$$('body')) + cy.clear() + }) - $('span on input') - .css({ - position: 'absolute', - left: $input.offset().left, - top: $input.offset().top, - padding: 5, - display: 'inline-block', - backgroundColor: 'yellow', - }) - .prependTo(cy.$$('body')) + it('throws when input cannot be cleared', function (done) { + const $input = $('') + .attr('id', 'input-covered-in-span') + .prependTo(cy.$$('body')) - cy.on('fail', (err) => { - expect(this.logs.length).to.eq(2) - expect(err.message).to.include('cy.clear() failed because this element') - expect(err.message).to.include('is being covered by another element') + $('span on input') + .css({ + position: 'absolute', + left: $input.offset().left, + top: $input.offset().top, + padding: 5, + display: 'inline-block', + backgroundColor: 'yellow', + }) + .prependTo(cy.$$('body')) - done() - }) + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(2) + expect(err.message).to.include('cy.clear() failed because this element') + expect(err.message).to.include('is being covered by another element') - cy.get('#input-covered-in-span').clear() + done() }) - it('eventually fails the assertion', function (done) { - cy.on('fail', (err) => { - const { lastLog } = this + cy.get('#input-covered-in-span').clear() + }) - expect(err.message).to.include(lastLog.get('error').message) - 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) + it('eventually fails the assertion', function (done) { + cy.on('fail', (err) => { + const { lastLog } = this - done() - }) + expect(err.message).to.include(lastLog.get('error').message) + 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) - cy.get('input:first').clear().should('have.class', 'cleared') + done() }) - it('does not log an additional log on failure', function (done) { - const logs = [] + cy.get('input:first').clear().should('have.class', 'cleared') + }) - cy.on('log:added', (attrs, log) => { - return logs.push(log) - }) + it('does not log an additional log on failure', function (done) { + const logs = [] - cy.on('fail', () => { - expect(this.logs.length).to.eq(3) + cy.on('log:added', (attrs, log) => { + return logs.push(log) + }) - done() - }) + cy.on('fail', () => { + expect(this.logs.length).to.eq(3) - cy.get('input:first').clear().should('have.class', 'cleared') + done() }) - }) - describe('.log', () => { - beforeEach(function () { - cy.on('log:added', (attrs, log) => { - this.lastLog = log - }) + cy.get('input:first').clear().should('have.class', 'cleared') + }) + }) - null + describe('.log', () => { + beforeEach(function () { + cy.on('log:added', (attrs, log) => { + this.lastLog = log }) - it('logs immediately before resolving', () => { - const $input = cy.$$('input:first') + null + }) + + it('logs immediately before resolving', () => { + const $input = cy.$$('input:first') - let expected = false + let expected = false - cy.on('log:added', (attrs, log) => { - if (log.get('name') === 'clear') { - expect(log.get('state')).to.eq('pending') - expect(log.get('$el').get(0)).to.eq($input.get(0)) + cy.on('log:added', (attrs, log) => { + if (log.get('name') === 'clear') { + expect(log.get('state')).to.eq('pending') + expect(log.get('$el').get(0)).to.eq($input.get(0)) - expected = true - } - }) + expected = true + } + }) - cy.get('input:first').clear().then(() => { - expect(expected).to.be.true - }) + cy.get('input:first').clear().then(() => { + expect(expected).to.be.true }) + }) - it('ends', () => { - const logs = [] + it('ends', () => { + const logs = [] - cy.on('log:added', (attrs, log) => { - if (log.get('name') === 'clear') { - logs.push(log) - } - }) + cy.on('log:added', (attrs, log) => { + if (log.get('name') === 'clear') { + logs.push(log) + } + }) - cy.get('input').invoke('slice', 0, 2).clear().then(() => { - _.each(logs, (log) => { - expect(log.get('state')).to.eq('passed') + cy.get('input').invoke('slice', 0, 2).clear().then(() => { + _.each(logs, (log) => { + expect(log.get('state')).to.eq('passed') - expect(log.get('ended')).to.be.true - }) + expect(log.get('ended')).to.be.true }) }) + }) - it('snapshots after clicking', () => { - cy.get('input:first').clear().then(function ($input) { - const { lastLog } = this + it('snapshots after clicking', () => { + cy.get('input:first').clear().then(function ($input) { + const { lastLog } = this - expect(lastLog.get('snapshots').length).to.eq(1) + expect(lastLog.get('snapshots').length).to.eq(1) - expect(lastLog.get('snapshots')[0]).to.be.an('object') - }) + expect(lastLog.get('snapshots')[0]).to.be.an('object') }) + }) - it('logs deltaOptions', () => { - cy.get('input:first').clear({ force: true, timeout: 1000 }).then(function () { - const { lastLog } = this + it('logs deltaOptions', () => { + cy.get('input:first').clear({ force: true, timeout: 1000 }).then(function () { + const { lastLog } = this - expect(lastLog.get('message')).to.eq('{force: true, timeout: 1000}') + expect(lastLog.get('message')).to.eq('{force: true, timeout: 1000}') - expect(lastLog.invoke('consoleProps').Options).to.deep.eq({ force: true, timeout: 1000 }) - }) + expect(lastLog.invoke('consoleProps').Options).to.deep.eq({ force: true, timeout: 1000 }) }) }) }) From 92b73f6107808a35857cbaa8d08c28f00eb2c33e Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 8 Oct 2019 15:04:42 -0400 Subject: [PATCH 080/370] temp 10/08/19 [skip ci] --- .../driver/src/cy/commands/actions/type.js | 211 +++++++----------- packages/driver/src/cypress/cy.coffee | 2 + .../driver/src/cypress/error_messages.coffee | 22 +- packages/driver/src/cypress/utils.coffee | 2 +- packages/driver/src/dom/elements.ts | 29 +-- packages/driver/src/dom/selection.ts | 2 +- packages/driver/src/dom/visibility.js | 2 +- .../integration/commands/actions/type_spec.js | 60 +++-- packages/reporter/src/lib/shortcuts.js | 2 +- 9 files changed, 152 insertions(+), 180 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index 211dcf080497..b7bf9ba2266d 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -26,8 +26,8 @@ module.exports = function (Commands, Cypress, cy, state, config) { let updateTable options = _.clone(options) - //# allow the el we're typing into to be - //# changed by options -- used by cy.clear() + // allow the el we're typing into to be + // changed by options -- used by cy.clear() _.defaults(options, { $el: subject, log: true, @@ -41,7 +41,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { }) 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 const deltaOptions = $utils.filterOutOptions(options) const table = {} @@ -74,7 +74,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { row[column] = value || 'preventedDefault' } - //# transform table object into object with zero based index as keys + // transform table object into object with zero based index as keys const getTableData = () => { return _.reduce(_.values(table), (memo, value, index) => { memo[index + 1] = value @@ -112,31 +112,6 @@ module.exports = function (Commands, Cypress, cy, state, config) { options._log.snapshot('before', { next: 'after' }) } - // const verifyElementForType = ($el) => { - // const el = $el.get(0) - // const isTextLike = $dom.isTextLike(el) - - // const isFocusable = $elements.isFocusable($el) - - // if (!isFocusable && !isTextLike) { - // const node = $dom.stringify($el) - - // $utils.throwErrByPath('type.not_on_typeable_element', { - // args: { node }, - // }) - // } - - // if (!isFocusable && isTextLike) { - // const node = $dom.stringify($el) - - // $utils.throwErrByPath('type.not_actionable_textlike', { - // args: { node }, - // }) - // } - // } - - // verifyElementForType(el) - if (options.$el.length > 1) { $utils.throwErrByPath('type.multiple_elements', { onFail: options._log, @@ -231,23 +206,23 @@ module.exports = function (Commands, Cypress, cy, state, config) { const defaultButton = getDefaultButton(form) - //# bail if the default button is in a 'disabled' state + // bail if the default button is in a 'disabled' state if (defaultButtonisDisabled(defaultButton)) { return } - //# issue the click event to the 'default button' of the form - //# we need this to be synchronous so not going through our - //# own click command - //# as of now, at least in Chrome, causing the click event - //# on the button will indeed trigger the form submit event - //# so we dont need to fire it manually anymore! + // issue the click event to the 'default button' of the form + // we need this to be synchronous so not going through our + // own click command + // as of now, at least in Chrome, causing the click event + // on the button will indeed trigger the form submit event + // so we dont need to fire it manually anymore! if (!clickedDefaultButton(defaultButton)) { - //# if we werent able to click the default button - //# then synchronously fire the submit event - //# currently this is sync but if we use a waterfall - //# promise in the submit command it will break again - //# consider changing type to a Promise and juggle logging + // if we werent able to click the default button + // then synchronously fire the submit event + // currently this is sync but if we use a waterfall + // promise in the submit command it will break again + // consider changing type to a Promise and juggle logging return cy.now('submit', form, { log: false, $el: form }) } } @@ -268,7 +243,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { return $elements.isNeedSingleValueChangeInputElement(el) } - //# see comment in updateValue below + // see comment in updateValue below let typed = '' const isContentEditable = $elements.isContentEditable(options.$el.get(0)) @@ -300,76 +275,32 @@ module.exports = function (Commands, Cypress, cy, state, config) { } }, - // onFocusChange (el, chars) { - // const lastIndexToType = validateTyping(el, chars) - // const [charsToType, nextChars] = _splitChars( - // `${chars}`, - // lastIndexToType - // ) - - // _setCharsNeedingType(nextChars) - - // return charsToType - // }, - onAfterType () { if (options.release === true) { state('keyboardModifiers', null) } - // if (charsNeedingType) { - // const lastIndexToType = validateTyping(el, charsNeedingType) - // const [charsToType, nextChars] = _splitChars( - // charsNeedingType, - // lastIndexToType - // ) - - // _setCharsNeedingType(nextChars) - - // return charsToType - // } - - // return false }, onBeforeType (totalKeys) { - //# for the total number of keys we're about to - //# type, ensure we raise the timeout to account - //# for the delay being added to each keystroke + // for the total number of keys we're about to + // type, ensure we raise the timeout to account + // for the delay being added to each keystroke return cy.timeout(totalKeys * options.delay, true, 'type') }, - onBeforeSpecialCharAction (id, key) { - //# don't apply any special char actions such as - //# inserting new lines on {enter} or moving the - //# caret / range on left or right movements - // if (isTypeableButNotAnInput) { - // return false - // } - }, - - // onBeforeEvent (id, key, column, which) { - // //# if we are an element which isnt text like but we have - // //# a tabindex then it can receive keyboard events but - // //# should not fire input or textInput and should not fire - // //# change events - // if (inputEvents.includes(column) && isTypeableButNotAnInput) { - // return false - // } - // }, - onEvent (...args) { if (updateTable) { return updateTable(...args) } }, - //# fires only when the 'value' - //# of input/text/contenteditable - //# changes + // fires only when the 'value' + // of input/text/contenteditable + // changes onValueChange (originalText, el) { debug('onValueChange', originalText, el) - //# contenteditable should never be called here. - //# only inputs and textareas can have change events + // contenteditable should never be called here. + // only inputs and textareas can have change events let changeEvent = state('changeEvent') if (changeEvent) { @@ -397,23 +328,23 @@ module.exports = function (Commands, Cypress, cy, state, config) { }, onEnterPressed (id) { - //# dont dispatch change events or handle - //# submit event if we've pressed enter into - //# a textarea or contenteditable + // dont dispatch change events or handle + // submit event if we've pressed enter into + // a textarea or contenteditable let changeEvent = state('changeEvent') if (isTextarea || isContentEditable) { return } - //# if our value has changed since our - //# element was activated we need to - //# fire a change event immediately + // if our value has changed since our + // element was activated we need to + // fire a change event immediately if (changeEvent) { changeEvent(id) } - //# handle submit event handler here + // handle submit event handler here return simulateSubmitHandler() }, @@ -431,7 +362,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { } const handleFocused = function () { - //# if it's the body, don't need to worry about focus + // if it's the body, don't need to worry about focus const isBody = options.$el.is('body') if (isBody) { @@ -468,34 +399,42 @@ module.exports = function (Commands, Cypress, cy, state, config) { // if we dont have a focused element // or if we do and its not ourselves // then issue the click - if (!$elements.isFocusedOrInFocused($elToClick[0])) { - //# click the element first to simulate focus - //# and typical user behavior in case the window - //# is out of focus - return cy.now('click', $elToClick, { - $el: $elToClick, - log: false, - verify: false, - _log: options._log, - force: true, //# force the click, avoid waiting - timeout: options.timeout, - interval: options.interval, - }) - .then(() => { - - return type() - - // BEOW DOES NOT APPLY - // cannot just call .focus, since children of contenteditable will not receive cursor - // with .focus() - - // focusCursor calls focus on first focusable - // then moves cursor to end if in textarea, input, or contenteditable - // $selection.focusCursor($elToFocus[0]) - }) + if ($elements.isFocusedOrInFocused($elToClick[0])) { + return type() } - return type() + // click the element first to simulate focus + // and typical user behavior in case the window + // is out of focus + return cy.now('click', $elToClick, { + $el: $elToClick, + log: false, + verify: false, + _log: options._log, + force: true, // force the click, avoid waiting + timeout: options.timeout, + interval: options.interval, + }) + .then(() => { + if ($elToClick.is('body') || !($elements.isFocusable($elToClick) || $elements.isFocusedOrInFocused($elToClick[0]))) { + const node = $dom.stringify($elToClick) + + $utils.throwErrByPath('type.not_on_typeable_element', { + onFail: options._log, + args: { node }, + }) + } + + return type() + + // cannot just call .focus, since children of contenteditable will not receive cursor + // with .focus() + + // focusCursor calls focus on first focusable + // then moves cursor to end if in textarea, input, or contenteditable + // $selection.focusCursor($elToFocus[0]) + }) + }, }) } @@ -504,8 +443,8 @@ module.exports = function (Commands, Cypress, cy, state, config) { cy.timeout($actionability.delay, true, 'type') return Promise.delay($actionability.delay, 'type').then(() => { - //# command which consume cy.type may - //# want to handle verification themselves + // command which consume cy.type may + // want to handle verification themselves let verifyAssertions if (options.verify === false) { @@ -522,20 +461,20 @@ module.exports = function (Commands, Cypress, cy, state, config) { } function clear (subject, options = {}) { - //# what about other types of inputs besides just text? - //# what about the new HTML5 ones? + // what about other types of inputs besides just text? + // what about the new HTML5 ones? _.defaults(options, { log: true, force: false, }) - //# blow up if any member of the subject - //# isnt a textarea or text-like + // blow up if any member of the subject + // isnt a textarea or text-like const clear = function (el) { 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 const deltaOptions = $utils.filterOutOptions(options) options._log = Cypress.log({ @@ -566,7 +505,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { .now('type', $el, '{selectall}{del}', { $el, log: false, - verify: false, //# handle verification ourselves + verify: false, // handle verification ourselves _log: options._log, force: options.force, timeout: options.timeout, diff --git a/packages/driver/src/cypress/cy.coffee b/packages/driver/src/cypress/cy.coffee index 519f2e86950e..d93844ee5d88 100644 --- a/packages/driver/src/cypress/cy.coffee +++ b/packages/driver/src/cypress/cy.coffee @@ -458,6 +458,8 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> ## since this failed this means that a ## specific command failed and we should ## highlight it in red or insert a new command + + err.name = err.name || 'CypressError' errors.commandRunningFailed(err) fail(err, state("runnable")) diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index 880bc8c34210..bb355bdd4fe0 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -117,7 +117,16 @@ module.exports = { > {{node}} - Cypress considers a 'textarea', any 'element' with a 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid clearable elements. + A clearable element matches one of the following selectors: + 'a[href]' + 'area[href]' + 'input' + 'select' + 'textarea' + 'button' + 'iframe' + '[tabindex]' + '[contenteditable]' """ clearCookie: @@ -941,7 +950,16 @@ module.exports = { > {{node}} - Cypress considers the 'body', 'textarea', any 'element' with a 'tabindex' or 'contenteditable' attribute, any focusable 'element', or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid typeable elements. + A typeable element matches one of the following selectors: + 'a[href]' + 'area[href]' + 'input' + 'select' + 'textarea' + 'button' + 'iframe' + '[tabindex]' + '[contenteditable]' """ not_actionable_textlike: """ #{cmd('type')} failed because it targeted a disabled element. diff --git a/packages/driver/src/cypress/utils.coffee b/packages/driver/src/cypress/utils.coffee index dd2a60307793..755481260dcb 100644 --- a/packages/driver/src/cypress/utils.coffee +++ b/packages/driver/src/cypress/utils.coffee @@ -86,7 +86,7 @@ module.exports = { ## because the browser has a cached ## dynamic stack getter that will ## not be evaluated later - stack = err.stack + stack = err.stack or '' ## preserve message ## and toString diff --git a/packages/driver/src/dom/elements.ts b/packages/driver/src/dom/elements.ts index 0367cf13f440..d7f30ddebff6 100644 --- a/packages/driver/src/dom/elements.ts +++ b/packages/driver/src/dom/elements.ts @@ -21,7 +21,7 @@ const focusable = [ 'button:not([disabled])', 'iframe', '[tabindex]', - '[contentEditable]', + '[contenteditable]', ] const focusableWhenNotDisabled = [ 'a[href]', @@ -32,22 +32,9 @@ const focusableWhenNotDisabled = [ 'button', 'iframe', '[tabindex]', - '[contentEditable]', + '[contenteditable]', ] -// const isTextInputable = (el: HTMLElement) => { -// if (isTextLike(el)) { -// return _.some([':not([readonly])'].map((sel) => $jquery.wrap(el).is(sel))) -// } - -// return false - -// } - -// const textinputable = ['input'] - -//'body,a[href],button,select,[tabindex],input,textarea,[contenteditable]' - const inputTypeNeedSingleValueChangeRe = /^(date|time|week|month|datetime-local)$/ const canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/ @@ -445,15 +432,15 @@ const getActiveElByDocument = (doc: Document): HTMLElement | null => { return null } -const isFocusedOrInFocused = (el) => { +const isFocusedOrInFocused = (el: HTMLElement) => { const doc = $document.getDocumentFromElement(el) - const { activeElement, body } = doc + const { activeElement } = doc - if (activeElementIsDefault(activeElement, body)) { - return false - } + // if (activeElementIsDefault(activeElement, body)) { + // return false + // } let elToCheckCurrentlyFocused @@ -467,6 +454,8 @@ const isFocusedOrInFocused = (el) => { return true } + return false + } const isElement = function (obj): obj is HTMLElement | JQuery { diff --git a/packages/driver/src/dom/selection.ts b/packages/driver/src/dom/selection.ts index baa3255bb86f..ffc1eb929cba 100644 --- a/packages/driver/src/dom/selection.ts +++ b/packages/driver/src/dom/selection.ts @@ -126,7 +126,7 @@ const getHostContenteditable = function (el) { // TODO: remove this when we no longer click before type and move // cursor to the end if (!_hasContenteditableAttr(curEl)) { - return el + return curEl // el.ownerDocument.body } return curEl diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index 84eec0621a57..6d6c8cf5ed7b 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -263,7 +263,7 @@ const elIsHiddenByAncestors = function ($el, $origEl = $el) { // in case there is no body // or if parent is the document which can // happen if we already have an element - if ($parent.is('body,html') || $document.isDocument($parent)) { + if (!$parent.length || $parent.is('body,html') || $document.isDocument($parent)) { return false } 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 350aff86c9e8..476bb2c2d9df 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -4,6 +4,36 @@ const { Promise } = Cypress const { getCommandLogWithText, findReactInstance, withMutableReporterState } = require('../../../support/utils') const { stripIndent } = require('common-tags') +// Cypress.on('test:after:run', (t) => { +// debugger +// }) + +describe('foo', ()=>{ + beforeEach(()=> { + cy.setCookie('foo', ' bar') + // Cypress.on('fail', (err) => { + // debugger + // }) + }) + it.only('test1', ()=>{ + + }) +}) + +it('get document frag', ()=>{ + cy.visit('https://www.lightningdesignsystem.com/components/input/') + cy.get(':nth-child(2) > .docs-codeblock-example > .slds-form-element > .slds-form-element__control > #text-input-id-1').click().type('foo') + + // cy.window().then(win => { + // win.document.getSelection().removeAllRanges() + // const r = win.document.createRange() + // r.selectNode(cy.$$('div:first')) + // const frag = r.cloneContents() + // cy.wrap(frag) + // }) +}) + + // trim new lines at the end of innerText // due to changing browser versions implementing // this differently @@ -1512,7 +1542,7 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('can type into an iframe with designmode = \'on\'', () => { + it(`can type into an iframe with designmode = 'on'`, () => { // append a new iframe to the body cy.$$('') .appendTo(cy.$$('body')) @@ -3299,14 +3329,15 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('accurately returns same el with no falsey contenteditable="false" attr', () => { + it('accurately returns documentElement el with no falsey contenteditable="false" attr', () => { cy.$$('
foo
').appendTo(cy.$$('body')) cy.get('#ce-inner1').then(($el) => { - expect(Cypress.dom.getHostContenteditable($el[0])).to.eq($el[0]) + expect(Cypress.dom.getHostContenteditable($el[0])).to.eq($el[0].ownerDocument.documentElement) }) }) + // https://github.com/cypress-io/cypress/issues/3001 describe('skip actionability if already focused', () => { it('inside input', () => { @@ -4291,21 +4322,15 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('throws when not textarea or text-like', () => { - cy.get('#specific-contains').type('foo') + it('throws when not textarea or text-like', (done) => { + cy.get('form#by-id').type('foo') cy.on('fail', (err) => { expect(err.message).to.include('cy.type() failed because it requires a valid typeable element.') expect(err.message).to.include('The element typed into was:') expect(err.message).to.include('
...
') - expect(err.message).to.include(stripIndent`Cypress considers any element matching the following selectors to be typeable: - - - - - - - - - `) - // done() - // }) + expect(err.message).to.include(`A typeable element matches one of the following selectors:`) + done() }) }) @@ -4932,7 +4957,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('
...
') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + expect(err.message).to.include(`A clearable element matches one of the following selectors:`) done() }) @@ -4945,7 +4970,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('
...
') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + expect(err.message).to.include(`A clearable element matches one of the following selectors:`) done() }) @@ -4958,8 +4983,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') - + expect(err.message).to.include(`A clearable element matches one of the following selectors:`) done() }) @@ -4971,7 +4995,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + expect(err.message).to.include(`A clearable element matches one of the following selectors:`) done() }) diff --git a/packages/reporter/src/lib/shortcuts.js b/packages/reporter/src/lib/shortcuts.js index 7fba35691c2a..df7261dead4a 100644 --- a/packages/reporter/src/lib/shortcuts.js +++ b/packages/reporter/src/lib/shortcuts.js @@ -12,7 +12,7 @@ class Shortcuts { _handleKeyDownEvent (event) { // if typing into an input, textarea, etc, don't trigger any shortcuts - if (dom.isTextLike($(event.target))) return + if (dom.isTextLike(event.target)) return switch (event.key) { case 'r': events.emit('restart') From 3050717e15b0bcb37593df8c4049f95a59d3939d Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 10 Oct 2019 11:33:01 -0400 Subject: [PATCH 081/370] Revert "temp 10/08/19 [skip ci]" This reverts commit 92b73f6107808a35857cbaa8d08c28f00eb2c33e. --- .../driver/src/cy/commands/actions/type.js | 211 +++++++++++------- packages/driver/src/cypress/cy.coffee | 2 - .../driver/src/cypress/error_messages.coffee | 22 +- packages/driver/src/cypress/utils.coffee | 2 +- packages/driver/src/dom/elements.ts | 29 ++- packages/driver/src/dom/selection.ts | 2 +- packages/driver/src/dom/visibility.js | 2 +- .../integration/commands/actions/type_spec.js | 60 ++--- packages/reporter/src/lib/shortcuts.js | 2 +- 9 files changed, 180 insertions(+), 152 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index b7bf9ba2266d..211dcf080497 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -26,8 +26,8 @@ module.exports = function (Commands, Cypress, cy, state, config) { let updateTable options = _.clone(options) - // allow the el we're typing into to be - // changed by options -- used by cy.clear() + //# allow the el we're typing into to be + //# changed by options -- used by cy.clear() _.defaults(options, { $el: subject, log: true, @@ -41,7 +41,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { }) 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 const deltaOptions = $utils.filterOutOptions(options) const table = {} @@ -74,7 +74,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { row[column] = value || 'preventedDefault' } - // transform table object into object with zero based index as keys + //# transform table object into object with zero based index as keys const getTableData = () => { return _.reduce(_.values(table), (memo, value, index) => { memo[index + 1] = value @@ -112,6 +112,31 @@ module.exports = function (Commands, Cypress, cy, state, config) { options._log.snapshot('before', { next: 'after' }) } + // const verifyElementForType = ($el) => { + // const el = $el.get(0) + // const isTextLike = $dom.isTextLike(el) + + // const isFocusable = $elements.isFocusable($el) + + // if (!isFocusable && !isTextLike) { + // const node = $dom.stringify($el) + + // $utils.throwErrByPath('type.not_on_typeable_element', { + // args: { node }, + // }) + // } + + // if (!isFocusable && isTextLike) { + // const node = $dom.stringify($el) + + // $utils.throwErrByPath('type.not_actionable_textlike', { + // args: { node }, + // }) + // } + // } + + // verifyElementForType(el) + if (options.$el.length > 1) { $utils.throwErrByPath('type.multiple_elements', { onFail: options._log, @@ -206,23 +231,23 @@ module.exports = function (Commands, Cypress, cy, state, config) { const defaultButton = getDefaultButton(form) - // bail if the default button is in a 'disabled' state + //# bail if the default button is in a 'disabled' state if (defaultButtonisDisabled(defaultButton)) { return } - // issue the click event to the 'default button' of the form - // we need this to be synchronous so not going through our - // own click command - // as of now, at least in Chrome, causing the click event - // on the button will indeed trigger the form submit event - // so we dont need to fire it manually anymore! + //# issue the click event to the 'default button' of the form + //# we need this to be synchronous so not going through our + //# own click command + //# as of now, at least in Chrome, causing the click event + //# on the button will indeed trigger the form submit event + //# so we dont need to fire it manually anymore! if (!clickedDefaultButton(defaultButton)) { - // if we werent able to click the default button - // then synchronously fire the submit event - // currently this is sync but if we use a waterfall - // promise in the submit command it will break again - // consider changing type to a Promise and juggle logging + //# if we werent able to click the default button + //# then synchronously fire the submit event + //# currently this is sync but if we use a waterfall + //# promise in the submit command it will break again + //# consider changing type to a Promise and juggle logging return cy.now('submit', form, { log: false, $el: form }) } } @@ -243,7 +268,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { return $elements.isNeedSingleValueChangeInputElement(el) } - // see comment in updateValue below + //# see comment in updateValue below let typed = '' const isContentEditable = $elements.isContentEditable(options.$el.get(0)) @@ -275,32 +300,76 @@ module.exports = function (Commands, Cypress, cy, state, config) { } }, + // onFocusChange (el, chars) { + // const lastIndexToType = validateTyping(el, chars) + // const [charsToType, nextChars] = _splitChars( + // `${chars}`, + // lastIndexToType + // ) + + // _setCharsNeedingType(nextChars) + + // return charsToType + // }, + onAfterType () { if (options.release === true) { state('keyboardModifiers', null) } + // if (charsNeedingType) { + // const lastIndexToType = validateTyping(el, charsNeedingType) + // const [charsToType, nextChars] = _splitChars( + // charsNeedingType, + // lastIndexToType + // ) + + // _setCharsNeedingType(nextChars) + + // return charsToType + // } + + // return false }, onBeforeType (totalKeys) { - // for the total number of keys we're about to - // type, ensure we raise the timeout to account - // for the delay being added to each keystroke + //# for the total number of keys we're about to + //# type, ensure we raise the timeout to account + //# for the delay being added to each keystroke return cy.timeout(totalKeys * options.delay, true, 'type') }, + onBeforeSpecialCharAction (id, key) { + //# don't apply any special char actions such as + //# inserting new lines on {enter} or moving the + //# caret / range on left or right movements + // if (isTypeableButNotAnInput) { + // return false + // } + }, + + // onBeforeEvent (id, key, column, which) { + // //# if we are an element which isnt text like but we have + // //# a tabindex then it can receive keyboard events but + // //# should not fire input or textInput and should not fire + // //# change events + // if (inputEvents.includes(column) && isTypeableButNotAnInput) { + // return false + // } + // }, + onEvent (...args) { if (updateTable) { return updateTable(...args) } }, - // fires only when the 'value' - // of input/text/contenteditable - // changes + //# fires only when the 'value' + //# of input/text/contenteditable + //# changes onValueChange (originalText, el) { debug('onValueChange', originalText, el) - // contenteditable should never be called here. - // only inputs and textareas can have change events + //# contenteditable should never be called here. + //# only inputs and textareas can have change events let changeEvent = state('changeEvent') if (changeEvent) { @@ -328,23 +397,23 @@ module.exports = function (Commands, Cypress, cy, state, config) { }, onEnterPressed (id) { - // dont dispatch change events or handle - // submit event if we've pressed enter into - // a textarea or contenteditable + //# dont dispatch change events or handle + //# submit event if we've pressed enter into + //# a textarea or contenteditable let changeEvent = state('changeEvent') if (isTextarea || isContentEditable) { return } - // if our value has changed since our - // element was activated we need to - // fire a change event immediately + //# if our value has changed since our + //# element was activated we need to + //# fire a change event immediately if (changeEvent) { changeEvent(id) } - // handle submit event handler here + //# handle submit event handler here return simulateSubmitHandler() }, @@ -362,7 +431,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { } const handleFocused = function () { - // if it's the body, don't need to worry about focus + //# if it's the body, don't need to worry about focus const isBody = options.$el.is('body') if (isBody) { @@ -399,42 +468,34 @@ module.exports = function (Commands, Cypress, cy, state, config) { // if we dont have a focused element // or if we do and its not ourselves // then issue the click - if ($elements.isFocusedOrInFocused($elToClick[0])) { - return type() + if (!$elements.isFocusedOrInFocused($elToClick[0])) { + //# click the element first to simulate focus + //# and typical user behavior in case the window + //# is out of focus + return cy.now('click', $elToClick, { + $el: $elToClick, + log: false, + verify: false, + _log: options._log, + force: true, //# force the click, avoid waiting + timeout: options.timeout, + interval: options.interval, + }) + .then(() => { + + return type() + + // BEOW DOES NOT APPLY + // cannot just call .focus, since children of contenteditable will not receive cursor + // with .focus() + + // focusCursor calls focus on first focusable + // then moves cursor to end if in textarea, input, or contenteditable + // $selection.focusCursor($elToFocus[0]) + }) } - // click the element first to simulate focus - // and typical user behavior in case the window - // is out of focus - return cy.now('click', $elToClick, { - $el: $elToClick, - log: false, - verify: false, - _log: options._log, - force: true, // force the click, avoid waiting - timeout: options.timeout, - interval: options.interval, - }) - .then(() => { - if ($elToClick.is('body') || !($elements.isFocusable($elToClick) || $elements.isFocusedOrInFocused($elToClick[0]))) { - const node = $dom.stringify($elToClick) - - $utils.throwErrByPath('type.not_on_typeable_element', { - onFail: options._log, - args: { node }, - }) - } - - return type() - - // cannot just call .focus, since children of contenteditable will not receive cursor - // with .focus() - - // focusCursor calls focus on first focusable - // then moves cursor to end if in textarea, input, or contenteditable - // $selection.focusCursor($elToFocus[0]) - }) - + return type() }, }) } @@ -443,8 +504,8 @@ module.exports = function (Commands, Cypress, cy, state, config) { cy.timeout($actionability.delay, true, 'type') return Promise.delay($actionability.delay, 'type').then(() => { - // command which consume cy.type may - // want to handle verification themselves + //# command which consume cy.type may + //# want to handle verification themselves let verifyAssertions if (options.verify === false) { @@ -461,20 +522,20 @@ module.exports = function (Commands, Cypress, cy, state, config) { } function clear (subject, options = {}) { - // what about other types of inputs besides just text? - // what about the new HTML5 ones? + //# what about other types of inputs besides just text? + //# what about the new HTML5 ones? _.defaults(options, { log: true, force: false, }) - // blow up if any member of the subject - // isnt a textarea or text-like + //# blow up if any member of the subject + //# isnt a textarea or text-like const clear = function (el) { 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 const deltaOptions = $utils.filterOutOptions(options) options._log = Cypress.log({ @@ -505,7 +566,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { .now('type', $el, '{selectall}{del}', { $el, log: false, - verify: false, // handle verification ourselves + verify: false, //# handle verification ourselves _log: options._log, force: options.force, timeout: options.timeout, diff --git a/packages/driver/src/cypress/cy.coffee b/packages/driver/src/cypress/cy.coffee index d93844ee5d88..519f2e86950e 100644 --- a/packages/driver/src/cypress/cy.coffee +++ b/packages/driver/src/cypress/cy.coffee @@ -458,8 +458,6 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> ## since this failed this means that a ## specific command failed and we should ## highlight it in red or insert a new command - - err.name = err.name || 'CypressError' errors.commandRunningFailed(err) fail(err, state("runnable")) diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index bb355bdd4fe0..880bc8c34210 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -117,16 +117,7 @@ module.exports = { > {{node}} - A clearable element matches one of the following selectors: - 'a[href]' - 'area[href]' - 'input' - 'select' - 'textarea' - 'button' - 'iframe' - '[tabindex]' - '[contenteditable]' + Cypress considers a 'textarea', any 'element' with a 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid clearable elements. """ clearCookie: @@ -950,16 +941,7 @@ module.exports = { > {{node}} - A typeable element matches one of the following selectors: - 'a[href]' - 'area[href]' - 'input' - 'select' - 'textarea' - 'button' - 'iframe' - '[tabindex]' - '[contenteditable]' + Cypress considers the 'body', 'textarea', any 'element' with a 'tabindex' or 'contenteditable' attribute, any focusable 'element', or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid typeable elements. """ not_actionable_textlike: """ #{cmd('type')} failed because it targeted a disabled element. diff --git a/packages/driver/src/cypress/utils.coffee b/packages/driver/src/cypress/utils.coffee index 755481260dcb..dd2a60307793 100644 --- a/packages/driver/src/cypress/utils.coffee +++ b/packages/driver/src/cypress/utils.coffee @@ -86,7 +86,7 @@ module.exports = { ## because the browser has a cached ## dynamic stack getter that will ## not be evaluated later - stack = err.stack or '' + stack = err.stack ## preserve message ## and toString diff --git a/packages/driver/src/dom/elements.ts b/packages/driver/src/dom/elements.ts index d7f30ddebff6..0367cf13f440 100644 --- a/packages/driver/src/dom/elements.ts +++ b/packages/driver/src/dom/elements.ts @@ -21,7 +21,7 @@ const focusable = [ 'button:not([disabled])', 'iframe', '[tabindex]', - '[contenteditable]', + '[contentEditable]', ] const focusableWhenNotDisabled = [ 'a[href]', @@ -32,9 +32,22 @@ const focusableWhenNotDisabled = [ 'button', 'iframe', '[tabindex]', - '[contenteditable]', + '[contentEditable]', ] +// const isTextInputable = (el: HTMLElement) => { +// if (isTextLike(el)) { +// return _.some([':not([readonly])'].map((sel) => $jquery.wrap(el).is(sel))) +// } + +// return false + +// } + +// const textinputable = ['input'] + +//'body,a[href],button,select,[tabindex],input,textarea,[contenteditable]' + const inputTypeNeedSingleValueChangeRe = /^(date|time|week|month|datetime-local)$/ const canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/ @@ -432,15 +445,15 @@ const getActiveElByDocument = (doc: Document): HTMLElement | null => { return null } -const isFocusedOrInFocused = (el: HTMLElement) => { +const isFocusedOrInFocused = (el) => { const doc = $document.getDocumentFromElement(el) - const { activeElement } = doc + const { activeElement, body } = doc - // if (activeElementIsDefault(activeElement, body)) { - // return false - // } + if (activeElementIsDefault(activeElement, body)) { + return false + } let elToCheckCurrentlyFocused @@ -454,8 +467,6 @@ const isFocusedOrInFocused = (el: HTMLElement) => { return true } - return false - } const isElement = function (obj): obj is HTMLElement | JQuery { diff --git a/packages/driver/src/dom/selection.ts b/packages/driver/src/dom/selection.ts index ffc1eb929cba..baa3255bb86f 100644 --- a/packages/driver/src/dom/selection.ts +++ b/packages/driver/src/dom/selection.ts @@ -126,7 +126,7 @@ const getHostContenteditable = function (el) { // TODO: remove this when we no longer click before type and move // cursor to the end if (!_hasContenteditableAttr(curEl)) { - return curEl // el.ownerDocument.body + return el } return curEl diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index 6d6c8cf5ed7b..84eec0621a57 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -263,7 +263,7 @@ const elIsHiddenByAncestors = function ($el, $origEl = $el) { // in case there is no body // or if parent is the document which can // happen if we already have an element - if (!$parent.length || $parent.is('body,html') || $document.isDocument($parent)) { + if ($parent.is('body,html') || $document.isDocument($parent)) { return false } 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 476bb2c2d9df..350aff86c9e8 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -4,36 +4,6 @@ const { Promise } = Cypress const { getCommandLogWithText, findReactInstance, withMutableReporterState } = require('../../../support/utils') const { stripIndent } = require('common-tags') -// Cypress.on('test:after:run', (t) => { -// debugger -// }) - -describe('foo', ()=>{ - beforeEach(()=> { - cy.setCookie('foo', ' bar') - // Cypress.on('fail', (err) => { - // debugger - // }) - }) - it.only('test1', ()=>{ - - }) -}) - -it('get document frag', ()=>{ - cy.visit('https://www.lightningdesignsystem.com/components/input/') - cy.get(':nth-child(2) > .docs-codeblock-example > .slds-form-element > .slds-form-element__control > #text-input-id-1').click().type('foo') - - // cy.window().then(win => { - // win.document.getSelection().removeAllRanges() - // const r = win.document.createRange() - // r.selectNode(cy.$$('div:first')) - // const frag = r.cloneContents() - // cy.wrap(frag) - // }) -}) - - // trim new lines at the end of innerText // due to changing browser versions implementing // this differently @@ -1542,7 +1512,7 @@ describe('src/cy/commands/actions/type', () => { }) }) - it(`can type into an iframe with designmode = 'on'`, () => { + it('can type into an iframe with designmode = \'on\'', () => { // append a new iframe to the body cy.$$('') .appendTo(cy.$$('body')) @@ -3329,15 +3299,14 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('accurately returns documentElement el with no falsey contenteditable="false" attr', () => { + it('accurately returns same el with no falsey contenteditable="false" attr', () => { cy.$$('
foo
').appendTo(cy.$$('body')) cy.get('#ce-inner1').then(($el) => { - expect(Cypress.dom.getHostContenteditable($el[0])).to.eq($el[0].ownerDocument.documentElement) + expect(Cypress.dom.getHostContenteditable($el[0])).to.eq($el[0]) }) }) - // https://github.com/cypress-io/cypress/issues/3001 describe('skip actionability if already focused', () => { it('inside input', () => { @@ -4322,15 +4291,21 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('throws when not textarea or text-like', (done) => { - cy.get('form#by-id').type('foo') + it('throws when not textarea or text-like', () => { + cy.get('#specific-contains').type('foo') cy.on('fail', (err) => { expect(err.message).to.include('cy.type() failed because it requires a valid typeable element.') expect(err.message).to.include('The element typed into was:') expect(err.message).to.include('
...
') - expect(err.message).to.include(`A typeable element matches one of the following selectors:`) - done() + expect(err.message).to.include(stripIndent`Cypress considers any element matching the following selectors to be typeable: + - + - + - + - + `) + // done() + // }) }) }) @@ -4957,7 +4932,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('
...
') - expect(err.message).to.include(`A clearable element matches one of the following selectors:`) + expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') done() }) @@ -4970,7 +4945,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('
...
') - expect(err.message).to.include(`A clearable element matches one of the following selectors:`) + expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') done() }) @@ -4983,7 +4958,8 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('') - expect(err.message).to.include(`A clearable element matches one of the following selectors:`) + expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + done() }) @@ -4995,7 +4971,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('') - expect(err.message).to.include(`A clearable element matches one of the following selectors:`) + expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') done() }) diff --git a/packages/reporter/src/lib/shortcuts.js b/packages/reporter/src/lib/shortcuts.js index df7261dead4a..7fba35691c2a 100644 --- a/packages/reporter/src/lib/shortcuts.js +++ b/packages/reporter/src/lib/shortcuts.js @@ -12,7 +12,7 @@ class Shortcuts { _handleKeyDownEvent (event) { // if typing into an input, textarea, etc, don't trigger any shortcuts - if (dom.isTextLike(event.target)) return + if (dom.isTextLike($(event.target))) return switch (event.key) { case 'r': events.emit('restart') From ea36638ec6fdedfc1760751e0e37e84ae3590991 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 10 Oct 2019 12:23:20 -0400 Subject: [PATCH 082/370] remove only, fix contenteditable test --- .../driver/src/cy/commands/actions/type.js | 211 +++++++----------- .../driver/src/cypress/error_messages.coffee | 22 +- packages/driver/src/dom/elements.ts | 28 +-- packages/driver/src/dom/selection.ts | 6 +- packages/driver/src/dom/visibility.js | 2 +- .../integration/commands/actions/type_spec.js | 30 +-- packages/reporter/src/lib/shortcuts.js | 4 +- 7 files changed, 119 insertions(+), 184 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index 211dcf080497..b7bf9ba2266d 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -26,8 +26,8 @@ module.exports = function (Commands, Cypress, cy, state, config) { let updateTable options = _.clone(options) - //# allow the el we're typing into to be - //# changed by options -- used by cy.clear() + // allow the el we're typing into to be + // changed by options -- used by cy.clear() _.defaults(options, { $el: subject, log: true, @@ -41,7 +41,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { }) 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 const deltaOptions = $utils.filterOutOptions(options) const table = {} @@ -74,7 +74,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { row[column] = value || 'preventedDefault' } - //# transform table object into object with zero based index as keys + // transform table object into object with zero based index as keys const getTableData = () => { return _.reduce(_.values(table), (memo, value, index) => { memo[index + 1] = value @@ -112,31 +112,6 @@ module.exports = function (Commands, Cypress, cy, state, config) { options._log.snapshot('before', { next: 'after' }) } - // const verifyElementForType = ($el) => { - // const el = $el.get(0) - // const isTextLike = $dom.isTextLike(el) - - // const isFocusable = $elements.isFocusable($el) - - // if (!isFocusable && !isTextLike) { - // const node = $dom.stringify($el) - - // $utils.throwErrByPath('type.not_on_typeable_element', { - // args: { node }, - // }) - // } - - // if (!isFocusable && isTextLike) { - // const node = $dom.stringify($el) - - // $utils.throwErrByPath('type.not_actionable_textlike', { - // args: { node }, - // }) - // } - // } - - // verifyElementForType(el) - if (options.$el.length > 1) { $utils.throwErrByPath('type.multiple_elements', { onFail: options._log, @@ -231,23 +206,23 @@ module.exports = function (Commands, Cypress, cy, state, config) { const defaultButton = getDefaultButton(form) - //# bail if the default button is in a 'disabled' state + // bail if the default button is in a 'disabled' state if (defaultButtonisDisabled(defaultButton)) { return } - //# issue the click event to the 'default button' of the form - //# we need this to be synchronous so not going through our - //# own click command - //# as of now, at least in Chrome, causing the click event - //# on the button will indeed trigger the form submit event - //# so we dont need to fire it manually anymore! + // issue the click event to the 'default button' of the form + // we need this to be synchronous so not going through our + // own click command + // as of now, at least in Chrome, causing the click event + // on the button will indeed trigger the form submit event + // so we dont need to fire it manually anymore! if (!clickedDefaultButton(defaultButton)) { - //# if we werent able to click the default button - //# then synchronously fire the submit event - //# currently this is sync but if we use a waterfall - //# promise in the submit command it will break again - //# consider changing type to a Promise and juggle logging + // if we werent able to click the default button + // then synchronously fire the submit event + // currently this is sync but if we use a waterfall + // promise in the submit command it will break again + // consider changing type to a Promise and juggle logging return cy.now('submit', form, { log: false, $el: form }) } } @@ -268,7 +243,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { return $elements.isNeedSingleValueChangeInputElement(el) } - //# see comment in updateValue below + // see comment in updateValue below let typed = '' const isContentEditable = $elements.isContentEditable(options.$el.get(0)) @@ -300,76 +275,32 @@ module.exports = function (Commands, Cypress, cy, state, config) { } }, - // onFocusChange (el, chars) { - // const lastIndexToType = validateTyping(el, chars) - // const [charsToType, nextChars] = _splitChars( - // `${chars}`, - // lastIndexToType - // ) - - // _setCharsNeedingType(nextChars) - - // return charsToType - // }, - onAfterType () { if (options.release === true) { state('keyboardModifiers', null) } - // if (charsNeedingType) { - // const lastIndexToType = validateTyping(el, charsNeedingType) - // const [charsToType, nextChars] = _splitChars( - // charsNeedingType, - // lastIndexToType - // ) - - // _setCharsNeedingType(nextChars) - - // return charsToType - // } - - // return false }, onBeforeType (totalKeys) { - //# for the total number of keys we're about to - //# type, ensure we raise the timeout to account - //# for the delay being added to each keystroke + // for the total number of keys we're about to + // type, ensure we raise the timeout to account + // for the delay being added to each keystroke return cy.timeout(totalKeys * options.delay, true, 'type') }, - onBeforeSpecialCharAction (id, key) { - //# don't apply any special char actions such as - //# inserting new lines on {enter} or moving the - //# caret / range on left or right movements - // if (isTypeableButNotAnInput) { - // return false - // } - }, - - // onBeforeEvent (id, key, column, which) { - // //# if we are an element which isnt text like but we have - // //# a tabindex then it can receive keyboard events but - // //# should not fire input or textInput and should not fire - // //# change events - // if (inputEvents.includes(column) && isTypeableButNotAnInput) { - // return false - // } - // }, - onEvent (...args) { if (updateTable) { return updateTable(...args) } }, - //# fires only when the 'value' - //# of input/text/contenteditable - //# changes + // fires only when the 'value' + // of input/text/contenteditable + // changes onValueChange (originalText, el) { debug('onValueChange', originalText, el) - //# contenteditable should never be called here. - //# only inputs and textareas can have change events + // contenteditable should never be called here. + // only inputs and textareas can have change events let changeEvent = state('changeEvent') if (changeEvent) { @@ -397,23 +328,23 @@ module.exports = function (Commands, Cypress, cy, state, config) { }, onEnterPressed (id) { - //# dont dispatch change events or handle - //# submit event if we've pressed enter into - //# a textarea or contenteditable + // dont dispatch change events or handle + // submit event if we've pressed enter into + // a textarea or contenteditable let changeEvent = state('changeEvent') if (isTextarea || isContentEditable) { return } - //# if our value has changed since our - //# element was activated we need to - //# fire a change event immediately + // if our value has changed since our + // element was activated we need to + // fire a change event immediately if (changeEvent) { changeEvent(id) } - //# handle submit event handler here + // handle submit event handler here return simulateSubmitHandler() }, @@ -431,7 +362,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { } const handleFocused = function () { - //# if it's the body, don't need to worry about focus + // if it's the body, don't need to worry about focus const isBody = options.$el.is('body') if (isBody) { @@ -468,34 +399,42 @@ module.exports = function (Commands, Cypress, cy, state, config) { // if we dont have a focused element // or if we do and its not ourselves // then issue the click - if (!$elements.isFocusedOrInFocused($elToClick[0])) { - //# click the element first to simulate focus - //# and typical user behavior in case the window - //# is out of focus - return cy.now('click', $elToClick, { - $el: $elToClick, - log: false, - verify: false, - _log: options._log, - force: true, //# force the click, avoid waiting - timeout: options.timeout, - interval: options.interval, - }) - .then(() => { - - return type() - - // BEOW DOES NOT APPLY - // cannot just call .focus, since children of contenteditable will not receive cursor - // with .focus() - - // focusCursor calls focus on first focusable - // then moves cursor to end if in textarea, input, or contenteditable - // $selection.focusCursor($elToFocus[0]) - }) + if ($elements.isFocusedOrInFocused($elToClick[0])) { + return type() } - return type() + // click the element first to simulate focus + // and typical user behavior in case the window + // is out of focus + return cy.now('click', $elToClick, { + $el: $elToClick, + log: false, + verify: false, + _log: options._log, + force: true, // force the click, avoid waiting + timeout: options.timeout, + interval: options.interval, + }) + .then(() => { + if ($elToClick.is('body') || !($elements.isFocusable($elToClick) || $elements.isFocusedOrInFocused($elToClick[0]))) { + const node = $dom.stringify($elToClick) + + $utils.throwErrByPath('type.not_on_typeable_element', { + onFail: options._log, + args: { node }, + }) + } + + return type() + + // cannot just call .focus, since children of contenteditable will not receive cursor + // with .focus() + + // focusCursor calls focus on first focusable + // then moves cursor to end if in textarea, input, or contenteditable + // $selection.focusCursor($elToFocus[0]) + }) + }, }) } @@ -504,8 +443,8 @@ module.exports = function (Commands, Cypress, cy, state, config) { cy.timeout($actionability.delay, true, 'type') return Promise.delay($actionability.delay, 'type').then(() => { - //# command which consume cy.type may - //# want to handle verification themselves + // command which consume cy.type may + // want to handle verification themselves let verifyAssertions if (options.verify === false) { @@ -522,20 +461,20 @@ module.exports = function (Commands, Cypress, cy, state, config) { } function clear (subject, options = {}) { - //# what about other types of inputs besides just text? - //# what about the new HTML5 ones? + // what about other types of inputs besides just text? + // what about the new HTML5 ones? _.defaults(options, { log: true, force: false, }) - //# blow up if any member of the subject - //# isnt a textarea or text-like + // blow up if any member of the subject + // isnt a textarea or text-like const clear = function (el) { 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 const deltaOptions = $utils.filterOutOptions(options) options._log = Cypress.log({ @@ -566,7 +505,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { .now('type', $el, '{selectall}{del}', { $el, log: false, - verify: false, //# handle verification ourselves + verify: false, // handle verification ourselves _log: options._log, force: options.force, timeout: options.timeout, diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index 880bc8c34210..bb355bdd4fe0 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -117,7 +117,16 @@ module.exports = { > {{node}} - Cypress considers a 'textarea', any 'element' with a 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid clearable elements. + A clearable element matches one of the following selectors: + 'a[href]' + 'area[href]' + 'input' + 'select' + 'textarea' + 'button' + 'iframe' + '[tabindex]' + '[contenteditable]' """ clearCookie: @@ -941,7 +950,16 @@ module.exports = { > {{node}} - Cypress considers the 'body', 'textarea', any 'element' with a 'tabindex' or 'contenteditable' attribute, any focusable 'element', or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid typeable elements. + A typeable element matches one of the following selectors: + 'a[href]' + 'area[href]' + 'input' + 'select' + 'textarea' + 'button' + 'iframe' + '[tabindex]' + '[contenteditable]' """ not_actionable_textlike: """ #{cmd('type')} failed because it targeted a disabled element. diff --git a/packages/driver/src/dom/elements.ts b/packages/driver/src/dom/elements.ts index 0367cf13f440..4c2eedabb67b 100644 --- a/packages/driver/src/dom/elements.ts +++ b/packages/driver/src/dom/elements.ts @@ -21,7 +21,7 @@ const focusable = [ 'button:not([disabled])', 'iframe', '[tabindex]', - '[contentEditable]', + '[contenteditable]', ] const focusableWhenNotDisabled = [ 'a[href]', @@ -32,22 +32,9 @@ const focusableWhenNotDisabled = [ 'button', 'iframe', '[tabindex]', - '[contentEditable]', + '[contenteditable]', ] -// const isTextInputable = (el: HTMLElement) => { -// if (isTextLike(el)) { -// return _.some([':not([readonly])'].map((sel) => $jquery.wrap(el).is(sel))) -// } - -// return false - -// } - -// const textinputable = ['input'] - -//'body,a[href],button,select,[tabindex],input,textarea,[contenteditable]' - const inputTypeNeedSingleValueChangeRe = /^(date|time|week|month|datetime-local)$/ const canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/ @@ -445,15 +432,15 @@ const getActiveElByDocument = (doc: Document): HTMLElement | null => { return null } -const isFocusedOrInFocused = (el) => { +const isFocusedOrInFocused = (el: HTMLElement) => { const doc = $document.getDocumentFromElement(el) - const { activeElement, body } = doc + const { activeElement } = doc - if (activeElementIsDefault(activeElement, body)) { - return false - } + // if (activeElementIsDefault(activeElement, body)) { + // return false + // } let elToCheckCurrentlyFocused @@ -467,6 +454,7 @@ const isFocusedOrInFocused = (el) => { return true } + return false } const isElement = function (obj): obj is HTMLElement | JQuery { diff --git a/packages/driver/src/dom/selection.ts b/packages/driver/src/dom/selection.ts index baa3255bb86f..7d92747be8a7 100644 --- a/packages/driver/src/dom/selection.ts +++ b/packages/driver/src/dom/selection.ts @@ -122,11 +122,9 @@ const getHostContenteditable = function (el) { } // if there's no host contenteditable, we must be in designmode - // so act as if the original element is the host contenteditable - // TODO: remove this when we no longer click before type and move - // cursor to the end + // so act as if the body element is the host contenteditable if (!_hasContenteditableAttr(curEl)) { - return el + return el.ownerDocument.body } return curEl diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index 84eec0621a57..6d6c8cf5ed7b 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -263,7 +263,7 @@ const elIsHiddenByAncestors = function ($el, $origEl = $el) { // in case there is no body // or if parent is the document which can // happen if we already have an element - if ($parent.is('body,html') || $document.isDocument($parent)) { + if (!$parent.length || $parent.is('body,html') || $document.isDocument($parent)) { return false } 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 350aff86c9e8..d8e1520d3270 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -2,7 +2,6 @@ const $ = Cypress.$.bind(Cypress) const { _ } = Cypress const { Promise } = Cypress const { getCommandLogWithText, findReactInstance, withMutableReporterState } = require('../../../support/utils') -const { stripIndent } = require('common-tags') // trim new lines at the end of innerText // due to changing browser versions implementing @@ -1512,7 +1511,7 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('can type into an iframe with designmode = \'on\'', () => { + it(`can type into an iframe with designmode = 'on'`, () => { // append a new iframe to the body cy.$$('') .appendTo(cy.$$('body')) @@ -3299,11 +3298,11 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('accurately returns same el with no falsey contenteditable="false" attr', () => { + it('accurately returns documentElement el with no falsey contenteditable="false" attr', () => { cy.$$('
foo
').appendTo(cy.$$('body')) cy.get('#ce-inner1').then(($el) => { - expect(Cypress.dom.getHostContenteditable($el[0])).to.eq($el[0]) + expect(Cypress.dom.getHostContenteditable($el[0])).to.eq($el[0].ownerDocument.body) }) }) @@ -4291,21 +4290,15 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('throws when not textarea or text-like', () => { - cy.get('#specific-contains').type('foo') + it('throws when not textarea or text-like', (done) => { + cy.get('form#by-id').type('foo') cy.on('fail', (err) => { expect(err.message).to.include('cy.type() failed because it requires a valid typeable element.') expect(err.message).to.include('The element typed into was:') expect(err.message).to.include('
...
') - expect(err.message).to.include(stripIndent`Cypress considers any element matching the following selectors to be typeable: - - - - - - - - - `) - // done() - // }) + expect(err.message).to.include(`A typeable element matches one of the following selectors:`) + done() }) }) @@ -4932,7 +4925,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('
...
') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + expect(err.message).to.include(`A clearable element matches one of the following selectors:`) done() }) @@ -4945,7 +4938,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('
...
') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + expect(err.message).to.include(`A clearable element matches one of the following selectors:`) done() }) @@ -4958,8 +4951,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') - + expect(err.message).to.include(`A clearable element matches one of the following selectors:`) done() }) @@ -4971,7 +4963,7 @@ https://on.cypress.io/type`) expect(err.message).to.include('cy.clear() failed because it requires a valid clearable element.') expect(err.message).to.include('The element cleared was:') expect(err.message).to.include('') - expect(err.message).to.include('Cypress considers a \'textarea\', any \'element\' with a \'contenteditable\' attribute, or any \'input\' with a \'type\' attribute of \'text\', \'password\', \'email\', \'number\', \'date\', \'week\', \'month\', \'time\', \'datetime\', \'datetime-local\', \'search\', \'url\', or \'tel\' to be valid clearable elements.') + expect(err.message).to.include(`A clearable element matches one of the following selectors:`) done() }) diff --git a/packages/reporter/src/lib/shortcuts.js b/packages/reporter/src/lib/shortcuts.js index 7fba35691c2a..06b1d8d498db 100644 --- a/packages/reporter/src/lib/shortcuts.js +++ b/packages/reporter/src/lib/shortcuts.js @@ -1,4 +1,4 @@ -import { $, dom } from '@packages/driver' +import { dom } from '@packages/driver' import events from './events' class Shortcuts { @@ -12,7 +12,7 @@ class Shortcuts { _handleKeyDownEvent (event) { // if typing into an input, textarea, etc, don't trigger any shortcuts - if (dom.isTextLike($(event.target))) return + if (dom.isTextLike(event.target)) return switch (event.key) { case 'r': events.emit('restart') From 63979e9fb9b82b99b943ff807be0b6acf5165ae5 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 10 Oct 2019 12:32:22 -0400 Subject: [PATCH 083/370] add test for datetime-local inputs --- .../integration/commands/actions/type_spec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 d8e1520d3270..5f5439963ba3 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -1341,6 +1341,20 @@ describe('src/cy/commands/actions/type', () => { }) }) + describe('input[type=datetime-local]', () => { + it('can change values', () => { + cy.get('[type="datetime-local"]').type('1959-09-13T10:10').should('have.value', '1959-09-13T10:10') + }) + + it('overwrites existing value', () => { + cy.get('[type="datetime-local"]').type('1959-09-13T10:10').should('have.value', '1959-09-13T10:10') + }) + + it('overwrites existing value input by invoking val', () => { + cy.get('[type="datetime-local"]').invoke('val', '2016-01-01').type('1959-09-13T10:10').should('have.value', '1959-09-13T10:10') + }) + }) + describe('input[type=month]', () => { it('can change values', () => { cy.get('#month-without-value').type('1959-09').then(($text) => { From 127fb33902a40d0297000743b3172e9faa1986e3 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 10 Oct 2019 12:51:08 -0400 Subject: [PATCH 084/370] add webpack devdep --- packages/driver/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/driver/package.json b/packages/driver/package.json index 86b817737387..5613d097506f 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -61,6 +61,7 @@ "url-parse": "1.4.7", "vanilla-text-mask": "5.1.1", "wait-on": "3.3.0", + "webpack": "4.41.0", "zone.js": "0.9.0" }, "files": [ From 7a434184bdfbf3d82e3234a9bf4f197027e7239e Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 10 Oct 2019 13:43:25 -0400 Subject: [PATCH 085/370] fix debugging code --- packages/driver/src/cy/retries.coffee | 2 +- .../test/cypress/integration/commands/waiting_spec.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/src/cy/retries.coffee b/packages/driver/src/cy/retries.coffee index b77eafc85356..c9c216bd1431 100644 --- a/packages/driver/src/cy/retries.coffee +++ b/packages/driver/src/cy/retries.coffee @@ -7,7 +7,7 @@ create = (Cypress, state, timeout, clearTimeout, whenStable, finishAssertions) - return { retry: (fn, options, log) -> ## FIXME: remove this debugging code - if options.error + if options.error && options.error.message if !options.error.message.includes('coordsHistory must be') console.error(options.error) ## remove the runnables timeout because we are now in retry diff --git a/packages/driver/test/cypress/integration/commands/waiting_spec.coffee b/packages/driver/test/cypress/integration/commands/waiting_spec.coffee index 5fadf4edfed6..c333e0a02757 100644 --- a/packages/driver/test/cypress/integration/commands/waiting_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/waiting_spec.coffee @@ -915,4 +915,4 @@ describe "src/cy/commands/waiting", -> # Command: "wait" # "Waited For": _.str.clean(fn.toString()) # Retried: "3 times" - # } \ No newline at end of file + # } From 5aa6e5e0823f626e11e8993aff88149f6ea1f880 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 10 Oct 2019 15:16:33 -0400 Subject: [PATCH 086/370] fix force:true on hidden input --- .../driver/src/cy/commands/actions/type.js | 29 ++++++++------ packages/driver/src/cy/keyboard.ts | 40 +++++++++---------- .../integration/commands/actions/type_spec.js | 16 ++++++++ 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index b7bf9ba2266d..feb4775199ec 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -383,7 +383,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { // and seeing if that is focused // Checking first if element is focusable accounts for focusable els inside // of contenteditables - if ($elements.isFocusedOrInFocused(options.$el.get(0))) { + if ($elements.isFocusedOrInFocused(options.$el[0])) { debug('element is already focused, only checking readOnly property') options.ensure = { notReadonly: true, @@ -406,6 +406,8 @@ module.exports = function (Commands, Cypress, cy, state, config) { // click the element first to simulate focus // and typical user behavior in case the window // is out of focus + // cannot just call .focus, since children of contenteditable will not receive cursor + // with .focus() return cy.now('click', $elToClick, { $el: $elToClick, log: false, @@ -416,23 +418,26 @@ module.exports = function (Commands, Cypress, cy, state, config) { interval: options.interval, }) .then(() => { - if ($elToClick.is('body') || !($elements.isFocusable($elToClick) || $elements.isFocusedOrInFocused($elToClick[0]))) { - const node = $dom.stringify($elToClick) + if ($elements.isFocusedOrInFocused($elToClick[0]) || options.force) { + return type() + } + + const node = $dom.stringify($elToClick) + const onFail = options._log + + if ($dom.isTextLike($elToClick[0])) { - $utils.throwErrByPath('type.not_on_typeable_element', { - onFail: options._log, + $utils.throwErrByPath('type.not_actionable_textlike', { + onFail, args: { node }, }) } - return type() - - // cannot just call .focus, since children of contenteditable will not receive cursor - // with .focus() + $utils.throwErrByPath('type.not_on_typeable_element', { + onFail, + args: { node }, + }) - // focusCursor calls focus on first focusable - // then moves cursor to end if in textarea, input, or contenteditable - // $selection.focusCursor($elToFocus[0]) }) }, diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index db2227421af2..97889f247d7e 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -267,13 +267,13 @@ const shouldUpdateValue = (el: HTMLElement, key: KeyDetails) => { if (noneSelected) { const ml = $elements.getNativeProp(el, 'maxLength') - //# maxlength is -1 by default when omitted - //# but could also be null or undefined :-/ - //# only care if we are trying to type a key + // maxlength is -1 by default when omitted + // but could also be null or undefined :-/ + // only care if we are trying to type a key if (ml === 0 || ml > 0) { - //# check if we should update the value - //# and fire the input event - //# as long as we're under maxlength + // 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)) { return false } @@ -333,26 +333,26 @@ const validateTyping = ( const clearChars = '{selectall}{delete}' const isClearChars = _.startsWith(chars.toLowerCase(), clearChars) - //# TODO: tabindex can't be -1 - //# TODO: can't be readonly + // TODO: tabindex can't be -1 + // TODO: can't be readonly if (isBody) { return {} } - if (!isFocusable && !isTextLike) { + if (!isFocusable) { + const node = $dom.stringify($el) - $utils.throwErrByPath('type.not_on_typeable_element', { - onFail, - args: { node }, - }) - } + if (isTextLike) { - if (!isFocusable && isTextLike) { - const node = $dom.stringify($el) + $utils.throwErrByPath('type.not_actionable_textlike', { + onFail, + args: { node }, + }) + } - $utils.throwErrByPath('type.not_actionable_textlike', { + $utils.throwErrByPath('type.not_on_typeable_element', { onFail, args: { node }, }) @@ -633,7 +633,7 @@ export default class Keyboard { options.chars.split(charsBetweenCurlyBracesRe), (chars) => { if (charsBetweenCurlyBracesRe.test(chars)) { - //# allow special chars and modifiers to be case-insensitive + // allow special chars and modifiers to be case-insensitive return parseCharsBetweenCurlyBraces(chars) //.toLowerCase() } @@ -654,7 +654,7 @@ export default class Keyboard { options.onBeforeType(numKeys) // # should make each keystroke async to mimic - //# how keystrokes come into javascript naturally + // how keystrokes come into javascript naturally // let prevElement = $elements.getActiveElByDocument(doc) @@ -956,7 +956,7 @@ export default class Keyboard { debug('handleModifier', key.key) const modifier = keyToModifierMap[key.key] - //# do nothing if already activated + // do nothing if already activated if (!!getActiveModifiers(this.state)[modifier] === setTo) { return false } 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 5f5439963ba3..c5c4e21f323c 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -4360,6 +4360,22 @@ describe('src/cy/commands/actions/type', () => { cy.get('input:text:first').type('foo') }) + it('throws when subject is disabled and force:true', function (done) { + cy.timeout(200) + + cy.$$('input:text:first').prop('disabled', true) + + cy.on('fail', (err) => { + // get + type logs + expect(this.logs.length).eq(2) + expect(err.message).to.include('cy.type() failed because it targeted a disabled element.') + + done() + }) + + cy.get('input:text:first').type('foo', { force: true }) + }) + it('throws when submitting within nested forms') it('logs once when not dom subject', function (done) { From 33f61062e2d895467f881d194c21c9486c675651 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Thu, 10 Oct 2019 15:45:19 -0400 Subject: [PATCH 087/370] up timeout for test in ci --- .../test/cypress/integration/commands/actions/type_spec.js | 1 + 1 file changed, 1 insertion(+) 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 c5c4e21f323c..352b7c4574bd 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -4305,6 +4305,7 @@ describe('src/cy/commands/actions/type', () => { }) it('throws when not textarea or text-like', (done) => { + cy.timeout(300) cy.get('form#by-id').type('foo') cy.on('fail', (err) => { From a7b45f388dfbb0840ec5a1f8c30bf39f145e7e04 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Fri, 11 Oct 2019 16:26:55 -0400 Subject: [PATCH 088/370] 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 089/370] 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 090/370] 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 091/370] 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 092/370] 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 093/370] 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 From 855ef95a475906a21af3ed64918e057be4e15eca Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 15 Oct 2019 12:37:18 -0400 Subject: [PATCH 094/370] fix display specific test --- .../test/cypress/integration/commands/actions/type_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 cda30e9419a8..ec7e0f03bc5a 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -4306,12 +4306,12 @@ describe('src/cy/commands/actions/type', () => { it('throws when not textarea or text-like', (done) => { cy.timeout(300) - cy.get('form#by-id').type('foo') + cy.get('div#nested-find').type('foo') cy.on('fail', (err) => { expect(err.message).to.include('cy.type() failed because it requires a valid typeable element.') expect(err.message).to.include('The element typed into was:') - expect(err.message).to.include('
...
') + expect(err.message).to.include('
Nested ...
') expect(err.message).to.include(`A typeable element matches one of the following selectors:`) done() }) From 85baac3fdb07186c5ec3fb3838776792d49979c7 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 15 Oct 2019 14:27:31 -0400 Subject: [PATCH 095/370] fix type follow focus when redirect on focus --- .../driver/src/cy/commands/actions/type.js | 25 ++++++++++--------- .../integration/commands/actions/type_spec.js | 10 ++++++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index feb4775199ec..499f41ada03d 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -418,26 +418,27 @@ module.exports = function (Commands, Cypress, cy, state, config) { interval: options.interval, }) .then(() => { - if ($elements.isFocusedOrInFocused($elToClick[0]) || options.force) { - return type() - } + if (!options.force && $elements.getActiveElByDocument($elToClick[0].ownerDocument) === null) { + + const node = $dom.stringify($elToClick) + const onFail = options._log - const node = $dom.stringify($elToClick) - const onFail = options._log + if ($dom.isTextLike($elToClick[0])) { - if ($dom.isTextLike($elToClick[0])) { + $utils.throwErrByPath('type.not_actionable_textlike', { + onFail, + args: { node }, + }) + } - $utils.throwErrByPath('type.not_actionable_textlike', { + $utils.throwErrByPath('type.not_on_typeable_element', { onFail, args: { node }, }) - } - $utils.throwErrByPath('type.not_on_typeable_element', { - onFail, - args: { node }, - }) + } + return type() }) }, 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 ec7e0f03bc5a..58ca7605597b 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -167,6 +167,16 @@ describe('src/cy/commands/actions/type', () => { }) }) + it('can type into element that redirects focus', () => { + cy.$$('div[tabindex]:first').on('focus', () => { + cy.$$('input:first').focus() + }) + + cy.get('div[tabindex]:first').type('foobar') + + cy.focused().should('have.value', 'foobar') + }) + describe('actionability', () => { it('can forcibly click even when element is invisible', () => { const $txt = cy.$$(':text:first').hide() From 653d1f2b246a9302f37c77569e136687c8654975 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 15 Oct 2019 14:28:35 -0400 Subject: [PATCH 096/370] cleanup new follow focus test --- .../test/cypress/integration/commands/actions/type_spec.js | 4 +++- 1 file changed, 3 insertions(+), 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 58ca7605597b..8f072c238c83 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -174,7 +174,9 @@ describe('src/cy/commands/actions/type', () => { cy.get('div[tabindex]:first').type('foobar') - cy.focused().should('have.value', 'foobar') + cy.get('input:first') + .should('be.focused') + .should('have.value', 'foobar') }) describe('actionability', () => { From cfa636100414cd8e98ed66328d9a7a199f5b2990 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Wed, 16 Oct 2019 15:06:40 -0400 Subject: [PATCH 097/370] upgrade eslint-plugin-dev to 5.0.0 --- cli/lib/exec/versions.js | 1 - cli/lib/tasks/download.js | 1 - cli/lib/tasks/install.js | 6 ---- cli/lib/tasks/unzip.js | 1 - cli/lib/tasks/verify.js | 2 -- cli/lib/util.js | 1 - cli/test/lib/exec/xvfb_spec.js | 1 - cli/test/lib/tasks/install_spec.js | 8 ----- cli/test/lib/tasks/verify_spec.js | 3 -- cli/test/lib/util_spec.js | 1 - cli/test/spec_helper.js | 2 -- package.json | 2 +- packages/desktop-gui/src/app/nav.jsx | 1 - packages/desktop-gui/src/auth/login-form.jsx | 1 - .../desktop-gui/src/dropdown/dropdown.jsx | 1 - packages/desktop-gui/src/lib/utils.js | 1 - .../desktop-gui/src/project/onboarding.jsx | 1 - .../src/runs/permission-message.jsx | 2 -- packages/desktop-gui/src/runs/runs-list.jsx | 2 -- .../desktop-gui/src/update/update-banner.jsx | 1 - .../driver/src/cy/commands/actions/click.js | 1 - .../driver/src/cy/commands/actions/type.js | 4 --- packages/driver/src/cy/keyboard.js | 8 ----- packages/driver/src/cy/mouse.js | 10 ------- packages/driver/src/dom/elements.js | 2 -- packages/driver/src/dom/jquery.js | 1 - packages/driver/src/dom/visibility.js | 1 - .../commands/actions/click_spec.js | 23 --------------- .../integration/commands/actions/type_spec.js | 10 ------- .../integration/cy/snapshot_css_spec.js | 1 - .../integration/dom/visibility_spec.js | 1 - .../integration/e2e/focus_blur_spec.js | 29 ------------------- .../e2e/selector_playground.spec.js | 3 -- .../cypress/integration/issues/3847_spec.js | 1 - .../integration/util/limited_map_spec.js | 1 - packages/driver/test/cypress/support/utils.js | 3 -- .../src/commands/command-model.spec.js | 1 - .../reporter/src/runnables/suite-model.js | 1 - packages/reporter/src/test/test.jsx | 1 - packages/runner/src/app/app.spec.jsx | 1 - packages/runner/src/dropdown/dropdown.jsx | 1 - packages/runner/src/iframe/visit-failure.js | 1 - packages/runner/src/lib/logger.js | 1 - packages/runner/src/lib/state.js | 1 - packages/runner/src/lib/util.js | 1 - packages/runner/test/helper.js | 1 - packages/server/lib/browsers/cri-client.ts | 1 - packages/server/lib/gui/menu.js | 1 - packages/server/lib/modes/run.js | 2 +- packages/server/lib/plugins/child/task.js | 1 - packages/server/lib/util/app_data.js | 1 - packages/server/lib/util/args.js | 1 - packages/server/lib/util/file.js | 1 - packages/server/lib/util/settings.js | 2 -- packages/server/lib/util/shell.js | 1 - packages/server/lib/util/system.js | 1 - packages/server/lib/util/terminal.js | 1 - .../visit_response_never_ends_failing_spec.js | 1 - packages/server/test/unit/gui/auth_spec.js | 1 - .../server/test/unit/stream_buffer_spec.js | 1 - packages/web-config/node-jsdom-setup.ts | 2 -- scripts/binary/util/testStaticAssets.js | 3 -- scripts/binary/util/transform-requires.js | 3 -- scripts/link-packages.js | 1 - scripts/unit/binary/util/packages-spec.js | 14 ++++----- 65 files changed, 7 insertions(+), 180 deletions(-) diff --git a/cli/lib/exec/versions.js b/cli/lib/exec/versions.js index 58ed2e1726b9..bb3589ef7494 100644 --- a/cli/lib/exec/versions.js +++ b/cli/lib/exec/versions.js @@ -8,7 +8,6 @@ const { throwFormErrorText, errors } = require('../errors') const getVersions = () => { return Promise.try(() => { - if (util.getEnv('CYPRESS_RUN_BINARY')) { let envBinaryPath = path.resolve(util.getEnv('CYPRESS_RUN_BINARY')) diff --git a/cli/lib/tasks/download.js b/cli/lib/tasks/download.js index 3bdda4ffd474..5afd2a7b95c3 100644 --- a/cli/lib/tasks/download.js +++ b/cli/lib/tasks/download.js @@ -176,7 +176,6 @@ const verifyDownloadedFile = (filename, expectedSize, expectedChecksum) => { debug('downloaded file lacks checksum or size to verify') return Promise.resolve() - } // downloads from given url diff --git a/cli/lib/tasks/install.js b/cli/lib/tasks/install.js index 6215a08e8e18..26cb7b23ccf8 100644 --- a/cli/lib/tasks/install.js +++ b/cli/lib/tasks/install.js @@ -27,7 +27,6 @@ const alreadyInstalledMsg = () => { } const displayCompletionMsg = () => { - // check here to see if we are globally installed if (util.isInstalledGlobally()) { // if we are display a warning @@ -103,7 +102,6 @@ const downloadAndUnzip = ({ version, installDir, downloadDir }) => { { title: util.titleize('Finishing Installation'), task: (ctx, task) => { - const cleanup = () => { debug('removing zip file %s', downloadDestination) @@ -129,7 +127,6 @@ const downloadAndUnzip = ({ version, installDir, downloadDir }) => { } const start = (options = {}) => { - // handle deprecated / removed if (util.getEnv('CYPRESS_BINARY_VERSION')) { return throwFormErrorText(errors.removed.CYPRESS_BINARY_VERSION)() @@ -152,7 +149,6 @@ const start = (options = {}) => { // let this environment variable reset the binary version we need if (util.getEnv('CYPRESS_INSTALL_BINARY')) { - // because passed file paths are often double quoted // and might have extra whitespace around, be robust and trim the string const trimAndRemoveDoubleQuotes = true @@ -175,7 +171,6 @@ const start = (options = {}) => { // if this doesn't match the expected version // then print warning to the user if (envVarVersion !== needVersion) { - // reset the version to the env var version needVersion = envVarVersion } @@ -211,7 +206,6 @@ const start = (options = {}) => { return state.getBinaryPkgVersionAsync(binaryDir) }) .then((binaryVersion) => { - if (!binaryVersion) { debug('no binary installed under cli version') diff --git a/cli/lib/tasks/unzip.js b/cli/lib/tasks/unzip.js index a7a7124290ec..c341d62eb3ad 100644 --- a/cli/lib/tasks/unzip.js +++ b/cli/lib/tasks/unzip.js @@ -14,7 +14,6 @@ const util = require('../util') // expose this function for simple testing const unzip = ({ zipFilePath, installDir, progress }) => { - debug('unzipping from %s', zipFilePath) debug('into', installDir) diff --git a/cli/lib/tasks/verify.js b/cli/lib/tasks/verify.js index 48830022aa94..41371d0ce738 100644 --- a/cli/lib/tasks/verify.js +++ b/cli/lib/tasks/verify.js @@ -221,7 +221,6 @@ function testBinary (version, binaryDir, options) { const maybeVerify = (installedVersion, binaryDir, options) => { return state.getBinaryVerifiedAsync(binaryDir) .then((isVerified) => { - debug('is Verified ?', isVerified) let shouldVerify = !isVerified @@ -317,7 +316,6 @@ const start = (options = {}) => { return state.getBinaryPkgVersionAsync(binaryDir) }) .then((binaryVersion) => { - if (!binaryVersion) { debug('no Cypress binary found for cli version ', packageVersion) diff --git a/cli/lib/util.js b/cli/lib/util.js index 029c0bb1893c..84f872db4ce5 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -326,7 +326,6 @@ const util = { } return os.release() - }) }, diff --git a/cli/test/lib/exec/xvfb_spec.js b/cli/test/lib/exec/xvfb_spec.js index 55db48b0f5dd..a5da2853ef4a 100644 --- a/cli/test/lib/exec/xvfb_spec.js +++ b/cli/test/lib/exec/xvfb_spec.js @@ -71,7 +71,6 @@ describe('lib/exec/xvfb', function () { }) context('#isNeeded', function () { - it('does not need xvfb on osx', function () { os.platform.returns('darwin') diff --git a/cli/test/lib/tasks/install_spec.js b/cli/test/lib/tasks/install_spec.js index c9c50bae9b85..8e9a9a5b229d 100644 --- a/cli/test/lib/tasks/install_spec.js +++ b/cli/test/lib/tasks/install_spec.js @@ -59,7 +59,6 @@ describe('/lib/tasks/install', function () { }) describe('skips install', function () { - it('when environment variable is set', function () { process.env.CYPRESS_INSTALL_BINARY = '0' @@ -76,7 +75,6 @@ describe('/lib/tasks/install', function () { }) describe('override version', function () { - it('warns when specifying cypress version in env', function () { const version = '0.12.1' @@ -182,7 +180,6 @@ describe('/lib/tasks/install', function () { describe('when version is already installed', function () { beforeEach(function () { state.getBinaryPkgVersionAsync.resolves(packageVersion) - }) it('doesn\'t attempt to download', function () { @@ -191,7 +188,6 @@ describe('/lib/tasks/install', function () { expect(download.start).not.to.be.called expect(state.getBinaryPkgVersionAsync).to.be.calledWith('/cache/Cypress/1.2.3/Cypress.app') }) - }) it('logs \'skipping install\' when explicit cypress install', function () { @@ -202,7 +198,6 @@ describe('/lib/tasks/install', function () { normalize(this.stdout.toString()) ) }) - }) it('logs when already installed when run from postInstall', function () { @@ -249,7 +244,6 @@ describe('/lib/tasks/install', function () { }) it('logs message and starts download', function () { - expect(download.start).to.be.calledWithMatch({ version: packageVersion, }) @@ -301,7 +295,6 @@ describe('/lib/tasks/install', function () { }) it('logs message and starts download', function () { - expect(download.start).to.be.calledWithMatch({ version: packageVersion, }) @@ -467,5 +460,4 @@ describe('/lib/tasks/install', function () { }) }) }) - }) diff --git a/cli/test/lib/tasks/verify_spec.js b/cli/test/lib/tasks/verify_spec.js index ed147aeef9df..9d18832c7245 100644 --- a/cli/test/lib/tasks/verify_spec.js +++ b/cli/test/lib/tasks/verify_spec.js @@ -178,7 +178,6 @@ context('lib/tasks/verify', () => { .then(() => { snapshot(normalize(slice(stdout.toString()))) }) - }) it('logs error when child process returns incorrect stdout (stderr when exists)', () => { @@ -205,7 +204,6 @@ context('lib/tasks/verify', () => { .then(() => { snapshot(normalize(slice(stdout.toString()))) }) - }) it('logs error when child process returns incorrect stdout (stdout when no stderr)', () => { @@ -231,7 +229,6 @@ context('lib/tasks/verify', () => { .then(() => { snapshot(normalize(slice(stdout.toString()))) }) - }) it('sets ELECTRON_ENABLE_LOGGING without mutating process.env', () => { diff --git a/cli/test/lib/util_spec.js b/cli/test/lib/util_spec.js index 73238da52e60..30be63f3a4c6 100644 --- a/cli/test/lib/util_spec.js +++ b/cli/test/lib/util_spec.js @@ -321,7 +321,6 @@ describe('util', () => { context('.printNodeOptions', () => { describe('NODE_OPTIONS is not set', () => { - it('does nothing if debug is not enabled', () => { const log = sinon.spy() diff --git a/cli/test/spec_helper.js b/cli/test/spec_helper.js index e795b33e8731..a5df843f7981 100644 --- a/cli/test/spec_helper.js +++ b/cli/test/spec_helper.js @@ -9,9 +9,7 @@ const { MockChildProcess } = require('spawn-mock') const _kill = MockChildProcess.prototype.kill const patchMockSpawn = () => { - MockChildProcess.prototype.kill = function (...args) { - this.emit('exit') return _kill.apply(this, args) diff --git a/package.json b/package.json index b774bc5ad511..6765aa00b74b 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@cypress/bumpercar": "2.0.9", "@cypress/commit-message-install": "2.7.0", "@cypress/env-or-json-file": "2.0.0", - "@cypress/eslint-plugin-dev": "4.0.0", + "@cypress/eslint-plugin-dev": "5.0.0", "@cypress/github-commit-status-check": "1.5.0", "@cypress/npm-run-all": "4.0.5", "@cypress/questions-remain": "1.0.1", diff --git a/packages/desktop-gui/src/app/nav.jsx b/packages/desktop-gui/src/app/nav.jsx index 43cce7741cd1..d91b3be222e4 100644 --- a/packages/desktop-gui/src/app/nav.jsx +++ b/packages/desktop-gui/src/app/nav.jsx @@ -118,7 +118,6 @@ export default class Nav extends Component { Log Out ) - } _select = (item) => { diff --git a/packages/desktop-gui/src/auth/login-form.jsx b/packages/desktop-gui/src/auth/login-form.jsx index 7577ab4fa857..39bf0ce98974 100644 --- a/packages/desktop-gui/src/auth/login-form.jsx +++ b/packages/desktop-gui/src/auth/login-form.jsx @@ -80,7 +80,6 @@ class LoginForm extends Component { Log In to Dashboard ) - } _error () { diff --git a/packages/desktop-gui/src/dropdown/dropdown.jsx b/packages/desktop-gui/src/dropdown/dropdown.jsx index 412caf8bec47..ebce51d0249a 100644 --- a/packages/desktop-gui/src/dropdown/dropdown.jsx +++ b/packages/desktop-gui/src/dropdown/dropdown.jsx @@ -60,7 +60,6 @@ class Dropdown extends Component { {this._buttonContent()} ) - } _buttonContent () { diff --git a/packages/desktop-gui/src/lib/utils.js b/packages/desktop-gui/src/lib/utils.js index 95ac38990d1b..5fbb8b70ecd5 100644 --- a/packages/desktop-gui/src/lib/utils.js +++ b/packages/desktop-gui/src/lib/utils.js @@ -110,5 +110,4 @@ export function stripSharedDirsFromDir2 (dir1, dir2, osName) { }) .join(sep) .value() - } diff --git a/packages/desktop-gui/src/project/onboarding.jsx b/packages/desktop-gui/src/project/onboarding.jsx index 992f0131fa77..57e011f9be40 100644 --- a/packages/desktop-gui/src/project/onboarding.jsx +++ b/packages/desktop-gui/src/project/onboarding.jsx @@ -116,7 +116,6 @@ class OnBoarding extends Component { ) - }) } diff --git a/packages/desktop-gui/src/runs/permission-message.jsx b/packages/desktop-gui/src/runs/permission-message.jsx index 2b4264889dee..2182835b79f0 100644 --- a/packages/desktop-gui/src/runs/permission-message.jsx +++ b/packages/desktop-gui/src/runs/permission-message.jsx @@ -45,7 +45,6 @@ class PermissionMessage extends Component { } return this._noResult() - } _button () { @@ -101,7 +100,6 @@ class PermissionMessage extends Component { {this._button()}
) - } _noResult () { diff --git a/packages/desktop-gui/src/runs/runs-list.jsx b/packages/desktop-gui/src/runs/runs-list.jsx index 4f293295b347..bbe30580cc37 100644 --- a/packages/desktop-gui/src/runs/runs-list.jsx +++ b/packages/desktop-gui/src/runs/runs-list.jsx @@ -192,7 +192,6 @@ class RunsList extends Component { // OR there are no runs to show if (!this.runsStore.runs.length) { - // AND they've never setup CI if (!project.id) { return this._projectNotSetup() @@ -201,7 +200,6 @@ class RunsList extends Component { } return this._empty() - } //--------End Run States----------// diff --git a/packages/desktop-gui/src/update/update-banner.jsx b/packages/desktop-gui/src/update/update-banner.jsx index ddd0f79ff29c..a987d6f5ca19 100644 --- a/packages/desktop-gui/src/update/update-banner.jsx +++ b/packages/desktop-gui/src/update/update-banner.jsx @@ -94,7 +94,6 @@ class UpdateBanner extends Component { ) - } _checkForUpdate () { diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index cd83ad7cbd14..bdab1663f29c 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -40,7 +40,6 @@ const formatMoveEventsTable = (events) => { const formatMouseEvents = (events) => { return _.map(events, (val, key) => { if (val.skipped) { - const reason = val.skipped return { diff --git a/packages/driver/src/cy/commands/actions/type.js b/packages/driver/src/cy/commands/actions/type.js index a5da732b2b59..eb83c72937cd 100644 --- a/packages/driver/src/cy/commands/actions/type.js +++ b/packages/driver/src/cy/commands/actions/type.js @@ -246,7 +246,6 @@ module.exports = function (Commands, Cypress, cy, state, config) { } return false - } const getDefaultButton = (form) => { @@ -421,7 +420,6 @@ module.exports = function (Commands, Cypress, cy, state, config) { onFail: options._log, args: { chars, allChars }, }) - }, }) @@ -480,7 +478,6 @@ module.exports = function (Commands, Cypress, cy, state, config) { } return type() - }, }) } @@ -577,7 +574,6 @@ module.exports = function (Commands, Cypress, cy, state, config) { } return verifyAssertions() - }) }, }) diff --git a/packages/driver/src/cy/keyboard.js b/packages/driver/src/cy/keyboard.js index 60c703916758..109a5e8c8307 100644 --- a/packages/driver/src/cy/keyboard.js +++ b/packages/driver/src/cy/keyboard.js @@ -125,7 +125,6 @@ const create = (state) => { 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 @@ -138,7 +137,6 @@ const create = (state) => { // contents and send the input event $selection.deleteSelectionContents(el) options.input = true - }) }, @@ -167,7 +165,6 @@ const create = (state) => { 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 @@ -180,7 +177,6 @@ const create = (state) => { // contents and send the input event $selection.deleteSelectionContents(el) options.input = true - }) }, @@ -418,7 +414,6 @@ const create = (state) => { } return memo + chars.length - } , 0) }, @@ -576,7 +571,6 @@ const create = (state) => { typeKey (el, key, options) { return kb.ensureKey(el, key, options, () => { - const isDigit = isSingleDigitRe.test(key) const isNumberInputType = $elements.isInput(el) && $elements.isType(el, 'number') @@ -636,7 +630,6 @@ const create = (state) => { if (kb.simulateKey(el, 'keydown', key, options)) { if (kb.simulateKey(el, 'keypress', key, options)) { if (kb.simulateKey(el, 'textInput', key, options)) { - let ml if ($elements.isInput(el) || $elements.isTextarea(el)) { @@ -703,7 +696,6 @@ const create = (state) => { }, resetModifiers (doc) { - const activeEl = $elements.getActiveElByDocument(doc) const activeModifiers = kb.getActiveModifiers(state) diff --git a/packages/driver/src/cy/mouse.js b/packages/driver/src/cy/mouse.js index ae5e5313313b..772478dcebe4 100644 --- a/packages/driver/src/cy/mouse.js +++ b/packages/driver/src/cy/mouse.js @@ -179,12 +179,10 @@ const create = (state, keyboard, focused) => { sendMouseleave(elToSend, _.extend({}, defaultMouseOptions, { relatedTarget: el })) }) } - } if (hoveredElChanged) { if (el && $elements.isAttachedEl(el)) { - mouseover = () => { return sendMouseover(el, _.extend({}, defaultMouseOptions, { relatedTarget: lastHoveredEl })) } @@ -215,7 +213,6 @@ const create = (state, keyboard, focused) => { }) } } - } pointermove = () => { @@ -270,7 +267,6 @@ const create = (state, keyboard, focused) => { * @param {HTMLElement} forceEl */ _downEvents (coords, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { - const { x, y } = coords const el = forceEl || mouse.moveToCoords(coords) @@ -323,7 +319,6 @@ const create = (state, keyboard, focused) => { pointerdownProps, mousedownProps, } - }, down (fromElViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { @@ -361,7 +356,6 @@ const create = (state, keyboard, focused) => { // the user clicked inside a focusable element focused.fireFocus($elToFocus.get(0)) } - } return mouseDownEvents @@ -414,7 +408,6 @@ const create = (state, keyboard, focused) => { const mouseClickEvents = mouse._mouseClickEvents(fromElViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend) return _.extend({}, mouseDownEvents, mouseUpEvents, mouseClickEvents) - }, /** @@ -424,7 +417,6 @@ const create = (state, keyboard, focused) => { * @param {Window} win */ _upEvents (fromElViewport, forceEl, skipMouseEvent, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) { - const win = state('window') let defaultOptions = mouse._getDefaultMouseOptions(fromElViewport.x, fromElViewport.y, win) @@ -458,7 +450,6 @@ const create = (state, keyboard, focused) => { pointerupProps, mouseupProps, } - }, _mouseClickEvents (fromElViewport, forceEl, skipClickEvent, mouseEvtOptionsExtend = {}) { @@ -583,7 +574,6 @@ const sendEvent = (evtName, el, evtOptions, bubbles = false, cancelable = false, el, modifiers, } - } const sendPointerEvent = (el, evtOptions, evtName, bubbles = false, cancelable = false) => { diff --git a/packages/driver/src/dom/elements.js b/packages/driver/src/dom/elements.js index 3228dce8fd06..9705fb4a3458 100644 --- a/packages/driver/src/dom/elements.js +++ b/packages/driver/src/dom/elements.js @@ -356,7 +356,6 @@ const isFocused = (el) => { } const isFocusedOrInFocused = (el) => { - const doc = $document.getDocumentFromElement(el) const { activeElement, body } = doc @@ -643,7 +642,6 @@ const isDescendent = ($el1, $el2) => { } const findParent = (el, fn) => { - const recurse = (curEl, prevEl) => { if (!curEl) { return null diff --git a/packages/driver/src/dom/jquery.js b/packages/driver/src/dom/jquery.js index 13336cbd8a17..7ed2b474b818 100644 --- a/packages/driver/src/dom/jquery.js +++ b/packages/driver/src/dom/jquery.js @@ -18,7 +18,6 @@ const unwrap = function (obj) { } return obj - } const isJquery = (obj) => { diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index 70b873f60c1a..85fee6aec49a 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -141,7 +141,6 @@ const elHasClippableOverflow = function ($el) { } const canClipContent = function ($el, $ancestor) { - // can't clip without overflow properties if (!elHasClippableOverflow($ancestor)) { return false 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 2f6a049c9df9..06bdf31146e2 100644 --- a/packages/driver/test/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/click_spec.js @@ -249,7 +249,6 @@ describe('src/cy/commands/actions/click', () => { }) it('will send all events even mousedown is defaultPrevented', () => { - const $btn = cy.$$('#button') $btn.get(0).addEventListener('mousedown', (e) => { @@ -279,7 +278,6 @@ describe('src/cy/commands/actions/click', () => { cy.getAll('$btn', 'pointerdown pointerup click').each(shouldBeCalledOnce) cy.getAll('$btn', 'mousedown mouseup').each(shouldNotBeCalled) - }) it('sends a click event', (done) => { @@ -299,7 +297,6 @@ describe('src/cy/commands/actions/click', () => { }) it('causes focusable elements to receive focus', () => { - const el = cy.$$(':text:first') attachFocusListeners({ el }) @@ -428,7 +425,6 @@ describe('src/cy/commands/actions/click', () => { metaKey: false, altKey: false, }) - }) }) @@ -611,7 +607,6 @@ describe('src/cy/commands/actions/click', () => { }) it('places cursor at the end of [contenteditable]', () => { - cy.get('[contenteditable]:first') .invoke('html', '

').click() .then(($el) => { @@ -1706,7 +1701,6 @@ describe('src/cy/commands/actions/click', () => { }) it('throws when attempting to click multiple elements', (done) => { - cy.on('fail', (err) => { 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.') @@ -1770,7 +1764,6 @@ describe('src/cy/commands/actions/click', () => { // Array(1).fill().map(()=> it('throws when any member of the subject isnt visible', function (done) { - // sometimes the command will timeout early with // Error: coordsHistory must be at least 2 sets of coords cy.timeout(300) @@ -2169,7 +2162,6 @@ describe('src/cy/commands/actions/click', () => { }) cy.get('input:first').click().then(function () { - const consoleProps = this.lastLog.invoke('consoleProps') expect(consoleProps.table[1]()).to.containSubset({ @@ -2237,7 +2229,6 @@ describe('src/cy/commands/actions/click', () => { }, ], }) - }) }) @@ -2334,7 +2325,6 @@ describe('src/cy/commands/actions/click', () => { cy.getAll('btn', 'mousemove mouseover').each(shouldBeCalledOnce) cy.getAll('btn', 'pointerdown mousedown pointerup mouseup click').each(shouldBeCalledWithCount(2)) .then(function () { - const { logs } = this const logsArr = logs.map((x) => x.invoke('consoleProps')) @@ -2387,7 +2377,6 @@ describe('src/cy/commands/actions/click', () => { ], }, ]) - }) }) @@ -2704,7 +2693,6 @@ describe('src/cy/commands/actions/click', () => { }) it('sends modifiers', () => { - const btn = cy.$$('button:first') attachMouseClickListeners({ btn }) @@ -2894,7 +2882,6 @@ describe('src/cy/commands/actions/click', () => { it('#consoleProps', function () { cy.on('log:added', (attrs, log) => { this.log = log - }) cy.get('button').first().dblclick().then(function ($btn) { @@ -3131,7 +3118,6 @@ describe('src/cy/commands/actions/click', () => { cy.getAll('el', 'pointerdown mousedown contextmenu pointerup mouseup').each(shouldBeCalled) cy.getAll('el', 'focus click').each(shouldNotBeCalled) - }) it('rightclick cancel pointerdown', () => { @@ -3147,7 +3133,6 @@ describe('src/cy/commands/actions/click', () => { cy.getAll('el', 'pointerdown pointerup contextmenu').each(shouldBeCalled) cy.getAll('el', 'mousedown mouseup').each(shouldNotBeCalled) - }) it('rightclick remove el on pointerdown', () => { @@ -3163,7 +3148,6 @@ describe('src/cy/commands/actions/click', () => { cy.getAll('el', 'pointerdown').each(shouldBeCalled) cy.getAll('el', 'mousedown mouseup contextmenu pointerup').each(shouldNotBeCalled) - }) it('rightclick remove el on mouseover', () => { @@ -3183,7 +3167,6 @@ describe('src/cy/commands/actions/click', () => { 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', () => { @@ -3442,7 +3425,6 @@ describe('src/cy/commands/actions/click', () => { }) }) }) - }) }) @@ -3911,7 +3893,6 @@ describe('mouse state', () => { cy.getAll('input', 'mouseover mouseout').each(shouldBeCalledOnce) cy.getAll('input', 'mousedown mouseup click').each(shouldNotBeCalled) - }) it('can click on a recursively moving element', () => { @@ -4185,18 +4166,15 @@ describe('mouse state', () => { expect(stub).to.be.calledOnce }) }) - }) 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 @@ -4253,5 +4231,4 @@ describe('mouse state', () => { }) }) }) - }) 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 b86ed25b71ef..ef2eab1680c6 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -1013,7 +1013,6 @@ describe('src/cy/commands/actions/type', () => { it('automatically moves the caret to the end if value is changed manually asynchronously', () => { cy.$$('#input-without-value').keypress((e) => { - const $input = $(e.target) _.defer(() => { @@ -1524,7 +1523,6 @@ describe('src/cy/commands/actions/type', () => { }) describe('specialChars', () => { - context('parseSpecialCharSequences: false', () => { it('types special character sequences literally', (done) => { cy.get(':text:first').invoke('val', 'foo') @@ -2538,9 +2536,7 @@ describe('src/cy/commands/actions/type', () => { }) describe('modifiers', () => { - describe('activating modifiers', () => { - it('sends keydown event for modifiers in order', (done) => { const $input = cy.$$('input:text:first') const events = [] @@ -2680,7 +2676,6 @@ describe('src/cy/commands/actions/type', () => { }) describe('release: false', () => { - it('maintains modifiers for subsequent type commands', (done) => { const $input = cy.$$('input:text:first') const events = [] @@ -2798,7 +2793,6 @@ describe('src/cy/commands/actions/type', () => { }) describe('case-insensitivity', () => { - it('special chars are case-insensitive', () => { cy.get(':text:first').invoke('val', 'bar').type('{leftarrow}{DeL}').then(($input) => { expect($input).to.have.value('ba') @@ -3209,7 +3203,6 @@ describe('src/cy/commands/actions/type', () => { }) describe('caret position', () => { - it('respects being formatted by input event handlers') it('accurately returns host contenteditable attr', () => { @@ -3266,7 +3259,6 @@ describe('src/cy/commands/actions/type', () => { }) it('inside textarea', () => { - cy.$$('body').append(Cypress.$(/*html*/`\
\ \ @@ -3278,7 +3270,6 @@ describe('src/cy/commands/actions/type', () => { }) it('inside contenteditable', () => { - cy.$$('body').append(Cypress.$(/*html*/`\
\
\ @@ -4042,7 +4033,6 @@ describe('src/cy/commands/actions/type', () => { if (log.get('name') === 'type') { expect(log.get('state')).to.eq('pending') expect(log.get('$el').get(0)).to.eq($txt.get(0)) - } }) diff --git a/packages/driver/test/cypress/integration/cy/snapshot_css_spec.js b/packages/driver/test/cypress/integration/cy/snapshot_css_spec.js index 9406cbffb3cc..2c32d1c33c32 100644 --- a/packages/driver/test/cypress/integration/cy/snapshot_css_spec.js +++ b/packages/driver/test/cypress/integration/cy/snapshot_css_spec.js @@ -97,7 +97,6 @@ describe('driver/src/cy/snapshots_css', () => { }) it('returns same id after css has been modified until a new window', () => { - cy.state('document').styleSheets[0].insertRule('.qux { color: orange; }') snapshotCss.onCssModified('http://localhost:3500/fixtures/generic_styles.css') const ids1 = snapshotCss.getStyleIds() diff --git a/packages/driver/test/cypress/integration/dom/visibility_spec.js b/packages/driver/test/cypress/integration/dom/visibility_spec.js index 60e70fb0e2ce..ec957c072180 100644 --- a/packages/driver/test/cypress/integration/dom/visibility_spec.js +++ b/packages/driver/test/cypress/integration/dom/visibility_spec.js @@ -53,7 +53,6 @@ describe('src/cypress/dom/visibility', () => { }) it('returns false window and body > window height', () => { - cy.$$('body').html('
foo
') const win = cy.state('window') diff --git a/packages/driver/test/cypress/integration/e2e/focus_blur_spec.js b/packages/driver/test/cypress/integration/e2e/focus_blur_spec.js index 7cb33e88d797..4bd0cee014ca 100644 --- a/packages/driver/test/cypress/integration/e2e/focus_blur_spec.js +++ b/packages/driver/test/cypress/integration/e2e/focus_blur_spec.js @@ -65,7 +65,6 @@ const requireWindowInFocus = () => { if (!hasFocus) { expect(hasFocus, 'this test requires the window to be in focus').ok } - } it('can intercept blur/focus events', () => { @@ -87,7 +86,6 @@ it('can intercept blur/focus events', () => { cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { - requireWindowInFocus() expect(cy.getFocused()).to.be.null @@ -131,9 +129,7 @@ it('can intercept blur/focus events', () => { expect(blur).calledOnce expect(handleBlur).not.called - }) - }) it('blur the activeElement when clicking the body', () => { @@ -254,7 +250,6 @@ describe('polyfill programmatic blur events', () => { cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { - // programmatically focus the first, then second input element const $one = cy.$$('#one') const $two = cy.$$('#two') @@ -302,7 +297,6 @@ describe('polyfill programmatic blur events', () => { }) }) .then(() => { - stub.reset() setActiveElement($two.get(0)) @@ -339,7 +333,6 @@ describe('polyfill programmatic blur events', () => { $one.get(0).blur() cy.then(() => { - expect(stub).calledTwice expect(_.toPlainObject(stub.getCall(0).args[0].originalEvent)).to.containSubset({ @@ -356,7 +349,6 @@ describe('polyfill programmatic blur events', () => { }) .then(() => { - stub.reset() setActiveElement(cy.$$('body').get(0)) @@ -365,7 +357,6 @@ describe('polyfill programmatic blur events', () => { expect(stub, 'should not send blur if not focused el').not.called }) - }) }) @@ -374,7 +365,6 @@ describe('polyfill programmatic blur events', () => { cy .visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { - // programmatically focus the first, then second input element const $one = cy.$$(` @@ -427,7 +417,6 @@ describe('polyfill programmatic blur events', () => { }) }) .then(() => { - stub.reset() setActiveElement($two.get(0)) @@ -468,7 +457,6 @@ describe('polyfill programmatic blur events', () => { $one.get(0).blur() cy.then(() => { - expect(stub).calledTwice expect(_.toPlainObject(stub.getCall(0).args[0].originalEvent)).to.containSubset({ @@ -485,7 +473,6 @@ describe('polyfill programmatic blur events', () => { }) .then(() => { - stub.reset() setActiveElement(cy.$$('body').get(0)) @@ -494,7 +481,6 @@ describe('polyfill programmatic blur events', () => { expect(stub, 'should not send blur if not focused el').not.called }) - }) }) @@ -508,7 +494,6 @@ describe('polyfill programmatic blur events', () => { it('does not send focus events for non-focusable elements', () => { cy.visit('http://localhost:3500/fixtures/active-elements.html') .then(() => { - cy.$$('
clearly not a focusable element
') .appendTo(cy.$$('body')) @@ -521,9 +506,7 @@ describe('polyfill programmatic blur events', () => { el1[0].focus() expect(stub).not.called - }) - }) }) @@ -548,7 +531,6 @@ describe('intercept blur methods correctly', () => { .should('have.focus') cy.wait(0).get('@selectionchange').should('not.be.called') - }) it('focus