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

wait for the network idle #1773

Open
intelcoder opened this issue May 24, 2018 · 32 comments
Open

wait for the network idle #1773

intelcoder opened this issue May 24, 2018 · 32 comments
Labels
stage: proposal 💡 No work has been done of this issue topic: network type: feature New feature that does not currently exist

Comments

@intelcoder
Copy link

Current behavior:

Currently, only way to wait for certain time is that using wait function. However, it is not reliable because internet speed and other variable is always different.

Desired behavior:

I would like to have wait for the network idle function.

Versions

^2.1.0

@jennifer-shehane
Copy link
Member

This seems to be referencing puppeteer's options for their equivalent "visit" method. https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagegotourl-options

waitUntil <string|Array<string>> When to consider navigation succeeded, defaults to load. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:

  • load - consider navigation to be finished when the load event is fired.
  • domcontentloaded - consider navigation to be finished when the DOMContentLoaded event is fired.
  • networkidle0 - consider navigation to be finished when there are no more than 0 network connections for at least 500 ms.
  • networkidle2 - consider navigation to be finished when there are no more than 2 network connections for at least 500 ms.

This is quite brilliant actually. I wonder if @brian-mann knows a way to do this today in our current API, but this would likely be something that goes into our network refactor, at least I'm a 👍on adding support for something like this. #687

@jennifer-shehane jennifer-shehane added the type: feature New feature that does not currently exist label May 25, 2018
@brian-mann
Copy link
Member

brian-mann commented May 25, 2018

The use case of network idle is not really about cy.visit. It'd be like as a catch all for that when users say: "how do I wait for all my network requests without specifying them"

Example:

cy.get('#search').type('foo')
cy.wait('@networkIdle0')

It sounds good in theory but this ultimately runs us right back to the same problem with non-determinism.

If you're waiting for the network to become idle who's to say that it's not already idle. What if the network request hasn't gone out yet? What if your app is polling for things that aren't necessarily related to the thing you want to wait on?

What if your app makes multiple requests but in between those request there's a delay? What about websockets? In that case you may not ever know when data is about to come down the pipe from the server.

Maybe there are answer for this, or maybe it works better than a preliminary guess, but I still prefer explicitly saying what you want to wait on - because then we know whether it has or hasn't happened. If it has happened we immediately resolve, and if is hasn't we wait until we know it has.

Networkidle is a better catch-all, but since its less specific it could lead to non-deterministic results that are left up to chance and are hard to debug.

@brian-mann
Copy link
Member

@jennifer-shehane to answer your original question - yes we can do this without any kind of refactor or rewrite because it can be done exclusively at the proxy layer without changing much in the driver.

@jennifer-shehane
Copy link
Member

I also prefer to explicitly wait on specific XHR requests as I believe this is a stronger testing strategy, but some of our users seem to be continually frustrated with having to do this and want a 'wait for all requests' solution.

@blm126
Copy link

blm126 commented Oct 3, 2018

I think adding this would be a great idea. Calling cy.visit() right now causes in progress network requests to be aborted. I can guarantee that the request is launched synchronously so the non-determinism draw back wouldn't matter at all.

@mlc-mlapis
Copy link

mlc-mlapis commented Oct 28, 2018

@jennifer-shehane @brian-mann ... actually in relation to one of our case in which I am solving ... when to exactly call cy.get and to have the hundred % of confidence that elements are in the correct place of DOM, I am thinking about the possibility how to control all XHR requests and exactly to know when the app state is stable.

Our app is Angular based SPA application where almost everything is based on async XHR calls, and where the whole DOM is created and maintained via JS ... so even a single click on a button could lead to several XHR calls which are resolved in unknown time and the whole app logic uses observable / subscribe pattern to run the code in the moment when all dependency things are ready .... and this moment is the right one when we need to call that cy.get.

