How to Extract Numeric Values from Odometer Displays in Cypress
Read animated odometer values in Cypress, normalize formatting, convert strings safely, and assert changing counters without flaky tests.
Use a stable selector, read the rendered value with .invoke('text') (or .invoke('val') for an input), normalize formatting, convert the result with Number, parseInt, or parseFloat, and finish with a retryable Cypress assertion.
cy.get('[data-cy="odometer"]')
.invoke('text')
.then((raw) => {
const normalized = raw.replace(/[^\\d.-]/g, '')
return Number(normalized)
})
.should('be.a', 'number')
.and('eq', 12345)
Cypress commands yield strings for DOM text and input values. Converting explicitly prevents lexicographic comparisons such as "900" > "1000", handles labels and grouping characters, and lets Cypress retry while an animated counter settles.
1. Give the odometer a stable selector
Prefer a dedicated data-cy or data-test attribute. Selectors based on classes, layout, or visible copy often change when the component is restyled or translated.
<div data-cy="odometer" aria-live="polite">12,345</div>
cy.get('[data-cy="odometer"]')
If the application exposes an accessible text node or a data attribute containing the complete value, prefer that stable representation over animation internals.
2. Read text and convert it to a number
Whole-number display
cy.get('[data-cy="odometer"]')
.invoke('text')
.then((raw) => Number(raw.replace(/[^\\d.-]/g, '')))
.should('eq', 12345)
.invoke('text') calls the jQuery method and yields its return value. Because it remains in the Cypress chain, a chained assertion can retry the query and conversion until the expected value appears.
Choosing the conversion function
| Display | Conversion | Reason |
|---|---|---|
| Strict integer or decimal string after normalization | Number(value) |
Converts the complete string and exposes invalid input as NaN. |
| Integer counter | parseInt(value, 10) |
Reads an integer; fractional digits are discarded. |
| Decimal counter | parseFloat(value) or Number(value) |
Preserves the fractional portion. |
cy.get('[data-cy="odometer"]')
.invoke('text')
.then((raw) => parseInt(raw.replace(/[^\\d-]/g, ''), 10))
.should('eq', 12345)
cy.get('[data-cy="odometer"]')
.invoke('text')
.then((raw) => Number(raw.replace(/[^\\d.-]/g, '')))
.should('be.closeTo', 1234.5, 0.01)
3. Normalize separators, labels, signs, and locales
Normalize only characters that are presentation characters for your known format. Blindly deleting commas is wrong when the comma is a decimal separator.
Thousands separators and units
cy.get('[data-cy="odometer"]')
.invoke('text')
.then((raw) => {
// Handles "12,345 visits" and whitespace around the value.
const numeric = raw.replace(/[^\\d.-]/g, '')
return Number(numeric)
})
.should('eq', 12345)
Extract the number from mixed text
cy.get('[data-cy="odometer"]')
.invoke('text')
.invoke('match', /-?\\d+(?:[.,]\\d+)?/)
.its('0')
.then((value) => Number(value.replace(',', '')))
.should('eq', 12345)
Adjust the regular expression for your sign rules, decimal precision, and locale. If the page displays 1.234,56, remove period grouping and change the comma to a decimal point explicitly:
const toGermanNumber = (raw) => {
const match = raw.match(/-?[\\d.]+(?:,\\d+)?/)
if (!match) return NaN
return Number(match[0].replace(/\\./g, '').replace(',', '.'))
}
cy.get('[data-cy="odometer"]')
.invoke('text')
.then(toGermanNumber)
.should('eq', 1234.56)
4. Read an input-backed odometer
Values from inputs are strings even when the input type is numeric. Use .invoke('val'), then convert.
<input data-cy="odometer" type="number" value="12345" />
cy.get('[data-cy="odometer"]')
.invoke('val')
.then(Number)
.should('eq', 12345)
Use .invoke('val') for the current value; reading an attribute gives you the attribute representation, which may not reflect later property changes.
5. Handle rolling digit elements
Some widgets render each digit separately and keep transitional or off-screen digits in the DOM. Inspect the rendered markup and select only the nodes representing the currently visible digits.
<div data-cy="odometer">
<span data-digit="visible">1</span>
<span data-digit="visible">2</span>
<span data-digit="visible">3</span>
<span data-digit="visible">4</span>
<span data-digit="visible">5</span>
</div>
cy.get('[data-cy="odometer"] [data-digit="visible"]')
.then(($digits) => [...$digits]
.map((element) => element.textContent.trim())
.join(''))
.then(Number)
.should('eq', 12345)
The child selector is application-specific. If the widget exposes one complete accessible value or a value attribute, use that instead of depending on animation layers.
6. Synchronize with changing counters
Keep extraction and the assertion in one Cypress command chain. Cypress retries queries and assertions until they pass or the command timeout expires, which is useful for counters updated by animation, polling, or an API response.
cy.get('[data-cy="odometer"]')
.invoke('text')
.then((raw) => Number(raw.replace(/[^\\d.-]/g, '')))
.should('be.greaterThan', 10)
To compare a value before and after an action, capture a static alias. A normal alias can re-run its query; { type: 'static' } preserves the value obtained at alias creation.
const readOdometer = () => cy.get('[data-cy="odometer"]')
.invoke('text')
.then((raw) => Number(raw.replace(/[^\\d.-]/g, '')))
readOdometer().as('before', { type: 'static' })
cy.get('[data-cy="increment"]').click()
readOdometer().then((after) => {
cy.get('@before').then((before) => {
expect(after).to.be.greaterThan(before)
})
})
For an exact target, assert the target. For an eventually increasing value, assert a relationship such as be.greaterThan and avoid arbitrary sleeps.
7. Common errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
expected '12,345' to equal 12345 |
Text was never converted or grouping punctuation remained. | Normalize the string, then call Number or parseInt. |
NaN |
The selector matched a label, an empty node, or an unsupported locale format. | Log the raw text, isolate the numeric substring, and implement explicit locale rules. |
| Decimal assertion is truncated | parseInt discards fractional digits. |
Use parseFloat or Number. |
| Assertion sees an old value | The check runs before the asynchronous update completes. | Keep the query, conversion, and assertion chained so Cypress retries. |
| Value changes between reads | The counter is live and the test reads it twice. | Capture the starting value with a static alias, then perform the action and read the ending value. |
| Digits are duplicated | Animated or hidden digit layers are included. | Select visible/current digit nodes or use the widget’s complete value representation. |
| Selector breaks after a redesign | It depends on CSS classes or visible copy. | Add and use a dedicated data-cy or data-test attribute. |
8. A reusable Cypress helper
Cypress.Commands.add('readOdometer', {
prevSubject: false
}, (selector, parse = (raw) => Number(raw.replace(/[^\\d.-]/g, ''))) => {
return cy.get(selector)
.invoke('text')
.then(parse)
.should('be.a', 'number')
})
// cypress/e2e/odometer.cy.js
it('reads the total', () => {
cy.readOdometer('[data-cy="odometer"]').should('eq', 12345)
})
Keep parsing policy close to the test or helper. A generic parser cannot safely infer whether punctuation is grouping or decimal notation.
9. Performance, reliability, and test design
- Use one precise selector rather than querying a broad container and filtering many nodes.
- Avoid fixed
cy.wait(1000)calls. Retryable assertions adapt to slower and faster runs. - Use a static alias only for a deliberate before/after comparison; otherwise let Cypress re-query the live value.
- Choose an assertion tolerance for floating-point displays, such as
be.closeTo, instead of requiring an unstable exact decimal. - For locale-sensitive products, test each supported formatting policy with representative fixtures.
- When the counter depends on a network response, wait on the relevant request or assert the resulting UI state rather than waiting for an animation duration.
Cypress command retries improve reliability, but they cannot repair an ambiguous selector or an incorrect parser. Make the rendered representation and locale contract explicit in the application.
10. Or skip the browser setup
If your goal is a screenshot of the odometer rather than a browser assertion, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/dashboard -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/dashboard"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/dashboard'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const image = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', image);
ScreenshotNeo includes 1,000 screenshots per month free with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
11. FAQ
Should I assert the displayed text or the parsed number?
Assert the parsed number when behavior is numeric, such as thresholds or increases. Assert text separately when formatting itself is part of the requirement.
Can I use parseFloat directly?
Yes, for a simple decimal string. Normalize labels, grouping separators, and locale punctuation first when they are present.
Why does my input assertion return a string?
HTML input values are exposed as strings. Read them with .invoke('val') and convert with Number.
How do I test a counter that never has a fixed final value?
Capture a static starting number, trigger the action, then assert a relationship such as greater than, less than, or within an expected range.
What if the odometer is rendered entirely with CSS?
Find the semantic value in an accessible node, attribute, or application state. Avoid scraping transform values or hidden animation layers unless that is the only representation available.


