Cypress: Full Network Layer Stubbing / fetch support

Created on 23 Sep 2017  路  101Comments  路  Source: cypress-io/cypress

Pull Request (in progress): https://github.com/cypress-io/cypress/pull/4176

What users want

  • To be able to stub any kind of HTTP request including window.fetch
  • To blacklist certain domains (google analytics, new relic, intercom)
  • To have more control over dynamically routing / stubbing requests and responses
  • To more easily stub graphQL calls

What we do now

  • We modify the host XMLHttpRequest object which limits us to only stubbing XHR's from the main application frame.

What we need to do

  • Throw away the entire existing system for stubbing
  • Move stubbing into the internal Cypress network proxy layer (outside of the browser)
  • Synchronize the state of the browser tests with the backend (so its aware what needs to be stubbed / routed)
  • Remove cy.server and instead just use cy.route
  • Expand the API's for more advanced configuration
  • Accept blacklistDomains option for cypress.json
  • As each request comes in, if we expose callback functions we'll need to constantly communicate with the driver for guidance on what to do - whether to proceed - halt, etc. This can get pretty complicated and might have significant performance issues. We'll need to experiment and we may not be able to provide callback functions anymore.

Things to consider

  • onRequest and onResponse currently provide the raw level xhr object. This would no longer be possible. We'd have to serialize the request / response into basic objects. No method calling would be available.
  • The onError callback could no longer exist
  • Additionally we hook into the onerror events for XHR's and will automatically fail the test if this is called. This would no longer be possible.
  • Blacklisting https cannot be dynamic. It would have to be set in cypress.json or environment variables. The reason is that for each blacklisted domain that goes over https, we would have to route that traffic to a self signed certificate. Browser freak out if traffic to https gets assigned one certificate, and then within the same session it gets another cert applied. What this means is that before spawning the browser we have to isolate that traffic and know ahead of time not to route it to the actual server. This enables us to send a single cert to the browser.
  • We could enable non https hosts to be dynamically blacklisted, but these API's would be async and likely difficult for users to understand / use. We might be able to bake these into cy.route but I'm not sure there's much of a use case here of only dynamically blacklisting non https traffic.
  • We will no longer be able to tell the difference between an XHR and any other kind of http request. We'd likely have to change the page events to say REQ and REQ STUB instead of XHR and XHR STUB.
  • It would be very difficult to provide accurate onResponse callback functions. It's possible for the internal server to know when an HTTP request has been completed, but it's not possible to know whether the browser has actually finished processing it. This may or may not be a problem.
  • How would we handle stubbing binary / streaming responses?
  • How would we handle stubbing things like images? (This is likely pretty simple)

Bonus Points

  • It would likely be possible to stub Websocket traffic in the same manner
  • The problem with this is that most Websocket libraries (ie socket.io) add their own "encoding" pattern on top of each websocket message frame. The ergonomics around stubbing this would be odd. The problem is this is library (and even version of library) specific. This may be a better use case for cy.stub, or perhaps a 3rd party extension
Epic 5锔忊儯 internal-priority pkhttps-proxy network feature

Most helpful comment

Any update on the timeline for this being released?

All 101 comments

@brian-mann I would be very willing to help out with this issue. This is a big blocker for use when testing our app.

@ericadamski could you go into more detail about this? What exactly is a big blocker?

@brian-mann making network requests (using a fetch polyfill) and asserting on those requests is important to our integration testing. Currently we are just spying on the window.fetch and doing some simple assertions on the params request. It would be ideal to provide the same functionality as XHRs to completely omit the server and mock responses when needed, understanding that we could stub the fetch as well, but this means stubbing each individual request with a tailored response.

You can do this right now using stubs. Albeit it's certainly not as nice as what we did for XHR stubbing, but it is possible and we have several examples of making this work. This is even in a new getting started video we're releasing later this week.

https://github.com/cypress-io/cypress-example-recipes/blob/master/cypress/integration/spy_stub_clock_spec.js

Yes we realize it can be done, albeit we would rather wait (or work toward) a more XHR style stubbing, it is just super nice.

Yeah it is. Okay just making sure we're on the same page.

