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

Add response alias support (res.alias) #3083

Closed
nickluger opened this issue Jan 8, 2019 · 35 comments
Closed

Add response alias support (res.alias) #3083

nickluger opened this issue Jan 8, 2019 · 35 comments
Labels
stage: proposal 💡 No work has been done of this issue topic: cy.intercept() topic: network type: feature New feature that does not currently exist

Comments

@nickluger
Copy link

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 #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)
//...
@jennifer-shehane
Copy link
Member

Would the feature proposed in this issue help solve your problem? #521

@jennifer-shehane jennifer-shehane added the stage: awaiting response Potential fix was proposed; awaiting response label Jan 8, 2019
@nickluger
Copy link
Author

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 #521 (comment), all this will be implemented as part of this large rewrite #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.

@jennifer-shehane jennifer-shehane added stage: proposal 💡 No work has been done of this issue type: feature New feature that does not currently exist and removed stage: awaiting response Potential fix was proposed; awaiting response labels Jan 9, 2019
@macalinao
Copy link

macalinao commented Apr 7, 2019

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

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

@asumaran
Copy link

asumaran commented Apr 24, 2019

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.

@devniel
Copy link

devniel commented Jun 9, 2019

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

@omerose
Copy link

omerose commented Jun 13, 2019

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

@Shayg91
Copy link

Shayg91 commented Jul 4, 2019

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

@vaughnwalters
Copy link

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.

@morficus
Copy link

morficus commented Nov 2, 2019

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.

@NicholasBoll
Copy link
Contributor

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.

@bmarwane
Copy link

bmarwane commented Feb 18, 2020

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);
  });

@umitkucuk
Copy link

umitkucuk commented Mar 17, 2020

@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

@slinden2
Copy link

slinden2 commented Mar 24, 2020

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

@ikornienko
Copy link

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

@SchDen
Copy link

SchDen commented Apr 21, 2020

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');

@agugut-nexgen
Copy link

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

@Jamescan
Copy link
Contributor

Jamescan commented May 20, 2020

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");

@Donaab
Copy link

Donaab commented Jun 1, 2020

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

@rodrigosanchezg8
Copy link

rodrigosanchezg8 commented Jun 2, 2020

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`
        }))

@cbrunnkvist
Copy link

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 😅

@cbrunnkvist
Copy link

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

@prashantabellad
Copy link

Hi @cbrunnkvist Can you pls post your solution using onResponse

@danisal
Copy link

danisal commented Jul 24, 2020

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

@prashantabellad
Copy link

prashantabellad commented Jul 24, 2020

@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")`;`

@agugut-nexgen
Copy link

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

@hboylan
Copy link

hboylan commented Oct 1, 2020

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)
})

@vrknetha
Copy link

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

@Nxtra
Copy link

Nxtra commented Nov 3, 2020

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.

@leilafi
Copy link

leilafi commented Nov 24, 2020

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

@bmarwane
Copy link

bmarwane commented Nov 24, 2020

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.

@travislloyd
Copy link

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',...]);

@hboylan
Copy link

hboylan commented Dec 9, 2020

@travislloyd Unfortunately, this didn't quite work for me. I think the cy.wait() passes before the response is resolved. To handle it, we can wait for the response.

Cypress.Commands.add('waitGraphql', (operationName: string) => {
  cy.intercept('POST', '/graphql', req => {
    if (req.body.operationName === operationName) {
      req.reply(() => {
        cy.emit(operationName)
      })
    }
  })

  cy.waitFor(operationName)
})

@Nxtra
Copy link

Nxtra commented Jan 22, 2021

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',...]);

Nice, how would you configure this in a cypress command? That would help out a lot!

@TomasBereczSDE
Copy link

Context

Performing verification on a component that contains a search bar. Entering two character into the search bar starts to send graphQL requests. Verification of filtered components starts once the final graphQL query is resolved.

Example

Test is entering the Mark Hamill string via cy.type(). After the entry of the sixth character, the request payload looks like this:

{
  operationName: "graphQLRouteName",
  query: "graphQLQuery",
  variables: {
    first: 15,
    archived: false,
    userFullName: "Mark H"
  }
}

Objective

Wait until the last character is entered via cy.intercept() and cy.wait().

Solution (tested with Cypress 6.6.0)

Create a specific alias via intercept targeting the final string:

  const routeToIntercept = "graphQLRouteName";
  const userToFilter = 'Mark Hamil';
  cy.intercept(`**/${routeToIntercept}`, (request) => {
    if (request.body.variables.userFullName === userToFilter) {
      request.alias = 'finalRequest';
    }
  });

Perform data entry:

  cy.get('#search').clear().type(userToFilter);

Intelligently wait until the last request is resolved:

  cy.wait('@finalRequest');

At this point the last character of the search string is entered and the response for the full string is received.

Happy testing. 🧡💛🧡

@flotwig flotwig changed the title Waiting for specific GraphQL queries - allow distinguishing by response type on the same route Add response alias support (res.alias) May 6, 2021
@jennifer-shehane
Copy link
Member

Closing as resolved as this is now possible using cy.intercept.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stage: proposal 💡 No work has been done of this issue topic: cy.intercept() topic: network type: feature New feature that does not currently exist
Projects
None yet
Development

No branches or pull requests