Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/driver/cypress/e2e/commands/waiting.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})
Expand Down
5 changes: 3 additions & 2 deletions packages/driver/src/cy/aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /^@.+/

Expand All @@ -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') || {}
Expand Down
128 changes: 77 additions & 51 deletions packages/driver/src/cy/commands/waiting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,45 @@ 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]

requests[alias] += 1

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,
Expand All @@ -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({
Expand All @@ -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,
Expand Down Expand Up @@ -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<waitOptions, 'error'>) {
let specifier: string | null | undefined

// we always want to strip everything after the last '.'
// since we support alias property 'request'
Expand All @@ -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)
Expand Down Expand Up @@ -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<Alias> = [].concat(referencesAlias)
const referencesAlias = log.get('referencesAlias') ?? []
const aliases = [...referencesAlias]

if (str) {
aliases.push({
Expand All @@ -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)) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 :-(
Expand Down Expand Up @@ -285,26 +306,32 @@ 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
if (responsesOrErr.hasSpecBridgeError) {
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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -370,7 +396,7 @@ export default (Commands, Cypress, cy, state) => {
throwErr(msOrAlias.toString())
}

let arg
let arg: string

try {
arg = JSON.stringify(msOrAlias)
Expand All @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion packages/driver/src/cypress/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface StateFunc {
(k: 'isStable', v?: boolean): boolean
(k: 'whenStable', v?: null | (() => Promise<any>)): () => Promise<any>
(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
Expand All @@ -54,6 +54,7 @@ export interface StateFunc {
(k: 'promise', v?: Bluebird<unknown>): Bluebird<unknown>
(k: 'reject', v?: (err: any) => any): (err: any) => any
(k: 'cancel', v?: () => void): () => void
(k: 'aliasRequests', v?: Record<string, number>): Record<string, number>
(k: string, v?: any): any
state: StateFunc
reset: () => Record<string, any>
Expand Down
3 changes: 2 additions & 1 deletion packages/driver/types/cypress/log.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ declare namespace Cypress {
_hasInitiallyLogged: boolean
get<K extends keyof InternalLogConfig>(attr: K): InternalLogConfig[K]
get(): InternalLogConfig
set<K extends keyof LogConfig | InternalLogConfig>(key: K, value: LogConfig[K]): InternalLog
set<K extends keyof LogConfig | keyof InternalLogConfig>(key: K, value: LogConfig[K]): InternalLog
set(options: Partial<LogConfig | InternalLogConfig>)
groupEnd(): void
}
Expand Down Expand Up @@ -144,5 +144,6 @@ declare namespace Cypress {
visible?: boolean
// the timestamp of when the command started
wallClockStartedAt?: string
options?: unknown
}
}