Cypress: Waiting for specific GraphQL queries - allow distinguishing by response type on the same route

Created on 8 Jan 2019  路  31Comments  路  Source: cypress-io/cypress

Current behavior:

It's a bit error prone to wait for specific GraphQL queries, if they run simultaneously or their order of execution is unknown or the response times cause race conditions, as they all run on the same endpoint, for example /graphql.

My current workaraound is a recursive function:

export function waitForResponseFromGraphQLServer(query) {
  return cy
    .wait("@graphqlResponse")
    .then(r => Cypress._.get(r, 'response.body'))
    .then(xhr => Cypress.Blob.blobToBinaryString(xhr))
    .then(respString => JSON.parse(respString).data)
    .then(response => {
      if (response[query]) {
       // response did match the  query to be waited for?
        return cy.wrap(response[query]);
      }
     // response was for some other query -> recursive invokation
      return waitForResponseFromGraphQLServer(query);
    });
}

This works _often_, but suffers from some flakyness, which could also be caused by https://github.com/cypress-io/cypress/issues/2700. Also i'm not 100% sure how this recursion works with Cypress.

Desired behavior:

A solution to define already on the route, _the content_ i'm waiting for, and keep waiting until i have found the specified content or the timeout is reached. This could be achieved with a validation callback option with the signature xhrResponse => Promise<boolean> | boolean. For example:

