How to stop loop in cypress
break
works only for native for
/while
loops.
To exit early from an .each
loop (as was suggested in the comments), the false
must be returned from the same callback you pass to it, so returning false
from the nested then
callback won't have an effect.
You can't even set a flag in the then
callback and check it in the each
callback, because .each()
command is deep down just a jQuery.fn.each
--- it's synchronous and by the time you'd set up the flag, all iterations will have run (and enqueued the nested commands).
Thus, the only option is not to use .each()
, and use some kind of recursive command. Let's build one.
function walk ( arr, cb, index = 0 ) {
if ( !arr.length ) return;
arr = arr.slice();
const ret = cb(index, arr.shift());
((ret && ret.chainerId) ? ret : cy.wrap(ret))
.then( ret => {
if ( ret === false ) return;
return walk(arr, cb, index + 1);
});
}
/**
* Your callback should return/yield `false` if the loop is to exit early.
*/
Cypress.Commands.add('eachSeries', { prevSubject: 'optional' }, (subject, arr, cb) => {
return subject
// assume `arr` to be `cb`
? walk(subject, arr)
: walk(arr, cb);
});
usage:
describe('test', () => {
it('test', () => {
cy.document().then(doc => {
doc.body.innerHTML = `
<div class="list-item">0</div>
<div class="list-item">1</div>
<div class="list-item">2</div>
<div class="list-item">3</div>
`;
});
var genArr = Array.from({ length: 40 }, (v, k) => k + 1);
// the command can be chained on an existing subject
cy.wrap(genArr).eachSeries((index) => {
return cy.get('.list-item').eq(index)
.invoke('text')
.then(text => {
if (text > 1) return false;
});
});
// or it can start the chain (the array is passed as 1st arg)
cy.eachSeries(genArr, (index) => {
return cy.get('.list-item').eq(index)
.invoke('text')
.then(text => {
if (text > 1) return false;
});
});
// you can also return a promise
cy.eachSeries(['a', 'b', 'c'], (index, value) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(value === 'b' ? false : true);
}, 500);
});
});
// non-thenable callbacks work too
cy.eachSeries(['a', 'b', 'c'], (index, value) => {
return value === 'b' ? false : true;
});
});
});
In the first two examples above, only the first 3 items are looped through and then the loop exits early.
I don't think you need to drive the test with genArr
.
The list of 40 items will itself give an 'array-like' subject to which you can apply the filter()
with a function argument.
Cypress just uses jquery, so refer here for the syntax (not given in Cypress .filter()
docs).
The filter function should return true
to include the item, so again we use jquery (Cypress.$) to construct the expression.
Since you want to break after the first non-zero item, we can just chain .eq(0)
,
it('clicks first item with content not "0"', () => {
cy.get('.list-item')
.filter((i, item) => Cypress.$(item).find('.number').text() !== '0')
.eq(0)
.click() // previous line yields list-item so we can now click it
.find('.number') // then go further into DOM to get the actual number
.invoke('text')
.then(pendingCount => {
cy.get('.list-table').find('.list-table-list-item')
.should('have.length', pendingCount); // Now test the number of table rows
})
});
I tested it with this html fragment, which does not have onClick()
, just a static table.
<ul>
<li class="list-item">
<span class="title">Item1</span>
<span class="number">0</span>
</li>
<li class="list-item">
<span class="title">Item2</span>
<span class="number">4</span>
</li>
<li class="list-item">
<span class="title">Item3</span>
<span class="number">0</span>
</li>
<li class="list-item">
<span class="title">Item4</span>
<span class="number">2</span>
</li>
</main>
</ul>
<div class="list-table">
<div class="list-table-list-item">Pending1</div>
<div class="list-table-list-item">Pending2</div>
<div class="list-table-list-item">Pending3</div>
<div class="list-table-list-item">Pending4</div>
</div>
Alternative selector using :not and :contains
You can also compose jquery :not
and :contains
selectors to produce alternative expressions.
For example the following work in my tests
cy.get('.list-item')
.not(':contains("0")')
.eq(0)
.click()
.find('.number')
...etc
cy.get('.list-item:not(:contains("0"))')
.eq(0)
.click()
.find('.number')
...etc
Note it seems that the Cypress .contains()
command can't be used in this way (to exclude certain values), although that may just be a lack of imagination on my part.
More complicated jquery is going to be harder to debug, so stick with chained Cypress commands where possible (you can hover over the steps in the command log).
I often chain .then(x => console.log(x))
to help test the test.