Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to stop loop in cypress

I have a loop checking 40 items. I want to stop my loop, when I am finding the first element, which > 0 This is my code

var genArr = Array.from({ length: 40 }, (v, k) => k + 1);

cy.wrap(genArr).each((index) => {
    cy.get('.list-item').eq(index - 1).find('.number')
        .invoke('text')
        .then(text => {
            const pendingCount = text;
            cy.get('.list-item').eq(index - 1).click();
            cy.get('.list-table').find('.list-table-list-item')
                .should('have.length', pendingCount);
            if (text > 0) break
         });
    });

});

But I have a syntax error. Help me pls, How can I break my loop

like image 417
Narine Poghosyan Avatar asked Sep 05 '19 13:09

Narine Poghosyan


2 Answers

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.

like image 163
dwelle Avatar answered Sep 22 '22 00:09

dwelle


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.

like image 42
Richard Matsen Avatar answered Sep 24 '22 00:09

Richard Matsen