Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(route2): dynamic request aliasing #8974

Merged
merged 4 commits into from
Nov 3, 2020
Merged
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
13 changes: 0 additions & 13 deletions packages/driver/cypress/integration/commands/aliasing_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,19 +497,6 @@ describe('src/cy/commands/aliasing', () => {
.get('input:first').as('firstInput')
.get('@lastDiv')
})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this test was duplicated b/t waiting_spec and aliasing_spec

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(() => {})
})
})
})
})
99 changes: 57 additions & 42 deletions packages/driver/cypress/integration/commands/net_stubbing_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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 })
})
})
})
})
25 changes: 23 additions & 2 deletions packages/driver/src/cy/commands/waiting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 },
Expand Down
1 change: 1 addition & 0 deletions packages/driver/src/cy/net-stubbing/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
13 changes: 12 additions & 1 deletion packages/driver/src/cy/net-stubbing/events/request-received.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export const onRequestReceived: HandlerFn<NetEventFrames.HttpRequestReceived> =
id: requestId,
request: req,
state: 'Received',
requestWaited: false,
responseWaited: false,
}

const continueFrame: Partial<NetEventFrames.HttpRequestContinue> = {
Expand Down Expand Up @@ -111,7 +113,7 @@ export const onRequestReceived: HandlerFn<NetEventFrames.HttpRequestReceived> =
destroy () {
userReq.reply({
forceNetworkError: true,
})
}) // TODO: this misnomer is a holdover from XHR, should be numRequests
},
}

Expand Down Expand Up @@ -200,6 +202,15 @@ export const onRequestReceived: HandlerFn<NetEventFrames.HttpRequestReceived> =
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
Expand Down
40 changes: 19 additions & 21 deletions packages/driver/src/cy/net-stubbing/wait-for-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Request> {
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
Expand Down
6 changes: 6 additions & 0 deletions packages/driver/types/internal-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ declare namespace Cypress {
interface State {
(k: '$autIframe', v?: JQuery<HTMLIFrameElement>): JQuery<HTMLIFrameElement> | 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
Expand All @@ -72,3 +73,8 @@ declare namespace Cypress {
document: Document
}
}

type AliasedRequest = {
alias: string
request: any
}
5 changes: 5 additions & 0 deletions packages/net-stubbing/lib/external-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down