The proposal here does a decent job outlining what needs to be done but it will be a significant amount of work.

Likely the next steps just need to be formalizing a plan for the API's themselves. There have been many requests for additional features and we should start there.

That sounds good.

Is it worth creating separate issues for some of the requested features for I know almost nothing about graphQL queries 馃槣

They mostly already are other issues. We need to factor in all of them and come up with a set of API's that unifies those different use cases and desired experiences.

Right OK, I see what you are saying.

@brian-mann side note, has it been thought of moving away from coffeescript and into es6?

Do you consider this feature in the next release? I reckon it would be highly valuable, for instance when

  • authentication/ other actions are performed at script level (

I do like nock's fluent API, but one problem is that it would end up adding a lottttt more Cypress methods. I also feel some of their method signatures are odd, or not what I expect, or I constantly have to look them up to remember - what is it that I use again? The surface area is really large. But definitely good stuff to pull from!

It might be an easier sell to mimic what request does -> since we built cy.request around the same API style....

Some generic pairing notes not intended to be canon

Client (Non Proxied Request) Browser

- GET https://www.github.com
  - DNS resolution (local computer)
  - DNS resolve (24.4512.23.23)
  - Server gets 
  - SSL Handshaking
  - Receives bytes

- GET http://localhost:98765/

Request Headers
---------------
GET / HTTP/1.1
Host: localhost:98765

--------------------------------

Client (Proxied Request) Browser

- CONNECT github.com:443
- CONNECT localhost:8080

GET http://localhost:8080/foo/bar
Host: localhost

GET https://www.github.com/ HTTP/1.1
Host: github.com


-------------------------------

Cypress creates a http server: localhost:98765
Cypress creates a file server server: localhost: 98764
Cypress creates an SNI proxy server: localhost: 98763


--proxy-server=http://localhost:98765


cy
  .route('GET', 'http://localhost:8081', {
    onRequest: req => 
    onResponse: res => 
  }).as('getFoo')
  ...
  ...
  ...
  .wait('@getFoo.request')


injecting hosts
  1. captureTrafficHosts: "*" by default we do it on '*'
  1. captureTrafficHosts: ['foo', 'bar'] by default we do it on '*'

1. know ahead of time what hosts to inject (generate a cert / have access to traffic)
2. when any request come in, capture a HAR
3. expose an API to the user to enable them to control traffic
4. when we encounter user rules, the internal traffic rules instance has to know about them
5. when network request match traffic rules, we have to apply those rules to the network requests
  - stubbing the responses
  - waiting on the responses (pass through)
  - modifying the requests
  - delaying the responses
6. if there are callbacks (onRequest / onResponse) we have to decode / buffer the responses (potentially) send data to the driver (websockets) after serializing into objects
  - wait for the user to potentially respond (if we support promises)
  - socket.pause() the responses so the browser hasn't completed the http request
7. complete the http request
8. notify the driver of a completed request 

Found the fetch-mock which has very similar api to cy.route() so it makes stubbing/mocking of window.fetch much easier and readable.

@FredyC do you use fetch-mock in your cypress tests?

@ifeanyi I do, but my case is really simplified to a login scenario only. Rest of a communication goes through the GraphQL which is much more simple to mock.

I did it basically like this.

// it would be nice to have some global onBeforeLoad
onBeforeLoad: win => {
    cy.stub(win, 'fetch', fetchMock.sandbox()).as('fetch')
},

// then in any test I can use API of fetch-mock like that
cy.get('@fetch').get('*', { token: 'AAA', userId: 'BBB' })

afterEach(() => {
  cy.get('@fetch'}.restore()
})

@FredyC I am doing something similar, I created a fetchMock module so ensure that I re-use the same sandbox. I was able to mock requests in my command file but I am having trouble mocking requests in individual tests. I'm gonna keeping hacking at it till I find a reliable pattern.

// fetchMock.js
module.exports = require('fetch-mock').sandbox();

It's your call, but "re-use the same sandbox" does not make any sense :) With that approach, you can easily stumble into a situation when one test is influenced by other because you forgot to clean up the sandbox. So yea, I think that onBeforeLoad is still the best spot.

Besides, I think it's the main reason you are struggling with it. You need to stub window.fetch before a code of the tested site runs.

Besides the missing support for fetch, it would be nice to support xhr/fetch stubbing requests made in web workers. Unless I'm missing something it doesn't seem straightforward to wrap the workers XmlHttpRequest implementation?

If you use Apollo and would like to be able to stub calls to your GraphQL layer in Cypress, here is how you can do it until this problem gets resolved:

+import unfetch from 'unfetch'

createHttpLink({
  uri: '/graphql',
  credentials: 'same-origin',
+ fetch: unfetch
})

And in the support file:

// Cypress doesn鈥檛 recognize `window.fetch` calls as XHR requests, which makes
// it impossible to stub them. We delete `fetch` from the window object so the
// `unfetch` polyfill (which uses proper `XMLHttpRequest`) kicks in.
Cypress.on('window:before:load', win => {
  delete win.fetch
})

@HugoGiraudel You don't even need to use unfetch in your HttpLink. As long as you make win.fetch unavailable in window:before:load, Apollo will revert to using XHR. I think Apollo uses isomorphic-fetch, which is built on top of Github's fetch polyfill.

In other words, you don't need to do the first part of your answer. You just need the delete win.fetch part.

Hi. Apologies if this has been answered or covered elsewhere, but after extensive searching, I'm struggling to nail down the answer I'm looking for, including within this issue.
In short, we would like to mock a websocket that we use with our Angular 6 UI, using Cypress (which is great, btw!).
I have seen in a couple of places that using cy.stub is the way to go. But are there any examples of doing this for a websocket? Specifically for an Angular 2+ UI? The Cypress examples cover stubbing AngularJS code, but not Angular 2+ (as far as I can tell).
Or is this issue coming up with another solution?
Any pointers to get me going in the right direction will be really appreciated.

I realize this is still in progress, but I wanted to mention the use case of simulating a network delay for specific requests; I have a bug that only manifests when a specific request resolves before another specific request. I would like to be able to simulate that in a test for a regression test; not to serve canned responses but to use real responses, just delay one of them so that it "comes back" after another.

Thanks for the work on this, I'm excited for this feature!

@bahmutov is starting back on this work this week.

work in progress

Hi,
Sorry for possibly duplicated question, but with this feature will it be possible to work (wait, check response body) with Websocket events? thank you

Do you have an ETA on this?

Hey guys,

is there any information, when this feature is going to be ready?

I have an issue when I use cy.server and cy.request as first operation in before() or it() blocks. There are sometimes test runs, where the request seems to be not to trigger. It runs into the requestTimeout but I can see the request in the developer tools. When I reload the spec, than it mostly runs without any problemens. But this is impossible for the CI server.

For me, it looks like the listeners listentens to late on my requests. Is this gona fixed with this issue too?

I see that this package has not been mentioned here. It has solved a lot of my team's problems: https://github.com/ijpiantanida/talkback

@Vages do I understand correctly, that talkback is replacement for my "original" / production server? Or is is possible to forward requests from cypress over talkback to my native apache?

Thanks for your reply

Talkback is a proxy which stores the last response for a given URL. Spin up one or more talkback-servers for the APIs you want to mock via cypress/plugins/. When in the test environment, set your frontend to fetch/get data from these URLs instead of the regular URLs.

I'm using talkback also, working really good!

I mentioned this here https://github.com/cypress-io/cypress/issues/95#issuecomment-433679170 but i wonder why PollyJS wouldnt be a perfect fit, it can do what cypress wants and more and already offers adaptors and other means to hook in. I use it with cypress and its amazing. Would love to see it become part of cypress :). I think it was meant to be :P

Also my 2 cents, Think you should leave the stubbing in the client as this gives you the best flexibility and control, other wise syncing mock settings will be required and adds more complexity. Additionally stubbing in the client renders the fastest response, which is nice for testing purposes. Having stubs that return dynamic content based of your test suite might be tricky as you have to send the response to the server so it can respond to your app which is already in client ???

I feel your mocks should originate from your suite since whats where its executed from, and since this is in the client it feels clunky to push this config to the server and have it respond from there back to your client.

Just use Polly, has the backing and its already developed :P

Could we add a recipe for using https://github.com/ijpiantanida/talkback for HTTP stubbing?

@Vages Are you able to provide some detail as to how to add Talkback to cypress. I'm getting a bit stuck trying.

Start a Talkback server in cypress/plugins/index.js. It will run automatically when Cypress starts. An example inspired by the Talkback Readme:

// cypress/plugins/index.js
const talkback = require("talkback");

const opts = {
  host: "https://api.myapp.com/foo",
  port: 5544,
  path: "./my-tapes"
};

module.exports = () => {
  const server = talkback(opts);
  server.start(() => console.log("Talkback Started"));
}

How this works: When a request is sent to localhost:5544, Talkback will look for a tape (previous request and response) matching the request, looking at the URL, headers, body, and all. If a matching tape is found, Talkback will respond with the stored response. If not, Talkback will forward the request to https://api.myapp.com/foo, storing the request and response as a tape. If you send a request to localhost:5544/bar, it will be forwarded to https://api.myapp.com/foo/bar and so on.

Common pitfall: I have seen people going "Why isn't Talkback storing any tapes?" because they are sending their requests "production style" during testing, e.g. https://api.myapp.com/foo/baz. Sadly, Talkback isn't magic (like Cypress): When you run your tests, you have to make your app request the localhost URL that the Talkback server is attached to. For example localhost:5544/baz instead of https://api.myapp.com/foo/baz. You've probably got a way of changing API base URLs from development to production already, so just make a test configuration that you use when running with Cypress. But it's likely _very_ project specific, so you have to do that homework yourself.

Thanks @Vages. That was a very complete explanation. Thanks for your effort. Clears things up for me.

I've been trying to get XMLHttpRequest mocking going using pollyjs and the basics have been working (after replacing the xhr adapter with one that works with cypress). I am seeing issues in some cases though, a maximum call stack issue keeps happening. Trying to track it down. Don't know if anyone else has had any luck with pollyjs.

@psachs21 i created this till i have a better solution https://github.com/timpur/cypress-polly-xhr-adapter (https://github.com/sinonjs/nise/issues/70#issuecomment-439674811)

Edit: Just add the git repo url to package.json and it should just work (install)

@timpur This is exactly what I was looking for. Figures someone had already solved it. Much appreciated (even as a temporary solution)

For anyone who is interested in auto stub based on existing cypress, I've created a simple example to demonstrate my solution https://github.com/PinkyJie/cypress-auto-stub-example

There has been some progress made on this issue - but it does involve many parts, so we do not have an ETA at the moment.

With the proxy approach, am I correct to say that it will not work for API with Kerberos authentication?

It would be great if the current browser side stubbing option is kept as an alternative.

Is sendBeacon in scope for this? I was testing to ensure Sentry (error tracking) fires errors appropriately, and saw that the calls to sendBeacon weren't intercepted by the route-stub I had created.

@frattaro yea, I was discussing this over on chat with someone the other day, and it seems that it's not possible to use cy.route on them, which is understandable, considering.

WTBS, you can stub them and use XHR instead.

@dwelle The use case was to ensure a client's website using Sentry (or not) didn't interfere with our own SDK's implementation of Sentry. I tried removing sendBeacon and polyfilling with XHR (like is suggested for fetch), but it wasn't successful for whatever reason.

Wasn't successful in what way?

The call wasn't even showing up in the network tab of chrome's dev tools. I was using this package: https://www.npmjs.com/package/navigator.sendbeacon

Otherwise mimicked the fetch workaround with the Cypress.on('window:before:load', win => { win.navigator.sendBeacon = null; });

Yea, that's because of how that lib checks for native support. You won't be able to go around it, unless it's fixed https://github.com/miguelmota/Navigator.sendBeacon/issues/17

For now, you can inline it:

cypress/support/index.js

Cypress.on( `window:before:load`, win => {
    cy.window().then(win => {
        // @copyright https://github.com/miguelmota/Navigator.sendBeacon
        win.navigator.sendBeacon = function sendBeacon (url, data) {
            const isString = val => typeof val === "string";
            const isBlob = val => val instanceof Blob;

            const event = this.event && this.event.type;
            const sync = event === "unload" || event === "beforeunload";

            const xhr = ("XMLHttpRequest" in this) ? new this.XMLHttpRequest() : new this.ActiveXObject("Microsoft.XMLHTTP");
            xhr.open("POST", url, !sync);
            xhr.withCredentials = true;
            xhr.setRequestHeader("Accept", "*/*");


            if (isString(data)) {
                xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
                xhr.responseType = "text/plain";
            } else if (isBlob(data) && data.type) {
                xhr.setRequestHeader("Content-Type", data.type);
            }

            try {
                xhr.send(data);
            } catch (error) {
                return false;
            }

            return true;
        }.bind(win);
    });
});

Got everything working - thanks so much! I had to make some changes:
1) remove the cy.window().then wrapper, (lengthy error)
2) remove the withCredentials = true; (error with CORS)
3) change xhr.responseType = "text/plain" to xhr.responseType = "text" to resolve a warning

