diff --git a/packages/driver/cypress/e2e/commands/waiting.cy.js b/packages/driver/cypress/e2e/commands/waiting.cy.js index d38c6f62ebd3..42fd792900d9 100644 --- a/packages/driver/cypress/e2e/commands/waiting.cy.js +++ b/packages/driver/cypress/e2e/commands/waiting.cy.js @@ -47,7 +47,7 @@ describe('src/cy/commands/waiting', () => { cy .wait(50) .then(() => { - expect(timeout).to.be.calledWith(50, true, 'wait') + expect(timeout).to.be.calledWith(50, true) }) }) }) diff --git a/packages/driver/src/cy/aliases.ts b/packages/driver/src/cy/aliases.ts index ab88f6ec5f9f..b221a7becd21 100644 --- a/packages/driver/src/cy/aliases.ts +++ b/packages/driver/src/cy/aliases.ts @@ -2,6 +2,7 @@ import _ from 'lodash' import type { $Cy } from '../cypress/cy' import $errUtils from '../cypress/error_utils' +import type { SubjectChain } from '../cypress/state' export const aliasRe = /^@.+/ @@ -12,13 +13,13 @@ const requestXhrRe = /\.request$/ const reserved = ['test', 'runnable', 'timeout', 'slow', 'skip', 'inspect'] -export const aliasDisplayName = (name) => { +export const aliasDisplayName = (name: string) => { return name.replace(aliasDisplayRe, '') } // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces export const create = (cy: $Cy) => ({ - addAlias (ctx, aliasObj) { + addAlias (ctx: Mocha.Context, aliasObj: { alias: string, subjectChain: SubjectChain }) { const { alias } = aliasObj const aliases = cy.state('aliases') || {} diff --git a/packages/driver/src/cy/commands/waiting.ts b/packages/driver/src/cy/commands/waiting.ts index b1314b66bdc1..b248437d8517 100644 --- a/packages/driver/src/cy/commands/waiting.ts +++ b/packages/driver/src/cy/commands/waiting.ts @@ -4,12 +4,27 @@ import { waitForRoute } from '../net-stubbing/wait-for-route' import { isDynamicAliasingPossible } from '../net-stubbing/aliasing' import ordinal from 'ordinal' -import $errUtils from '../../cypress/error_utils' +import $errUtils, { type CypressError, type InternalCypressError } from '../../cypress/error_utils' +import type { $Cy } from '../../cypress/cy' +import type { StateFunc } from '../../cypress/state' +import type { Log } from '../../cypress/log' + +type waitOptions = { + _log?: Cypress.Log + _runnableTimeout?: number + error?: CypressError | InternalCypressError + isCrossOriginSpecBridge?: boolean + log: boolean + requestTimeout?: number + responseTimeout?: number + timeout: number + type?: 'request' | 'response' +} -const getNumRequests = (state, alias) => { - const requests = state('aliasRequests') || {} +const getNumRequests = (state: StateFunc, alias: string) => { + const requests = state('aliasRequests') ?? {} - requests[alias] = requests[alias] || 0 + requests[alias] = requests[alias] ?? 0 const index = requests[alias] @@ -17,23 +32,17 @@ const getNumRequests = (state, alias) => { state('aliasRequests', requests) - return [index, ordinal(requests[alias])] + return [index, ordinal(requests[alias])] as const } -const throwErr = (arg) => { +const throwErr = (arg: string) => { $errUtils.throwErrByPath('wait.invalid_1st_arg', { args: { arg } }) } -type Alias = { - name: string - cardinal: number - ordinal: number -} - -export default (Commands, Cypress, cy, state) => { - const waitNumber = (subject, ms, options) => { +export default (Commands: Cypress.Commands, Cypress: Cypress.Cypress, cy: $Cy, state: StateFunc) => { + const waitNumber = (subject: unknown, ms: number, options: waitOptions) => { // increase the timeout by the delta - cy.timeout(ms, true, 'wait') + cy.timeout(ms, true) options._log = Cypress.log({ hidden: options.log === false, @@ -51,7 +60,7 @@ export default (Commands, Cypress, cy, state) => { .return(subject) } - const waitString = (subject, str, options) => { + const waitString = async (str: string | string[], options: waitOptions) => { // if this came from the spec bridge, we need to set a few additional properties to ensure the log displays correctly // otherwise, these props will be pulled from the current command which will be cy.origin on the primary const log = options._log = Cypress.log({ @@ -69,7 +78,13 @@ export default (Commands, Cypress, cy, state) => { }) } - const checkForXhr = async function (alias, type, index, num, options) { + const checkForXhr = async function ( + alias: string, + type: 'request'|'response', + index: number, + num: string, + options: waitOptions, + ) { options.error = $errUtils.errByPath('wait.timed_out', { timeout: options.timeout, alias, @@ -98,15 +113,16 @@ export default (Commands, Cypress, cy, state) => { return xhr } - const args: [any, any, any, any, any] = [alias, type, index, num, options] - return cy.retry(() => { - return checkForXhr.apply(window, args) - }, options) + return checkForXhr.apply(window, [alias, type, index, num, options]) + }, + // TODO: What should `_log`'s type be? + // @ts-expect-error - Incompatible types. + options) } - const waitForXhr = function (str, options) { - let specifier + const waitForXhr = async function (str: string, options: Omit) { + let specifier: string | null | undefined // we always want to strip everything after the last '.' // since we support alias property 'request' @@ -126,7 +142,7 @@ export default (Commands, Cypress, cy, state) => { } } - let aliasObj + let aliasObj: { alias: string, command?: unknown } try { aliasObj = cy.getAlias(str, 'wait', log) @@ -161,8 +177,8 @@ export default (Commands, Cypress, cy, state) => { // build up an array of referencesAlias // because wait can reference an array of aliases if (log) { - const referencesAlias = log.get('referencesAlias') || [] - const aliases: Array = [].concat(referencesAlias) + const referencesAlias = log.get('referencesAlias') ?? [] + const aliases = [...referencesAlias] if (str) { aliases.push({ @@ -182,13 +198,13 @@ export default (Commands, Cypress, cy, state) => { return commandsThatCreateNetworkIntercepts.includes(commandName) } - const findInterceptAlias = (alias) => { - const routes = cy.state('routes') || {} + const findInterceptAlias = (alias: string) => { + const routes = cy.state('routes') ?? {} return _.find(_.values(routes), { alias }) } - const isInterceptAlias = (alias) => Boolean(findInterceptAlias(alias)) + const isInterceptAlias = (alias: string) => Boolean(findInterceptAlias(alias)) if (command && !isNetworkInterceptCommand(command)) { if (!isInterceptAlias(alias)) { @@ -203,11 +219,15 @@ export default (Commands, Cypress, cy, state) => { // but slice out the error since we may set // the error related to a previous xhr const { timeout } = options + // TODO: If `options.requestTimeout` and `options.responseTimeout` are + // `0`, is this code going to work the way it was intended to? const requestTimeout = options.requestTimeout || timeout const responseTimeout = options.responseTimeout || timeout const waitForRequest = () => { options = _.omit(options, '_runnableTimeout') + // TODO: If `requestTimeout` is `0`, is this code going to work the way + // it was intended to? options.timeout = requestTimeout || Cypress.config('requestTimeout') if (log) { @@ -219,6 +239,8 @@ export default (Commands, Cypress, cy, state) => { const waitForResponse = () => { options = _.omit(options, '_runnableTimeout') + // TODO: If `responseTimeout` is `0`, is this code going to work the way + // it was intended to? options.timeout = responseTimeout || Cypress.config('responseTimeout') if (log) { @@ -237,8 +259,7 @@ export default (Commands, Cypress, cy, state) => { return waitForRequest().then(waitForResponse) } - return Promise - .map([].concat(str), (str) => { + return Promise.map(([] as string[]).concat(str), (str) => { // we may get back an xhr value instead // of a promise, so we have to wrap this // in another promise :-( @@ -285,18 +306,24 @@ export default (Commands, Cypress, cy, state) => { }) } - Cypress.primaryOriginCommunicator.on('wait:for:xhr', ({ args: [str, options] }, { origin }) => { - options.isCrossOriginSpecBridge = true - waitString(null, str, options).then((responses) => { - Cypress.primaryOriginCommunicator.toSpecBridge(origin, 'wait:for:xhr:end', responses) - }).catch((err) => { - options._log?.error(err) - err.hasSpecBridgeError = true - Cypress.primaryOriginCommunicator.toSpecBridge(origin, 'wait:for:xhr:end', err) - }) - }) + Cypress.primaryOriginCommunicator.on( + 'wait:for:xhr', + ( + { args: [str, options] }: { args: [string | string[], waitOptions] }, + { origin }, + ) => { + options.isCrossOriginSpecBridge = true + waitString(str, options).then((responses) => { + Cypress.primaryOriginCommunicator.toSpecBridge(origin, 'wait:for:xhr:end', responses) + }).catch((err) => { + options._log?.error(err) + err.hasSpecBridgeError = true + Cypress.primaryOriginCommunicator.toSpecBridge(origin, 'wait:for:xhr:end', err) + }) + }, + ) - const delegateToPrimaryOrigin = ([_subject, str, options]) => { + const delegateToPrimaryOrigin = (str: string | string[], options: waitOptions) => { return new Promise((resolve, reject) => { Cypress.specBridgeCommunicator.once('wait:for:xhr:end', (responsesOrErr) => { // determine if this is an error by checking if there is a spec bridge error @@ -304,7 +331,7 @@ export default (Commands, Cypress, cy, state) => { delete responsesOrErr.hasSpecBridgeError if (options.log) { // skip this 'wait' log since it was already added through the primary - Cypress.state('onBeforeLog', (log) => { + Cypress.state('onBeforeLog', (log: Log) => { if (log.get('name') === 'wait') { // unbind this function so we don't impact any other logs cy.state('onBeforeLog', null) @@ -328,7 +355,7 @@ export default (Commands, Cypress, cy, state) => { } Commands.addAll({ prevSubject: 'optional' }, { - wait (subject, msOrAlias, options: { log?: boolean } = {}) { + wait (subject: unknown, msOrAlias: number | string | string[], options: waitOptions) { // 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 @@ -342,19 +369,18 @@ export default (Commands, Cypress, cy, state) => { } options = _.defaults({}, options, { log: true }) - const args: any = [subject, msOrAlias, options] try { - if (_.isFinite(msOrAlias)) { - return waitNumber.apply(window, args) + if (typeof msOrAlias === 'number' && _.isFinite(msOrAlias)) { + return waitNumber.apply(window, [subject, msOrAlias, options]) } if (_.isString(msOrAlias) || (_.isArray(msOrAlias) && !_.isEmpty(msOrAlias))) { if (Cypress.isCrossOriginSpecBridge) { - return delegateToPrimaryOrigin(args) + return delegateToPrimaryOrigin(msOrAlias, options) } - return waitString.apply(window, args) + return waitString.apply(window, [msOrAlias, options]) } // figure out why this error failed @@ -370,7 +396,7 @@ export default (Commands, Cypress, cy, state) => { throwErr(msOrAlias.toString()) } - let arg + let arg: string try { arg = JSON.stringify(msOrAlias) @@ -379,7 +405,7 @@ export default (Commands, Cypress, cy, state) => { } return throwErr(arg) - } catch (err: any) { + } catch (err) { if (err.name === 'CypressError') { throw err } else { diff --git a/packages/driver/src/cypress/state.ts b/packages/driver/src/cypress/state.ts index ccda98d84c6d..73e599b30b2e 100644 --- a/packages/driver/src/cypress/state.ts +++ b/packages/driver/src/cypress/state.ts @@ -33,7 +33,7 @@ export interface StateFunc { (k: 'isStable', v?: boolean): boolean (k: 'whenStable', v?: null | (() => Promise)): () => Promise (k: 'current', v?: $Command): $Command - (k: 'canceld', v?: boolean): boolean + (k: 'canceled', v?: boolean): boolean (k: 'error', v?: Error): Error (k: 'assertUsed', v?: boolean): boolean (k: 'currentAssertionUserInvocationStack', v?: string): string @@ -54,6 +54,7 @@ export interface StateFunc { (k: 'promise', v?: Bluebird): Bluebird (k: 'reject', v?: (err: any) => any): (err: any) => any (k: 'cancel', v?: () => void): () => void + (k: 'aliasRequests', v?: Record): Record (k: string, v?: any): any state: StateFunc reset: () => Record diff --git a/packages/driver/types/cypress/log.d.ts b/packages/driver/types/cypress/log.d.ts index 2a793a3b70db..583c14f6f51f 100644 --- a/packages/driver/types/cypress/log.d.ts +++ b/packages/driver/types/cypress/log.d.ts @@ -8,7 +8,7 @@ declare namespace Cypress { _hasInitiallyLogged: boolean get(attr: K): InternalLogConfig[K] get(): InternalLogConfig - set(key: K, value: LogConfig[K]): InternalLog + set(key: K, value: LogConfig[K]): InternalLog set(options: Partial) groupEnd(): void } @@ -144,5 +144,6 @@ declare namespace Cypress { visible?: boolean // the timestamp of when the command started wallClockStartedAt?: string + options?: unknown } }