It wouldn't be a big problem to create some state management which would allow us to exactly know when the app state is stable for each tested functionality. We can do it right know but it would be much more powerful if we could use some standardized way for it ... to use a feature like hey, cy.get ... wait till the moment when this / that state is stable.

It's not the super heroes generic solution like network idle ... but I'm afraid there is no such thing in reality. What do you think about it?

@jennifer-shehane jennifer-shehane added the stage: proposal 💡 No work has been done of this issue label Nov 19, 2018
@paul-sachs
Copy link

I have a similar issue where i have additional resources being pulled in via requirejs and I'd like to wait until all resources are downloaded before continuing the test. I hope this feature request would copy that case as well.

@paul-sachs
Copy link

paul-sachs commented Nov 28, 2018

As a workaround, this has worked for me:

cy.window().then({
  timeout: 120000
}, win => new Cypress.Promise((resolve, reject) => win.requestIdleCallback(resolve)));

This uses the requestIdleCallback method, which is experimental.

Also weird, because I would have expected the options is the second arg of then (as the typescript types define it) but the docs (and the impl) say otherwise.

@bierik
Copy link

bierik commented Mar 15, 2019

@jennifer-shehane Here

I also prefer to explicitly wait on specific XHR requests as I believe this is a stronger testing strategy

you mentioned how to wait for a specific XHR request. Is this only working for network stubs using cy.route as documented here: https://docs.cypress.io/api/commands/wait.html#Alias or is there a way to wait for actual network requests.
Because my problem is that I have to wait for a specific XHR request to finish before reloading the page.

@baleeds
Copy link

baleeds commented Mar 15, 2019

@bierik I'm trying to solve the same problem right now with no luck. Let me know if you figure it out...

I have this code:

it('can be marked as incomplete', () => {
    cy.server();
    cy.route('/graphql').as('api');

    cy.get('[data-hook=flagArticleButton]').click();
    cy.wait('@api');
    cy.get('[data-hook=incompleteFlag]').contains('Steve Steve');

    cy.reload();
    // more stuff
  });

The expected behavior is that after clicking the button, cypress would wait for the api call to finish. In reality, it never catches the api call.
I can't rely on the presence of the incompleteFlag because the app does optimistic updating.

Here's my runner:

image

@bierik
Copy link

bierik commented Mar 19, 2019

Hi @baleeds I guess we have a different problem. You are trying to await a request mocked by the cy.server and cy.route command. But I have a real backend which is running during the tests. And I'm wondering if there is a way to await a request to a real backend.

@baleeds
Copy link

baleeds commented Mar 19, 2019

@bierik I also have a real backend. The docs indicate that cy.server and cy.route can apply to a non-stubbed backend, which I take to mean a real backend. I could definitely be wrong though, because I don't feel like any of the examples explicitly say they apply to a real backend.

Whether or not you choose to stub responses, Cypress enables you to declaratively cy.wait() for requests and their responses.

cy.server()
cy.route('activities/*', 'fixture:activities').as('getActivities')
cy.route('messages/*', 'fixture:messages').as('getMessages')

// visit the dashboard, which should make requests that match
// the two routes above
cy.visit('http://localhost:8888/dashboard')

// pass an array of Route Aliases that forces Cypress to wait
// until it sees a response for each request that matches
// each of these aliases
cy.wait(['@getActivities', '@getMessages'])

The above is from the docs on network requests. It seems like they are returning a fake response here, but the text says it should work with or without stubbing.

But I still couldn't get it to work in my testing.

@bierik
Copy link

bierik commented Mar 20, 2019

@baleeds I did try it out the cy.wait and alias approach, but as you experienced, it's not working for me as well. I guess the Whether or not you choose to stub responses is a bit misleading the purpose of cy.wait. Also, the examples are using fixture:whatever in the cy.route part which indicates that it's only working for stubbed responses. The requestIdleCallback approach from @psachs21 seems to work for me.

@m-burger
Copy link

