George Katsanos
George Katsanos

Reputation: 14175

Clicking list of elements on Cypress using a for loop without using each

I have a list of options/buttons which I need to be sure they're all set to a specific value. Every wrapper can have several buttons but the first one is always what I want set before my tests are run.

Therefore I need loop these wrappers and target the first child / button of each of them.

Typically this would be a case for each() but Cypress errors after the first click - the DOM re-renders and Cypress can't find the remaining buttons.

Therefore I need an alternative solution. One of them would be a classic for loop. Here's the code:

<div>
  <div class="ab-test-switch__experiment">
    <div class="ab-test-switch__buttons"><button type="button" class="button ab-test-switch__button button--primary button--lg" data-variant="49_a">
      49_a
      </button><button type="button" class="button ab-test-switch__button button--secondary button--lg" data-variant="49_b">
      49_b
      </button>
    </div>
  </div>
  <div class="ab-test-switch__experiment">
    <div class="ab-test-switch__experiment__title">
      <div class="v-popover">
        <div class="trigger" style="display: inline-block;">
          <p data-v-d7151c42="">(detail)</p>
        </div>
      </div>
    </div>
    <div class="ab-test-switch__buttons"><button data-v-7fea8896="" data-v-d7151c42="" type="button" class="button ab-test-switch__button button--secondary button--lg" data-variant="old_business_section" data-v-5c4d7450="">
      old_business_section
      </button><button data-v-7fea8896="" data-v-d7151c42="" type="button" class="button ab-test-switch__button button--primary button--lg" data-variant="new_business_section" data-v-5c4d7450="">
      new_business_section
      </button>
    </div>
  </div>
  <div class="ab-test-switch__experiment">
    <div class="ab-test-switch__experiment__title">
      <h2 data-v-d7151c42="" data-v-5c4d7450="">53_mobile_banner</h2>
      <div data-v-d7151c42="" class="v-popover" data-v-5c4d7450="">
        <div class="trigger" style="display: inline-block;">
          <p data-v-d7151c42="">(detail)</p>
        </div>
      </div>
    </div>
    <div class="ab-test-switch__buttons"><button type="button" class="button ab-test-switch__button button--secondary button--lg" data-variant="none" data-v-5c4d7450="">
      none
      </button><button type="button" class="button ab-test-switch__button button--primary button--lg" data-variant="mobile_banner" data-v-5c4d7450="">
      mobile_banner
      </button>
    </div>
  </div>
  <div class="ab-test-switch__experiment">
    <div class="ab-test-switch__experiment__title">
       <div class="v-popover">
        <div class="trigger" style="display: inline-block;">
          <p>(detail)</p>
        </div>
      </div>
    </div>
    <div class="ab-test-switch__buttons"><button type="button" class="button ab-test-switch__button button--primary button--lg" data-v-5c4d7450="">
      explore_a
      </button><button type="button" class="button ab-test-switch__button button--secondary button--lg">
      explore_b
      </button>
    </div>
  </div>
</div>
  // this fails as the DOM changes after each click
  cy.get('.ab-test-switch__buttons > :nth-child(1)').each(($el) => {
    cy.wrap($el).click()
    cy.wait(1000) // didn't help, there's no race condition here
  })
before(() => {
  cy.visit('/company_profile_frontend/ab-test-switch')
  // cy.get('.ab-test-switch__buttons > :nth-child(1)').click({ multiple: true, force: true }) (this didn't work either)
  const numberOfAbTests = document.getElementsByClassName('ab-test-switch__buttons').length

  for (let i = 1; i <= numberOfAbTests; i += 1) {
    cy.get(`.ab-test-switch__buttons > :nth-child(${i})`).click().pause()
  }
  // cy.get('.ab-test-switch__buttons > :nth-child(1)').each(($el) => {
  //   cy.wrap($el).click().pause()
  //   // eslint-disable-next-line cypress/no-unnecessary-waiting
  //   cy.wait(1000)
  // }) (another failed attempt)
})

Any other way to make this work?

Upvotes: 4

Views: 8182

Answers (2)

user14783414
user14783414

Reputation:

The for-loop works as long as numberOfAbTests is known when the test starts and not calculated from a previous command, or fetched asynchronously.

