Parse Account Number
Recently I saw a question on /r/Cypress: 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 Explained
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 for two main reasons:
- it grabs multiple
div
elements. We need to limit ourselves to thediv
element with the account number only. Since the page does not provide any good selectors, we can simply find thediv
element with a 9-digit number using cy.contains with a regular expression. - the commands do not retry. We simply get the text from the
div
elements and pass it to the cy.then command. The callback function runs a regular expression to get the account number. The extracted value (could benull
) is used inside the expressionexpect(number, 'account').to.equal('056256265')
. If the assertion throws, thecy.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.
cy.should
instead of cy.then
Use Let's make sure the test retries getting all div
elements if there is no account number. Simply use cy.should instead of cy.then.
<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.
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.contains.
<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')
})
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')
Recommended
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}`)
})