Resource library

QA How-To

How to Use Playwright getByText (2026)

Learn Playwright getByText in 2026 with exact and regex matching, whitespace rules, scoping, async assertions, debugging, and runnable TypeScript tests.

21 min read | 2,710 words

TL;DR

Use `page.getByText(text)` to locate readable content, with `exact: true` for a whole normalized string or a regular expression for controlled variation. Scope repeated text to a meaningful component and use web-first assertions to verify visibility or content.

Key Takeaways

  • Use getByText for meaningful readable copy, especially noninteractive content.
  • Remember that strings use case-insensitive substring matching by default.
  • Use exact matching for whole normalized text and bounded regex for controlled variation.
  • Scope repeated copy to a dialog, row, card, region, or list item.
  • Prefer role locators for interactive controls and labels for form fields.
  • Use web-first assertions instead of fixed waits for asynchronous content.

Playwright getByText locates content by the text users can read, making it a strong choice for notices, descriptions, table values, product names, and other noninteractive copy. Use page.getByText('Payment received') for stable text, add exact: true when the whole normalized string matters, and use a regular expression for controlled variation.

The method returns a Locator, so it resolves lazily and works with Playwright's web-first assertions. Reliability still depends on choosing the right scope and matching rule. This 2026 guide explains the mechanics, practical patterns, and limits that matter in production TypeScript suites.

TL;DR

import { test, expect } from '@playwright/test';

test('shows a successful payment message', async ({ page }) => {
  await page.goto('/billing');
  await page.getByRole('button', { name: 'Pay invoice' }).click();

  await expect(
    page.getByText('Payment received', { exact: true })
  ).toBeVisible();
});
Need Recommended expression Notes
Stable phrase inside longer copy getByText('Payment received') String matching is substring-based by default
Whole normalized text getByText('Payment received', { exact: true }) Exact string matching is case-sensitive
Controlled variation `getByText(/^payment (received approved)$/i)`
Repeated text in one component card.getByText('In stock') Scope before matching
Interactive element getByRole(..., { name }) Usually expresses action more clearly

1. What Playwright getByText Actually Does

getByText() creates a Locator for elements containing the specified text. It accepts a string or regular expression plus an optional exact setting for string matching. Because it is a Locator, creating it does not immediately freeze a DOM node. Playwright resolves the query when an assertion, action, count, or other operation runs.

<section>
  <h2>Delivery update</h2>
  <p>Your package is ready for pickup.</p>
</section>
await expect(page.getByText('ready for pickup')).toBeVisible();

A plain string performs case-insensitive substring matching. Therefore ready for pickup can match the longer sentence. Setting exact: true requests a case-sensitive match against the whole normalized text.

Text locators normalize whitespace. Multiple spaces, line breaks, and tabs are treated as normalized whitespace, even with exact matching. This lets the locator represent readable copy rather than the source formatting used by a template.

The method has special behavior for input elements of type button and submit: it can match their value rather than ordinary text content. For modern application controls, a native button with readable content and a role locator is often clearer.

Text matching is not the same as proving visibility. A matching node may exist while hidden, and an action or visibility assertion applies the relevant checks. Use await expect(locator).toBeVisible() when visible presentation is the requirement.

2. Set Up a Runnable Text-Locator Test

Use the official Playwright initializer to create a TypeScript project. The generated configuration supports browser projects, traces, and the test runner.

npm init playwright@latest
npx playwright test

The following test uses role locators for actions and getByText for the resulting copy. That split usually reads more clearly than clicking a button by its text alone.

import { test, expect } from '@playwright/test';

test('applies a valid promotion code', async ({ page }) => {
  await page.goto('/cart');

  await page.getByLabel('Promotion code').fill('SAVE20');
  await page.getByRole('button', { name: 'Apply code' }).click();

  await expect(page.getByText('Promotion applied', { exact: true }))
    .toBeVisible();
  await expect(page.getByText('You saved $20.00')).toBeVisible();
});

The first assertion proves feedback appeared. The second proves the financial outcome shown to the shopper. Both are web-first assertions, so Playwright repeatedly evaluates the locator until the condition passes or the assertion timeout expires.

Avoid reading textContent() and applying a one-time generic assertion when the UI updates asynchronously:

// Preferred: retrying assertion
await expect(page.getByText('Promotion applied')).toBeVisible();

// Usually weaker for asynchronous UI:
// expect(await page.locator('.notice').textContent()).toContain('Promotion applied');

The second form captures one value at one moment and can race the render. Raw text is still useful for transformations or calculations, but visibility and content assertions should normally use Playwright matchers. For a broader test structure, review the Playwright automation guide.

3. Use String, Exact, and Regular Expression Matching

Choose the narrowest matching rule that represents the product requirement. A default string is ideal when stable words appear inside a longer sentence. Exact matching is appropriate for short labels that could otherwise match longer alternatives. A regular expression handles documented variation.

// Matches "Order shipped today" and similar longer content.
const shippedFragment = page.getByText('Order shipped');

// Matches only the whole normalized string with the same case.
const shippedExact = page.getByText('Order shipped', { exact: true });

// Matches two allowed statuses, regardless of case.
const finalStatus = page.getByText(/^(order shipped|order delivered)$/i);

Do not use exact: true reflexively. If the interface legitimately appends a date or amount, exact matching couples the test to that detail. Conversely, a short substring such as Save may also match Save draft and Save as template. The required behavior should determine the boundary.

Regular expressions should be readable and bounded. Prefer /^invoice #\d+ paid$/i to /paid/i when the entire status line is the contract. Escape special characters when they are literal. A currency value such as $42.00 needs \$ and \. in a regex.

await expect(page.getByText(/^total: \$42\.00$/i)).toBeVisible();

Use string matching when a regex adds no value. A heavily escaped expression is harder to maintain than a clear exact string. Matching strategy is part of test design, not merely syntax for getting the test to pass.

4. Understand Whitespace and Nested Elements

Application text is often split across nested elements or formatted over several source lines. Playwright's text matching normalizes whitespace so tests can use the readable phrase.

<p class="summary">
  Your order
  <strong>#4821</strong>
  is ready.
</p>
await expect(page.getByText('Your order #4821 is ready.'))
  .toBeVisible();

Do not copy indentation or line breaks from the HTML into the expected string. Write the text as a person reads it. Exact string matching still normalizes whitespace, so exact: true does not mean byte-for-byte DOM source comparison.

Nested content can also produce multiple plausible ancestors. Playwright's text locator aims at an element matching the text query, but broad text may still produce several actionable candidates in a complex component. Treat strictness as feedback and choose a more specific string or container.

const receipt = page.getByRole('region', { name: 'Receipt' });
await expect(receipt.getByText('Total $42.00', { exact: true }))
  .toBeVisible();

If markup adds visually hidden text for accessibility, the text behavior may differ from a simple textContent guess. Inspect the rendered page and locator in UI mode. For accessible interactive names, getByRole is usually a better fit because it follows the accessible-name computation explicitly.

When testing whitespace itself, such as a code editor or preformatted output, consider a targeted locator and an assertion on textContent or toHaveText with an appropriate expected value. getByText intentionally treats ordinary whitespace as user-readable spacing.

5. Scope Repeated Copy to a Meaningful Container

Text such as Active, Pending, Free, or In stock may appear many times. Do not fix duplicate matches with first() unless the first occurrence is the requirement. Locate the relevant card, row, dialog, region, or list item and query text inside it.

test('shows the status for one workspace', async ({ page }) => {
  await page.goto('/workspaces');

  const workspace = page.getByRole('article').filter({
    has: page.getByRole('heading', { name: 'Mobile Platform' }),
  });

  await expect(
    workspace.getByText('Active', { exact: true })
  ).toBeVisible();
  await expect(workspace.getByText('12 members')).toBeVisible();
});

The parent is identified by a semantic heading. The status and member count are then meaningful within that component. A new workspace inserted above it cannot redirect the assertion.

A text filter can also identify a container:

const row = page.getByRole('row').filter({
  hasText: 'INV-2048',
});
await expect(row.getByText('Overdue', { exact: true })).toBeVisible();

Keep the filter text specific enough that one candidate remains. When several rows legitimately include the same invoice fragment, use a cell locator, test ID, or additional filter that expresses stable identity.

Scoping improves both resilience and error messages. Variables such as workspace, row, and dialog show reviewers where the content belongs. A global page query hides that relationship and may pass against copy in navigation, a toast, or an offscreen template.

