cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.72k stars 3.16k forks source link

How to wait for page finish reloading after click? #1805

Closed kkorus closed 6 years ago

kkorus commented 6 years ago

I got form and when I trigger click on submit button this cause to refresh current page. After that I would like to do some assertions. How I know that page finished refreshing and I can start to searching an element to do assertions?

Looking for something like page.waitForNavigation in Puppeteer.

cy.get('#formButton').click() // adds new item and refresh page
// do assertions on page
jennifer-shehane commented 6 years ago

Hey @kkorus I think this FAQ should help answer your question, but basically cy.visit() automatically does was Puppeteer's page.page.waitForNavigation() does - waits until the load event fires.

https://on.cypress.io/using-cypress-faq#How-do-I-wait-for-my-application-to-load

Edit by @jennifer-shehane - Added link to FAQ. Sorry y'all 😬

tivnet commented 6 years ago

@jennifer-shehane

tivnet commented 6 years ago

A workaround I found was

    // click (page starts reloading)
    cy.wait(3000);
    // check the changed content
kkorus commented 6 years ago

@tivnet yeah, I saw that, but it looks like a hack for me rather then solution.

brian-mann commented 6 years ago

Cypress automatically detects and waits for the page to finish loading. You don't need to be concerned with it. It will pause command execution, prevent them from timing out, and then begin re-running them once the page transition is complete.

tivnet commented 6 years ago

@brian-mann

The script below:

Cypress.config(
    {
        "baseUrl": "https://www.artbylena.com"
    }
);
describe("Test", () => {
    const DOM_PRICE_CURRENCY_SYMBOL = ".woocommerce-Price-amount > .woocommerce-Price-currencySymbol";
    it('Switches currencies', () => {
        cy.visit("/paintings/");
        cy.get(DOM_PRICE_CURRENCY_SYMBOL).contains("US$");
        cy.get('.wpglobus-currency-selector').select("CAD");
        // cy.wait(2000);
        cy.get(DOM_PRICE_CURRENCY_SYMBOL).contains("C$");
    });
});

fails without cy.wait:

image

and works with it:

image

davidzambrana commented 6 years ago

@tivnet I faced this sort of thing already, and even though it works setting a cy.wai() in between, you don't want to go that way as setting an implicit wait might delay your tests, you can read more here

Have also in mind that .contains('C$') is not an assertion, and .should('contain', 'C$') is.

Hope this is useful info

tivnet commented 6 years ago

@davidzambrana That's a general "anti-pattern" notice. However, I am not sure how to avoid it in my specific case. When a click results in a different page, I do not need to wait. Here, it's the same page, reloaded by a JS call. As you see, without the wait, it results in a timeout.

P.S. Regarding the .contains - yes, that's not an assertion, but it works perfectly. I use .should when I need more than one assertion within the same DOM node.

davidzambrana commented 6 years ago

In my case for example when I had to check a field that changed its value after some action, I also had to check that a toast showed up, so when I triggered the click, I included in a .then clause that the toast showed up and afterwards that the new value was loaded in the field. But this was a workaround that worked in my case.

bahmutov commented 6 years ago

@tivnet excellent example - in your test, you need something observable that shows how the new page is different after reload. Since the url stays the same, the only difference is that the new page sets the cookie wpglobus-forced_currency to a new value. So here is the test - just use getCookie https://docs.cypress.io/api/commands/getcookie.html and make an assertion.

/// <reference types="cypress" />
describe("Test", () => {
  const DOM_PRICE_CURRENCY_SYMBOL = ".woocommerce-Price-amount > .woocommerce-Price-currencySymbol";
  it('Switches currencies', () => {
      cy.visit("/paintings/");
      cy.contains(DOM_PRICE_CURRENCY_SYMBOL, "US$");
      cy.get('.wpglobus-currency-selector').select("CAD");
      // cy.wait(2000);
      cy.getCookie('wpglobus-forced_currency').should('have.property', 'value', 'CAD')
      cy.contains(DOM_PRICE_CURRENCY_SYMBOL, "C$");
  });
});

The GUI shows how this assertion only passes after the page reloads

screen shot 2018-06-07 at 10 57 25 am

@davidzambrana contains is a valid command that finds element that contains given text. But you can collapse get(selector).contains(text) to just cy.contains(selector, text) https://docs.cypress.io/api/commands/contains.html#Syntax

Bonus: notice /// <reference types="cypress" /> at the top of my JS spec file. In VSCode this will turn on correct IntelliSense which we highly recommend https://docs.cypress.io/guides/tooling/intelligent-code-completion.html#Triple-slash-directives

tivnet commented 6 years ago

