Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a lot of this has to do with a mismatch between the internal log and external log type. Could be we just update the declaration types but it isn't likely worth the effort since it isn't consumer facing

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 = structuredClone(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
}
}