6. How to Use Playwright getByText for Async Messages

Toast notifications, validation messages, loading results, and background-job statuses appear asynchronously. Do not precede a text assertion with a fixed delay. The locator and web-first assertion already provide retry behavior.

test('waits for report generation feedback', async ({ page }) => {
  await page.goto('/reports/monthly');
  await page.getByRole('button', { name: 'Generate report' }).click();

  await expect(page.getByText('Preparing your report...'))
    .toBeVisible();

  await expect(page.getByText('Report is ready', { exact: true }))
    .toBeVisible();

  await expect(page.getByText('Preparing your report...'))
    .toBeHidden();
});

The scenario verifies a meaningful transition without sleeping. Use an assertion timeout configured for the known operation if report generation has an accepted service-level expectation. Do not apply a huge global timeout to compensate for one slow workflow.

When text changes in the same element, it can be clearer to locate a stable status container by role and assert its content:

const status = page.getByRole('status');
await expect(status).toHaveText('Preparing your report...');
await expect(status).toHaveText('Report is ready');

This targets one live region across the transition. getByText is stronger when the appearance or disappearance of a phrase anywhere within a meaningful scope is the product behavior.

If a message appears too briefly for a user to perceive, making the test race it is not a robust solution. Discuss the UX duration or assert a persistent outcome instead. Automation should validate intended observability, not rely on accidental timing.

7. Compare getByText with Role, Label, and Test ID

Choose a locator based on the information the user or product contract provides.

Locator Best target Example contract Main risk
getByText Noninteractive visible copy Notice or price text Repetition and copy changes
getByRole Controls and semantic regions Button name or dialog title Requires correct semantics
getByLabel Form controls Associated field label Missing label relationship
getByTestId Explicit component identity Stable automation hook Can ignore user semantics
locator(CSS) Intentional DOM structure Integration-specific markup Refactor coupling

A button contains text, so getByText('Submit') may find it. Still, getByRole('button', { name: 'Submit' }) communicates that the test intends to operate a button and validates its semantic identity. Similarly, a field label should normally be targeted with getByLabel rather than matching a surrounding text node.

Use getByText for a paragraph, status phrase, explanatory copy, price, or other readable content whose semantic role does not add useful identity. The Playwright getByRole examples cover control interaction, while the getByTestId best practices explain explicit hooks.

These choices can coexist in one scenario. A role locator performs the action, a test ID selects a repeated component, and a text locator verifies copy inside it. Consistency means every choice is explainable, not that every test uses one method.

8. Handle Dynamic Values Without Weak Wildcards

Dates, currencies, order numbers, usernames, and counts make text dynamic. Stabilize test data when the exact value is part of the requirement. Use a bounded regular expression only when variation is intentional.

test('shows a generated order reference', async ({ page }) => {
  await page.goto('/checkout/confirmation');

  await expect(page.getByText(/^Order reference: QF-\d{6}$/))
    .toBeVisible();
});

This expression validates the documented format. It does not assert which reference was created. If the scenario must connect the UI reference to an API response or fixture, capture the expected reference and use an exact string.

const expectedCustomer = 'Rina Patel';
await expect(page.getByText(`Welcome, ${expectedCustomer}`, {
  exact: true,
})).toBeVisible();

Avoid patterns such as /.*/, /\d+/, or a single common word. They can match unrelated content and create false passes. Also avoid hard-coding the current date when the application uses a user timezone. Inject a known clock where the application supports it, calculate the expected locale deliberately, or assert a stable surrounding contract.

Localized applications need a defined testing approach. Run locale-specific fixtures and assert translated copy when localization is under test. For general workflows, semantic locators or stable test IDs may reduce coupling to language. Do not make one permissive regex accept every locale, because that no longer validates the presented translation.

9. Hidden Content, Shadow DOM, and Frame Boundaries

A text locator can resolve to content that is attached but not visible. This often occurs with responsive menus, collapsed panels, duplicated templates, or transition layers. Use a visibility assertion and scope to the active component.

const dialog = page.getByRole('dialog', { name: 'Terms of service' });
await expect(dialog.getByText('Cancellation policy')).toBeVisible();

If both mobile and desktop markup contain the phrase, locating the visible navigation boundary or asserting a responsive state is more reliable than selecting the first text match.