@bahmutov Thanks, Gleb, it works perfectly and even adds an additional assertion for the cookie value.

Don't you think that Cypress should wait for the page reload anyway? It knows that the page is being reloaded, but tries to search the old DOM.


Intellisense works in *Storm IDE without additional setup. Do you know if there an easy way to have it for the custom commands, too?


Thanks for the GREAT TESTING SUITE 🥇

bahmutov commented 6 years ago

@tivnet so the problem is that Cypress cannot know how long to wait for potential page reload. Maybe the page will reload in 50ms or 500ms or 5 seconds - there is no way to predict this. So when it can grab the element right away (and it is found!) then it just goes with that.

Glad to hear about WebStorm IDE doing good job understanding types, nice. For custom commands you need to describe them, which is extra work (I prefer just using functions instead of adding custom commands). See https://github.com/cypress-io/add-cypress-custom-command-in-typescript

Thanks a lot for positive feedback, appreciate this

jennifer-shehane commented 6 years ago

Sorry, I failed to paste the FAQ link on loading 😬

https://on.cypress.io/using-cypress-faq#How-do-I-wait-for-my-application-to-load

jindrake commented 5 years ago

I have this page where a user can register and reloads into the same url with the user logged in. I'm doing an assertion with cy.getCookie('uid') after triggering signup requests and a page reload but the assertion triggers before it:

image

jennifer-shehane commented 5 years ago

@jindrake Providing the test code run would be helpful.

Also try asking in our community chat, searching our existing GitHub issues, reading through our documentation, or searching Stack Overflow for relevant answers.

darasandeep91 commented 5 years ago

Hi @jennifer-shehane

I am also seeing the same issue as @jindrake mentioned

In my application SessionID is set in local storage after user clicks on login

I have Code Like This:

Given I login in to App

 And I navigate to addresses

in the step definition of " I navigate to addresses" i am checking if session is not null or not using this code

expect(localStorage.read('CD-SessionID')).not.to.be.null;

The Assertion is being executed right away with out waiting for first step

image

jennifer-shehane commented 5 years ago

Hey @darasandeep91, could you provide the full Cypress test code that is being run? The first and second step?

darasandeep91 commented 5 years ago

HI @jennifer-shehane

Here are the steps i am trying to perform:

  1. I am passing user type to the step and fetching the details from fixtures
  2. Pass the user name and password to custom login command
  3. wait for the login [meaning wait for CD-SessionID to be set in local storage] and then navigate to addresses

when i run the test even before the login step i am getting the error stating expected null not to be null

Given(/^I have logged in to app as "([^"]*)"$/, (userType) => {
       cy.fixture('selfcareUsers').then((user) => {
        const { [userType]: { username } } = user;
        const { [userType]: { password } } = user;
        cy.Login(username, password);
    });
});
Given(/^I navigate to "([^"]*)" page$/, (pageName) => {
    expect(localstorage.read('CD-SessionID')).not.to.be.null;
    cy.fixture('pages').then((pages) => {
        cy.visit((pages[pageName]));
    });
});

Here is code for Cy.login()


Cypress.Commands.add('Login', (userName, password, environment, businessUnit) => {
    cy.visit('/login');
    setEnvironment(environment, businessUnit);
    cy.get('#login').type(userName);
    cy.get('#password').type(password);
    cy.get('button:contains("Sign In")').click();
});
jennifer-shehane commented 5 years ago

I believe the localstorage.read('CD-SessionID') is evaluating to null, so your assertion is accurately failing. Can you verify that?

msty commented 5 years ago

The real solution to the problem in the original post is this:

cy.click('#someButtonToNavigateOnNewPage');

cy.location('pathname', {timeout: 10000})
  .should('include', '/newPage');

cy.click('#YouAreOnNewPage');

@jennifer-shehane Not sure why you link to docs about how cy.visit() works when the issue is clearly about waiting for a page to load after the cy.click()

hawaiikaos commented 5 years ago

I have the same problem except that I don't know the path ahead of time. I'm submitting a form that creates a new object whose id is in the location url, so I can't even use @msty's solution. The only option I have is to slap an arbitrary cy.wait() in there. Why isn't there a generic, 'wait until page loads' function? Or am I missing something?

tarponjargon commented 5 years ago

I'm new-ish to cypress but have been using it for a few months and this is still the thing I struggle with the most. I think testing that a page loads (refresh, nagivation, whatever) is something that should be easily testable. For example, if the application triggers a refresh upon user selection of a select menu, you should be able to easily test that it occurs.

So like:

cy.get('[data-test="shipping-select"]').select("UPS");
cy.pageRefreshed(); // <-- why not?

