cypress
cypress copied to clipboard
Add option to cy.intercept() to match request body
I was trying to cy.wait()
some specific GraphQL requests that were made using fetch
and using cy.route2()
but I don't see an option to add a matcher for the request body. According to the type definition there's not body
param
The closest thing is query
but it doesn't consider the request body. Is this by design or there are plans to add it?
Current behavior:
All GraphQL queries go to the same endpoint "server.com/graphql" using the POST
method. The query itself is sent in the request payload and it's a JSON-like string as follows:
{
"query": "query ($limit: Int) {
searchShippingTemplate(limit: $limit) {
shippingTemplates {
id
}
}
}",
"variables": {
"limit": 25
}
}
My plan was add a match for searchShippingTemplate
like:
cy.route2({
url: "http://localhost:3000/graphql",
method: "POST",
body: "*searchShippingTemplate*",
}).as("getQuery");
But there's no way to do that.
Desired behavior:
Add body
option to RouteMatcherOptions
in order to match a string in the request body.
It would be even better to be able to pass a function (for debugging purposes) since there's no easy way to know what exactly is .route2()
making the comparison against for each RouteMatcherOptions
option.
cy.route2({
url: "http://localhost:3000/graphql",
method: "POST",
body: (bodyString) => {
console.log(bodyString); // In theory we could even mutate the request
return bodyString;
},
}).as("getQuery");
Test code to reproduce
This is not a bug and more like a feature request.
Versions
"cypress": "^5.1.0"
@asumaran yes, this would be nice to add, it is currently not implemented but as you say it would help with some implementations.
You can modify the request body and response body already.
// modify an outgoing request
cy.route2({
url: "http://localhost:3000/graphql",
method: "POST"
}, (req) => {
console.log(req.body)
req.body.replace('foo, 'bar') // modifies outgoing request
})
You can also use a deferred promise to .wait
on a request that you've "matched" via the request handler.
// wait on a dynamically-matched request using a deferred Promise
const p = Cypress.Promise.defer()
cy.route2({
url: "http://localhost:3000/graphql",
method: "POST"
}, (req) => {
if (req.body.includes('searchShippingTemplate')) {
p.resolve() // resolve the deferred promise
}
})
// ... do some more Cypress stuff here ...
// now, wait on that deferred Promise to resolve:
cy.wrap(p)
I hope this helps in the meantime while we add a more formal API around this. :)
Thank you @flotwig. Iām going to try your suggestions.
I'm interested in this as well.
+1 It would be great to add this feature for the same reasons commented here.
It would be a great feature to match GraphQl queries.
@flotwig given Promise.defer()
is deprecated, is there any way to do this with aliases? Does returning a promise that is resolved/rejected based on the request work?
cy.route2({
url: "http://localhost:3000/graphql",
method: "POST"
}, (req) => {
return new Promise((resolve, reject) => {
if (req.body.includes('searchShippingTemplate')) {
resolve() // resolve the deferred promise
} else {
reject()
}
})}).as(`searchShipping`)
cy.wait(`@searchShipping`)
(edit)
To answer my own question - no :(
š @bahmutov wrote a great post about graphQL requests and cy.route2
and matching request body's.
- https://glebbahmutov.com/blog/smart-graphql-stubbing/
š @m4dc4p Additionally there apparently is a new way to alias specific requests now too.
- https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests
cy.route2('POST', '/graphql', (req) => {
if (req.body.includes('mutation')) {
req.alias = 'gqlMutation'
}
})
// assert that a matching request has been made
cy.wait('@gqlMutation')
š @bahmutov wrote a great post about graphQL requests and
cy.route2
and matching request body's.* https://glebbahmutov.com/blog/smart-graphql-stubbing/
I've read it. Unfortunately, it does not cover matching specific requests ...
š @m4dc4p Additionally there apparently is a new way to alias specific requests now too.
* https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests
š¤Æ - that looks like what I need. Thank you!
@flotwig given
Promise.defer()
is deprecated, is there any way to do this with aliases? Does returning a promise that is resolved/rejected based on the request work?
@m4dc4p bah, I wish they hadn't stuck that deprecated warning on defer
, defer
is totally fine to use in situations like this imo.
You can create your own deferred promise:
function deferredPromise() {
let resolve, reject
const promise = new Cypress.Promise((_resolve, reject) => {
resolve = _resolve
reject = _reject
})
return { resolve, reject, promise }
}
Works the same as Promise.defer
.
https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests also works well, especially if you need to match on dynamic criteria.
This link is dead: https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests
@mehrad77 https://on.cypress.io/intercept#Aliasing-individual-requests
+1 I have a similar situation when POST requests query params are the same, but actions are located in the payload, so it would be nice to catch only requests with action that you're waiting for.
+1 here as well. This would be great to have.
Another +1
+1
+1
I went through the very well-crafted doc many times and couldn't believe this wasn't a feature yet.
+1+1+1+1+1+1+1
This is really needed +1
Maybe the easiest way to support this and potentially many other use cases, would be if intercept
would optionally take a function as the first parameter, and expect this function to return a boolean. (true = request matched, false = request did not match).
I am thinking of something like this:
cy.intercept((req) => {
req.method === 'POST' && req.url.match('some-url-fragment') && req.body.match('someParam')
})
I think it would be nice to do what the request header matching does - you just need one of the headers to match. I often use it to stub GraphQL resources using the custom "X-operation-name" header
from https://github.com/bahmutov/todo-graphql-example/blob/master/cypress/integration/intercept-spec.js
// we have special middleware in our GraphQL client
// that puts the operation name in the request header "x-gql-operation-name"
// we can define intercepts using this custom header
cy.intercept({
method: 'POST',
url: '/',
headers: {
'x-gql-operation-name': 'allTodos',
},
}).as('allTodos')
cy.intercept({
method: 'POST',
url: '/',
headers: {
'x-gql-operation-name': 'AddTodo',
},
}).as('addTodo')
cy.intercept({
method: 'POST',
url: '/',
headers: {
'x-gql-operation-name': 'updateTodo',
},
}).as('updateTodo')
We cannot modify the application code like this, so I would love to be able to match by a part of the request body. In GraphQL requests I have operationName
field for example and I would love to use it. Maybe something like bodyPart
which could be a nested object; if the request body includes it, then the matcher fires?
cy.intercept({
method: 'POST',
url: '/',
bodyPart: {
'operationName': 'allTodos',
},
}).as('allTodos')
Maybe the easiest way to support this and potentially many other use cases, would be if
intercept
would optionally take a function as the first parameter, and expect this function to return a boolean. (true = request matched, false = request did not match).I am thinking of something like this:
cy.intercept((req) => { req.method === 'POST' && req.url.match('some-url-fragment') && req.body.match('someParam') })
This would be the simplest yet most versatile solution. We have $batch request for odata where the endpoint stays the same while the request path is specified in the request body (or request body part if you will). If the function matcher were to be supported, we could write our own requestMatcher with ease.
+1
+1
This is really needed +1
+1
This is really needed +1
+1
+1
+1
+1