17
votes

In Cypress, it is well-documented that you can alias specific network requests, which you can then "wait" on. This is especially helpful if you want to do something in Cypress after a specific network request has fired and finished.

Example below from Cypress documentation:

cy.server()
cy.route('POST', '**/users').as('postUser') // ALIASING OCCURS HERE
cy.visit('/users')
cy.get('#first-name').type('Julius{enter}')
cy.wait('@postUser')

However, since I'm using GraphQL in my app, aliasing no longer becomes a straightforward affair. This is because all GraphQL queries share one endpoint /graphql.

Despite it not being possible to differentiate between different graphQL queries using the url endpoint alone, it is possible to differentiate graphQL queries using operationName (refer to following image).

enter image description here

Having dug through the documentation, there doesn't appear to be a way to alias graphQL endpoints using operationName from the request body. I'm also returning the operationName (yellow arrow) as a custom property in my response header; however, I haven't managed to find a way to use it to alias specific graphQL queries either.

FAILED METHOD 1: This method attempts to use the purple arrow shown in image.

cy.server();
cy.route({
    method: 'POST',
    url: '/graphql',
    onResponse(reqObj) {
        if (reqObj.request.body.operationName === 'editIpo') {
            cy.wrap('editIpo').as('graphqlEditIpo');
        }
    },
});
cy.wait('@graphqlEditIpo');

This method doesn't work since the graphqlEditIpo alias is registered at runtime and as such, the error I receive is as follows.

CypressError: cy.wait() could not find a registered alias for: '@graphqlEditIpo'. Available aliases are: 'ipoInitial, graphql'.

FAILED METHOD 2: This method attempts to use the yellow arrow shown in image.

cy.server();
cy.route({
    method: 'POST',
    url: '/graphql',
    headers: {
        'operation-name': 'editIpo',
    },
}).as('graphql');
cy.wait('graphql');

This method doesn't work because the headers property in the options object for cy.route is actually meant to accept response headers for stubbed routes per the docs. Here, I'm trying to use it to identify my specific graphQL query, which obviously won't work.

Which leads me to my question: How can I alias specific graphQL queries/mutations in Cypress? Have I missed something?

9
Is there a solution using the nock library as an alternative to cy.route(), as mentioned by Gleb Bahmutov here. Essentially, an intercepter of some sort would solve a lot of problems when testing network requests. - Richard Matsen
Cypress now has documentation on how to support graphql, including some utility functions: [link] docs.cypress.io/guides/testing-strategies/… - Darren G

9 Answers

8
votes

The intercept API introduced in 6.0.0 supports this via the request handler function. I used it in my code like so:

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

The docs have a similar example here: https://docs.cypress.io/api/commands/intercept.html#Aliasing-individual-requests.

5
votes

This works for me!

Cypress.Commands.add('waitForGraph', operationName => {
  const GRAPH_URL = '/api/v2/graph/';
  cy.route('POST', GRAPH_URL).as("graphqlRequest");
  //This will capture every request
  cy.wait('@graphqlRequest').then(({ request }) => {
    // If the captured request doesn't match the operation name of your query
    // it will wait again for the next one until it gets matched.
    if (request.body.operationName !== operationName) {
      return cy.waitForGraph(operationName)
    }
  })
})

Just remember to write your queries with unique names as posible, because the operation name relies on it.

4
votes

If 'waiting' and not 'aliasing' in itself is the main purpose, the easiest way to do this, as I've encountered thus far, is by aliasing the general graphql requests and then making a recursive function call to 'wait' targeting the newly created alias until you find the specific graphql operation you were looking for. e.g.

Cypress.Commands.add('waitFor', operationName => {
  cy.wait('@graphqlRequest').then(({ request }) => {
    if (request.body.operationName !== operationName) {
      return cy.waitFor(operationName)
    }
  })
})

This of course have its caveats and may or may not work in your context. But it works for us.

I hope Cypress enables this in a less hacky way in the future.

PS. I want to give credit to where I got the inspiration to this from, but it seemt to be lost in cyberspace.

4
votes

Since I was having the same issue and I did not find a real solution for this problem I combined different options and created a workaround that solves my problem. Hopefully this can help someone else too.

I do not really 'wait' for the request to be happen but I catch them all, based on **/graphql url and match the operationName in the request. On a match a function will be executed with the data as parameter. In this function the tests can be defined.

graphQLResponse.js

export const onGraphQLResponse = (resolvers, args) => {
    resolvers.forEach((n) => {
        const operationName = Object.keys(n).shift();
        const nextFn = n[operationName];

        if (args.request.body.operationName === operationName) {
            handleGraphQLResponse(nextFn)(args.response)(operationName);
        }
    });
};

const handleGraphQLResponse = (next) => {
    return (response) => {

        const responseBody = Cypress._.get(response, "body");

        return async (alias) => {
            await Cypress.Blob.blobToBase64String(responseBody)
                .then((blobResponse) => atob(blobResponse))
                .then((jsonString) => JSON.parse(jsonString))
                .then((jsonResponse) => {
                    Cypress.log({
                        name: "wait blob",
                        displayName: `Wait ${alias}`,
                        consoleProps: () => {
                            return jsonResponse.data;
                        }
                    }).end();

                    return jsonResponse.data;
                })
                .then((data) => {
                    next(data);
                });
        };
    };
};

In a test file

Bind an array with objects where the key is the operationName and the value is the resolve function.

import { onGraphQLResponse } from "./util/graphQLResponse";

