Cypress: Allow dynamically matching requests with a function

Created on 23 Jan 2017  路  14Comments  路  Source: cypress-io/cypress

To allow highly flexible matching of which requests you want to stub, we could add the option to accept a function instead of a fixed method/URL. This would recieve the request object (or some wrapper around it). The motivation is that I have and SPA that uses a GraphQL backend where all requests have the same URL and I want to filter them by the query name which is sent as a parameter in the POST body.

cy.route({
  matcher: (request) => !! request.body.match(//)
})
4锔忊儯 pkdriver network feature

Most helpful comment

Thank you for the update @jennifer-shehane

I've been able to find a work-around that provides me with the required functionality. I feel it's a bit hacky but it works for now.

My support/index.js contains:

beforeEach(() => {
  cy
    .server()
    .route({
      method: 'POST',
      url: '/api/graphql' <---- this is our graphQL endpoint
    })
    .as('graphqlRequest');
});

And the following in my support/commands.js

Cypress.Commands.add('waitForQuery', operationName => {
  cy.wait('@graphqlRequest').then(({ request }) => {
    if (request.body.operationName !== operationName) {
      return cy.waitForQuery(operationName); <---- this is the hacky bit
    }
  });
});

The above can be used as follows:

      it('Should Foo Bar', () => {
        cy
          .waitForQuery('fooQuery')

          .get('[data-cy=bar]')
          .should('exist')

           // can also be used to check XHR payloads
          .waitForQuery('fooQuery')
          .its('request')
          .then(({ body: { variables: { search: { foo } } } }) => expect(foo).to.equal(1))
       });

HTH anybody with the same requirements.

All 14 comments

I have the same use case (using GraphQL). At the moment, rather than being able to wait on requests, having to hardcode timeouts wherever there are requests that can take a couple seconds. Being able to match on operation name and waiting on it to return would let us write tests in a way more similar to how the Cypress docs recommend.

Hey @davidnorth - I'm researching for solutions for the same issue you reported. I'd be extremely grateful if you could share what solutions, if any, you have used to enable defining multiple graphQL routes in a test and waiting for specific ones to complete.

Were you able to do this in Cypress (or any other e2e testing framework)?

Hey @jennifer-shehane - any progress or rough ETAs on this feature 馃檹 ?

We are increasingly working on applications that utilise graphQL and Cypress' lack of isolating specific requests to wait via cy.server().route() on is becoming a blocker for us, especially for applications that exclusively use graphQL.

+1 here
We use the same URL and HTTP methods for performing different tasks (unfortunately that's how Parse is designed). So it'd be useful to be able to customize routes matching with more granularity than HTTP verb and URL.

@nazar There has been no work currently done on this feature.

Thank you for the update @jennifer-shehane

I've been able to find a work-around that provides me with the required functionality. I feel it's a bit hacky but it works for now.

My support/index.js contains:

beforeEach(() => {
  cy
    .server()
    .route({
      method: 'POST',
      url: '/api/graphql' <---- this is our graphQL endpoint
    })
    .as('graphqlRequest');
});

And the following in my support/commands.js

Cypress.Commands.add('waitForQuery', operationName => {
  cy.wait('@graphqlRequest').then(({ request }) => {
    if (request.body.operationName !== operationName) {
      return cy.waitForQuery(operationName); <---- this is the hacky bit
    }
  });
});

The above can be used as follows:

      it('Should Foo Bar', () => {
        cy
          .waitForQuery('fooQuery')

          .get('[data-cy=bar]')
          .should('exist')

           // can also be used to check XHR payloads
          .waitForQuery('fooQuery')
          .its('request')
          .then(({ body: { variables: { search: { foo } } } }) => expect(foo).to.equal(1))
       });

HTH anybody with the same requirements.

I tried to solve the issue a different way where not to actually wait for the request to happen, but to execute a function that contains the tests when the request got a response. stackoverflow The reason I want to solve it this way is because I also think the solution that nazar is using is a bit hacky.

Now the only issue I get is that the test passes while one of the expects fails (see screenshot). I think this is because route onResponse does not trigger the error like I expected. There are no console errors and also the graphql request get server response 200.

Is there a wait to tell Cypress that the test should fail?

test-passes-assert-fails

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);
                }).catch((error) => {
                    return error;
                });
        };
    };
};

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

Opened a PR for this feature request after spending way to much time trying to hack it in in userland. This should be supported out of the box IMO.

https://github.com/cypress-io/cypress/pull/3984

I'm using this approach at the moment:

const stubFetch = (win, routes) => {
  const fetch = win.fetch;
  cy.stub(win, 'fetch').callsFake((...args) => {
    const routeIndex = routes.findIndex(r => matchRoute(r, args));
    if (routeIndex >= 0) {
      const route = routes.splice(routeIndex, 1)[0];
      const response = {
        status: route.status,
        headers: new Headers(route.headers),
        text: () => Promise.resolve(route.response),
        json: () => Promise.resolve(JSON.parse(route.response)),
        ok: route.status >= 200 && route.status <= 299,
      };
      return Promise.resolve(response);
    } else {
      console.log('No route match for:', args[0]);
      return fetch(...args);
    }
  });
};

Cypress.Commands.add('stubFetch', ({ fixture }) => {
  return cy.fixture(fixture, { log: false }).then(routes => {
    cy.on('window:before:load', win => stubFetch(win, routes));
  });
});

matchRoute is a custom implementation and fixture is a json file with an array of the the routes to mock:

const escapeRegExp = string => {
  return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
};

const matchRoute = (route, fetchArgs) => {
  const url = fetchArgs[0];
  const options = fetchArgs[1];

  const method = options && options.method ? options.method : 'GET';
  const body = options && options.body;

  // use pattern matching of the timestamp parameter
  const urlRegex = escapeRegExp(route.url);

  if (method === route.method && decodeURIComponent(url).match(new RegExp(urlRegex))) {
    if (body && route.body) {
      // some custom logic to match the bodies     
    } else {
      return route;
    }
  }
};

I've run into the same issue and have created a command that supports 2 different methods which should be a good jumping off point for those that need it.

for those who got the same question, the official doc has provide a solution:

https://docs.cypress.io/api/commands/route.html#Options

you can use

onResponse: (xhr) => {
    // do something with the
    // raw XHR object when the
    // response comes back
  }

to change the xhr.response, an example willl looks like this:

onResponse: xhr => {
        const index = xhr.request.body.targetUrl.replace(
          /abc(\d+).tree/i,
          (match, number) => number
        );

        if (index) {
          xhr.response.body = mockDataList[index];
        }
      }

@thinkerelwin How can you use onResponse to wait for a response body that satisfies some application specific criteria? How do you prevent cy.wait('@namedRoute') from completing on "just any" request that matches the location and method declared in cy.route?

Once #8974 is merged, you will be able to dynamically alias cy.route2 requests from the function callback:

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

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

The code for this is done in cypress-io/cypress#8974, but has yet to be released.
We'll update this issue and reference the changelog when it's released.

Released in 5.6.0.

This comment thread has been locked. If you are still experiencing this issue after upgrading to
Cypress v5.6.0, please open a new issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Hipska picture Hipska  路  83Comments

RandallKent picture RandallKent  路  83Comments

chrisbreiding picture chrisbreiding  路  114Comments

bahmutov picture bahmutov  路  69Comments

terinjokes picture terinjokes  路  119Comments