diff --git a/packages/driver/cypress/integration/commands/actions/click_spec.js b/packages/driver/cypress/integration/commands/actions/click_spec.js index 634023a705ab..a566f9544bdb 100644 --- a/packages/driver/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/cypress/integration/commands/actions/click_spec.js @@ -752,6 +752,20 @@ describe('src/cy/commands/actions/click', () => { }) }) + it('each click gets a full command timeout', () => { + cy.spy(cy, 'retry') + + cy.get('#three-buttons button').click({ multiple: true }).then(() => { + const [firstCall, secondCall] = cy.retry.getCalls() + const firstCallOptions = firstCall.args[1] + const secondCallOptions = secondCall.args[1] + + // ensure we clone the options object passed to `retry()` so that + // each click in `{ multiple: true }` gets its own full timeout + expect(firstCallOptions !== secondCallOptions, 'Expected click retry options to be different object references between clicks').to.be.true + }) + }) + // this test needs to increase the height + width of the div // when we implement scrollBy the delta of the left/top it('can click elements which are huge and the center is naturally below the fold', () => { diff --git a/packages/driver/src/cy/actionability.js b/packages/driver/src/cy/actionability.js index da4dd01f4c7c..1426ec7079f7 100644 --- a/packages/driver/src/cy/actionability.js +++ b/packages/driver/src/cy/actionability.js @@ -261,16 +261,29 @@ const getCoordinatesForEl = function (cy, $el, options) { } const ensureNotAnimating = function (cy, $el, coordsHistory, animationDistanceThreshold) { - // if we dont have at least 2 points - // then automatically retry + // if we dont have at least 2 points, we throw this error to force a + // retry, which will get us another point. + // this error is purposefully generic because if the actionability + //check times out, this error is the one displayed to the user and + // saying something like "coordsHistory must be at least 2 sets + // of coords" is not very useful. + // that would only happen if the actionability check times out, which + // shouldn't happen with default timeouts, but could theoretically + // on a very, very slow system + // https://github.com/cypress-io/cypress/issues/3738 if (coordsHistory.length < 2) { - $errUtils.throwErrByPath('dom.animation_coords_history_invalid') + $errUtils.throwErrByPath('dom.actionability_failed', { + args: { + node: $dom.stringify($el), + cmd: cy.state('current').get('name'), + }, + }) } // verify that our element is not currently animating // by verifying it is still at the same coordinates within // 5 pixels of x/y - return cy.ensureElementIsNotAnimating($el, coordsHistory, animationDistanceThreshold) + cy.ensureElementIsNotAnimating($el, coordsHistory, animationDistanceThreshold) } const verify = function (cy, $el, options, callbacks) { @@ -336,7 +349,6 @@ const verify = function (cy, $el, options, callbacks) { } return Promise.try(() => { - let retryActionability const coordsHistory = [] const runAllChecks = function () { @@ -426,7 +438,7 @@ const verify = function (cy, $el, options, callbacks) { // element passes every single check, we MUST fire the event // synchronously else we risk the state changing between // the checks and firing the event! - return (retryActionability = function () { + const retryActionability = () => { try { return runAllChecks() } catch (err) { @@ -434,7 +446,9 @@ const verify = function (cy, $el, options, callbacks) { return cy.retry(retryActionability, options) } - })() + } + + return retryActionability() }) } diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index 3d74cbb868c3..cb56c8fab5d3 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -116,9 +116,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { }) } - // we want to add this delay delta to our - // runnables timeout so we prevent it from - // timing out from multiple clicks + // add this delay delta to the runnables timeout because we delay + // by it below before performing each click cy.timeout($actionability.delay, true, eventName) const createLog = (domEvents, fromElWindow, fromAutWindow) => { @@ -169,11 +168,17 @@ module.exports = (Commands, Cypress, cy, state, config) => { .return(null) } + // if { multiple: true }, make a shallow copy of options, since + // properties like `total` and `_retries` are mutated by + // $actionability.verify and retrying, but each click should + // have its own full timeout + const individualOptions = { ... options } + // must use callbacks here instead of .then() // because we're issuing the clicks synchronously // once we establish the coordinates and the element // passes all of the internal checks - return $actionability.verify(cy, $el, options, { + return $actionability.verify(cy, $el, individualOptions, { onScroll ($el, type) { return Cypress.action('cy:scrolled', $el, type) }, diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index 0b9082516c1d..dfbb903c9dad 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -293,6 +293,13 @@ module.exports = { }, dom: { + actionability_failed: stripIndent` + ${cmd('{{cmd}}')} could not be issued because we could not determine the actionability of this element: + + \`{{node}}\` + + You can prevent this by passing \`{force: true}\` to disable all error checking. + `, animating: { message: stripIndent`\ ${cmd('{{cmd}}')} could not be issued because this element is currently animating: @@ -305,7 +312,6 @@ module.exports = { - Passing \`{animationDistanceThreshold: 20}\` which decreases the sensitivity`, docsUrl: 'https://on.cypress.io/element-is-animating', }, - animation_coords_history_invalid: 'coordsHistory must be at least 2 sets of coords', animation_check_failed: 'Not enough coord points provided to calculate distance.', center_hidden: { message: stripIndent`\