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

Allow dynamically matching requests with a function #387

Closed
davidnorth opened this issue Jan 23, 2017 · 14 comments · Fixed by #8974
Closed

Allow dynamically matching requests with a function #387

davidnorth opened this issue Jan 23, 2017 · 14 comments · Fixed by #8974
Assignees
Labels
pkg/driver This is due to an issue in the packages/driver directory topic: network type: feature New feature that does not currently exist

Comments

@davidnorth
Copy link

davidnorth commented Jan 23, 2017

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(//)
})
@brian-mann brian-mann added type: question type: feature New feature that does not currently exist and removed type: question labels Jan 23, 2017
@jennifer-shehane jennifer-shehane added the stage: ready for work The issue is reproducible and in scope label Apr 17, 2018
mgurov added a commit to mgurov/kibanator that referenced this issue Jul 16, 2018
a workaround for missing dynamic routing capabilities
see cypress-io/cypress#521
and cypress-io/cypress#387
@TLadd
Copy link
Contributor

TLadd commented Sep 26, 2018

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.

@nazar
Copy link

nazar commented Dec 6, 2018

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.

@agoldis
Copy link

agoldis commented Jan 17, 2019

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

@jennifer-shehane
Copy link
Member

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

@jennifer-shehane jennifer-shehane added the pkg/driver This is due to an issue in the packages/driver directory label Jan 30, 2019
@nazar
Copy link

nazar commented Jan 30, 2019

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.

@stanbluijs
Copy link

stanbluijs commented Feb 14, 2019

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

@godspeedelbow
Copy link

godspeedelbow commented Apr 17, 2019

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.

#3984

@erezrokah
Copy link

erezrokah commented Sep 8, 2019

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

@matthewdaniel
Copy link

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.

@thinkerelwin
Copy link

thinkerelwin commented Apr 29, 2020

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

@vicatcu
Copy link

vicatcu commented Oct 29, 2020

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

@flotwig
Copy link
Contributor

flotwig commented Nov 2, 2020

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

@cypress-bot
Copy link
Contributor

cypress-bot bot commented Nov 3, 2020

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.

@cypress-bot
Copy link
Contributor

cypress-bot bot commented Nov 9, 2020

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.

@cypress-bot cypress-bot bot locked as resolved and limited conversation to collaborators Nov 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
pkg/driver This is due to an issue in the packages/driver directory topic: network type: feature New feature that does not currently exist
Projects
None yet
Development

Successfully merging a pull request may close this issue.