0

I am trying to avoid the wait() function on my tests.

I'm aware, based on the official docs, that Cypress works async and we dont need to use the wait() function, especially the visit() command handles that because it loads the page and then it moves on.

In my test case I want to handle two things that cause an issue:

  1. open a dropdown menu which is on the Left nav menu. There are 5 menus and I want the 2nd

  2. click on an option to go to another page

it("clicks on the 'Front End' and navigates to the correct page", () => {
  visit(path, {
    timeout: 120000,
    pageLoadTimeout: 120000,
  });

  cy.get(selectors.CATEGORIES)
    .eq(2)
    // I use 'within', because I want to search **inside the 
    // selectors.CATEGORIES.eq(2) and not on the whole DOM**
    .within(() => {
      cy.get(dataCySelector("gridRow")).then(($optionsWrapper) => {
        const parentEl = $optionsWrapper.parent();
        const isMenuOpen = parentEl.css("display");

        // if i dont add the wait(), it selects the 1st 'menu options' 
        // instead of the **3rd**
        cy.wait(3000);

        if (isMenuOpen === "none") {
          console.log("*MENU IS CLOSE I OPEN*");
          cy.contains("category").click(); // OPEN THE MENU
          cy.contains("Front End").should("be.visible")
            .click(); // click  the 'front end'
        } else {
          console.log("*MENU IS OPEN I DONT CLICK ON IT*");
          cy.contains("Front End").should("be.visible")
            .click(); // JUST click on the 'front end'
        }

        cy.url().then(() => {
          cy.urlIncludes("/path/to/menu/option");
          cy.wait(3000);
          cy.contains(dataCySelector("AN_ELEMENT"));
        });
      });
    });
});

So the flow that I have is this:


  1. visit the page that i want
  2. get the CATEGORIES selector (there are 5 menus on the left nav bar)
  3. get the 3rd
  4. Use within so to drill down in into its children (i replaced then because it searched on the whole DOM instead)!!
  5. I get the parent of the 'gridRow' and look if display=none
  6. ! if I don't add the wait(3000), the parentEl is the first menu wrapper!!
  7. After the comparison, click on the 'option link'
  8. the user is redirected to the new page, but again i need the wait() so to check if the element selector exists.

Something must be wrong in here, can I get rid of the waits ? thanks.

Chrome Artie
  • 148
  • 4
Theo Itzaris
  • 4,321
  • 3
  • 37
  • 68

4 Answers4

5

In general, changing .then() to .should() will give you retry and remove the need to wait.

Retry is the smart way of waiting since it only waits as long as the condition is not met.

You must use expect() or assert() to trigger the retry. Cypress patches those chai methods and uses the error thrown to trigger each retry attempt.

Checking the URL

cy.url().should((url) => {
  expect(url).to.include("/path/to/menu/option")  // retries if not true
})
// or 
cy.url().should('include', "/path/to/menu/option")  

// this command can be separate, 
// Cypress has a queue and this line is only run after the above succeeds
cy.contains(dataCySelector("AN_ELEMENT"))  

Checking the visibility of gridRow parent

You have a clean page load at the top of the test, so your menu will not be open initially (the page should load predictably).

The test could simply be:

cy.get(selectors.CATEGORIES)
  .eq(2)
  .within(() => {
    cy.contains("category").click()
    cy.contains("Front End").should("be.visible").click()
    cy.url().should('include', '/path/to/menu/option')
    cy.contains(dataCySelector("AN_ELEMENT"))  
  })

But for the purpose of illustration, this should open the menu if not already open.

cy.get(dataCySelector("gridRow"))
  .parent()
  .then($menu => {
    const menuIsClosed = $menu.css("display") === 'none';
    if (menuIsClosed) {
      $menu.click()            // jQuery click()
    }
  })

The note it selects the 1st 'menu options' instead of the 3rd seems to indicate the menu items are lazy-loaded.

You can overcome that by adding a .should() assertion for the number of menu options (length property).

cy.get(dataCySelector("gridRow"))
  .should('have.length', 3)
cy.contains("Front End").should("be.visible").click()

The whole test

You want to "decouple" steps so you can check them individually.

It's hard to be exact about the code since you have abstracted the basic Cypress commands into custom commands, and not given details, but here's the approach I would take:

cy.get(selectors.CATEGORIES)
  .eq(2)
  .within(() => {

    // open the parent
    cy.get(dataCySelector("gridRow"))
      .parent()
      .then($menu => {
        if ($menu.css("display") === 'none') {
          $menu.click()            // jQuery click()
        }
      })
      .should('not.have.css', 'display', 'none')   // add retry in case of lag

    // ensure all options loaded
    cy.get(dataCySelector("gridRow"))
      .should('have.length', 3)                  

    // click required option
    cy.contains("Front End").should("be.visible").click()
    cy.url().should('include', '/path/to/menu/option')

    // verify page
    cy.contains(dataCySelector("AN_ELEMENT"))  
  })
Fody
  • 23,754
  • 3
  • 20
  • 37
4

I agree with Fody's approach, but I think you want to assert on selectors.CATEGORIES length instead.

Also, .find() can replace .within() to simplify that chain.

This IMO is the cleanest way:

// open menu
cy.get(selectors.CATEGORIES)
  .should('have.length', 'gte', 3)
  .eq(2)
  .find(dataCySelector("gridRow"))
  .invoke('css', 'display')
  .then(display => {
    if(display === 'none') {
      cy.contains("category").click()
    }
  })

// select option
cy.contains("Front End").should("be.visible").click()

// check has naviagted
cy.url().should('include', '/path/to/menu/option')
1

This is a cleaner way for your test that uses intercepts and waits that align with the best practices. As mention before, it is good to have deterministic behavior when it comes to your tests so you know where the failures are exactly.

**NOTE: there is no reproducible example so this may need tweaks for your use

it("clicks on the 'Front End' and navigates to the correct page", () => {
  // intercept and wait on request you need for your test
  cy.intercept('requests').as('requests')

  // not sure what this function does besides visit a page
  // visit waits only on page 'load' event so having the timeout and
  // pageTimeout options may not be needed
  visit(path);
  cy.wait('@requests')

  // if possible use a more unique selector to avoid .eq(2) approach
  cy.get(selectors.CATEGORIES)
    .eq(2)
    // always good to chain an assertion to a query command
    .should('be.visible')
    .within(() => {
        cy.contains("category").click(); // OPEN THE MENU
        cy.intercept('morerequests').as('moreRequests')
        cy.contains("Front End").should("be.visible").click(); // click on the 'front end'
    });
    
  // move this outside of .within() since it doesn't rely
  // on the previous selector and user is redirected to new page
  //   
  cy.location('pathname', "/path/to/menu/option");
  cy.wait('@moreRequests');
  cy.contains(dataCySelector("AN_ELEMENT"))
    .should('be.visible')
   
});


jjhelguero
  • 2,281
  • 5
  • 13
0

For the second wait(), if your problem is linked to the fact that your page needs more time to load than the default timeout, you can still override it.

  cy.url().then(() => {
    cy.url().should("contain", "/path/to/menu/option");
    cy.contains(dataCySelector("AN_ELEMENT"), {timeout: 30_000});
  });

For the first wait, the If/Else is pretty weird. It should be known when it's opened or not, so I would avoid it (by creating for example 2 functions, for both cases) and then, for each case, I would add a {timeout: 30_000} where it's necessary.

Wandrille
  • 6,267
  • 3
  • 20
  • 43