m-burger commented Feb 5, 2020

Hello,
Do you have any news on this feature ?

I am really interested in it as I try to deploy Cypress on a medium sized project.

Thank you

@NPC
Copy link

NPC commented Feb 7, 2020

Hello,

Trying to “wait for all current requests” as well, where number of requests is unknown. My use case is a quick test to go through all app's pages, and, when all requests complete, check for a standard error popup showing. I can't find a way to do it without specifying an arbitrary delay in milliseconds (which makes the run-though very slow).

I've seen references to alias.all “undocumented feature” in #3516 (with cy.wait, but on additional questions the user is instructed to “please check the docs”, odd for an undocumented feature) and #4700 (with cy.get this time — not useful for my case).

I'd be convenient to create a catch-all aliased route, and cy.wait for .all currently known requests on that alias. From the above discussion looks like it's not currently possible, but perhaps it's a simple enhancement?

@nik-lampe
Copy link

nik-lampe commented Feb 23, 2020

I came up with a workaround solution which seems to work for me so far.

On the site I'm testing I'll keep track of all the running network requests I need to wait for by just incrementing and decrementing a value on a window property on start / end of the request.

The app I'm currently building is based on https://github.com/marmelab/react-admin, so it's easy, as they already do this for their loading indicator. Just implement another redux saga, that listens on the dispatched fetch start and fetch end actions and writes the loading state into window.fetch_loading or something like this.

You might have to implement your own logic for this, which might need a little bit of setup, but at least it works and currently is the only solution I am satisfied with.

I am only running this when the Node Env is not production, because I won't need it in production build.

Now I can use something like https://github.com/NoriSte/cypress-wait-until to just wait until this value is 0.
I am waiting for it to be > 0 first, because it might happen, that it gets called before the first network request starts and then it might immediately resolve.

cy.waitUntil(() => cy.window().then(win => win.fetch_loading > 0))
cy.waitUntil(() => cy.window().then(win => win.fetch_loading === 0))

This is not beautiful, but it works. I am always open for other, more elegant solutions though.

@JasonTheAdams
Copy link

JasonTheAdams commented Mar 18, 2020

We're running into this situation where a previous test loads a dashboard which has a fist-full of GraphQL requests, the next test then starts while the server is still processing the requests from the previous test. The responses then come in (during the wrong test) and break things.

Ideally, we would have a way of idling a a test until all requests have finished. We could stub the requests, but really the point of the test is to just check that it got to the dashboard, not test the requests the dashboard itself (including its requests).

I did try the snippet here: #1773 (comment) but it didn't work, unfortunately.

@karuhanga
Copy link

karuhanga commented Mar 29, 2020

Curious as to whether there would be any downsides with this dirty approach;

Cypress.Commands.add('runAndAwait', actionTriggeringGetRequests => {
    const requestId = `apiRequest-${uuid()}`;

    cy.server();
    cy.route('**').as(requestId);  // start recording requests
    actionTriggeringGetRequests();
    cy.wait(`@${requestId}`);
    cy.route('**').as('untrackedRequest'); // stop recording requests
  }
)

@stahloss
Copy link

We prefer to use Wiremock as a stub server, because it integrates with Spring Cloud Contract well. So a feature like this would be much appreciated.
Now we're checking getAllAngularTestabilities, but it's not the prettiest.

@johnpolacek
Copy link

Couldn't you add a command that checks document.readyState after taking an action (e.g. click a link) that causes page navigation?

@JasonTheAdams
Copy link

I also prefer to explicitly wait on specific XHR requests as I believe this is a stronger testing strategy, but some of our users seem to be continually frustrated with having to do this and want a 'wait for all requests' solution.

@jennifer-shehane I notice here that you're differentiating between waiting on specific XHR requests versus all requests. Is there a way to wait for specific requests without mocking them? How could I tell Cypress "Hey, visit this page, then wait for request X, Y, and Z to finish before moving on"?