cy.route({
  method: 'DELETE',
  url: '**/user/*',
  waitForContent: xhr => isResponseForGraphQLQuery(xhr, query)
//...
proposal 馃挕 network feature

Most helpful comment

We are also experiencing the behavior @ikornienko was describing, where graphQL calls that are initiated at approximately the same time are grouped together and can't be waited on individually.

The workaround we're using is to append a query string with an empty parameter to the GraphQL endpoint based on the query/mutation name.

For example, if your GraphQL queries and mutation hit /graphql, you can do something like:

if (window.Cypress) {
   url = `/graphql?${operationName}`
}
// make the ajax call

e.g., if your query is named getFoo, the endpoint would look like /graphql?getFoo. Then you can route and wait on your operations independently, like:

    cy.route("POST", "**/graphql?getFoo").as("getFoo");
    // ...
    cy.wait("@getFoo");

instead of

    cy.route("POST", "**/graphql").as("graphql");
    // ...
    cy.wait("@graphql");

All 31 comments

Would the feature proposed in this issue help solve your problem? https://github.com/cypress-io/cypress/issues/521

Actually not. As far as i understand, this issue is about either stubbing a response or returning the real response depending on an certain conditions. It either stubs the response immediately, if these conditions are met (true) or otherwise waits for the real response. But basically it waits for one response.

My request is about waiting for a real response on a single route, until it satisfies certain conditions. Responses that arrive in the meantime (before these conditions are met), are treated like they were for another route, and are thus ignored on a wait. (It keeps waiting).

As i saw in https://github.com/cypress-io/cypress/issues/521#issuecomment-341501074, all this will be implemented as part of this large rewrite https://github.com/cypress-io/cypress/issues/687.

I'd be happy if this issue could be included there as related to it.

By the way. Thank you for this invaluable testing tool.

I have this issue too. Hope this can get resolved!

Being able to filter on the operation name would be pretty useful too.

I'm having a hard time figuring out how to achieve this. I use aws-amplify which seems to use XHR to get data from a Graphql server using the same endpoint. "/graphql"

I would like to intercept certain requests (depending on the request body) and respond with a fixture or plain object without hitting the server.

Same problem here, by now I need to use arbitraries wait.

Also having this issue.

I've been tracking should.exist & should.not.exist of a <loading\> element and that can tell the graphql query resolved

same problem here, would like to know what your workarounds are..

Any progress on this issue? Or another place to track this? Also needing to work with real GraphQL requests to the same endpoint, not stubbed.

Hey @jennifer-shehane - the functionality described in #521 would actually not cover this.

What would be really useful is to be able to distinguish a request based on its POST body or response body.

One of the love-it/hate-it things of graphql is that all requests go over the same URL and use the same HTTP port (typically POST /graphql). Then the body of the request contains a graphql-query which tells the server what data it needs to return.

This means using the typical cy.route('POST', '/graphql).as('someApiCall') is not great because there is no way to identify what the call is only based on its URL.

For reason reason (as @NicholasBoll @nickluger is proposing), giving the ability to inspect the network response a way to match a request would be the ideal solution.

Oops. Probably a mistaken mention. The last place I worked had a similar issue because we used websockets for a lot of request/response communication. We solved it by making a custom request/wait pair using the built-in Cypress commands as a baseline.

It worked well, but unfortunately I don't have access to the source code.

I remember the implementation had an internal mapping of requests and spied on the websockets APIs to match requests. The code wasn't sharable because we had our own protocol based on socket.io. It was doable, but not trivial. GraphQL is defined enough to come up with a generalized plugin.

I had the same issue and solved it in a "messy but working" kind of way, this is my solution :

in a javascript file add this function

example : utils/networking.js

export function waitForGQL(queryName, onQueryFoundFn) {
  function waitOnce() {
    cy.wait('@gql-call').then(xhr => {
      if (xhr.requestBody && xhr.requestBody.query.includes(queryName)) {
        if (onQueryFoundFn) onQueryFoundFn(xhr);
      } else {
        waitOnce();
      }
    });
  }

  waitOnce();
}

In the test file :

import { waitForGQL } from '../utils/networking';

// start by listening on the api endpoint

cy.server();
cy.route('POST', '/api/graphql').as('gql-call');

// ...  some test code here

// then when you want to wait for a GraphQL query you do this : 

waitForGQL('myQuery', xhr => {
    const someValue= xhr.responseBody.data.myQuery.someValue;

   expect(someValue).to.be.above(0);
  });

@bmarwane, thank you for your solution. I want to say that the solution doesn't always work for us. We have four different GraphQL queries on the page that we want to test and let's say we are trying to wait for the third query. It seems cy.wait() gets stuck sometimes (it causes timeout issue).

We are trying to look at operationName instead of query:

export function waitForGQL(operationName, onQueryFoundFn) {
  function waitOnce() {
    cy.wait('@gql-call').then(xhr => {
      // then block doesn't always work
      if (xhr.requestBody && xhr.requestBody.operationName === operationName) {
        if (onQueryFoundFn) onQueryFoundFn(xhr)
      } else {
        waitOnce()
      }
    })
  }

  waitOnce()
}

and the usage:

waitForGQL('companies', xhr => console.log(xhr))

cypress v4.2.0

@bmarwane Perfect, this works for me for now.

It would be nice to have a built-in solution though.

EDIT: Actually this is not that robust in my case. I have 5 other graphql calls firing before the one I am waiting for. If one of those fails, the test doesn't complete.

@umitkucuk the reason why it doesn't work AFAIU is because if you have two requests happening simultaneously to the same URL, Cypress is grouping them sometimes (you can see they're literally grouped in the event log when using Cypress UI).

What it means for cy.wait('@alias'): in this case it'll be triggered only with the first request in this group and all others will be swallowed. GraphQL requests are grouped quite often (same URL, quite often happen simultaneously on page load), so if you're out of luck, your cy.wait will never see some of them, and among them could be the GraphQL operation you're waiting for.

I tried this way, but it's also working not stable

cypress/support/commands.js

Cypress.Commands.add('waitForQuery', (operationName, checkStatus = true) => {
    Cypress.log({
        name: 'api request',
        displayName: 'Wait for API request',
    });

    cy
        .wait('@apiRequest')
        .then(async ({ response, request }) => {
            if (request.body.operationName !== operationName) {
                return cy.waitForQuery(operationName);
            }

            const bodyText = await response.body.text();
            const json = JSON.parse(bodyText);

            Cypress.log({
                name: 'api response',
                displayName: 'API response',
                message: operationName,
                consoleProps: () => ({ Response: json.data }),
            });

            if (checkStatus) {
                expect(json.errors, 'API response without errors').not.exist;
            }

            return {
                ...response,
                json: json.data,
            };
        });
});

In a test file

cy.waitForQuery('savePreview');

Hello I have the same p

@bmarwane, thank you for your solution. I want to say that the solution doesn't always work for us. We have four different GraphQL queries on the page that we want to test and let's say we are trying to wait for the third query. It seems cy.wait() gets stuck sometimes (it causes timeout issue).

We are trying to look at operationName instead of query:

export function waitForGQL(operationName, onQueryFoundFn) {
  function waitOnce() {
    cy.wait('@gql-call').then(xhr => {
      // then block doesn't always work
      if (xhr.requestBody && xhr.requestBody.operationName === operationName) {
        if (onQueryFoundFn) onQueryFoundFn(xhr)
      } else {
        waitOnce()
      }
    })
  }

  waitOnce()
}

and the usage:

waitForGQL('companies', xhr => console.log(xhr))

cypress v4.2.0

I've tried to reply your suggestion but I have the same problem, this aproach not always work
Are there any alternative ?
Thanks in advance

We are also experiencing the behavior @ikornienko was describing, where graphQL calls that are initiated at approximately the same time are grouped together and can't be waited on individually.

The workaround we're using is to append a query string with an empty parameter to the GraphQL endpoint based on the query/mutation name.

For example, if your GraphQL queries and mutation hit /graphql, you can do something like:

if (window.Cypress) {
   url = `/graphql?${operationName}`
}
// make the ajax call

e.g., if your query is named getFoo, the endpoint would look like /graphql?getFoo. Then you can route and wait on your operations independently, like:

    cy.route("POST", "**/graphql?getFoo").as("getFoo");
    // ...
    cy.wait("@getFoo");

instead of

    cy.route("POST", "**/graphql").as("graphql");
    // ...
    cy.wait("@graphql");

We have the same problems. @bahmutov @jennifer-shehane Is there a plan when support for grapQL can be implemented?

Followed @Jamescan approach. When using Apollo you can edit the link setup and have the operationName param.

link: concat(authMiddleware, httpLink.create({
            uri: ({operationName}) => window.Cypress ?
                    `${environmentProd.apiUrl}/graphql?${operationName}` :
                    `${environmentProd.apiUrl}/graphql`
        }))

If your app can be made test-aware (as it should - in the elusive "app actions" fashion) then perhaps it is enough to have the app register its responses with some well-known custom event on the window, and wait for them using e.g. cypress-wait-until:

(in app's response handler)

if(window.Cypress) { 
  const evt = new Event(`graphql-response:${req.operationName}`)
  evt.json = res.json
  window.dispatchEvent(evt)

(in test)

cy.waitUntil(
  () => new Promise((resolve, reject)=> { window.addEventListener('graphql-response:getUser', function(e){ resolve(e) })})
)

_note: untested, just off the top of my head 馃槄_

N.b. I was able to solve a very similar case recently, by using cy.route's onResponse: (xhr) => { ... } option: in this callback we have access to _both_ xhr.request and xhr.response, so at that stage we can save a map of _"endpoint calls per operationName"_.

Hi @cbrunnkvist Can you pls post your solution using onResponse

I've tried the solutions purposed but none worked for me, cypress doesn't identify the request and a timeout occurs...

image

Is there some other solution?

Using cypress version 4.7.0

@danisal I figured out following way:

Step 1:
This works with Cypress 4.9.0 and above
Set "experimentalFetchPolyfill": true in cypress.json file

Step 2: Intercept fetch request and retrieve operation name. Call the original fetch by appending the operation name to the original url

Cypress.Commands.add("mockGraphQL", () => {
  cy.on("window:before:load", (win) => {
    const originalFetch = win.fetch;
    const fetch = (path, options, ...rest) => {
      if (options && options.body) {
        try {
          const body = JSON.parse(options.body);
          if (body.operationName) {
            return originalFetch(`${path}?operation=${body.operationName}`, options, ...rest);
          }
        } catch (e) {
          return originalFetch(path, options, ...rest);
        }
      }
      return originalFetch(path, options, ...rest);
    };
    cy.stub(win, "fetch", fetch);
  });
});

Step 3: Create a route with query as "/graphql?operation=${operationName}"
`cy.route("POST",/graphql?operation=${operationName}).as("gql-call");`

Hello, Somebody use cy.route2() to intercept graphql queries??
thanks in advance

FWIW, I was interested in waiting for a specific operation to complete.

This is my solution on v5.3.0 with exprimentalFetchPolyfill: true:

Cypress.Commands.add('waitGraphql', (operationName: string) => {
  cy.route({
    method: 'POST',
    url: '**/graphql',
    onResponse: ({ request }) => {
      if (request.body.operationName === operationName) {
        cy.emit(operationName)
      }
    },
  })

  cy.waitFor(operationName)
})

@jennifer-shehane when we can expect this to be closed with proper support to wait for graphql. We need the same support how we have for a rest api.

I worked around it by extending on @umitkucuk 's answer:

commands.js:

Cypress.Commands.add("waitForGQL", (operationName, routeAlias, onQueryFoundFn) => {
  function waitOnce() {
    cy.wait(`@${routeAlias}`).then((xhr) => {
      if (
        xhr.requestBody.query &&
        xhr.requestBody.query.toLowerCase().includes(operationName.toLowerCase())
      ) {
        if (onQueryFoundFn) onQueryFoundFn(xhr);
      } else {
        console.log("Checking again");
        waitOnce();
      }
    });
  }

  waitOnce();
});

your.spec.js

      cy.server();
      cy.route("POST", "/graphql").as("gql-call");

      ... your logic ..

      cy.findByTestId("formSubmitButton").click({ force: true });
      cy.waitForGQL("createSupplier", "gql-call", (xhr) => console.log(xhr));

The last line can also be cy.waitForGQL("createSupplier", "gql-call"); if you don't want to execute any further logic on the response.

Any update on this on version 6? Has anyone managed to use cy.intercept() for graphql?

The solution that i shared last time was a bit buggy, so i tried something more robust based on some answers in this thread, i settled on this solution for the moment :

in commands.js :

Cypress.Commands.add('startObservingGqlQueries', () => {
  cy.server().route({
    method: 'POST',
    url: '**/graphql',
    onResponse: ({ request, response }) => {
      window.Cypress.emit('gql', { request, response });
    },
  });
});

Cypress.Commands.add('waitForGQL', queryName => {
  return new Cypress.Promise(resolve => {
    cy.on('gql', ({ request, response }) => {
      if (request.body && request.body.query && request.body.query.includes(queryName)) {
        resolve(response);
      }
    });
  });
});

in my test file i do this :

// first line in my test file 
cy.startObservingGqlQueries();

// ...  some test code here

// then when you want to wait for a GraphQL query you do this : 

// wait inside a cy.wrap so tell cypress to wait for the response to be resolved before going to the next step

  cy.wrap(null).then(() => {
    return cy.waitForGQL('someQueryName').then(response => {
      expect(response.someValue).to.be.equal(1);
    });
  });

It's not a perfect solution, but i hope you find it helpful.

As @leilafi mentioned, the intercept API introduced in 6.0.0 supports this:

cy.intercept('POST', '/graphql', req => {
  if (req.body.operationName === 'queryName') {
    req.alias = 'queryName';
  } else if (req.body.operationName === 'mutationName') {
    req.alias = 'mutationName';
  } else if (...) {
    ...
  }
});

Where queryName and mutationName are the names of your GQL operations. You can add an additional condition for each request that you would like to alias. You can then wait for them like so:

// Wait on single request
cy.wait('@mutationName');

// Wait on multiple requests. 
// Useful if several requests are fired at once, for example on page load. 
cy.wait(['@queryName, @mutationName',...]);
Was this page helpful?
0 / 5 - 0 ratings