cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.66k stars 3.16k forks source link

Add option to cy.intercept() to match request body #8560

Open asumaran opened 3 years ago

asumaran commented 3 years ago

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"
flotwig commented 3 years ago

@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. :)

asumaran commented 3 years ago

Thank you @flotwig. I’m going to try your suggestions.

AnderssonChristian commented 3 years ago

I'm interested in this as well.

jandrade commented 3 years ago

+1 It would be great to add this feature for the same reasons commented here.

vrknetha commented 3 years ago

It would be a great feature to match GraphQl queries.

m4dc4p commented 3 years ago

@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 :(

hartzis commented 3 years ago

πŸ““ @bahmutov wrote a great post about graphQL requests and cy.route2 and matching request body's.

πŸ““ @m4dc4p Additionally there apparently is a new way to alias specific requests now too.

m4dc4p commented 3 years ago

πŸ““ @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 commented 3 years ago

@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.

mehrad77 commented 3 years ago

This link is dead: https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests

jennifer-shehane commented 3 years ago

@mehrad77 https://on.cypress.io/intercept#Aliasing-individual-requests

antonlegkiy commented 3 years ago

+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.

aturkewi commented 3 years ago

+1 here as well. This would be great to have.

ChapDDR commented 3 years ago

Another +1

locinus commented 3 years ago

+1

phattanun commented 2 years ago

+1

Raph-Capitale commented 2 years ago

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

ozthekoder commented 2 years ago

This is really needed +1

andi-dev commented 2 years ago

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')
  })
bahmutov commented 2 years ago

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')
johnmiroki commented 2 years ago

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.

tripflex commented 2 years ago

+1

mcollins2-quantium commented 2 years ago

+1

Grutula commented 1 year ago

This is really needed +1

amyzhao-seismic commented 1 year ago

+1

Baudry-G commented 1 year ago

This is really needed +1

ohirnyak commented 1 year ago

+1

lokriet commented 1 year ago

+1

ilibilibom commented 1 year ago

+1

shinsid commented 1 year ago

+1

coolduebtn commented 1 year ago

+1 . I am surprised that this is still open and the team has not implemented this yet

aturkewi commented 1 year ago

+1. Please, this would be really helpful to have.

arm22 commented 1 year ago

+1

DavitMkhitaryan commented 1 year ago

+1

ercgrat commented 1 year ago

+1

hamidjafari commented 1 year ago

+1

agrandin0 commented 7 months ago

+1

aowss commented 7 months ago

+1

simplyaboutcode commented 6 months ago

+1

GraceZaborski commented 6 months ago

+1

ahlivehr commented 6 months ago

+1

dlangsam commented 3 months ago

+1

rahilarparkar commented 2 months ago

+1

lawrensylvan commented 2 days ago

+1 This is really needed, i can't believe I have to intercept all calls of the same url with the same handler if their request body differ. I have to catch them all and then return a different response body depending on the received request body, all in the same function. I would rather be able to write separate intercept for that.