Getting Text from List of Elements

Imagine we have HTML elements.

<div>
  <div class="matching">first</div>
  <div>second</div>
  <div class="matching">third</div>
  <div class="matching">fourth</div>
  <div>fifth</div>
</div>

We want to get the text values of elements with the class matching

cy.get('.matching')
  .should('have.length', 3)
  .then(($els) => {
    // we get a list of jQuery elements
    // let's convert the jQuery object into a plain array
    return (
      Cypress.$.makeArray($els)
        // and extract inner text from each
        .map((el) => el.innerText)
    )
  })
  .should('deep.equal', ['first', 'third', 'fourth'])

// let's use Lodash to get property "innerText"
// from every item in the array
cy.log('**using Lodash**')
cy.get('.matching')
  .should('have.length', 3)
  .then(($els) => {
    // jQuery => Array => get "innerText" from each
    return Cypress._.map(Cypress.$.makeArray($els), 'innerText')
  })
  .should('deep.equal', ['first', 'third', 'fourth'])

cy.log('**using Lodash to convert and map**')
cy.get('.matching')
  .should('have.length', 3)
  .then(($els) => {
    expect(Cypress.dom.isJquery($els), 'jQuery input').to.be.true
    // Lodash can iterate over jQuery object
    return Cypress._.map($els, 'innerText')
  })
  .should('be.an', 'array')
  .and('deep.equal', ['first', 'third', 'fourth'])

You can extract the logic to get the text from the list of elements into its own utility function.

const getTexts = ($el) => {
  return Cypress._.map($el, 'innerText')
}
cy.get('.matching')
  .should('have.length', 3)
  .then(getTexts)
  .should('deep.equal', ['first', 'third', 'fourth'])

So the final advice to extract text from the list of found elements is to use the Lodash _.map method.

cy.get('.matching').then(($els) =>
  Cypress._.map($els, 'innerText'),
)

Find this recipe in the video Get Text From A List Of Elementsopen in new window.

Array vs jQuery object

Note: we cannot use cy.get(...).then(Cypress.$.makeArray).then(els => ...) to convert from jQuery object first, because the result of the $.makeArray is an array of elements, and it gets immediately wrapped back into jQuery object after returning from .then

<div>
  <div class="matching">first</div>
  <div>second</div>
  <div class="matching">third</div>
  <div class="matching">fourth</div>
  <div>fifth</div>
</div>
cy.get('.matching')
  .then(($els) => {
    expect(Cypress.dom.isJquery($els), 'jQuery object').to.be
      .true
    const elements = Cypress.$.makeArray($els)
    expect(Cypress.dom.isJquery(elements), 'converted').to.be
      .false
    expect(elements, 'to array').to.be.an('array')
    // we are returning an array of DOM elements
    return elements
  })
  .then((x) => {
    // but get back a jQuery object again
    expect(Cypress.dom.isJquery(x), 'back to jQuery object').to
      .be.true
    expect(x, 'an not a array').to.not.be.an('array')
    expect(x.length, '3 elements').to.equal(3)
  })
<ul id="items">
  <li>Apples</li>
  <li>Oranges</li>
  <li>Pears</li>
  <li>Grapes</li>
</div>
cy.get('#items li').each(($li) => cy.log($li.text()))

Collect the items then print

<ul id="items">
  <li>Apples</li>
  <li>Oranges</li>
  <li>Pears</li>
  <li>Grapes</li>
</div>
const items = []
cy.get('#items li')
  .each(($li) => items.push($li.text()))
  .then(() => {
    cy.log(items.join(', '))
  })

Collect the items then assert the list

<ul id="items">
  <li>Apples</li>
  <li>Oranges</li>
  <li>Pears</li>
  <li>Grapes</li>
</div>
const items = []
cy.get('#items li').each(($li) => items.push($li.text()))
// the items reference is set once
// and the new items are added by the above commands
// that is why we don't have to use "cy.then"
cy.wrap(items).should('deep.equal', [
  'Apples',
  'Oranges',
  'Pears',
  'Grapes',
])

