UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

330 lines (258 loc) 8.96 kB
const _ = require('lodash') const Promise = require('bluebird') const { waitForRoute } = require('../net-stubbing/wait-for-route') const { isDynamicAliasingPossible } = require('../net-stubbing/aliasing') const ordinal = require('ordinal') const $errUtils = require('../../cypress/error_utils') const getNumRequests = (state, alias) => { const requests = state('aliasRequests') || {} requests[alias] = requests[alias] || 0 const index = requests[alias] requests[alias] += 1 state('aliasRequests', requests) return [index, ordinal(requests[alias])] } const throwErr = (arg) => { $errUtils.throwErrByPath('wait.invalid_1st_arg', { args: { arg } }) } module.exports = (Commands, Cypress, cy, state) => { let userOptions = null const waitNumber = (subject, ms, options) => { // increase the timeout by the delta cy.timeout(ms, true, 'wait') if (options.log !== false) { options._log = Cypress.log({ timeout: ms, consoleProps () { return { 'Waited For': `${ms}ms before continuing`, 'Yielded': subject, } }, }) } return Promise .delay(ms, 'wait') .return(subject) } const waitString = (subject, str, options) => { let log if (options.log !== false) { log = options._log = Cypress.log({ type: 'parent', aliasType: 'route', options: userOptions, }) } const checkForXhr = async function (alias, type, index, num, options) { options.error = $errUtils.errByPath('wait.timed_out', { timeout: options.timeout, alias, num, type, }) options.type = type // check cy.intercept routes const req = waitForRoute(alias, state, type) if (req) { return req } // append .type to the alias const xhr = cy.getIndexedXhrByAlias(`${alias}.${type}`, index) // return our xhr object if (xhr) { return xhr } const args = [alias, type, index, num, options] return cy.retry(() => { return checkForXhr.apply(window, args) }, options) } const waitForXhr = function (str, options) { let specifier // we always want to strip everything after the last '.' // since we support alias property 'request' if ((_.indexOf(str, '.') === -1) || _.keys(cy.state('aliases')).includes(str.slice(1))) { specifier = null } else { // potentially request, response const allParts = _.split(str, '.') const last = _.last(allParts) if (last === 'request' || last === 'response') { str = _.join(_.dropRight(allParts, 1), '.') specifier = _.last(allParts) } else { specifier = null } } let aliasObj try { aliasObj = cy.getAlias(str, 'wait', log) } catch (err) { // before cy.intercept, we could know when an alias did/did not exist, because they // were declared synchronously. with cy.intercept, req.alias can be used to dynamically // create aliases, so we cannot know at wait-time if an alias exists or not if (!isDynamicAliasingPossible(state)) { throw err } // could be a dynamic alias aliasObj = { alias: str.slice(1) } } if (!aliasObj) { cy.aliasNotFoundFor(str, 'wait', log) } // if this alias is for a route then poll // until we find the response xhr object // by its alias const { alias, command } = aliasObj str = _.compact([alias, specifier]).join('.') const type = cy.getXhrTypeByAlias(str) const [index, num] = getNumRequests(state, alias) // if we have a command then continue to // build up an array of referencesAlias // because wait can reference an array of aliases if (log) { const referencesAlias = log.get('referencesAlias') || [] const aliases = [].concat(referencesAlias) if (str) { aliases.push({ name: str, cardinal: index + 1, ordinal: num, }) } log.set('referencesAlias', aliases) } const isNetworkInterceptCommand = (command) => { const commandsThatCreateNetworkIntercepts = ['route', 'route2', 'intercept'] const commandName = command.get('name') return commandsThatCreateNetworkIntercepts.includes(commandName) } const findInterceptAlias = (alias) => { const routes = cy.state('routes') || {} return _.find(_.values(routes), { alias }) } const isInterceptAlias = (alias) => Boolean(findInterceptAlias(alias)) const isRouteAlias = (alias) => { // has all aliases saved using cy.as() command const aliases = cy.state('aliases') || {} const aliasObject = aliases[alias] if (!aliasObject) { return false } // cy.route aliases have subject that has all XHR properties // let's check one of them return aliasObj.subject && Boolean(aliasObject.subject.xhrUrl) } if (command && !isNetworkInterceptCommand(command)) { if (!isInterceptAlias(alias) && !isRouteAlias(alias)) { $errUtils.throwErrByPath('wait.invalid_alias', { onFail: options._log, args: { alias }, }) } } // create shallow copy of each options object // but slice out the error since we may set // the error related to a previous xhr const { timeout } = options const requestTimeout = options.requestTimeout || timeout const responseTimeout = options.responseTimeout || timeout const waitForRequest = () => { options = _.omit(options, '_runnableTimeout') options.timeout = requestTimeout || Cypress.config('requestTimeout') if (log) { log.set('timeout', options.timeout) } return checkForXhr(alias, 'request', index, num, options) } const waitForResponse = () => { options = _.omit(options, '_runnableTimeout') options.timeout = responseTimeout || Cypress.config('responseTimeout') if (log) { log.set('timeout', options.timeout) } return checkForXhr(alias, 'response', index, num, options) } // if we were only waiting for the request // then resolve immediately do not wait for response if (type === 'request') { return waitForRequest() } return waitForRequest().then(waitForResponse) } return Promise .map([].concat(str), (str) => { // we may get back an xhr value instead // of a promise, so we have to wrap this // in another promise :-( return waitForXhr(str, _.omit(options, 'error')) }) .then((responses) => { // if we only asked to wait for one alias // then return that, else return the array of xhr responses const ret = responses.length === 1 ? responses[0] : responses if (log) { log.set('consoleProps', () => { return { 'Waited For': (_.map(log.get('referencesAlias'), 'name') || []).join(', '), 'Yielded': ret, } }) log.snapshot().end() } return ret }) } Commands.addAll({ prevSubject: 'optional' }, { wait (subject, msOrAlias, options = {}) { userOptions = options // check to ensure options is an object // if its a string the user most likely is trying // to wait on multiple aliases and forget to make this // an array if (_.isString(userOptions)) { $errUtils.throwErrByPath('wait.invalid_arguments') } options = _.defaults({}, userOptions, { log: true }) const args = [subject, msOrAlias, options] try { if (_.isFinite(msOrAlias)) { return waitNumber.apply(window, args) } if (_.isString(msOrAlias)) { return waitString.apply(window, args) } if (_.isArray(msOrAlias) && !_.isEmpty(msOrAlias)) { return waitString.apply(window, args) } // figure out why this error failed if (_.isNaN(msOrAlias)) { throwErr('NaN') } if (msOrAlias === Infinity) { throwErr('Infinity') } if (_.isSymbol(msOrAlias)) { throwErr(msOrAlias.toString()) } let arg try { arg = JSON.stringify(msOrAlias) } catch (error) { arg = 'an invalid argument' } return throwErr(arg) } catch (err) { if (err.name === 'CypressError') { throw err } else { // whatever was passed in could not be parsed // by our switch case return throwErr('an invalid argument') } } }, }) }