Does anyone know if it's possible to test service workers prior to this change going in?

Anyone had any joy mocking responses?

Keeping an eye on this one, thanks for the hard work.

Is there a way of waiting for a specific JS script to run before this feature lands? In my case it's a 3rd party script that runs after the onload event so I haven't been able to reliably wait for its execution without some cy.wait() magic numbers.

Might be worth looking at Puppeteer鈥檚 implementation of:

  • Page.on('request')
  • Page.on('requestfailed')
  • Page.on('requestfinsihed')
  • Page.on('response')

see their docs for details.

Edit: to clarify, I'm saying that the Cypress team might want to take inspiration from Puppeteer's implementation.

@timpur This is exactly what I was looking for. Figures someone had already solved it. Much appreciated (even as a temporary solution)

Can you show how you were able to use this package and get it set up? No luck for me.

@alexlee-dev so I had to overwrite my visit because i wanted to preserve keep recording between navigations. That was the only thing that really hung up my use of the adapter.

import XHRAdapter from 'cypress-polly-xhr-adapter';
import { Polly } from '@pollyjs/core';

const startPolly = (mode, _win, _name?: string) => {
    XHRAdapter.setWindow(_win || window);

    const polly = new Polly(filename, {
        adapters: [XHRAdapter],
                 mode,
        ...
    });
    return polly;
};

