Reputation: 14175
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
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
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