skip to content

In Playwright, what is the difference between page.getByText('Open') and page.getByText('Open', { exact: true })?

level: juniorimportance: must knowfreq 78%

answer

  1. One mode forgives, the other does not
  2. The default is not a whole-string test
  3. Case matters in exactly one mode
  4. Whitespace is normalized in both modes
  5. The option flips two switches at once

basics

~10 s

By default page.getByText matches a case-insensitive substring of an element's whitespace-normalized text, so Open also matches Reopened. With exact set to true the whole string must match, case-sensitively. Whitespace is normalized either way.

solid answer

~40 s

`page.getByText('Open')` matches any element whose text *contains* `Open`, ignoring case, so on an issue board it also matches a `Reopened` history entry. `{ exact: true }` flips two switches at once: the match becomes whole-string and case-sensitive, so only an element whose text is exactly `Open` qualifies. What `exact` does not change is whitespace handling — Playwright collapses runs of spaces and newlines and trims the ends before comparing, in both modes, so text indented across three lines in the template still matches a single-line argument. The same `exact` option exists on `getByLabel`, `getByPlaceholder`, `getByAltText` and `getByTitle` with identical meaning. Reach for it when a short label is a substring of a longer one; otherwise the loose default keeps tests readable.

code

typescript · 14 lines
typescript
import { expect, test } from '@playwright/test';

test('the Open chip is distinct from a Reopened entry', async ({ page }) => {
  await page.setContent(`
    <button class="chip">Open</button>
    <li class="activity">Reopened</li>
  `);

  // Case-insensitive substring: matches the chip and the activity entry.
  await expect(page.getByText('Open')).toHaveCount(2);

  // Whole string, case-sensitive: only the chip.
  await expect(page.getByText('Open', { exact: true })).toHaveCount(1);
});

go deeper

for a junior

Recall the two defaults: substring, and case-insensitive. Then recall that exact true turns both into whole-string and case-sensitive. Being able to say which one you need is enough at this level.

for a middle

Explain the order of operations: whitespace is normalized first, then the comparison runs. That is why exact never gives you a raw, markup-faithful match, and why indented template text still matches a one-line argument.

for a senior

Show the diagnostic habit: zero matches means the rendered text differs from your string, several matches means you need exact or a narrower search root. Say which of those two a given failure calls for.

for a principal

Own the convention. Decide when a suite locates by visible text at all versus by a stable identifier, and make sure the choice is consistent so copy edits break a predictable, small set of tests.

## What the default match does `page.getByText('Open')` builds a locator over every element whose text **contains** the argument, compared **case-insensitively**. On an issue tracker whose board header reads `Open issues`, all of `getByText('Open')`, `getByText('open')` and even `getByText('pen iss')` resolve to that heading. The default is deliberately forgiving because visible copy belongs to designers, not to test authors: a chip may be uppercased by CSS while the DOM still holds mixed case, and a substring survives copy edits that a whole-string match would not. Before any comparison happens, Playwright **normalizes whitespace** in the element's text. Runs of spaces and tabs collapse to a single space, line breaks become spaces, and leading and trailing whitespace is dropped. A template that renders a heading indented across three lines is therefore compared as the single line `Open issues`, and `getByText('Open issues')` matches it even though the raw DOM text is nothing like that string. ## What `exact: true` changes The `exact` option flips **two** switches at once, and candidates usually remember only the first: 1. The match becomes **whole-string** — the element's normalized text must equal the argument, not merely contain it. 2. The match becomes **case-sensitive** — `getByText('open', { exact: true })` no longer matches `Open`. What `exact` does **not** turn off is whitespace normalization. The multi-line heading above still matches `getByText('Open issues', { exact: true })`, because normalization runs first and the comparison sees `Open issues` on both sides. That is the most common surprise here: people reach for `exact` expecting a byte-for-byte comparison against raw markup and get a normalized one instead. ## Side by side | Element text | `getByText('Open')` | `getByText('Open', { exact: true })` | |---|---|---| | `Open` | matches | matches | | `Open issues` | matches | no match, extra text | | `OPEN` | matches, case ignored | no match, wrong case | | `Open` padded with newlines | matches | matches, whitespace normalized | | `Reopened` | matches, substring | no match | The last row is the practical reason to reach for `exact`. On a board where a filter chip reads `Open` and an activity entry reads `Reopened`, the loose default matches both, and a locator that resolves to two elements cannot be clicked — Playwright refuses ambiguous locators for actions. ## The same option on the sibling queries `exact` is not special to `getByText`. `page.getByLabel`, `page.getByPlaceholder`, `page.getByAltText` and `page.getByTitle` all accept the same `{ exact }` option with the same meaning: case-insensitive substring by default, case-sensitive whole string when set. `page.getByTestId` is the exception — it has no `exact` option because it always compares the whole test id, case-sensitively. - Keep the **default** when the copy is long and only a fragment of it is stable, such as a comment body or a toast message. - Set **`exact: true`** when the target string is a prefix or substring of neighbouring copy, such as `Open` beside `Reopened`, or `Done` beside `Done today`. - Pass a **RegExp** when neither fits — for text with an embedded count or date. `exact` is ignored for a RegExp, and a RegExp is case-sensitive unless you add the `i` flag yourself. - Remember that `exact` narrows **what text counts**, never **which elements are searched**; every element on the page is still a candidate. ## Where the option stops helping Two failures look like an `exact` problem and are not. The first is a *nesting* question: when several nested elements all contain the string, `getByText` keeps only the innermost one whose own text matches, so an outer card is not returned alongside its inner span. Tightening `exact` does nothing about ancestors, because they were never returned. The second is *repetition*: on a board where every card carries a `Comments` heading, no amount of exactness makes one of them unique. That is a scoping problem, solved by narrowing the search root to one card first and then applying the text query inside it. ## A rule of thumb Write the loose form first, run it, and let the failure tell you what to tighten. If it resolves to zero elements, your string is not in the DOM as rendered — check for a non-breaking space, an ellipsis character, or copy split across elements. If it resolves to several, decide whether the fix is `exact: true` (you matched too much text) or scoping (you matched the right text in too many places). Choosing between those two is the actual skill; `exact` is only half of the toolkit.

  • Does exact: true make the match sensitive to the whitespace in the markup?
    No. Playwright normalizes whitespace before comparing in both modes: runs of spaces and newlines collapse to one space and the ends are trimmed. So text indented over several lines in a template still matches a single-line argument, even with `exact: true`. The option only controls whole-string versus substring and case sensitivity.
  • Which other built-in queries accept the same exact option?
    `page.getByLabel`, `page.getByPlaceholder`, `page.getByAltText` and `page.getByTitle` all take `{ exact }` with the same meaning as `getByText`. `page.getByTestId` does not — a test id is always compared as a whole, case-sensitive string. `page.getByRole` has `exact` too, where it applies to the accessible name.

The default behaves like a contains filter you would type into a search box; exact behaves like an equals sign in a spreadsheet formula.

saying these in an interview costs you the question

  • Says getByText is case-sensitive by default
  • Thinks the default matches the whole string
  • Believes exact: true compares raw markup whitespace
  • Assumes exact also restricts which elements are searched
  • Claims exact fixes a locator that matched several cards