const stopPolly = polly => {
    if (!polly) throw new Error('Polly not initialised');
    cy.wrap(polly).invoke('stop');
    cy.state('polly.instance', null)
};

Cypress.Commands.overwrite('visit', (orig, url, options) => {
        const mode = cy.state('polly.mode');
    const previousPolly = cy.state('polly.instance');
    const doVisit = () =>
        orig(url, {
            ...options,
            onBeforeLoad: win => {
                if (mode) {
                    const polly = startPolly(mode, win);
                                         cy.state('polly.instance', polly);
                }
                if (options && options.onBeforeLoad) {
                    options.onBeforeLoad(window, options);
                }
            }
        });
        if (previousPolly) {
        stopPolly(previousPolly);
    }
    return doVisit();
});

@alexlee-dev yeah i linked this https://github.com/timpur/cypress-polly-xhr-adapter though it was meant for temp, i was more thinking cypress could utilize Polly since it already has a community instead of reinventing, though i cant say it ticks all of cypress's requirements ... (i dont know enough to really comment)

But if you like to use polly my repo may help though dont expect much :P
@psachs21 also has you covered with an example (thanks mate)

Thanks @psachs21 and @timpur . I'll give it another go. I'm trying to do it within several layers of a multi-proxy-ed corporate application, so sometimes it's difficult to know what it really causing Cypress to not work correctly.

