diff --git a/packages/driver/src/cy/commands/misc.js b/packages/driver/src/cy/commands/misc.js index 0d08708c5a..91b49ffcb1 100644 --- a/packages/driver/src/cy/commands/misc.js +++ b/packages/driver/src/cy/commands/misc.js @@ -1,6 +1,8 @@ const _ = require('lodash') +const Promise = require('bluebird') const $dom = require('../../dom') +const $errUtils = require('../../cypress/error_utils') module.exports = (Commands, Cypress, cy) => { Commands.addAll({ prevSubject: 'optional' }, { @@ -33,7 +35,13 @@ module.exports = (Commands, Cypress, cy) => { wrap (arg, options = {}) { const userOptions = options - options = _.defaults({}, userOptions, { log: true }) + options = _.defaults({}, userOptions, { + log: true, + timeout: Cypress.config('defaultCommandTimeout'), + }) + + // we'll handle the timeout ourselves + cy.clearTimeout() if (options.log !== false) { options._log = Cypress.log({ @@ -45,14 +53,28 @@ module.exports = (Commands, Cypress, cy) => { } } - const resolveWrap = () => { - return cy.verifyUpcomingAssertions(arg, options, { - onRetry: resolveWrap, + return Promise.resolve(arg) + .timeout(options.timeout) + .catch(Promise.TimeoutError, () => { + $errUtils.throwErrByPath('wrap.timed_out', { + args: { timeout: options.timeout }, }) - .return(arg) - } + }) + .catch((err) => { + $errUtils.throwErr(err, { + onFail: options._log, + }) + }) + .then((subject) => { + const resolveWrap = () => { + return cy.verifyUpcomingAssertions(subject, options, { + onRetry: resolveWrap, + }) + .return(subject) + } - return resolveWrap() + return resolveWrap() + }) }, }) } diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index eb256ea18f..c9b9e796ba 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -327,7 +327,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { state('nestedIndex', state('index')) return command.get('args') - }).all() + }) .then((args) => { // store this if we enqueue new commands diff --git a/packages/driver/src/cypress/error_messages.js b/packages/driver/src/cypress/error_messages.js index 37fea89b25..360ab303b8 100644 --- a/packages/driver/src/cypress/error_messages.js +++ b/packages/driver/src/cypress/error_messages.js @@ -661,7 +661,7 @@ module.exports = { message: stripIndent`\ ${cmd('{{cmd}}')} timed out after waiting \`{{timeout}}ms\`. - Your callback function returned a promise which never resolved. + Your callback function returned a promise that never resolved. The callback function was: @@ -1798,6 +1798,19 @@ module.exports = { }, }, + wrap: { + timed_out: { + message: stripIndent` + ${cmd('wrap')} timed out waiting \`{{timeout}}ms\` to complete. + + You called \`cy.wrap()\` with a promise that never resolved. + + To increase the timeout, use \`{ timeout: number }\` + `, + docsUrl: 'https://on.cypress.io/wrap', + }, + }, + xhr: { aborted: 'This XHR was aborted by your code -- check this stack trace below.', missing: '`XMLHttpRequest#xhr` is missing.', diff --git a/packages/driver/test/cypress/integration/commands/connectors_spec.js b/packages/driver/test/cypress/integration/commands/connectors_spec.js index 44d42ec021..fdebd4febc 100644 --- a/packages/driver/test/cypress/integration/commands/connectors_spec.js +++ b/packages/driver/test/cypress/integration/commands/connectors_spec.js @@ -1955,7 +1955,7 @@ describe('src/cy/commands/connectors', () => { cy.on('fail', (err) => { // get + each expect(this.logs.length).to.eq(2) - expect(err.message).to.include('`cy.each()` timed out after waiting `50ms`.\n\nYour callback function returned a promise which never resolved.') + expect(err.message).to.include('`cy.each()` timed out after waiting `50ms`.\n\nYour callback function returned a promise that never resolved.') expect(err.docsUrl).to.include('https://on.cypress.io/each') done() diff --git a/packages/driver/test/cypress/integration/commands/misc_spec.js b/packages/driver/test/cypress/integration/commands/misc_spec.js index 0d078b6249..6a9dfb1d3f 100644 --- a/packages/driver/test/cypress/integration/commands/misc_spec.js +++ b/packages/driver/test/cypress/integration/commands/misc_spec.js @@ -191,7 +191,32 @@ describe('src/cy/commands/misc', () => { }) }) + it('can extend the default timeout', () => { + Cypress.config('defaultCommandTimeout', 100) + + const timeoutPromise = new Promise((resolve, reject) => { + return setTimeout(() => { + resolve(null) + }) + }, 200) + + cy.wrap(timeoutPromise, { timeout: 300 }) + }) + describe('errors', () => { + beforeEach(function () { + this.logs = [] + + cy.on('log:added', (attrs, log) => { + if (attrs.name === 'wrap') { + this.lastLog = log + this.logs.push(log) + } + }) + + return null + }) + it('throws when wrapping an array of windows', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('`cy.scrollTo()` failed because it requires a DOM element.') @@ -219,6 +244,61 @@ describe('src/cy/commands/misc', () => { cy.wrap([doc]).screenshot() }) }) + + it('throws when exceeding default timeout', function (done) { + Cypress.config('defaultCommandTimeout', 100) + + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(1) + expect(err.message).to.include('`cy.wrap()` timed out waiting `100ms` to complete.') + expect(err.message).to.include('You called `cy.wrap()` with a promise that never resolved.') + expect(err.message).to.include('To increase the timeout, use `{ timeout: number }`') + expect(this.lastLog.get('error')).to.eq(err) + done() + }) + + const timeoutPromise = new Promise((resolve) => { + setTimeout((() => { + resolve(null) + }), 200) + }) + + cy.wrap(timeoutPromise) + }) + + it('throws when exceeding custom timeout', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(1) + expect(err.message).to.include('`cy.wrap()` timed out waiting `100ms` to complete.') + expect(err.message).to.include('You called `cy.wrap()` with a promise that never resolved.') + expect(err.message).to.include('To increase the timeout, use `{ timeout: number }`') + expect(this.lastLog.get('error')).to.eq(err) + done() + }) + + const timeoutPromise = new Promise((resolve) => { + setTimeout((() => { + resolve(null) + }), 200) + }) + + cy.wrap(timeoutPromise, { timeout: 100 }) + }) + + it('logs once when promise parameter is rejected', function (done) { + cy.on('fail', (err) => { + expect(this.logs.length).to.eq(1) + expect(err.message).to.include('custom error') + expect(this.lastLog.get('error')).to.eq(err) + done() + }) + + const rejectedPromise = new Promise((resolve, reject) => { + reject(new Error('custom error')) + }) + + cy.wrap(rejectedPromise) + }) }) describe('.log', () => {