@DamienCassou
Copy link

DamienCassou commented May 11, 2020

@JasonTheAdams without stubbing sure: in an end-to-end test, you can use cy.route(verb, path).as("alias") and then cy.wait("@alias"). This way, the requests will still go to your server and you can wait for them.

How would you specify the requests to wait for without mocking? And what is the problem with mocking?

Documentation is at: https://docs.cypress.io/api/commands/route.html.

@stahloss
Copy link

@JasonTheAdams without stubbing sure: in an end-to-end test, you can use cy.route(verb, path).as("alias") and then cy.wait("@alias"). This way, the requests will still go to your server and you can wait for them.

How would you specify the requests to wait for without mocking? And what is the problem with mocking?

Documentation is at: https://docs.cypress.io/api/commands/route.html.

I'm currently using the routes and aliases and it's cumbersome. An example:

    Gegeven de volgende stubs:
      | Alias            | Method | URI                                | Bestand                        | Status |
      | getVervoerders   | GET    | /vervoerders                       | vervoerders/vervoerders.json   | 200    |
      | postBuffertijd   | POST   | /basisversie-normen/1/buffertijden | buffertijd/buffertijd-400.json | 400    |
    Wanneer er op "Toevoegen" geklikt wordt
    En er op "Opslaan" geklikt wordt
    En er gewacht wordt tot de volgende requests zijn afgehandeld:
     | getVervoerders   |
     | postBuffertijd   |

It's Dutch, but you probably recognize the Given/When/Then. So what happens here is I have to define all my stubs and explicitly wait for specific ones to finish. My cucumber scenario gets polluted with technical stuff.
It's much easier to just define stubs, preferably globally (in Wiremock) and have the test framework deal with XHR requests.

@vishalsangave
Copy link

Curious as to whether there would be any downsides with this dirty approach;

Cypress.Commands.add('runAndAwait', actionTriggeringGetRequests => {
    const requestId = `apiRequest-${uuid()}`;

    cy.server();
    cy.route('**').as(requestId);  // start recording requests
    actionTriggeringGetRequests();
    cy.wait(`@${requestId}`);
    cy.route('**').as('untrackedRequest'); // stop recording requests
  }
)

I think this will wait for first request to complete if you need all the request it won't wait, we need to wait for all request to complete.

@cooleiwhistles
Copy link

Some sort of functionality to wait for idle network or all xhr requests to finish would be very helpful. I am also in a situation of writing a test that clicks through each menu item on our site, and verifies successful page load. So knowing each route to watch for is not viable, as pages can change often.

@actuallymentor
Copy link

Dropping in to point out that many developers use services like firebase which do realtime syncing, which is not easy to stub/alias since the requests are not in your control. Yet many would like to do something like cy.wait( /* firebase has stopped syncing */ ).

@kaisermann
Copy link

kaisermann commented Mar 16, 2021

I'm also trying to use cy.intercept to intercept firebase changes, but it seems to be very unstable. Is there any kind of article about testing this kind of apps where you don't have complete control over the requests? I find cypress-firebase kind of overkill for my use case.

@JGJP
Copy link

JGJP commented Apr 6, 2021

I have managed a workaround thanks to this article. My code is modified for my needs but I think this function should be good enough for most use cases. It basically polls the browser's resources to see if it's still loading something or not. It checks every 2 seconds and will resolve the promise when it has seen the same thing 3 times (no new progress). You can also pass it an array of specific resources that you want to wait for and the number of each resource that you require.