As mentioned before, I'm supposed to be looking for something that changed in the DOM rather than looking for the page refresh itself. But in this case, there is nothing different. If you try to assert that "UPS" is selected it will be true both before and after the page is refreshed.

Perhaps there is a way to do this or perhaps I am indulging in an anti-pattern.

bahmutov commented 5 years ago

Here is a solution for anyone wondering what to do if the URL stays the same, and the page itself stays the same: add a property to the window object. That property will be gone after reload.

Example: here is the page markup

<body>
  <button id="button">Click to reload</button>
  <script>
    document.getElementById('button').addEventListener('click', () => {
      console.log('clicked')
      setTimeout(() => {
        console.log('reloading page after delay')
        location.reload()
      }, 2000)
    })
  </script>
</body>

and here is the test

it('reloads', () => {
  cy.visit('index.html')
  // mark our window object to "know" when it gets reloaded
  cy.window().then(w => w.beforeReload = true)
  // initially the new property is there
  cy.window().should('have.prop', 'beforeReload', true)
  cy.get('#button').click()
  // after reload the property should be gone
  cy.window().should('not.have.prop', 'beforeReload')
})

Notice how the test waits as long as necessary using .should and built-in https://on.cypress.io/retry-ability

reload

tarponjargon commented 5 years ago

Nice - that's a great idea. Thanks for cypress, it's really alot of fun to work with!

kuisathaverat commented 5 years ago

I've used the @msty solution with one modification, I've to check that the body is there before to access anything cy.get('body');, I'm waiting for an API call after the change the location with cy.wait('@api') and this wait only works if I get the body first.

cy.click('#someButtonToNavigateOnNewPage');

cy.location('pathname', {timeout: 10000})
  .should('include', '/newPage');

cy.get('body');
dark-swordsman commented 4 years ago

I also used the @msty solution but with a little bit of modification, which I think it a bit more specific and nice since you can do whatever logic you want with the function.

cy.location({ timeout: 15000 }).should(($url) => {
    expect($url).to.match(/http:\/\/localhost:3000\/event\/1dc49a776b63be235aecccfd\/1573165592600\/payment\/v2\/.{0,}\/confirmation/)
});

And I use the regex .{0,} for any object id, since the payment object is different for each payment.

vitorpiovezam commented 4 years ago

Could be a feature request a method like page.waitForNavigation({ waitUntil: 'networkidle0' }), from puppetter ? Any other 'solution' seems not correct for me.

HeWei-imagineer commented 4 years ago

I met the same problem, and I use cy.wait() https://docs.cypress.io/api/commands/wait.html#Syntax solved it.

AllanPooley commented 4 years ago

Here's another solution from the Cypress docs:

// Wait for the route aliased as 'getAccount' to respond
// without changing or stubbing its response
cy.server()
cy.route('/accounts/*').as('getAccount')
cy.visit('/accounts/123') // or, in our case: cy.get('.submit-button').click()
cy.wait('@getAccount').then((xhr) => {
  // Make assertions here
})
RiZKiT commented 4 years ago

My solution to check for a reload ist:

cy.window().its('performance.navigation.type').should('eq', 0); // checks for 'TYPE_NAVIGATE'
doReloadStuff();
cy.window().its('performance.navigation.type').should('eq', 1); // checks for 'TYPE_RELOAD'

Look here for further details: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation/type., it's deprecated but still works. The newer option performance.getEntriesByType('navigation')[0].type will need more code.

jennifer-shehane commented 4 years ago

@vitorpiovezam There is an open issue proposing a 'waitForNetworkIdle' feature here: https://github.com/cypress-io/cypress/issues/1773

vijenderoutwork commented 4 years ago

Cypress automatically detects and waits for the page to finish loading. You don't need to be concerned with it. It will pause command execution, prevent them from timing out, and then begin re-running them once the page transition is complete.

Its is not happening in my case I have to give wait cy.wait() to avoid timeout issue to navigate to another page after a click button .

quantuminformation commented 3 years ago

My use case:

what is my best option?

Im using svelte btw

emilong commented 3 years ago

If it's helpful to anyone else, I adapted https://github.com/cypress-io/cypress/issues/1805#issuecomment-525482440 into this custom command:

/**
 * Given a function with some commands that cause the page to change or even
 * just reload, this command runs the command then waits for that page load.
 *
 * Ideally this command should be used sparingly, instead preferring to use
 * matching functionality to wait for reload.
 *
 * Adapted from:
 * https://github.com/cypress-io/cypress/issues/1805#issuecomment-525482440
 */