I am also interested in a solution so that we can be aware of loading scripts such as <script src="www....">, similar to the Pupetteer option setRequestInterception

Hey guys,

Any updates on this? Or maybe you can suggest a temporary workaround for people running into this issue?

@DanielStoica85 I've been actively working on this feature, so hopefully it will be in the next release after 3.3.0. What are you trying to test, exactly? Maybe I can help you find a temporary workaround.

@flotwig any updates on this? Love a workaround we can use with Apollo client to see our graphql queries and make sure cypress waits on them before moving to the next test

I've created a Gist of a more scalable workaround until this is implemented: https://gist.github.com/yagudaev/2ad1ef4a21a2d1cfe0e7d96afc7170bc

It was inspired by the official cypress recipe with some tweaking.

Hopefully it helps somebody out there until the official feature is in place.

In a situation that I think is related to this issue. I have to wait for all initial bundles to be loaded for some interactions to be available. Once I perform the interaction another resource is loaded and this one is required for my assertions.

The test case would look something like this

cy.visit('example.com');
cy.waitForResourceToBeLoaded('bundle23.js')
cy.get('.btn').click();
cy.waitForResourceToBeLoaded('below-the-fold.css');
performAssertions();

Are there any updates on

we may not be able to provide callback functions anymore

from the op?

