Iterate Over Elements

There are lots of ways to iterate over page elements using the built-in Cypress commands or cypress-mapopen in new window queries.

using cy.eachopen in new window

<ul id="fruits">
  <li>Kiwi</li>
  <li>Grapes</li>
</ul>
cy.get('#fruits li').each(($el) => {
  expect($el, 'is jQuery').to.satisfy(Cypress.dom.isJquery)
  expect($el, 'is not a DOM element').to.not.satisfy(
    Cypress._.isElement,
  )
})

Unfortunately, cy.each does not change the current subject, so we cannot easily extract data from multiple elements.

using $.each

We can change the current subject by using jQuery $.each to iterate over objects, and by putting the iteration inside cy.then command.

<ul id="fruits">
  <li>Kiwi</li>
  <li>Grapes</li>
</ul>
cy.get('#fruits li').then(($el) => {
  $el.each((k, el) => {
    expect(el, 'is DOM element').to.satisfy(Cypress._.isElement)
  })
})

Unfortunately, cy.then is not a retry-able query command, which might cause unexpected flake if the application is still refreshing the DOM elements.

using cy.invoke and $.each

<ul id="fruits">
  <li>Kiwi</li>
  <li>Grapes</li>
</ul>
cy.get('#fruits li').invoke('each', (k, el) => {
  expect(el, 'is DOM element').to.satisfy(Cypress._.isElement)
})

The cy.invokeopen in new window is a query command, thus it will retry.

using cy.invoke and $.map

We can change the current subject in the chain of commands but using jQuery $.map method, instead of $.each. Unfortunately, the subject is still a jQuery object, only instead of DOM elements, it now holds whatever the $.map(callback) returns.

<ul id="fruits">
  <li>Kiwi</li>
  <li>Grapes</li>
</ul>
cy.get('#fruits li')
  .invoke('map', (k, el) => {
    return el.innerText
  })
  // convert jQuery object to an array
  .invoke('toArray')
  .should('deep.equal', ['Kiwi', 'Grapes'])

using cy.map

Let's bring the cypress-mapopen in new window queries.

<ul id="fruits">
  <li>Kiwi</li>
  <li>Grapes</li>
</ul>
cy.get('#fruits li')
  // current subject is jQuery<Element> object
  .map((el) => {
    expect(el, 'is DOM element').to.satisfy(Cypress._.isElement)
    return el
  })
  // the mapped result is an array of elements
  .should('be.an', 'Array')

If we need to get something specific from the elements, we need to use DOM element methods.

<ul id="fruits">
  <li data-product-id="kiwi">Kiwi</li>
  <li data-product-id="grape">Grapes</li>
</ul>
cy.get('#fruits li')
  .map((el) => {
    return el.dataset.productId
  })
  .should('deep.equal', ['kiwi', 'grape'])

Tip: you can achieve the same result by using the built-in deep property shortcut in cy.map

cy.get('#fruits li')
  .map('dataset.productId')
  .should('deep.equal', ['kiwi', 'grape'])

using cy.mapInvoke

<ul id="fruits">
  <li data-product-id="kiwi">Kiwi</li>
  <li data-product-id="grape">Grapes</li>
</ul>
cy.get('#fruits li')
  .map((el) => {
    return el.getAttribute('data-product-id')
  })
  .should('deep.equal', ['kiwi', 'grape'])

The same "invoke" method on each DOM element can be done using cy.mapInvoke

<ul id="fruits">
  <li data-product-id="kiwi">Kiwi</li>
  <li data-product-id="grape">Grapes</li>
</ul>
cy.get('#fruits li')
  .mapInvoke('getAttribute', 'data-product-id')
  .should('deep.equal', ['kiwi', 'grape'])

We can even wrap each DOM element using jQuery to be able to its methods, if you really want to.

<ul id="fruits">
  <li data-product-id="kiwi">Kiwi</li>
  <li data-product-id="grape">Grapes</li>
</ul>
cy.get('#fruits li')
  .map((el) => {
    return Cypress.$(el).attr('data-product-id')
  })
  .should('deep.equal', ['kiwi', 'grape'])

Finally, let's take the elements, make a jQuery from each one -> we get an array of jQuery objects, each with one element. Then we can use cy.mapInvoke to call the jQuery method attr on each jQuery object.

<ul id="fruits">
  <li data-product-id="kiwi">Kiwi</li>
  <li data-product-id="grape">Grapes</li>
</ul>
cy.get('#fruits li')
  .map(Cypress.$)
  .should('be.an', 'Array')
  .mapInvoke('attr', 'data-product-id')
  .should('deep.equal', ['kiwi', 'grape'])