diff --git a/packages/driver/cypress/integration/commands/aliasing_spec.js b/packages/driver/cypress/integration/commands/aliasing_spec.js index db17200a01cf..bd7f964a40ac 100644 --- a/packages/driver/cypress/integration/commands/aliasing_spec.js +++ b/packages/driver/cypress/integration/commands/aliasing_spec.js @@ -497,19 +497,6 @@ describe('src/cy/commands/aliasing', () => { .get('input:first').as('firstInput') .get('@lastDiv') }) - - it('throws when alias is missing \'@\' but matches an available alias', (done) => { - cy.on('fail', (err) => { - expect(err.message).to.eq('Invalid alias: `getAny`.\nYou forgot the `@`. It should be written as: `@getAny`.') - - done() - }) - - cy - .server() - .route('*', {}).as('getAny') - .wait('getAny').then(() => {}) - }) }) }) }) diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index ec56cfe8497b..2615c44b18d8 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -1787,6 +1787,63 @@ describe('network stubbing', { retries: 2 }, function () { }) }) + context('with an intercepted request', function () { + it('can dynamically alias the request', function () { + cy.route2('/foo', (req) => { + req.alias = 'fromInterceptor' + }) + .then(() => { + $.get('/foo') + }) + .wait('@fromInterceptor') + }) + + it('can time out on a dynamic alias', function (done) { + cy.on('fail', (err) => { + expect(err.message).to.contain('for the 1st request to the route') + done() + }) + + cy.route2('/foo', (req) => { + req.alias = 'fromInterceptor' + }) + .wait('@fromInterceptor', { timeout: 100 }) + }) + + it('dynamic aliases are fulfilled before route aliases', function (done) { + cy.on('fail', (err) => { + expect(err.message).to.contain('for the 1st request to the route: `fromAs`') + done() + }) + + cy.route2('/foo', (req) => { + req.alias = 'fromInterceptor' + }) + .as('fromAs') + .then(() => { + $.get('/foo') + }) + .wait('@fromInterceptor') + // this will fail - dynamic aliasing maintains the existing wait semantics, including that each request can only be waited once + .wait('@fromAs', { timeout: 100 }) + }) + + it('fulfills both dynamic aliases when two are defined', function () { + cy.route2('/foo', (req) => { + req.alias = 'fromInterceptor' + }) + .route2('/foo', (req) => { + expect(req.alias).to.be.undefined + req.alias = 'fromInterceptor2' + }) + .then(() => { + $.get('/foo') + }) + .wait('@fromInterceptor') + .wait('@fromInterceptor2') + }) + }) + // @see https://github.com/cypress-io/cypress/issues/8695 context('yields request', function () { it('when not intercepted', function () { @@ -1842,47 +1899,5 @@ describe('network stubbing', { retries: 2 }, function () { .then(testResponse('something different', done)) }) }) - - // NOTE: was undocumented in cy.route2, may not continue to support - // @see https://github.com/cypress-io/cypress/issues/7663 - context.skip('indexed aliases', function () { - it('can wait for things that do not make sense but are technically true', function () { - cy.route2('/foo') - .as('foo.bar') - .then(() => { - $.get('/foo') - }) - .wait('@foo.bar.1') - .wait('@foo.bar.1') // still only asserting on the 1st response - .wait('@foo.bar.request') // now waiting for the next request - }) - - it('can wait on the 3rd request using "alias.3"', function () { - cy.route2('/foo') - .as('foo.bar') - .then(() => { - _.times(3, () => { - $.get('/foo') - }) - }) - .wait('@foo.bar.3') - }) - - it('can timeout waiting on the 3rd request using "alias.3"', function (done) { - cy.on('fail', (err) => { - expect(err.message).to.contain('No response ever occurred.') - done() - }) - - cy.route2('/foo') - .as('foo.bar') - .then(() => { - _.times(2, () => { - $.get('/foo') - }) - }) - .wait('@foo.bar.3', { timeout: 100 }) - }) - }) }) }) diff --git a/packages/driver/src/cy/commands/waiting.js b/packages/driver/src/cy/commands/waiting.js index 037108aeadd4..25029720c867 100644 --- a/packages/driver/src/cy/commands/waiting.js +++ b/packages/driver/src/cy/commands/waiting.js @@ -24,6 +24,13 @@ const throwErr = (arg) => { } module.exports = (Commands, Cypress, cy, state) => { + const isDynamicAliasingPossible = () => { + // dynamic aliasing is possible if cy.route2 is enabled and a route with dynamic interception has been defined + return Cypress.config('experimentalNetworkStubbing') && _.find(state('routes'), (route) => { + return _.isFunction(route.handler) + }) + } + const waitFunction = () => { $errUtils.throwErrByPath('wait.fn_deprecated') } @@ -111,7 +118,21 @@ module.exports = (Commands, Cypress, cy, state) => { specifier = _.last(allParts) } - const aliasObj = cy.getAlias(str, 'wait', log) + let aliasObj + + try { + aliasObj = cy.getAlias(str, 'wait', log) + } catch (err) { + // before cy.route2, we could know when an alias did/did not exist, because they + // were declared synchronously. with cy.route2, req.alias can be used to dynamically + // create aliases, so we cannot know at wait-time if an alias exists or not + if (!isDynamicAliasingPossible()) { + throw err + } + + // could be a dynamic alias + aliasObj = { alias: str.slice(1) } + } if (!aliasObj) { cy.aliasNotFoundFor(str, 'wait', log) @@ -146,7 +167,7 @@ module.exports = (Commands, Cypress, cy, state) => { log.set('referencesAlias', aliases) } - if (!['route', 'route2'].includes(command.get('name'))) { + if (command && !['route', 'route2'].includes(command.get('name'))) { $errUtils.throwErrByPath('wait.invalid_alias', { onFail: options._log, args: { alias }, diff --git a/packages/driver/src/cy/net-stubbing/events/index.ts b/packages/driver/src/cy/net-stubbing/events/index.ts index 9423eda8abd8..f0eb6a9ac9b0 100644 --- a/packages/driver/src/cy/net-stubbing/events/index.ts +++ b/packages/driver/src/cy/net-stubbing/events/index.ts @@ -56,6 +56,7 @@ export function registerEvents (Cypress: Cypress.Cypress) { Cypress.on('test:before:run', () => { // wipe out callbacks, requests, and routes when tests start state('routes', {}) + state('aliasedRequests', []) }) Cypress.on('net:event', (eventName, frame: NetEventFrames.BaseHttp) => { diff --git a/packages/driver/src/cy/net-stubbing/events/request-received.ts b/packages/driver/src/cy/net-stubbing/events/request-received.ts index 5b727fa98ceb..19ef1ef8edcf 100644 --- a/packages/driver/src/cy/net-stubbing/events/request-received.ts +++ b/packages/driver/src/cy/net-stubbing/events/request-received.ts @@ -53,6 +53,8 @@ export const onRequestReceived: HandlerFn = id: requestId, request: req, state: 'Received', + requestWaited: false, + responseWaited: false, } const continueFrame: Partial = { @@ -111,7 +113,7 @@ export const onRequestReceived: HandlerFn = destroy () { userReq.reply({ forceNetworkError: true, - }) + }) // TODO: this misnomer is a holdover from XHR, should be numRequests }, } @@ -200,6 +202,15 @@ export const onRequestReceived: HandlerFn = resolved = true }) .then(() => { + if (userReq.alias) { + Cypress.state('aliasedRequests').push({ + alias: userReq.alias, + request: request as Request, + }) + + delete userReq.alias + } + if (!replyCalled) { // handler function resolved without resolving request, pass on continueFrame.tryNextRoute = true diff --git a/packages/driver/src/cy/net-stubbing/wait-for-route.ts b/packages/driver/src/cy/net-stubbing/wait-for-route.ts index 19cb3495db56..7d129d2e2681 100644 --- a/packages/driver/src/cy/net-stubbing/wait-for-route.ts +++ b/packages/driver/src/cy/net-stubbing/wait-for-route.ts @@ -7,33 +7,31 @@ import { const RESPONSE_WAITED_STATES: RequestState[] = ['ResponseIntercepted', 'Complete'] -export function waitForRoute (alias: string, state: Cypress.State, specifier: 'request' | 'response' | string): Request | null { - // if they didn't specify what to wait on, they want to wait on a response - if (!specifier) { - specifier = 'response' +function getPredicateForSpecifier (specifier: string): Partial { + if (specifier === 'request') { + return { requestWaited: false } } - // 1. Get route with this alias. + // default to waiting on response + return { responseWaited: false } +} + +export function waitForRoute (alias: string, state: Cypress.State, specifier: 'request' | 'response' | string): Request | null { + // 1. Create an array of known requests that have this alias. + // Start with request-level (req.alias = '...') aliases that could be a match. + const candidateRequests = _.filter(state('aliasedRequests'), { alias }) + .map(({ request }) => request) + + // Now add route-level (cy.route2(...).as()) aliased requests. const route: Route = _.find(state('routes'), { alias }) - if (!route) { - // TODO: once XHR stubbing is removed, this should throw - return null + if (route) { + Array.prototype.push.apply(candidateRequests, _.values(route.requests)) } - // 2. Find the first request without responseWaited/requestWaited/with the correct index - let i = 0 - const request = _.find(route.requests, (request) => { - i++ - switch (specifier) { - case 'request': - return !request.requestWaited - case 'response': - return !request.responseWaited - default: - return i === Number(specifier) - } - }) + // 2. Find the first request without responseWaited/requestWaited + const predicate = getPredicateForSpecifier(specifier) + const request = _.find(candidateRequests, predicate) as Request | undefined if (!request) { return null diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index eee093b9b2de..2b439a8b1e7f 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -60,6 +60,7 @@ declare namespace Cypress { interface State { (k: '$autIframe', v?: JQuery): JQuery | undefined (k: 'routes', v?: RouteMap): RouteMap + (k: 'aliasedRequests', v?: AliasedRequest[]): AliasedRequest[] (k: 'document', v?: Document): Document (k: 'window', v?: Window): Window (k: string, v?: any): any @@ -72,3 +73,8 @@ declare namespace Cypress { document: Document } } + +type AliasedRequest = { + alias: string + request: any +} diff --git a/packages/net-stubbing/lib/external-types.ts b/packages/net-stubbing/lib/external-types.ts index 9c12de2465ae..74f625937204 100644 --- a/packages/net-stubbing/lib/external-types.ts +++ b/packages/net-stubbing/lib/external-types.ts @@ -111,6 +111,11 @@ export namespace CyHttpMessages { * not follow redirects before yielding the response (the 3xx redirect is yielded) */ followRedirect?: boolean + /** + * If set, `cy.wait` can be used to await the request/response cycle to complete for this + * request via `cy.wait('@alias')`. + */ + alias?: string } export interface IncomingHttpRequest extends IncomingRequest {