Tip: you can create a flexible OR assertion using match and a predicate callback. For example, the next assertion passes if any element gets true from the predicate function.

// verify the list using .should("match", callback) assertion
// passes if at least for one element the callback returns true
cy.get('#items li').should('match', (k, li) => {
  // this predicate function gets called once for each element
  // "li" is the DOM element
  // we should return true|false if the text is what we expected to find
  return li.innerText === 'Nope' || li.innerText === 'Oranges'
})

Confirm the number of items

Let's imagine the page shows the number of items in the list, and we want to confirm the displayed number is correct.

<div>There are <span id="items-count">4</span> items</div>
<ul id="items">
  <li>Apples</li>
  <li>Oranges</li>
  <li>Pears</li>
  <li>Grapes</li>
</div>

Because the commands are asynchronous we cannot get the number "4" and immediately use it to assert the number of <LI> items. Instead we need to use .then callback.

cy.get('#items-count')
  .invoke('text')
  .then(parseInt)
  .then((n) => {
    cy.get('#items li').should('have.length', n)
  })

with text matching

Sometimes the number of items is a part of the text and needs to be extracted first before parsing into a number. Notice the number "4" is hiding inside the brackets and does not have its own element to query.

<div id="items-intro">There are [ 4 ] items</div>
<ul id="my-items">
  <li>Apples</li>
  <li>Oranges</li>
  <li>Pears</li>
  <li>Grapes</li>
</div>

We need to grab the entire text

cy.get('#items-intro')
  .invoke('text')
  .then((s) => {
    // by adding an assertion here we print
    // the text in the command log for simple debugging
    expect(s).to.be.a('string')
    const matches = /\[\s*(\d+)\s*\]/.exec(s)
    return matches[1]
  })
  .then(parseInt)
  // another assertion to log the parsed number in the command log
  .should('be.a', 'number')
  // we expect between 1 and 10 items
  .and('be.within', 1, 10)
  .then((n) => {
    cy.get('#my-items li').should('have.length', n)
  })

Text at index K

Let's say we want to robustly check each item's text. But the items can be added dynamically, and the item's order matters.

<ul id="fruits"></ul>
<script>
  const fruits = document.getElementById('fruits')
  function addFruit(name, timeout) {
    setTimeout(function () {
      const li = document.createElement('li')
      li.appendChild(document.createTextNode(name))
      fruits.appendChild(li)
    }, timeout)
  }
  // insert each list item asynchronously
  addFruit('apples', 500)
  addFruit('kiwi', 1000)
  addFruit('grapes', 1600)
</script>

Let's check if "apples" is in the list at first position. Then "kiwi", followed by "grapes"

// check each fruit in this list
const fruits = ['apples', 'kiwi', 'grapes']
fruits.forEach((fruit, k) => {
  // the order matters, thus we use the index k
  // to form CSS "nth-child" selector
  // Note: the CSS selector starts at 1, thus
  // we need to add 1 to the index k
  cy.contains(`#fruits li:nth-child(${k + 1})`, fruit)
})

Filter items by text

Let's get all items that contain the words "one" or "two", ignoring the others. We can get all list items, then filter the items by text using the cy.filteropen in new window command.

<ol>
  <li>four</li>
  <li>one</li>
  <li>one</li>
  <li>three</li>
  <li>two</li>
  <li>one</li>
  <li>five</li>
  <li>six</li>
</ol>
cy.get('ol li')
  // check if all items have loaded
  .should('have.length.greaterThan', 5)
  .filter((k, el) => {
    return el.innerText === 'one' || el.innerText === 'two'
  })
  .should('have.length', 4)

Index of the element with the text

Watch the explanation for this example in Confirm The Index Of The Element With The Given Textopen in new window.

<ul id="fruits">
  <li>Apples</li>
  <li>Oranges</li>
  <li>Pears</li>
  <li>Grapes</li>
</div>

If we want to find the list element containing the text "Pear", and confirm it is the element at index 2 (the index starts at zero), we can invoke the jQuery method indexopen in new window.

cy.contains('#fruits li', 'Pear')
  .invoke('index')
  .should('equal', 2)