Playwright locators work through open shadow roots for supported locator strategies. Closed shadow roots remain inaccessible by design. A component should expose user-facing behavior or a deliberate testing interface if critical content lives behind a closed root.

Iframe content belongs to a different document. Select its frame context first:

const support = page.frameLocator('iframe[title="Support center"]');
await expect(support.getByText('How can we help?')).toBeVisible();

A page-level getByText will not search inside that iframe. Frames are a common cause of zero-match failures.

Visibility can also be affected by clipping, overlays, and animation. A toBeVisible assertion answers whether Playwright considers the element visible, while a click applies additional actionability checks. If the text itself is the subject, visibility may be enough. If the user must interact with a control, locate that control by role and let the action verify actionability.

10. Debug Playwright getByText Failures Systematically

Classify the error before changing the locator. Zero matches can mean wrong text, wrong case with exact matching, unexpected punctuation, a different page state, an iframe, or content that never rendered. Multiple matches mean the text or scope is too broad. A visibility failure means a match exists but is not presented as expected.

Use Playwright's UI mode or inspector:

npx playwright test tests/billing.spec.ts --ui
npx playwright test tests/billing.spec.ts --debug

At the target state, inspect the rendered wording and experiment with a locator. Confirm whether whitespace normalization, nested markup, or a duplicate hidden component affects the result. Use count() only as a diagnostic or when collection size is the requirement.

const notice = page.getByText('Payment received');
console.log('matching nodes:', await notice.count());

Traces from CI preserve snapshots and action history. They can reveal that the expected text appeared on a different route, was replaced by server error copy, or vanished before the assertion. A screenshot alone may miss attached hidden nodes and exact DOM wording.

Do not respond to a zero match by weakening an exact phrase to one common word. Do not respond to duplicates by adding first() without explaining order. Correct the page state, test data, scope, or expected copy. For timeout triage, see how to fix Playwright timeout failures.

11. Create Maintainable Content Assertions

Content assertions should capture product meaning without copying entire paragraphs unnecessarily. Assert the smallest stable phrase that distinguishes the expected state, or assert the exact string when wording is itself a requirement.

test('explains why an account is locked', async ({ page }) => {
  await page.goto('/account/security');

  const alert = page.getByRole('alert');
  await expect(alert).toContainText('Account temporarily locked');
  await expect(alert).toContainText('Try again in 15 minutes');
});

Here the alert role establishes a semantic boundary, and content assertions verify two important facts. A single getByText query for the entire paragraph would couple the test to editorial changes between those facts.

Create helper functions only for repeated domain assertions, not for hiding Playwright syntax. A helper named expectInvoiceStatus(row, 'Overdue') can clarify a table contract. A generic findTextAndWait(value) removes context and often encourages global searches.

Coordinate copy changes with tests according to risk. A marketing paragraph should not block a critical checkout test unless that copy is the behavior under test. A fraud warning, consent statement, or validation message may require exact wording. Tag or organize content-focused tests so expected editorial updates are easy to review.

A mature suite separates flow coverage from copy coverage while using the same locator fundamentals. That keeps tests sensitive to meaningful regressions without turning every text edit into unrelated automation work.

Interview Questions and Answers

Q: What matching behavior does getByText use for a string?

A string uses case-insensitive substring matching by default. With exact: true, it matches the whole normalized string and becomes case-sensitive. Whitespace is normalized in either mode.

Q: Why would you use a regular expression?

I use a regex for documented variation, explicit boundaries, or a validated format such as an order reference. I keep it narrow enough to prevent unrelated matches. For fixed copy, a string is more readable.

Q: Is getByText the best way to click a button?

Usually I prefer getByRole('button', { name }) because it expresses both control type and accessible name. getByText is better suited to noninteractive readable content. The role locator also exposes semantic regressions.

Q: How do you fix multiple text matches?

I scope the query to a meaningful dialog, row, card, region, or list item. I may also use exact text or a bounded regex if that represents the requirement. I avoid positional selection unless order matters.

Q: Does getByText wait for dynamic content?

The Locator resolves lazily, and web-first assertions retry until their timeout. An action also applies its relevant waiting and actionability behavior. I use expect(locator).toBeVisible() or a content assertion instead of a fixed sleep.

Q: How is getByText different from toHaveText?

