Query Multiple Elements

Can you confirm that the subtotal + tax + tip is equal to the total displayed on the page? What if the page is loading dynamically? Elements might be added after a delay. An element might show -- before showing the real number?

📺 Watch this recipe explained in the video Query Multiple Elements In Orderopen in new window.

Use a single should callback

<style>
  .dollars::before {
    content: '$';
    margin-right: 2px;
  }
  .dollars {
    width: 6rem;
    text-align: right;
  }
</style>
<div>
  Subtotal <span id="subtotal" class="dollars">20.15</span>
</div>
<div>Total <span id="total" class="dollars">--</span></div>
<div><strong>Fees</strong></div>
<div>Tax <span id="tax" class="dollars">1.70</span></div>
<div>Tip&nbsp;</div>
<script>
  setTimeout(() => {
    document.getElementById('total').innerText = '24.85'
  }, 2000)

  setTimeout(() => {
    Cypress.$('#live div:contains("Tip")').append(
      '<span id="tip" class="dollars">3.00</span>',
    )
  }, 1000)
</script>
cy.get('#subtotal, #tax, #tip, #total').should(($el) => {
  // all elements are present
  expect($el).to.have.length(4)
  // we do not know the order of elements
  // so we need to find each one
  // Because our jQuery object has [el1, el2, el3, el4]
  // need to use search it like an array of objects
  // using Lodash convenient utility _.find
  const subtotal = Number(
    Cypress._.find($el, { id: 'subtotal' }).innerText,
  )
  const tax = Number(
    Cypress._.find($el, { id: 'tax' }).innerText,
  )
  const tip = Number(
    Cypress._.find($el, { id: 'tip' }).innerText,
  )
  const total = Number(
    Cypress._.find($el, { id: 'total' }).innerText,
  )
  expect(total, 'total').to.greaterThan(0)
  expect(subtotal + tax + tip, 'total sum').to.be.closeTo(
    total,
    0.001,
  )
})

Note: I am using should(callback) and not cy.spread(callback)open in new window command, because I want to the query chain to retry if the numbers do not add up. The cy.spread command does not retry, so if the numbers are not immediately available, the test would fail.

Waiting for each number to load

We can wait for each number before computing the sum. We can avoid the pyramid of Doom by using aliases.

<style>
  .dollars::before {
    content: '$';
    margin-right: 2px;
  }
  .dollars {
    width: 6rem;
    text-align: right;
  }
</style>
<div>
  Subtotal <span id="subtotal" class="dollars">20.15</span>
</div>
<div>Total <span id="total" class="dollars">--</span></div>
<div><strong>Fees</strong></div>
<div>Tax <span id="tax" class="dollars">1.70</span></div>
<div>Tip&nbsp;</div>
<script>
  setTimeout(() => {
    document.getElementById('total').innerText = '24.85'
  }, 2000)

  setTimeout(() => {
    Cypress.$('#live div:contains("Tip")').append(
      '<span id="tip" class="dollars">3.00</span>',
    )
  }, 1000)
</script>
cy.get('#subtotal')
  .should('not.have.text', '--')
  .invoke('text')
  .as('subtotal')
cy.get('#tax')
  .should('not.have.text', '--')
  .invoke('text')
  .as('tax')
cy.get('#tip')
  .should('not.have.text', '--')
  .invoke('text')
  .as('tip')
cy.get('#total')
  .should('not.have.text', '--')
  .invoke('text')
  .as('total')
  // by now, all number should be there and we can run the assertion
  // without retrying using cy.then callback
  // grab all saved aliases using the test context "this" object
  // inside a "function () { }" callback
  .then(function () {
    const subtotal = Number(this.subtotal)
    const tax = Number(this.tax)
    const tip = Number(this.tip)
    const total = Number(this.total)
    expect(total, 'total').to.greaterThan(0)
    expect(subtotal + tax + tip, 'total sum').to.be.closeTo(
      total,
      0.001,
    )
  })

Using getInOrder and a should callback

We can query all 4 elements we need using cy.getInOrder from my cypress-mapopen in new window plugin. Then we can transform text to numbers and use a single should(callback) to confirm the sum.

<style>
  .dollars::before {
    content: '$';
    margin-right: 2px;
  }
  .dollars {
    width: 6rem;
    text-align: right;
  }
</style>
<div>
  Subtotal <span id="subtotal" class="dollars">20.15</span>
</div>
<div>Total <span id="total" class="dollars">--</span></div>
<div><strong>Fees</strong></div>
<div>Tax <span id="tax" class="dollars">1.70</span></div>
<div>Tip&nbsp;</div>
<script>
  setTimeout(() => {
    document.getElementById('total').innerText = '24.85'
  }, 2000)

  setTimeout(() => {
    Cypress.$('#live div:contains("Tip")').append(
      '<span id="tip" class="dollars">3.00</span>',
    )
  }, 1000)
</script>
cy.getInOrder('#subtotal', '#tax', '#tip', '#total')
  .map('innerText')
  .map(Number)
  .print(
    'Subtotal {0.0} + tax {0.1} + tip {0.2} should be {0.3}',
  )
  // if the should(callback) assertion fails
  // it will retry the entire chain of queries
  // starting with cy.getInOrder query
  .should(([subtotal, tax, tip, total]) => {
    expect(total, 'total').to.greaterThan(0)
    expect(subtotal + tax + tip, 'total sum').to.be.closeTo(
      total,
      0.001,
    )
  })

Note: I am using should(callback) and not cy.spread(callback)open in new window command, because I want to the query chain to retry if the numbers do not add up. The cy.spread command does not retry, so if the numbers are not immediately available, the test would fail.