@paulfalgout We want to allow users to dynamically intercept network traffic in tests; however, this would also introduce some latency in network requests because instead of the traffic going...

Browser <-> Cypress <-> Internet

...it would go:

Browser <-> Cypress <-> Browser Websocket <-> Cypress <-> Internet

But even though this is slow, this is really the easiest way for users to write intercepting code. This is the reason why "we may not be able to provide callbacks" is in there, but it's not really representative of the direction things are going.

We might also expose network interception in such a way that it can be handled by your plugin file, which would be a little faster than sending it through the browser (IPC vs. websockets). This would allow users to shave off milliseconds if they want to intercept all traffic and don't need assertions:

Browser <-> Cypress <-> Plugin File IPC <-> Cypress <-> Internet

@flotwig It would be great if there was also support for navigator.sendBeacon.

@thearegee Any type of HTTP traffic, including beacons, will be covered as part of this. :)

Any update on the timeline for this being released?

@flotwig Any update on the timeline of adding dynamic headers to any http requests? Are there any workarounds available?

I am a newbie with Cypress (and JavaScript), so I hope I am posting this in the correct thread. I am trying to identify ad urls when I visit a website that contain certain key/value pairs in the query string. I used to download the HAR file once the entire test completed, and parse through the JSON. However, if I understand this correctly, I can do this within cypress automatically via route(). I'm still trying to figure it out....

Anyway, from what I have been reading above, capturing anything but XHR requests is not yet available. When I use the Chrome Dev Tools Network Tab, the request is listed under Other, or Type text/html, so it's not even in a JS request.

Would I be able to query ALL requests, once this options is available? Any resources that may steer me in the right direction would be appreciated. Thanks!!!

@dipakspawar @mattvukas It's is in active development, see the PR here for progress updates: https://github.com/cypress-io/cypress/pull/4176

@simi823 Yeah, right now capturing anything except XHRs is not possible. This feature will allow querying/intercepting any type of network request. It should also eliminate some of the parts of the XHR implementation which cause strange behavior in the Network tab.

@flotwig Thanks for the updates.
Just to give you some context: I am trying here at Airbnb to setup automated UI tests. To able to route requests to specific server we need capability to add headers to all http requests.

I wrote my own chrome extension to do this and loaded it in cypress chrome (using plugins/index.js). It does load the plugin, in chrome webtools I can see request being sent with header but when I check server logs, the header doesn't seem to get propagated.

Is there any way to workaround this?

@flotwig Thanks for the updates.

yes, thank you @flotwig! I am working on a project that needs the ability to inject custom http headers into api/websocket requests originating from the application under test & this feature will enable me to use cypress for our e2e testing.

Could any moderators (@brian-mann, @jennifer-shehane or @flotwig) please restrict this discussion? I'm watching this issue because I want updates on it. However, every new comment is a request for help or 芦When will this be available?禄, ruining the watch feature.

Sorry to ruin your watch but with all due respect this is a public ticket not a private watchlist and Its always good practise check of available issues before posting new one. Most of existing issues with api not being captures are getting closed re-pointing to this issue. Maybe my post was not clear but it looks related to this open issue as i have other xhrs which are getting identified ..
I understand you guys might be looking for this ticket for long but usually new examples helps with more info on what is needed for ongoing feature. So please be patient we are also actively looking for solutions for our own project