Cypress.Commands.add("waitForResources", function (resources = []) {
	const globalTimeout = 20000
	const resourceCheckInterval = 2000
	const idleTimesInit = 3
	let idleTimes = idleTimesInit
	let resourcesLengthPrevious
	let timeout

	return new Cypress.Promise((resolve, reject) => {
		const checkIfResourcesLoaded = () => {
			const resourcesLoaded = cy.state("window")
				.performance.getEntriesByType("resource")
				.filter(r => !["script", "xmlhttprequest"].includes(r.initiatorType))

			const allFilesFound = resources.every(
				resource => {
					const found = resourcesLoaded.filter(
						resourceLoaded => {
							return resourceLoaded.name.includes(resource.name)
						},
					)
					if (found.length === 0) {
						return false
					}
					return !resource.number || found.length >= resource.number
				},
			)

			if (allFilesFound) {
				if (resourcesLoaded.length === resourcesLengthPrevious) {
					idleTimes--
				}
				else {
					idleTimes = idleTimesInit
					resourcesLengthPrevious = resourcesLoaded.length
				}
			}
			if (!idleTimes) {
				resolve()
				return
			}

			timeout = setTimeout(checkIfResourcesLoaded, resourceCheckInterval)
		}

		checkIfResourcesLoaded()
		setTimeout(() => {
			reject()
			clearTimeout(timeout)
		}, globalTimeout)
	})
})

usage:

cy.waitForResources() // wait for networkidle
cy.waitForResources([
	{ name: "fa-solid-900.woff2" },
	{ name: "fonts.gstatic.com/s/worksans", number: 2 }, // won't resolve until it has loaded 2 matching resources
])

@Perustaja
Copy link

I have managed a workaround thanks to this article. My code is modified for my needs but I think this function should be good enough for most use cases. It basically polls the browser's resources to see if it's still loading something or not. It checks every 2 seconds and will resolve the promise when it has seen the same thing 3 times (no new progress). You can also pass it an array of specific resources that you want to wait for and the number of each resource that you require.

<snipped code>

This worked, personally I was using a different loading indicator specific to Vue. Initially I was just checking if .nprogress-busy was existent, however to my dismay this class can come up after being removed (say for 3 API calls it will come and go 3 times). So I'm using this approach. I just can't be bothered to sit here and intercept 6+ API calls for one test at a time, and then the test will fail if other requests are added. So thumbs up for the approach, not perfect but it's a blanket approach that saves time.

@dannycoulombe
Copy link

dannycoulombe commented Aug 16, 2021

I rewrote @JGJP 's solution using a PerformanceObserver of type "fetch" and it ended up to be very fast and effective. It succeeds if the total amount of fetch resources match the total amount of queries detected by the PerformanceObserver. It tries 3 times to be sure, because sometime when a promise end, another promise could be launch a few milliseconds afterwards. Otherwise, the tries variable could be completely avoided.

Otherwise, I believe this is a solid solution and should be implemented in Cypress with some adjustments.

let totalRunningQueries = 0;
const observer = new PerformanceObserver((list) => {
	for (const entry of list.getEntries()) {
		if (entry.initiatorType === "fetch") {
			totalRunningQueries++;
		}
	}
});

observer.observe({
	entryTypes: ["resource"]
});

Cypress.Commands.add("waitForResources", function (resources = []) {
	let tries = 0;
	return new Cypress.Promise((resolve, reject) => {
		const check = () => {
			const requests = window.performance
				.getEntriesByType("resource")
				.filter(n => n.initiatorType === "fetch");
			if (requests.length === totalRunningQueries) {
				tries++;
				if (tries === 3) {
					resolve();
				} else {
					setTimeout(check, 100);
				}
			} else {
				tries = 0;
				setTimeout(check, 100);
			}
		};
		check();
	});
});

@slikts
Copy link

slikts commented Nov 4, 2021

The PerformanceObserver solution is quite neat and it would be useful to have it as built-in command.

@sandeepvortexa
Copy link

Use this plugin to wait for network to idle, Thank you @bahmutov !!!

https://www.npmjs.com/package/cypress-network-idle

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stage: proposal 💡 No work has been done of this issue topic: network type: feature New feature that does not currently exist
Projects
None yet
Development

No branches or pull requests