Cypress.Commands.add("waitForPageLoadAfter", block => {
  // mark our window object to "know" when it gets reloaded
  cy.window().then(win => {
    // eslint-disable-next-line no-param-reassign
    win.beforeReload = true;
  });
  // initially the new property is there
  cy.window().should("have.prop", "beforeReload", true);

  // Run the code that triggers the page reload/change
  block();

  // after reload the property should be gone
  cy.window().should("not.have.prop", "beforeReload");
});

Then use it like:

cy.waitForPageLoadAfter(() => { cy.contains("button", "Click here to reload"); });
cy.contains("page reloaded!").should("exist");

thanks, @bahmutov !

rzegnam commented 3 years ago

How about: npm i -D cypress-wait-until

In cypress/support/commands.js: import 'cypress-wait-until';

And:

cy.click('#someButtonToNavigateOnNewPage');

cy.waitUntil(() => cy.url().should('contain', '/newPage'));

cy.click('#YouAreOnNewPage');
saschanos commented 3 years ago

@emilong, by adding a timeout, it's possible to have a longer running request:

cy.window({timeout: 15000}).should("not.have.prop", "beforeReload");
kkoomen commented 3 years ago

How about: npm i -D cypress-wait-until

In cypress/support/commands.js: import 'cypress-wait-until';

And:

cy.click('#someButtonToNavigateOnNewPage');

cy.waitUntil(() => cy.url().should('contain', '/newPage'));

cy.click('#YouAreOnNewPage');

This worked for me, although I still rather have built-in function, because Cypress is able to know when a page refresh is being made.

bahmutov commented 3 years ago

@kkoomen and @rzegnam what you are describing is built into Cypress already, see https://on.cypress.io/location

cy.location('pathname').should('equal', '/newPage')
kkoomen commented 3 years ago

@bahmutov I was talking about the part where cypress should wait for the page to be reloaded. That part hasn't been solved yet natively by Cypress, right?

bahmutov commented 3 years ago

No, so it all requires just observing something external like URL or even a property, no need to use cypress-wait-until. Another reading I recommend is https://www.cypress.io/blog/2020/11/17/when-can-the-test-submit-a-form/#waiting-for-page-load

vivasaayi commented 3 years ago

Anyone looking answers to this question, implement your own wait until behavior as you do in puppeteer, or webdriver.

When going through the intro & docs it gave an impression that cypress abstracted the behavior, by waiting till all the network request completes, or wait till a given DOM node is loaded.

But its not the case. As discussed in this question, you have add additional logic to handle this anyway.

Also, the logic with setting a property in the window object is not possible if you are writing an automated test for an existing application. And this is a bad hack.

T3rm1 commented 3 years ago

It's really frustrating that such a simple and basic thing as click on an element and wait for the next page to load is so complicated with Cypress.

mwalewsk commented 3 years ago

Try this: cy.contains('button', 'Save').click(); cy.get('[data-ng-show="user.manage"]', { timeout: 10000 }).should('be.visible').then(() => { cy.get('[data-ng-show="user.manage"]').click(); })

diaraujo13 commented 3 years ago

In my case, the only solution to mimic an 'async/await' behavior was to increase execTimeout in cypress.json to a higher value

Perustaja commented 3 years ago

I know that this is REALLY situational but in my version of Vue setup with how my company does, the root html element has a class nprogress-busy that shows during network calls. This is easier for me than checking for a spinner or a skeleton element. Example

Cypress.Commands.add('waitForLoad', () => {
    const loaderTimeout = 60000;

    // Wait until loading is finished if necessary
    cy.get('.nprogress-busy', { timeout: loaderTimeout }).should('not.exist');
});

If not, usually other JS frameworks have similar tricks. My previous tests were for an Angular project and there was some specific state you could check on whether calls were finished. If not, waiting for spinner elements or skeleton elements to disappear was working for me previously. Not good but it gets the job done.

harunurhan commented 3 years ago

@jennifer-shehane

Looks like only initial cy.visit is blocking within a cypress test.

cy.visit('/page');
doSomeActions();
assertResults();

// check results persist after that
cy.visit('/page'); // THIS ONE IS NOT BLOCKING
assertResults(); // SO THIS COULD PASS IMMEDIATELY FOR THE CURRENT PAGE
nasimjontohirov commented 1 year ago

Hi, for my side, worked this: cy.location({ timeout: 10000 }).should(($url) => { expect($url).to.match(/newpage\/.+$/);

Amyx000 commented 1 year ago

The hack is to change the default timeout for cypress, bydefault the timeout for cypress task is 4s, you can change it in cypress.config.js and you don't have to use cy.wait() for that.

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    defaultCommandTimeout: 10000,
  },
})

now each line will run for either the test condition match or 10s and not like the wait() function which wait for exactly the given time.