getByText finds an element based on expected content. toHaveText asserts the content of an element already identified through another stable locator. The second is often better for one status region whose text changes over time.

Q: Can getByText search inside an iframe?

Yes, after selecting the frame context with frameLocator or a Frame. A page-level text locator does not cross document boundaries automatically.

Common Mistakes

  • Using a very short substring that matches navigation, content, and hidden templates.
  • Adding exact: true when legitimate dynamic suffixes are part of the copy.
  • Assuming exact matching preserves source whitespace rather than normalized text.
  • Clicking interactive elements by text when role and accessible name are clearer.
  • Choosing first() to hide duplicate matches without establishing component scope.
  • Using permissive regex patterns that validate almost nothing.
  • Hard-coding dates, money, or localized strings without controlling locale and data.
  • Treating attached text as proof that users can see it.
  • Using waitForTimeout() before an assertion instead of web-first retrying.
  • Expecting a page locator to cross an iframe.
  • Copying an entire long paragraph when only two stable business facts matter.

Review the target's role, scope, visibility, stability, and timing before changing the matcher. Most flaky text tests are under-specified contracts rather than slow browsers.

Conclusion

Playwright getByText is the right locator when readable content is the element's meaningful identity. Use a plain string for stable fragments, exact: true for whole normalized copy, and bounded regular expressions for intentional variation. Scope repeated text to the smallest useful component and verify visibility or content with web-first assertions.

Use roles for interactive controls, labels for form fields, and test IDs for explicit component identity. With that division, text locators remain clear and resilient while still catching the content regressions users actually notice.

Interview Questions and Answers

How does string matching work in getByText?

By default, Playwright performs case-insensitive substring matching for a string. The `exact: true` option requires the whole normalized string and is case-sensitive. Whitespace is normalized in both modes.

When do you use getByText instead of getByRole?

I use getByText for meaningful noninteractive copy such as notices, descriptions, prices, and status phrases. For a button, link, dialog, or other semantic element, I normally prefer role and accessible name. The choice should express the user contract.

How do you handle dynamic text?

I control test data when the exact value matters. For intentional variation, I use a bounded regular expression or build the expected string from fixture data. I avoid broad wildcards that could match unrelated content.

What causes strictness errors with text locators?

The phrase often appears in multiple components, navigation, or hidden responsive markup. I scope to a meaningful container and refine the match based on product identity. Positional methods are appropriate only when order is part of the scenario.

What is the difference between getByText and toHaveText?

getByText identifies an element through its content. `toHaveText` verifies content on an element that may have been identified by role, test ID, or another stable contract. I prefer `toHaveText` for one status element whose wording changes.

Does exact text preserve source whitespace?

No. Text matching normalizes whitespace even when `exact: true` is used. If literal whitespace is the requirement, I use a targeted content assertion designed for that specialized case.

How do you debug a getByText failure?

I classify it as zero matches, duplicate matches, or visibility failure. Then I inspect the live page in UI mode or a trace, confirm the rendered wording and scope, and check frame boundaries. I fix identity or state before increasing timeouts.

Frequently Asked Questions

Is Playwright getByText case sensitive?

A plain string is case-insensitive and substring-based by default. With `exact: true`, whole-string matching is case-sensitive; a regular expression gives explicit case control.

Does getByText ignore whitespace?

Playwright normalizes whitespace for text matching, including with exact string matching. Write the phrase as a user reads it instead of copying source indentation and line breaks.

How do I get exact text in Playwright?

Call `getByText('Expected text', { exact: true })`. This matches the whole normalized string, so account for punctuation and case.

How do I handle multiple getByText matches?

Scope the locator to a meaningful container such as a dialog, row, article, or list item. Use exact text or a bounded regex if that accurately represents the requirement.

Should I use getByText to click buttons?

It can match button text, but `getByRole('button', { name: '...' })` usually communicates intent and semantics better. Reserve getByText mainly for content whose role adds no useful identity.

Does getByText wait for text to appear?

The Locator resolves lazily, and web-first assertions such as `toBeVisible()` retry. Use an assertion on the locator rather than a hard-coded timeout.

Can getByText match a regular expression?

Yes. Use a regular expression for intentional variation, boundaries, or formats, and keep it narrow enough to avoid false matches.

Related Guides