I can see from your account that you are relatively new to Github, @ayandebbarman, so I understand that you don't have any bad intentions. But I think you'll find that you get better help more quickly if you post questions to Stack Overflow and the Cypress Gitter (https://on.cypress.io/chat) rather than as issues. If it turns out that your problem is actually related to some issue (or represents a new one), _then_ you turn to Github.

Also, see https://stackoverflow.com/help/how-to-ask; it's useful no matter where you post.

I think there are some business scenarios for supporting jsonp requests

You can do this right now using stubs. Albeit it's certainly not as nice as what we did for XHR stubbing, but it is possible and we have several examples of making this work. This is even in a new getting started video we're releasing later this week.

https://github.com/cypress-io/cypress-example-recipes/blob/master/cypress/integration/spy_stub_clock_spec.js

this link is giving me a 404. Did things get moved?

This is also related to #2014 though it's not mentioned here. Will this add support for iframe request stubbing as well? That ticket led me here :)

For anyone looking into awaiting specific GQL requests we got this response from the support team: https://gist.github.com/CypressCecelia/29dfb1bcdc673d00702ab62ee99100e8

Edit: We've tested it and it works nicely. Yay. Thanks @amirrustam and @CypressCecelia

This is also related to #2014 though it's not mentioned here. Will this add support for iframe request stubbing as well? That ticket led me here :)

Yes, it will allow you to stub any type of HTTP request, including requests for subresources like iframes.

@Peterabsolon The solution you received from our support team is what we've recommended to some users. We're interested to know if you've had success with it or if you have any feedback.

It does have some caveats and requires so additional work if you are batching GQL queries. Keep us posted within the Gist's comment section. Thank you.

@amirrustam Works nicely! Thanks. Do you by any chance have a workaround for stubbing GQL requests too? I suppose we need to wait for this PR with that, don't we.

I wrote one, as detailed here: https://siawyoung.com/stub-graphql-cypress. The simple (non-batched) version is pasted here for convenience:

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

Cypress.Commands.add('graphql', stubbedGQLResponses => {
  cy.on('window:before:load', win => {
    const originalFetch = win.fetch
    const fetch = (path, options, ...rest) => {
      if (options.body) {
        try {
          const body = JSON.parse(options.body)
          if (body.operationName in stubbedGQLResponses) {
            const stubbedResponse = stubbedGQLResponses[body.operationName]
            return responseStub(stubbedResponse)
          }
          return originalFetch(path, options, ...rest)
        } catch (e) {
          return originalFetch(path, options, ...rest)
        }
      }
      return originalFetch(path, options, ...rest)
    }
    cy.stub(win, 'fetch', fetch)
  })
})
context('Accounts page', () => {
  beforeEach(() => {
    cy.graphql({
      userAccount: {
        payload: {
          data: {
            user: {
              id: 1,
              first_name: 'Bob',
              last_name: 'Smith',
              __typename: 'User',
            },
          },
        },
      },
    })
    cy.visit('/account')
  })

  it('should display user first and last name correctly', () => {
    cy.get('.first-name').should('have.text', 'Bob')
    cy.get('.last-name').should('have.text', 'Smith')
  })
})

There's also a version that works with batched Apollo requests in the post itself. I hope this helps!

@Peterabsolon My original solution that was shared earlier used cy.spy, but to stub/mock you can use cy.stub. @siawyoung's solution does this, but his method might not be scalable depending on your needs.

You could use something like https://github.com/tgriesser/cypress-graphql-mock if you don't need to wait on the GQL requests using the way I linked because it completely stubs out fetch internally.

Temporary experimental window.fetch polyfill available in v4.9.0

The experimentalFetchPolyfill configuration option has been released as part of 4.9.0. When this option is true, Cypress will automatically replace window.fetch with a polyfill that Cypress can spy on and stub.

You should be able to remove any of the following code regarding fetch support:

Cypress.on('window:before:load', (win) => { 
  delete win.fetch  
})  

or

cy.visit('/', { 
  onBeforeLoad (win) {  
    delete win.fetch    
  },    
})  

And replace it with the below in your configuration file (cypress.json)

{
  "experimentalFetchPolyfill": true
}

You can see a full example of this in use here: https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/stubbing-spying__window-fetch

If you encounter any issues with the new experimentalFetchPolyfill, please open a new issue with a reproducible example.

Is it possible to stub network calls in a way that that doesn't care about whether the app uses fetch or XHR? Seems like in most cases that is an implementation detail we don't want to enshrine in a test.

I was using xhook for stubbing and unfetch to polyfill fetches for Cypress. It doesn't appear that xhook is compatible with experimentalFetchPolyfill.

Using xhook with experimentalFetchPolyfill resulted in every network request failing. Once I removed xhook and just used experimentalFetchPolyfill, only the tests that relied on a stubbed response failed. So still waiting for #4176.

Looks good otherwise.

@alexkrolick We're working on bigger changes as part of #4176 to improve the experience behind network requests in general. The current workaround is meant to be helpful for users starting out so they don't have to dive through issues to figure out how to get fetch working.

@ngbrown could you please provide a small reproducible example showing how experimentalFetchPolyfill fails?

I was using xhook for stubbing and unfetch to polyfill fetches for Cypress. It doesn't appear that xhook is compatible with experimentalFetchPolyfill.

Using xhook with experimentalFetchPolyfill resulted in every network request failing. Once I removed xhook and just used experimentalFetchPolyfill, only the tests that relied on a stubbed response failed. So still waiting for #4176.

Looks good otherwise.

cc @bahmutov

xhook replaces fetch as well and changes the interface, you can support both if you properly restore native/polyfilled-fetch after enabling xhook, something like this below:

return cy
    .readFile("node_modules/xhook/dist/xhook.min.js", { log: false })
    .as("xhook")
    .then((xhook) => {
      Cypress.on("window:before:load", (win) => {
        // save original fetch (which is probably Cypress's polyfill)
        const originalFetch = win.fetch
        win.eval(xhook)
        win.fetch = originalFetch // make sure we restore `fetch` to original
        win.xhook.before((req) => {
          let url = req.url
          if (url.indexOf("someapi") > -1 && req.body) {
            // do stuff
          }
        })
      })
    })

I was using xhook for stubbing and unfetch to polyfill fetches for Cypress. It doesn't appear that xhook is compatible with experimentalFetchPolyfill.
Using xhook with experimentalFetchPolyfill resulted in every network request failing. Once I removed xhook and just used experimentalFetchPolyfill, only the tests that relied on a stubbed response failed. So still waiting for #4176.
Looks good otherwise.

cc @bahmutov

xhook replaces fetch as well and changes the interface, you can support both if you properly restore native/polyfilled-fetch after enabling xhook, something like this below:

return cy
    .readFile("node_modules/xhook/dist/xhook.min.js", { log: false })
    .as("xhook")
    .then((xhook) => {
      Cypress.on("window:before:load", (win) => {
        // save original fetch (which is probably Cypress's polyfill)
        const originalFetch = win.fetch
        win.eval(xhook)
        win.fetch = originalFetch // make sure we restore `fetch` to original
        win.xhook.before((req) => {
          let url = req.url
          if (url.indexOf("someapi") > -1 && req.body) {
            // do stuff
          }
        })
      })
    })

So this turns out not to be as straightforward as I thought. I finally have the two working in my project but it required me to patch xhook if anyone is interested in I can share.

It would be very difficult to provide accurate onResponse callback functions. It's possible for the internal server to know when an HTTP request has been completed, but it's not possible to know whether the browser has actually finished processing it. This may or may not be a problem.

You can correlate it by hashing content length or other things like the URL & use https://developers.google.com/web/tools/chrome-devtools/network/resource-loading to have Cypress "see" in the network tab if the network request finished.

To play devil's advocate, it could also be argued people should not test in this way, because a normal user would not see if a network request has finished, and tests should closely match how a user behaves (maybe waiting for some element on the page to update in response to the network request finishing instead).

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.

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

This 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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rbung picture rbung  路  3Comments

szabyg picture szabyg  路  3Comments

brian-mann picture brian-mann  路  3Comments

tahayk picture tahayk  路  3Comments

zbigniewkalinowski picture zbigniewkalinowski  路  3Comments