it('clicks buttons which become detached', () => {

  const numberOfAbTests = 2;
  ...
  for (let i = 1; i <= numberOfAbTests; i += 1) {    // nth-child is 1-based not 0-based
    cy.get(`.ab-test-switch__buttons > :nth-child(${i})`)  
      .click()
  }
})

is equivalent to

it('clicks all the buttons', () => {
  cy.get('.ab-test-switch__buttons > :nth-child(1)').click()
  cy.get('.ab-test-switch__buttons > :nth-child(2)').click()
})

because Cypress runs the loop and queues the button click commands, which are then run, as you say, asynchronously.


When numberOfAbTests is not statically known, you need to use recursion as @RosenMihaylov says, but his implementation misses out a key factor - you must re-query the buttons in situations that they become detached/replaced.

it('clicks all the buttons', () => {

  cy.get('.ab-test-switch__buttons')
    .then(buttons => {
      const count = buttons.length;  // button count not known before the test starts

      clickButtonsInSuccession();

      function clickButtonsInSuccession(i = 1) {
        if (buttonIndex <= count) {
          const buttonSelector = `.ab-test-switch__buttons > :nth-child(${i})`;
          cy.get(buttonSelector)                           // re-query required here
            .click()
          clickButtonsInSuccession(i +1);
        }
      }
    })
})

This assumes .ab-test-switch__buttons is the container for the buttons, so DOM is structured something like this

<div class=".ab-test-switch__buttons">
  <button>one</button>
  <button>two</button>
</div>

Looking at the expanded code

You need to get the count of tests by querying the DOM after it has loaded, but

const numberOfAbTests = document.getElementsByClassName('ab-test-switch__buttons').length;

is synchronous code and it runs before any commands, including cy.visit(), so it returns 0.

Think of the test running in two passes, the first pass all the synchronous code runs, then the commands run.

The exception is synchronous code inside callbacks, such as .then(callbackFn) which effectively pushes the callbackFn onto the command queue to run sequentially between commands.

You can use a command to query for numberOfAbTests and pass the value into a .then()

cy.get('.ab-test-switch__buttons')
  .its('length')
  .then(numberOfAbTests => {

    for (let i = 1; i <= numberOfAbTests; i += 1) {
      ...
    }
  })

or visit and count in before() then loop inside it(),

let numberOfAbTests;

before(() => {
  // All commands here run before it() starts
  cy.visit('../app/ab-test-switch.html').then(() => {
    numberOfAbTests = Cypress.$('.ab-test-switch__buttons').length;
  })
})

it('tests the button', () => {

  for (let i = 1; i <= numberOfAbTests; i += 1) {
    ...
  }
})

or forget about counting the tests and just use .each()

cy.get('.ab-test-switch__buttons')
  .each($abTest => {
    cy.wrap($abTest).find('button')
      .each($button => {
        cy.wrap($button).click(); 
      })
  })

Selector

The selector .ab-test-switch__buttons > :nth-child(${i}) is problematic because the index i refers to the abTest group of buttons, but you are trying to use it to click individual buttons.

So using the for-loop,

for (let i = 0; i < numberOfAbTests; i += 1) {      // NB :nth() is 0-based
                                                    // in contrast to :nth-child()
                                                    // which is 1-based

  cy.get(`.ab-test-switch__buttons:nth(${i}) > button`)
    .eq(0).click()                                       // click button A

  cy.get(`.ab-test-switch__buttons:nth(${i}) > button`)
    .eq(1).click()                                       // click button B
}

Upvotes: 5

Rosen Mihaylov
Rosen Mihaylov

Reputation: 1427

Your approach for a for loop is good. Since the page re-renders after a click - a recursion as a chain to a cypress command is better. For example:

cy.get(".ab-test-switch__buttons > :nth-child(1)").then(
   (buttons) => {
       clickButtonsInSuccession();
       function clickButtonsInSuccession(buttonsClicked = 0) {
          if (buttonsClicked < buttons.length) {
             cy.wrap(buttons[buttonsClicked]).click().pause();
             // eslint-disable-next-line cypress/no-unnecessary-waiting
             cy.wait(1000);
             buttonsClicked++;
             clickButtonsInSuccession(buttonsClicked);
          }
      }
   }
);

Upvotes: 0

Related Questions