Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the value your test is meant to protect: assert the rendered string when formatting is part of the contract, or extract and convert the text when the numeric meaning matters. A label of 42% can represent 42 percentage points or the ratio 0.42; your Cypress conversion and expected value must use the same scale.

Decide what “equal” means before writing the assertion

Percentage comparisons have four independent dimensions. Decide each one explicitly so a passing test cannot hide a wrong interpretation.

Decision Typical choices When to use each
Display fidelity Exact string or normalized text Use exact text when the percent sign, spacing, sign, decimal places and formatting are user-visible requirements. Normalize only differences your product intentionally ignores.
Numeric scale Percentage points (42) or ratio (0.42) Use percentage points when the application exposes 42 as the value. Use a ratio when the data contract stores one as the fraction of a whole.
Parsing strictness Leading-number parsing or full-format validation Permissive parsing is convenient for controlled output. Full validation catches missing symbols, labels and malformed values.
Timing Immediate read or retryable assertion Use a retryable .should(callback) when the UI updates after a request, animation or calculation.

Compare the exact displayed percentage

If the visible contract is exactly 42%, keep the assertion at the text level:

cy.get('[data-testid="completion"]')
  .should('have.text', '42%')

have.text checks the complete text yielded by the element. It will fail for 42.0%, an extra space, a different sign, or a non-breaking space. That strictness is useful for a UI whose formatting is deliberate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Whitespace and non-breaking spaces

Some templates render a non-breaking space (Unicode u00a0) between the number and percent sign or around adjacent text. Assert the actual contract rather than silently stripping it:

cy.get('[data-testid="completion"]')
  .should('have.text', '42u00a0%')

If spacing is not meaningful to users, use a normalization function instead of weakening every assertion:

const normalizeText = (value) =>
  value.replace(/u00a0/g, ' ').replace(/s+/g, ' ').trim()

cy.get('[data-testid="completion"]')
  .invoke('text')
  .then(normalizeText)
  .should('equal', '42%')

Do not normalize decimal separators, signs or the percent marker unless those differences are also outside the product contract.

Extract the number when 42 means 42 percentage points

Cypress can retrieve text, convert it and apply a Chai numeric assertion. The documented pattern is equivalent to:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
cy.get('div').invoke('text').then(parseFloat).should('be.gt', 10)

For an exact value, write the conversion and expected scale plainly:

cy.get('[data-testid="completion"]')
  .invoke('text')
  .then((text) => Number.parseFloat(text))
  .should('eq', 42)

Number.parseFloat('42%') reads the leading numeric portion and returns 42. It does not turn the value into 0.42. This is appropriate only when the output format is controlled and trailing content cannot change the meaning.

Use ranges and relationships where exact equality is not the requirement

cy.get('[data-testid="completion"]')
  .invoke('text')
  .then((text) => Number.parseFloat(text))
  .should('be.within', 40, 45)

For a relationship between two displayed percentages, parse both values and compare them on the same scale:

cy.get('[data-testid="current-percent"]')
  .invoke('text')
  .then((currentText) => Number.parseFloat(currentText))
  .then((current) => {
    cy.get('[data-testid="target-percent"]')
      .invoke('text')
      .then((targetText) => Number.parseFloat(targetText))
      .should((target) => {
        expect(target).to.equal(current)
      })
  })

If one element stores a ratio and the other displays percentage points, convert one before comparing; never compare 0.42 directly with 42.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Parse and normalize 42% to the ratio 0.42

When your application’s semantic value is a fraction, validate the complete percentage string and divide by 100. This prevents arbitrary leading text from being accepted:

const parsePercentAsRatio = (text) => {
  const match = text.trim().match(/^([+-]?d+(?:.d+)?)%$/)
  if (!match) {
    throw new Error(`Expected a percentage, received: ${text}`)
  }
  return Number(match[1]) / 100
}

cy.get('[data-testid="completion"]')
  .invoke('text')
  .then(parsePercentAsRatio)
  .should('eq', 0.42)

The regular expression accepts an optional sign and decimal fraction, then requires the percent suffix. Adapt it to your application: decide whether values such as .5%, a trailing space, values outside 0–100, or locale-specific decimal separators are valid. CSS’s percentage type is a number followed by %, but the meaning of a percentage depends on the CSS property and context; that syntax does not define how your application stores business data.

Make asynchronous values retry safely

Cypress retries assertions automatically. A .should(callback) callback is rerun until all assertions pass or the command times out, which makes it suitable for a percentage that changes after a network response:

cy.get('[data-testid="completion"]')
  .should(($el) => {
    const text = $el.text().trim()
    expect(text).to.match(/^[+-]?d+(?:.d+)?%$/)
    expect(Number.parseFloat(text)).to.equal(42)
  })

Keep the callback synchronous. Cypress advises against invoking Cypress commands inside the callback; put commands in the chain and use the callback for assertions against the yielded subject. If the application can legitimately settle at several values, assert the final state with a stable condition rather than adding an arbitrary delay.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Retry a ratio conversion

cy.get('[data-testid="completion"]')
  .should(($el) => {
    const ratio = parsePercentAsRatio($el.text())
    expect(ratio).to.equal(0.42)
  })

