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 Dynamic Stubbing and Responses #521

Closed
Kyle-Mendes opened this issue Jun 1, 2017 · 41 comments · Fixed by #4176
Closed

Allow Dynamic Stubbing and Responses #521

Kyle-Mendes opened this issue Jun 1, 2017 · 41 comments · Fixed by #4176
Assignees
Labels
topic: network type: feature New feature that does not currently exist

Comments

@Kyle-Mendes
Copy link

Kyle-Mendes commented Jun 1, 2017

Current behavior:

Currently, all mocked responses need to know the response data upfront.

cy
  .server()
  .route("/users", [{id: 1, name: "Kyle"}])

The stubbed route can also include dynamic routes, but the response will always be static upon the route being initialized.

cy
  .server()
  .route(/api\.server\.com, SingleFixture)

Expected behavior:

  1. The ability to setup fixture responses dynamically. Perhaps using some sort of controller function.
cy
  .server()
  .route(/api\.server\.com/, FixtureController)

function FixtureController(request, response) {
  if (request.url.contains('users') {
    response.body = [{id: 1, name: "Pat"}]
    return request
  }

  if (request.url.contains('login') {
    response.body = { token: '123abc' }
    return request
}
  1. The ability to match all routes, but conditionally allow some responses to be stubbed, and others to request real data. In the above, perhaps if response wasn't set, the request would continue to the server.

Test code:

function FixtureController(request, response) {
	if (request.url === 'api.server.com/users') {
		response.body = { hello: "world" }
		return response
	}
}
describe(`Dynamic Stubbing`, () => {

	beforeEach(() => {
		cy.server().route(/api\.server\.com/, FixtureController)
	})

	it('should stub a route dynamically', () => {
		cy.request('http://api.server.com/users').then(function(response) {
			expect(response.body).to.have.property("hello", "world")
		})
	})
})
  • Operating System: OSX
  • Cypress Version: 0.19.2
  • Browser/Browser Version: Chrome Latest
@jennifer-shehane jennifer-shehane added the type: feature New feature that does not currently exist label Jun 1, 2017
@tracykm
Copy link

tracykm commented Aug 13, 2017

I second this 👍

I found myself wanting it for POST requests mostly. I think you could make much more complicated and realistic integration tests if it was possible to return whatever the request got and tack on an id or createdAt.

cy
  .server()
  .route('POST', /api/comment, (xhr) => {
     const comment = xhr.request.body
     comment.id = 1
     return comment
  })

Also great work so far, I love the possibilities it's opening up!

@paulfalgout
Copy link
Contributor

I think this is possible now using route's response method?

I realize this'll probably be slightly outdated come 0.20.0 but we solve this issue like this:

Cypress.addParentCommand({
    routeUsers(mutator = _.identity) {
        cy.fixture('users').as('fxUsers');

        cy.route({
            url: '/users/*',
            response() {
                return mutator({
                    data: this.fxUsers
                });
            }
        }).as('routeUsers');
    }
});

And later we do something like:

// Only return 2
cy.routeUsers(fx => {
  return _.sample(fx.users, 2);
});
cy.visit('some-foo.html#users').wait('@routeUsers');

@ianwalter
Copy link
Contributor

ianwalter commented Nov 2, 2017

@paulfalgout That doesn't really solve the problem though because you still don't have access to the request object.

The response callback should be passed the request object. I also like @tracykm 's suggestion of making the response argument be able to be a function and not just a String or Object. I can submit a PR if that API can be agreed upon.

@jennifer-shehane jennifer-shehane added the stage: proposal 💡 No work has been done of this issue label Nov 2, 2017
@brian-mann
Copy link
Member

@ianwalter we are not likely going to change or update any of the existing route / wait API's because ultimately the whole thing needs to be rewritten and implemented in a completely different fashion.

Here's an issue describing it: #687

When we rewrite those API's we will take this use case into account.

@orlandohohmeier
Copy link

@brian-mann any chance that we can enable dynamic stubs on top of the current API today? The epic you were referring to looks like a rather big undertaking and the lack of dynamic stubs is currently blocking us.

orlandohohmeier pushed a commit to dcos/dcos-ui that referenced this issue Dec 29, 2017
Add flags (query parameters) to identify and stub Mesos Operator API
requests to fix the integration tests that were otherwise failing or
flaky due to parsing errors in the `MesosStateStore`. The store failed
to parse the `GET_MASTER` response as we could only register one stub
for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON
parse errors due to the different response formats (RecoredIO<JSON>,
JSON).

The flags are necessary as Cypress can only stub requests based on the
URL and request method and doesn't provide the means to dynamically stub
requests based on the payload as documented in
cypress-io/cypress#521. We will remove the
flags once the Cypress team rewrote the network handling and added
support for dynamic stubs as documented in
cypress-io/cypress#687 or we've  addressed
https://jira.mesosphere.com/browse/DCOS_OSS-2021.
orlandohohmeier pushed a commit to dcos/dcos-ui that referenced this issue Dec 29, 2017
Add flags (query parameters) to identify and stub Mesos Operator API
requests to fix the integration tests that were otherwise failing or
flaky due to parsing errors in the `MesosStateStore`. The store failed
to parse the `GET_MASTER` response as we could only register one stub
for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON
parse errors due to the different response formats (RecoredIO<JSON>,
JSON).

The flags are necessary as Cypress can only stub requests based on the
URL and request method and doesn't provide the means to dynamically stub
requests based on the payload as documented in
cypress-io/cypress#521. We will remove the
flags once the Cypress team rewrote the network handling and added
support for dynamic stubs as documented in
cypress-io/cypress#687 or we've  addressed
https://jira.mesosphere.com/browse/DCOS_OSS-2021.
orlandohohmeier pushed a commit to dcos/dcos-ui that referenced this issue Dec 29, 2017
Add flags (query parameters) to identify and stub Mesos Operator API
requests to fix the integration tests that were otherwise failing or
flaky due to parsing errors in the `MesosStateStore`. The store failed
to parse the `GET_MASTER` response as we could only register one stub
for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON
parse errors due to the different response formats (RecoredIO<JSON>,
JSON).

The flags are necessary as Cypress can only stub requests based on the
URL and request method and doesn't provide the means to dynamically stub
requests based on the payload as documented in
cypress-io/cypress#521. We will remove the
flags once the Cypress team rewrote the network handling and added
support for dynamic stubs as documented in
cypress-io/cypress#687 or we've  addressed
https://jira.mesosphere.com/browse/DCOS_OSS-2021.
orlandohohmeier pushed a commit to dcos/dcos-ui that referenced this issue Dec 29, 2017
Add flags (query parameters) to identify and stub Mesos Operator API
requests to fix the integration tests that were otherwise failing or
flaky due to parsing errors in the `MesosStateStore`. The store failed
to parse the `GET_MASTER` response as we could only register one stub
for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON
parse errors due to the different response formats (RecoredIO<JSON>,
JSON).

The flags are necessary as Cypress can only stub requests based on the
URL and request method and doesn't provide the means to dynamically stub
requests based on the payload as documented in
cypress-io/cypress#521. We will remove the
flags once the Cypress team rewrote the network handling and added
support for dynamic stubs as documented in
cypress-io/cypress#687 or we've  addressed
https://jira.mesosphere.com/browse/DCOS_OSS-2021.
orlandohohmeier pushed a commit to dcos/dcos-ui that referenced this issue Jan 2, 2018
Add flags (query parameters) to identify and stub Mesos Operator API
requests to fix the integration tests that were otherwise failing or
flaky due to parsing errors in the `MesosStateStore`. The store failed
to parse the `GET_MASTER` response as we could only register one stub
for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON
parse errors due to the different response formats (RecoredIO<JSON>,
JSON).

The flags are necessary as Cypress can only stub requests based on the
URL and request method and doesn't provide the means to dynamically stub
requests based on the payload as documented in
cypress-io/cypress#521. We will remove the
flags once the Cypress team rewrote the network handling and added
support for dynamic stubs as documented in
cypress-io/cypress#687 or we've  addressed
https://jira.mesosphere.com/browse/DCOS_OSS-2021.
@egucciar
Copy link
Contributor

egucciar commented Jan 9, 2018

Unable to write certain test cases because of this issue, it's quite depressing :(

@kerrykimrusso
Copy link

kerrykimrusso commented Feb 4, 2018

I'm new to Cypress and pretty quickly ran into the situation where I would've liked to be able to return a dynamic response based on the request.body, which led me here. After kind of grumbling that I couldn't build up a response object based off of the request, I eventually landed what I think is an acceptable solution for @tracykm 's use case. It's not dynamic but I find that it more aligns with Cypress' mantra of not having flakey tests.

My specific case was that I wanted to test that a user could add a new contact. I ended up splitting my contacts fixture up to be the list contacts that'd be initially loaded, called existingContacts, and a single contact to be added, named newContact:

{
  "existingContacts": [
    {
      "id": 1,
      "name": "Fake User 1",
      "email": "fu1@fake.com"
    },
    {
      "id": 2,
      "name": "Fake User 2",
      "email": "fu2@fake.com"
    },
    {
      "id": 3,
      "name": "Fake User 3",
      "email": "fu3@fake.com"
    }
  ],
  "newContact": {
      "id": 4,
      "name": "Fake User 4",
      "email": "fu4@fake.com"
    }
}

I return the existingContacts in the GET version of the /contacts route. The newContact is first aliased so that I can reference it later on in the test and then returned in the POST version of /contacts. Finally, I can use this.newContact.<prop> as the input values during my test (notice I'm using a regular anonymous function for the test and not a lambda/fat-arrow function).

describe('Users', function() {
  beforeEach(function() {
    cy.fixture('contacts').then(({ 
      existingContacts,
      newContact,
    }) => {
      cy.wrap(newContact).as('newContact');
      
      cy.server()
        .route({
          method: 'GET',
          url: Cypress.env('SERVER_URL') + '/contacts',
          response: existingContacts,
        }).as('getContacts')
        .route({
          method: 'POST',
          url: Cypress.env('SERVER_URL') + '/contacts',
          response: newContact,
        }).as('postContact');
    });
  });

  it('can add a new contact', function() {
    cy.visit('/', {
      // https://github.com/cypress-io/cypress/issues/95#issuecomment-281273126
      onBeforeLoad: (win) => {
        win.fetch = null;
      }
    })
      .wait('@getContacts')
      .get('.add-contact')
      .click()
      .get('input[name="name"]')
      .type(this.newContact.name)
      .get('input[name="email"]')
      .type(this.newContact.email)
      .get('button')
      .click()
      .wait('@postContact')
      .get('.contact-list > li')
      .should('have.length', 4);
  });
})

I feel more confident testing this way as it takes any code I would've written to pass the request body through to my response out of the equation i.e. server logic, the whole point of stubbing routes. Anyway, hopefully this is coherent and I thought I'd add to the thread seeing as it's not closed yet and I ran into this issue almost immediately.

@vrockai
Copy link

vrockai commented Jun 4, 2018

I'd like to add, that it's important not only to support dynamic response body, but the status, too. I'm trying to mock the resumable upload into google storage and while redefining the same route inside a single test works in windowed mode, it doesn't work in the headless mode - I guess the application executes requests quicker than cypress is capable of changing the route stub. The problem is a situation where a single PUT endpoint works in two modes:

  1. To check whether the upload was already started before and it's about to be resumed or started from scratch.
  2. The upload itself.

I want to test upload retries in our app, so I need 1. to pass (ideally talk to the live server) and stub 2. to return a 404 and not only I don't know how to do it, I don't even know if it's possible with cypress.

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
@jaredtmartin
Copy link

My code works with PayPal's Payflow API and multiple requests to the same endpoint return different values depending on the request. I must have either a reference to the request or the ability to return different responses for each subsequent request.

@henryhobhouse
Copy link

Similar problem with testing on app using GraphQL. Expecting a dynamic response dependant on request body. Very hard/hacky to test with cypress at the moment. Current solution to use callFake() on the stubbed 'fetch' -> graphqlURL to allow us to introduce logic dependant on request body.

const responseStub = result => ({
  json() {
    return Promise.resolve(result);
  },
  text() {
    return Promise.resolve(JSON.stringify(result));
  },
  ok: true,
});

Cypress.Commands.add('visitStubbed', (url, operations = {}) => {
  cy.visit(url, {
    onBeforeLoad: win => {
      cy.stub(win, 'fetch')
        .withArgs('/graphql')
        .callsFake((_, req) => {
          const { operationName } = JSON.parse(req.body);
          const resultStub = get(operations, operationName, {});
          return Promise.resolve(responseStub(resultStub));
        });
    },
  });
});

The only problem is that this hides the request within the cypress promise itself and thus never exposed to the test. We are having to assume it's correct from the basic response logic return. Not ideal and therefore would be awesome if the network request stubbing would allow us to use the request to apply logic in forming the response. We could then spy on the fetch/XHR call to graphql to expose the request itself to the tests for full e2e integration testing.

@egucciar
Copy link
Contributor

egucciar commented Oct 5, 2018

@henryhobhouse I have enhanced my fetch stubbing abilities to allow for dynamic responses and conditional stubbing (i.e. letting real request go through sometimes) can share the code if you'd like

@henryhobhouse
Copy link

@egucciar Sounds great!

@jaredtmartin
Copy link

jaredtmartin commented Oct 5, 2018 via email

@egucciar
Copy link
Contributor

egucciar commented Oct 5, 2018

Sorry guys for the delay i totally forgot. BE AWARE this is only for fetch even though the subject matter of the post is XHR requests, this is in response to henryhobhouse's wish to be able to stub out his graphQL requests.

Similar problem with testing on app using GraphQL.

So this is 100% only geared towards fetch/graphQL & I hope that it helps. Sorry it probably wont be great for the XHR usecase

export const stubFetch = (stubs = defaultStubs) => win => {
  const { fetch } = win;
  cy.stub(win, 'fetch', (...args) => {
    console.log('Handling fetch stub', args);
    const [url, request] = args;
    if (!request) {
      return fetch(...args);
    }
    const postBody = JSON.parse(request.body);
    let promise;
    if (url.indexOf('graphql') !== -1) {
      stubs.some(stub => {
        if (postBody.operationName === stub.operation) {
          console.log('STUBBING', stub.operation);
          const response = makeApolloResponse(stub.response, postBody);
          // if response is false, let the request go through anyway
          // i.e. conditionally stub this operation if the response is not false
          if (response === false) {
            return false;
          }
          promise = Promise.resolve(response);
          return true;
        }
        return false;
      });
    }
    if (promise) {
      return promise;
    }
    console.warn('Real Fetch Getting Called With Args', args);
    return fetch(...args);
  });
};

function makeApolloResponse(resp, body) {
  const response = isFunction(resp) ? resp(body) : resp;
  if (response === false) {
    return false;
  }
  return {
    ok: true,
    json() {
      return Promise.resolve(response);
    },
    text() {
      return Promise.resolve(JSON.stringify(response));
    },
    clone() {
      return {
        arrayBuffer() {
          return {
            then() {
              return {
                byteLength: 10,
              };
            },
          };
        },
      };
    },
  };
}

function isFunction(x) {
  return Object.prototype.toString.call(x) === '[object Function]';
}

This is how stubs can be defined

cy.visit('/', {
    onBeforeLoad: stubFetch([
          {
                operation: 'gqlNamedOperation',
                response: { // json response - static }
          },
          {
                operation: 'gqlNamedOperation2',
                response: (body) => {
                    if ( // some condition) {
                          return // json response 
                    }
                    return false; // lets real request pass through / real response is returned
          },
        ])
    }
)

@ianwalter
Copy link
Contributor

@egucciar Nice!

Here is an alternative workaround I wrote a while ago using xhook to intercept XHRs:

  cy.visit('/checkout/review', {
    onBeforeLoad: window => {
      const script = window.document.createElement('script')
      script.onload = function () {
        window.xhook.before(req => {
          if (req.method === 'post' && req.url === '/api/orders') {
            const body = JSON.parse(req.body)
            body.stripeToken = 'tok_visa'
            req.body = JSON.stringify(body)
          }
        })
      }
      script.src = '//unpkg.com/xhook@latest/dist/xhook.min.js'
      window.document.head.appendChild(script)
    }
  })

@gpstmp
Copy link

gpstmp commented Oct 22, 2018

Just a short idea about dynamic changing of responses:

describe('Change Response', () => {
  const comments = [
    { id: 1, text: 'First comment' },
  ];

  beforeEach(() => {
    cy.server();
    cy.route('GET', '/comments', comments);
    cy.visit('/')
  });

  it('First call with one comment', () => {
    cy.get('[data-cy=comment]').should('have.length', 1);
  });

  it('First call with one comment', () => {
    comments.push({
      id: 2, text: 'Second comment',
    });
    cy.get('[data-cy=comment]').should('have.length', 2);
  });
});

Of course this idea has a problem: you will not have a proper value while using time travel (but i can live with this problem for now and just wait for the bright future).

@pstuermlinger
Copy link

I found a solution for the following scenario:

  1. Get list of all speakers
  2. Update one of them
  3. Request again all speakers (which expects one of them to be updated)
beforeEach(() => {
    cy.server();
    cy.route({
      method: 'GET',
      url: '**/api/v1/speaker',
      response: 'fixture:ResponseSpeakerMulti.json',
      delay: 500,
    }).as('getAllSpeakers');

    cy.route({
      method: 'PUT',
      url: '**/api/v1/speaker/2',
      response: 'fixture:ResponseSpeakerSingleUpdate.json',
      delay: 500,
    }).as('updateSpeaker');
});

it('should update after edit', () => {
    cy.wait([ '@getAllSpeakers' ]);

    // Overwrite the route.
    cy.route({
      method: 'GET',
      url: '**/api/v1/speaker',
      response: 'fixture:ResponseSpeakerMultiUpdated.json',
      delay: 500,
    }).as('getAllSpeakersUpdated');

    cy.get('#edit-2').click();
    cy.get('input[formcontrolname="lastName"]').clear().type('Doe');
    cy.get('[type="submit"]').click();
    cy.wait([ '@updateSpeaker' ]);
    cy.wait([ '@getAllSpeakersUpdated' ])
});

@actuallyReallyAlex
Copy link

So we are almost 2 years after this problem was initially brought up. I see people have written some pseudo fixes for different problems, but none of them seem like a catch all (at least for XHR).

Does anyone from Cypress have an update on this? @brian-mann ?

Love Cypress, and I'd like to be able to use it more dynamically.

@schroedermarius
Copy link

Bumping this, because there is still now answer if its beeing tracked.
In my opinion its something essential.

@cain
Copy link

cain commented Apr 16, 2020

Any update on this at all? Updating values of the real response would be an amazing capability for us in testing many scenarios with real data without having to stub the whole thing. @flotwig :)

@hehoo
Copy link

hehoo commented Apr 28, 2020

Any update on this? It doesn't work even if I changed/mutated the real response in onResponse method similar like:

cy.route({
        url: 'xxx/url',
        onResponse: (xhr) => {
          xhr.response.body.xxx=xxx;
          // or even return the response. 
         // return { ...xhr.response }
        },
      });

I still got the real response rather than the updated one.

@sam3k
Copy link

sam3k commented May 4, 2020

Any update on this? It doesn't work even if I changed/mutated the real response in onResponse method similar like:

cy.route({
        url: 'xxx/url',
        onResponse: (xhr) => {
          xhr.response.body.xxx=xxx;
          // or even return the response. 
         // return { ...xhr.response }
        },
      });

I still got the real response rather than the updated one.

I think you just need to uncomment your code return { ...xhr.response }. Seems to work for me.

@cain
Copy link

cain commented May 5, 2020

Looking at the documentation, onResponse is a callback function and doesn't accept any returned data.

@sam3k Did you test mutating the data? I've tried locally in my tests to change the data and return onResponse and doesn't seem to work. Would love to know if this is possible changing something?

@jennifer-shehane
Copy link
Member

This feature will be a part of the work delivered in #4176 for #687.

I realize it seems like it has not had work in a while. We are working on first delivering #5273 which is a dependency of continuing with the Network Layer rewrite.

@cain
Copy link

cain commented May 6, 2020

@jennifer-shehane Appreciate the update :)

@danidelgadoz
Copy link

Any update on this? It doesn't work even if I changed/mutated the real response in onResponse method similar like:

cy.route({
        url: 'xxx/url',
        onResponse: (xhr) => {
          xhr.response.body.xxx=xxx;
          // or even return the response. 
         // return { ...xhr.response }
        },
      });

I still got the real response rather than the updated one.

@hehoo I have the same behavior

Did you find out a solution?

@Oterem
Copy link

Oterem commented Jun 11, 2020

This feature will be a part of the work delivered in #4176 for #687.

I realize it seems like it has not had work in a while. We are working on first delivering #5273 which is a dependency of continuing with the Network Layer rewrite.

Do you have any time estimation for delivery?

@hehoo
Copy link

hehoo commented Jun 12, 2020

Any update on this? It doesn't work even if I changed/mutated the real response in onResponse method similar like:

cy.route({
        url: 'xxx/url',
        onResponse: (xhr) => {
          xhr.response.body.xxx=xxx;
          // or even return the response. 
         // return { ...xhr.response }
        },
      });

I still got the real response rather than the updated one.

@hehoo I have the same behavior

Did you find out a solution?

No, I mock the request to solve my issue. Waiting for the fix in cypress :)

@chulman444
Copy link

chulman444 commented Jun 12, 2020

Apparently, Cypress uses X-Cypress-RESPONSE header to stub the response. And one way to do this is to use:

cy.route({
  ...,
  onRequest(xhr) {
    xhr.setRequestHeader("X-Cypress-RESPONSE", encodeURI(fake_response))
  },
  response: "",
})

or use encodeURI(JSON.stringify(fake_response)) if the fake_response is an object value as done in this line of the code.

But this results in an unexpected response because the way setRequestHeader works. It adds the fake_response after , . So if you had:

cy.route({
  onRequest(xhr) {
    fake_response = "foo"
    ...
  },
  response: ""
})

the response becomes , foo. With JSON type, the type becomes a string. If you have no problem with working with such an awkward response, you are good to go.

I think the least they can do is to execute o.response done in this code later and do it in here where they add all the headers. What's the context of:

cy.route({
  response() {
    this // What the heck is this? state("runnable")
  }
})

What the heck is a runnable? And why does it have to be this of response instead of being a parameter?

Or they could make shouldApplyStub (code):

    shouldApplyStub (route) {
      return hasEnabledStubs && route && (route.response != null)
    },

more strict or lenient and do

... && (route.response === null` or `route.response != null || route.stubUsingHeader == true)

and let us fake the response using setRequstHeader with encodeURI(JSON.stringify(...)). I think this is better for us and them for the time being if it's too difficult to check if response is a function and call it within applyStubProperties.


This is going to be very helpful for people who want to mock socket.io, like I do. You could just use cy.route({ url: "/socket.io/*" }) method GET to get response and POST to get requests. And I don't want to care about websocket (or socket.io) handshake protocol.

So, by giving me the access to the request object, I can decide whether to use the fake response or not; don't fake it if it's a handshake protocol

So much potential in a 3 year and 10 days old issue. Please!!

@cypress-bot cypress-bot bot added stage: needs review The PR code is done & tested, needs review and removed stage: ready for work The issue is reproducible and in scope labels Jul 7, 2020
@piecioshka
Copy link

piecioshka commented Aug 5, 2020

@chulman444 Thanks for inspiration!

My workaround works on v4.9.0:

  cy.route({
    method: 'POST',
    url: `/searches`,
    response: '{"errors": {}',                             // First part of response (1)
    onRequest: ({ xhr, request }) => {
      const { sessionId } = request.body.data[0];          // Data from request POST body
      xhr.setRequestHeader(
        'X-Cypress-Response',
        `"data": { "${sessionId}": "9A2183464FA47403" }}`, // Second part of response (2)
      );
    },
  }).as('searches');

Cypress's will join response (1) key and X-Cypress-Response (2) with a comma, when returns response for cy.route.

@cbovis
Copy link

cbovis commented Aug 7, 2020

Thanks @ianwalter @cy6581! This was a big blocker for us and your solutions work well. I decided to put xhook into a local fixture though as I wasn't keen to rely on the CDN.

@cain
Copy link

cain commented Aug 10, 2020

@cbovis I wasnt able to get their solution to work. Are you able to share a code snippet for an example?

@cbovis
Copy link

cbovis commented Aug 10, 2020

Sure thing @cain.

Place the contents of https://unpkg.com/xhook@latest/dist/xhook.min.js into cypress/fixtures/xhook-js.txt (or wherever your fixtures folder is). Make sure you've got fixtures enabled in cypress config.

Reusable helper (can be reworked to fit your needs):

/**
 * Command which allows us to conditionally mock XHR calls. This is actually
 * quite difficult to do out of the box and there's a three year old issue
 * tackling the problem still open on GitHub:
 *
 * https://github.com/cypress-io/cypress/issues/521
 *
 * This solution is constructed from some of the suggestions there.
 */
Cypress.Commands.add('mockXhrCalls', mocks => {
  cy.fixture('xhook-js').then(xHookJs => {
    Cypress.on('window:before:load', win => {
      /**
       * Load xhook JS into the headless browser environment.
       * After loading it will be available via window.xhook
       */
      win.eval(xHookJs);

      mocks.forEach(mock => {
        win.xhook.before(req => {
          if (req.method === mock.method && req.url.endsWith(mock.urlPath)) {
            if (_.isFunction(mock.response)) {
              return mock.response(req);
            }

            return mock.response;
          }

          return undefined; // Pass request through to actual API
        });
      });
    });
  });
});

Example of consumption inside test suite:

cy.mockXhrCalls([
      {
        method: 'POST',
        urlPath: '/gql',
        response: request => {
          const body = JSON.parse(request.body);
          const { query } = body;

          if (query.indexOf('updateProfile') !== -1) {
            const mockResponse = {
              data: {
                success: true,
              },
            };

            return {
              headers: {
                'content-type': 'application/json; charset=utf-8',
              },
              status: 200,
              text: JSON.stringify(mockResponse),
              data: mockResponse,
            };
          }

          // Anything which didn't match the above condition should pass-through to original API
          return undefined;
        },
      },
    ]);
  });

@cypress-bot cypress-bot bot added stage: work in progress stage: needs review The PR code is done & tested, needs review and removed stage: needs review The PR code is done & tested, needs review stage: work in progress labels Aug 25, 2020
@cypress-bot
Copy link
Contributor

cypress-bot bot commented Sep 1, 2020

Released in 5.1.0.

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

@cypress-bot cypress-bot bot removed stage: needs review The PR code is done & tested, needs review stage: pending release labels Sep 1, 2020
@cypress-bot cypress-bot bot locked as resolved and limited conversation to collaborators Sep 1, 2020
@jennifer-shehane
Copy link
Member

The features requested in this issue are now possible as part of cy.route2().

cy.route2() is currently experimental and requires being enabled by passing "experimentalNetworkStubbing": true through your Cypress configuration. This will eventually be merged in as part of our standard API.

Please see the cy.route2() docs for full details: https://on.cypress.io/route2

If you encounter any issues or unexpected behavior while using cy.route2() we encourage you to open a new issue so that we can work out all the issues before public release. Thanks!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
topic: network type: feature New feature that does not currently exist
Projects
None yet
Development

Successfully merging a pull request may close this issue.