describe("Foo and Bar", function() {
    it("Should be able to test GraphQL response data", () => {
        cy.server();

        cy.route({
            method: "POST",
            url: "**/graphql",
            onResponse: onGraphQLResponse.bind(null, [
                {"some operationName": testResponse},
                {"some other operationName": testOtherResponse}
            ])
        }).as("graphql");

        cy.visit("");

        function testResponse(result) {
            const foo = result.foo;
            expect(foo.label).to.equal("Foo label");
        }

        function testOtherResponse(result) {
            const bar = result.bar;
            expect(bar.label).to.equal("Bar label");
        }
    });
}

Credits

Used the blob command from glebbahmutov.com

3
votes

This is what you're looking for (New in Cypress 5.6.0):

cy.route2('POST', '/graphql', (req) => {
  if (req.body.includes('operationName')) {
    req.alias = 'gqlMutation'
  }
})

// assert that a matching request has been made
cy.wait('@gqlMutation')

Documentation: https://docs.cypress.io/api/commands/route2.html#Waiting-on-a-request

I hope that this helps!

2
votes

I used some of these code examples but had to change it slightly to add the onRequest param to the cy.route and also add the date.Now (could add any auto incrementer, open to other solutions on this) to allow multiple calls to the same GraphQL operation name in the same test. Thanks for pointing me in the right direction!

Cypress.Commands.add('waitForGraph', (operationName) => {
  const now = Date.now()
  let operationNameFromRequest
  cy.route({
    method: 'POST',
    url: '**graphql',
    onRequest: (xhr) => {
      operationNameFromRequest = xhr.request.body.operationName
    },
  }).as(`graphqlRequest${now}`)

  //This will capture every request
  cy.wait(`@graphqlRequest${now}`).then(({ xhr }) => {
    // If the captured request doesn't match the operation name of your query
    // it will wait again for the next one until it gets matched.
    if (operationNameFromRequest !== operationName) {
      return cy.waitForGraph(operationName)
    }
  })
})

to use:

cy.waitForGraph('QueryAllOrganizations').then((xhr) => { ...
1
votes

This is how I managed to differentiate each GraphQL request. We use cypress-cucumber-preprocessor so we have a common.js file in /cypress/integration/common/ where we can call a before and beforeEach hook which are called before any feature file.

I tried the solutions here, but couldn't come up with something stable since, in our application, many GraphQL requests are triggered at the same time for some actions.

I ended up storing every GraphQL requests in a global object called graphql_accumulator with a timestamp for each occurence.

It was then easier to manage individual request with cypress command should.

common.js:

beforeEach(() => {
  for (const query in graphql_accumulator) {
    delete graphql_accumulator[query];
  }

  cy.server();
  cy.route({
    method: 'POST',
    url: '**/graphql',
    onResponse(xhr) {
      const queryName = xhr.requestBody.get('query').trim().split(/[({ ]/)[1];
      if (!(queryName in graphql_accumulator)) graphql_accumulator[queryName] = [];
      graphql_accumulator[queryName].push({timeStamp: nowStamp('HHmmssSS'), data: xhr.responseBody.data})
    }
  });
});

I have to extract the queryName from the FormData since we don't have (yet) the key operationName in the request header, but this would be where you would use this key.

commands.js

Cypress.Commands.add('waitGraphQL', {prevSubject:false}, (queryName) => {
  Cypress.log({
    displayName: 'wait gql',
    consoleProps() {
      return {
        'graphQL Accumulator': graphql_accumulator
      }
    }
  });
  const timeMark = nowStamp('HHmmssSS');
  cy.wrap(graphql_accumulator, {log:false}).should('have.property', queryName)
    .and("satisfy", responses => responses.some(response => response['timeStamp'] >= timeMark));
});

It's also important to allow cypress to manage GraphQL requests by adding these settings in /cypress/support/index.js:

Cypress.on('window:before:load', win => {
  // unfilters incoming GraphQL requests in cypress so we can see them in the UI
  // and track them with cy.server; cy.route
  win.fetch = null;
  win.Blob = null; // Avoid Blob format for GraphQL responses
});

I use it like this:

cy.waitGraphQL('QueryChannelConfigs');
cy.get(button_edit_market).click();

cy.waitGraphQL will wait for the latest target request, the one that will be stored after the call.

Hope this helps.

1
votes
0
votes

Our use case involved multiple GraphQL calls on one page. We had to use a modified version of the responses from above:

Cypress.Commands.add('createGql', operation => {
    cy.route({
        method: 'POST',
        url: '**/graphql',
    }).as(operation);
});

Cypress.Commands.add('waitForGql', (operation, nextOperation) => {
    cy.wait(`@${operation}`).then(({ request }) => {
        if (request.body.operationName !== operation) {
            return cy.waitForGql(operation);
        }

        cy.route({
            method: 'POST',
            url: '**/graphql',
        }).as(nextOperation || 'gqlRequest');
    });
});

The issue is that ALL GraphQL requests share the same URL, so once you create a cy.route() for one GraphQL query, Cypress will match all the following GraphQL queries to that. After it matches, we set cy.route() to just a default label of gqlRequest or the next query.

Our test:

cy.get(someSelector)
  .should('be.visible')
  .type(someText)
  .createGql('gqlOperation1')
  .waitForGql('gqlOperation1', 'gqlOperation2') // Create next cy.route() for the next query, or it won't match
  .get(someSelector2)
  .should('be.visible')
  .click();

cy.waitForGql('gqlOperation2')
  .get(someSelector3)
  .should('be.visible')
  .click();