Here a malformed interim value throws and causes Cypress to retry. Once the element contains a valid percentage with the expected ratio, the callback passes.

Compare formatting, numeric meaning and timing together

Use a decision like this when choosing an assertion:

  • Exact UI contract: should('have.text', '42%').
  • Numeric percentage-point contract: invoke('text'), Number.parseFloat, then compare with 42.
  • Fractional data contract: validate the full suffix and divide by 100, then compare with 0.42.
  • Formatting-insensitive contract: normalize only known irrelevant whitespace before comparing.
  • Eventually consistent UI: put synchronous parsing and assertions inside .should(callback).

Prefer a positive assertion of the desired value or state. A negative assertion alone can pass after several unexpected application states—for example, when the element is missing, empty or showing a different intermediate value.

Locale, rounding and boundary cases

Locale-specific output

If the UI displays a comma decimal separator, the strict parser above will reject it. Either assert the localized string exactly, or convert according to the locale your application promises. Do not replace every comma globally if commas can also appear in thousands separators.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Rounding

A display rounded to one decimal place may show 41.7% while the underlying ratio is more precise. Decide whether the test protects the rounded display or the underlying calculation. For the former, assert the rendered string; for the latter, test the data or calculation separately and allow an explicit tolerance where appropriate.

Signs and ranges

Negative percentages and values above 100 may be valid for a particular metric. The parser should reflect that domain rule rather than imposing a generic range. If the UI must stay between 0 and 100, add a separate boundary assertion:

cy.get('[data-testid="completion"]')
  .invoke('text')
  .then((text) => Number.parseFloat(text))
  .should('be.gte', 0)
  .and('be.lte', 100)

Troubleshooting common failures

Symptom Likely cause Fix
have.text fails although the browser appears to show 42% Whitespace, a non-breaking space, hidden text or decimal formatting differs. Inspect the exact text, then either assert the real contract or apply narrowly scoped normalization.
Numeric assertion returns NaN The element contains a label, placeholder, empty state or localized number that parseFloat cannot read. Wait for the settled state and use full-format validation so malformed output fails clearly.
42 is compared with 0.42 The test mixes percentage points and ratio semantics. Choose one scale and convert both operands to it.
The test passes too early A transient value satisfies the assertion, or the test uses a negative assertion. Assert the final positive state in a retryable callback and ensure the selector identifies the intended element.
The callback keeps retrying The parser rejects the actual format, the value never reaches the expected state, or the selector is wrong. Log or inspect the yielded text, verify the application contract, and adjust the parser or expected value rather than adding blind waits.
Two percentages do not compare equal One is localized, rounded, or stored on a different scale. Normalize locale and scale explicitly, and compare rounded values only when rounding is part of the requirement.

Keep the test maintainable and fast

  • Use a stable selector such as data-testid instead of a styling class.
  • Put reusable parsers in a support utility and unit-test their accepted grammar.
  • Keep one assertion focused on display formatting and another on numeric semantics when both contracts matter.
  • Let Cypress’s retry mechanism handle normal UI latency; reserve custom timeouts for a measured application requirement.
  • Capture the element once per assertion chain where practical, but do not sacrifice clarity by nesting unrelated commands.
  • When values are generated randomly or from live data, assert a documented invariant or range instead of a hard-coded number.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is to capture a page that displays a percentage rather than run a Cypress assertion, ScreenshotNeo returns a screenshot or PDF with one GET request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and response headers identify the page verdict and billing result.

Use the API documentation at https://screenshotneo.com/docs/ for the full option list. A minimal cURL request is:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

And in Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also supports full-page and selector captures, dark mode, device presets, retina scale, custom CSS and JavaScript, waits, request blocking, headers, cookies, geolocation, resizing, chosen cache TTLs, asynchronous webhooks, bulk capture and PDF options. Its MCP server provides take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Sign up for the free plan.

FAQ

Does have.text trim whitespace automatically?

Do not assume that it does. Treat whitespace—including non-breaking spaces—as part of the string unless you deliberately normalize it before comparison.

Can I use parseInt for a percentage?

You can, but it discards the decimal portion. Use Number.parseFloat for decimal percentages and validate the complete format when malformed suffixes must fail.

Should a percentage test assert the DOM or application data?

Assert the DOM when the user-visible representation is the requirement. Test application data or calculations separately when you need precision beyond the displayed rounding.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does a callback assertion run more than once?

Cypress retries the callback until its assertions pass or the command times out. Repeated execution is expected for values that update asynchronously.

Frequently Asked Questions

Does have.text trim whitespace automatically?

Do not assume that it does. Treat whitespace—including non-breaking spaces—as part of the string unless you deliberately normalize it before comparison.

Can I use parseInt for a percentage?

You can, but it discards the decimal portion. Use Number.parseFloat for decimal percentages and validate the complete format when malformed suffixes must fail.

Should a percentage test assert the DOM or application data?

Assert the DOM when the user-visible representation is the requirement. Test application data or calculations separately when you need precision beyond the displayed rounding.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does a callback assertion run more than once?

Cypress retries the callback until its assertions pass or the command times out. Repeated execution is expected for values that update asynchronously.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.