Parse Account Number

Recently I saw a question on /r/Cypressopen in new window: How do I cy.get a 9-digit number? The application displays something like this after a delay:

Confirmation

Welcome newUser.

Your Account Number is 056256265.

It's unique to you. Use it whenever you need to confirm your membership.

📺 Watch this recipe explained in the video Parse The Account Number Explainedopen in new window

The user code

The user tried to get the number using the following code:

<div>
  <h1 data-testid="title">Confirmation</h1>
  <div></div>
</div>
<script>
  const div = document.querySelector(
    'h1[data-testid=title] + div',
  )
  setTimeout(() => {
    div.innerText =
      "Welcome newUser. Your Account Number is 056256265. It's unique to you. Use it whenever you need to confirm your membership."
  }, 1000)
</script>
cy.get('div')
  .invoke('text')
  .then((fullText) => {
    var pattern = /[0-9]+/g
    var number = fullText.match(pattern)
    expect(number, 'account').to.equal('056256265')
  })

The test fails

The test fails for two main reasons:

  • it grabs multiple div elements. We need to limit ourselves to the div element with the account number only. Since the page does not provide any good selectors, we can simply find the div element with a 9-digit number using cy.containsopen in new window with a regular expression.
  • the commands do not retry. We simply get the text from the div elements and pass it to the cy.thenopen in new window command. The callback function runs a regular expression to get the account number. The extracted value (could be null) is used inside the expression expect(number, 'account').to.equal('056256265'). If the assertion throws, the cy.then(callback) fails and the entire test fails.

Imagine we cannot modify the application to give the account its own element selector, like <span data-testid="account">...</span>. We can still improve the test.

Use cy.should instead of cy.then

Let's make sure the test retries getting all div elements if there is no account number. Simply use cy.shouldopen in new window instead of cy.thenopen in new window.

<div>
  <h1 data-testid="title">Confirmation</h1>
  <div>
    Welcome newUser. Your Account Number is ... It's unique to
    you. Use it whenever you need to confirm your membership.
  </div>
</div>
<script>
  const div = document.querySelector(
    'h1[data-testid=title] + div',
  )
  setTimeout(() => {
    div.innerText = div.innerText.replace(
      '...',
      '05625' + '6265',
    )
  }, 1000)
</script>
cy.get('div')
  .invoke('text')
  .should((fullText) => {
    var pattern = /[0-9]+/g
    var number = fullText.match(pattern)
    expect(number, 'account').to.equal('056256265')
  })

The test retries until the account number shows up, but then fails, since it finds multiple div elements and gets both.

The test fails after finding two div elements

We need to limit ourselves to a single element showing the account number.

Single element

We can find just a single element that has text matching a regular expression using my favorite command cy.containsopen in new window.

<div>
  <h1 data-testid="title">Confirmation</h1>
  <div>
    Welcome newUser. Your Account Number is ... It's unique to
    you. Use it whenever you need to confirm your membership.
  </div>
</div>
<script>
  const div = document.querySelector(
    'h1[data-testid=title] + div',
  )
  setTimeout(() => {
    div.innerText = div.innerText.replace(
      '...',
      '05625' + '6265',
    )
  }, 1000)
</script>
cy.contains('div', /[0-9]+/)
  .invoke('text')
  .should((fullText) => {
    var pattern = /[0-9]+/g
    var number = fullText.match(pattern)
    expect(number[0], 'account').to.equal('056256265')
  })

The test passes after about one second

Cleanup

Let's simplify the test by removing all temporary variables and using a chain of retry-able commands.

<div>
  <h1 data-testid="title">Confirmation</h1>
  <div>
    Welcome newUser. Your Account Number is ... It's unique to
    you. Use it whenever you need to confirm your membership.
  </div>
</div>
<script>
  const div = document.querySelector(
    'h1[data-testid=title] + div',
  )
  setTimeout(() => {
    div.innerText = div.innerText.replace(
      '...',
      '05625' + '6265',
    )
  }, 1000)
</script>
// use named capture group expression
const accountRegex = /(?<account>[0-9]{9})/
cy.contains('div', accountRegex)
  .invoke('text')
  // the current subject is a string
  // and we invoke its method "match"
  // passing the regular expression
  .invoke('match', accountRegex)
  // if the expression matches
  // then it yields an object
  // and we can get the matched value by name
  .its('groups.account')
  // and use an implicit assertion against the subject
  .should('equal', '056256265')

If I could modify the application code, I would give the account number its own element. That would make selecting the element much simpler and would eliminate the regular expression.

<div>
  <h1 data-testid="title">Confirmation</h1>
  <div>
    Welcome newUser. Your Account Number is
    <span id="acc">...</span> It's unique to you. Use it whenever
    you need to confirm your membership.
  </div>
</div>
<script>
  const el = document.querySelector('#acc')
  setTimeout(() => {
    el.innerText = '05625' + '6265'
  }, 1000)
</script>

Our solution could be a single cy.contains command.

cy.contains('#acc', '056256265')

Or if we need the account number text:

cy.get('#acc').invoke('text').should('equal', '056256265')

If you do not know the expected account number, you could use a regular expression

cy.contains('#acc', /[0-9]{9}/)

You can confirm the initial text "..." disappears and the account pattern is present instead

cy.get('#acc')
  .should('not.have.text', '...')
  .invoke('text')
  .should('match', /^[0-9]{9}$/)
  .then((account) => {
    cy.log(`account ${account}`)
  })