Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

html

dom

cypress

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?

like image 519
George Katsanos Avatar asked Oct 26 '25 18:10

George Katsanos


1 Answers

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
}

Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!