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.

Use a Playwright Locator, then choose the method that matches what you mean by “text”: locator.textContent() reads the DOM node’s text, while locator.innerText() reads rendered text as a user would see it. For multiple matches, use allTextContents() or allInnerTexts(). If you are checking text in a test, prefer expect(locator).toHaveText() instead of extracting a string yourself.

Get text from one element

Start with a resilient locator. Locators are Playwright’s central mechanism for auto-waiting and retrying, so they are preferable to passing raw CSS selectors to page-level methods.

DOM text with textContent()

textContent() returns the node’s DOM textContent. It includes text nodes even when CSS makes part of the element invisible, and it does not apply the browser’s visual layout rules.

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

test('reads a button label', async ({ page }) => {
  await page.goto('https://example.com');

  const saveButton = page.getByRole('button', { name: 'Save' });
  const buttonText = await saveButton.textContent();

  console.log(buttonText);
});

The return type is string | null in TypeScript because a DOM element can have no text node. In normal use, a matched element may still produce an empty string when it contains no textual content. Handle null if the value is optional:

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.
const text = await page.getByTestId('optional-caption').textContent();
if (text !== null) {
  console.log(text.trim());
}

Rendered text with innerText()

innerText() follows the element’s rendered-text semantics. It reflects visibility, line breaks and layout-related whitespace more closely than textContent(). Use it when the question is “what would a user read?” rather than “what text nodes are in the DOM?”

const renderedText = await page.getByRole('button', { name: 'Save' }).innerText();
console.log(renderedText);

Because rendered text depends on the page’s current state and CSS, it can differ from textContent(). For example, text in a hidden descendant may be present in textContent() but omitted from innerText(). Choose one deliberately instead of trimming whichever value happens to pass today.

textContent() versus innerText()

Need Use What it represents
Inspect DOM content locator.textContent() The node’s textContent, including text not currently rendered.
Read visible, user-facing copy locator.innerText() The element’s rendered innerText, affected by visibility and layout.
Collect every matching node’s DOM text locator.allTextContents() One textContent string per match.
Collect every matching node’s rendered text locator.allInnerTexts() One innerText string per match.
Verify text in a test expect(locator).toHaveText() An auto-retrying assertion using text-content semantics by default.

Whitespace is an important distinction. Playwright’s text matching and string assertions normalize whitespace and line breaks for matching. The raw value returned by a text-reading method is still the value you requested, so normalize it yourself when converting it into a key, snapshot, or comparison:

const normalized = (await page.getByRole('heading').innerText()).replace(/s+/g, ' ').trim();

Choose a locator that survives UI changes

The text method is only as reliable as the locator before it. Prefer locators that describe user-facing meaning:

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

Roles for controls and landmarks

const heading = page.getByRole('heading', { name: 'Account' });
const save = page.getByRole('button', { name: 'Save' });
const status = page.getByRole('status');

Role locators use the accessible role and name, so they usually survive class-name refactors. They are especially appropriate for buttons, links, headings, list items and status messages.

Text locators for copy

const exactCopy = page.getByText('Welcome, John', { exact: true });
const dynamicCopy = page.getByText(/welcome, [A-Z a-z]+$/i);

getByText() supports substring matching by default, exact-string matching with exact: true, and regular expressions. Playwright normalizes whitespace, line breaks and surrounding whitespace while matching text. If identical copy appears in several places, narrow the search with a parent locator:

const profileCard = page.getByRole('article', { name: 'Profile' });
const email = profileCard.getByText('Email');

CSS, test IDs and other fallbacks

Use a CSS locator, getByTestId() or another locator when there is no stable accessible name. Keep the selector as close as possible to the component boundary and avoid selecting a generated class that changes between builds.

const price = page.locator('[data-testid="price"]');
const priceText = await price.innerText();

Get text from all matching elements

When a locator intentionally matches a collection, use the collection methods rather than repeatedly calling a single-element method.

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

All DOM text

const domTexts = await page.getByRole('listitem').allTextContents();
console.log(domTexts);

allTextContents() returns an array with one DOM textContent value per match.

All rendered text

const renderedTexts = await page.getByRole('listitem').allInnerTexts();
console.log(renderedTexts);

allInnerTexts() returns one rendered innerText value per match. The order follows the locator’s match order in the page.

When each item needs separate logic

If you must inspect attributes, descendants or state for each item, count the locator and iterate. Keep the locator scoped so a late-rendered item is resolved through Playwright’s retrying model:

const rows = page.getByRole('row');
const count = await rows.count();
const names: string[] = [];

for (let i = 0; i < count; i++) {
  names.push((await rows.nth(i).innerText()).trim());
}

For a simple list of strings, the bulk methods are shorter and communicate intent better.

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

Use assertions when you are testing

If the purpose is to prove that text appears, do not pull it into test code and write a separate assertion. Playwright’s locator assertions retry until the expectation passes or the configured timeout expires.

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

test('shows the save confirmation', async ({ page }) => {
  await page.goto('https://example.com/settings');
  await page.getByRole('button', { name: 'Save' }).click();

  await expect(page.getByRole('status')).toHaveText('Saved');
});

toHaveText() uses text-content semantics by default. Ask for rendered semantics with useInnerText: true:

await expect(page.getByRole('status')).toHaveText('Saved', {
  useInnerText: true
});

String expectations normalize whitespace and line breaks before matching. Regular expressions are useful for variable values:

await expect(page.getByRole('status')).toHaveText(/saved at d{2}:d{2}/i);

Use extraction only when the text is an input to another operation, such as creating a filename, passing a value to an API, or comparing two independently located values.

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

Python Playwright equivalents

The Python binding exposes the same behavior with snake_case method names.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com")

    button = page.get_by_role("button", name="Save")
    dom_text = button.text_content()
    rendered_text = button.inner_text()

    items_dom = page.get_by_role("listitem").all_text_contents()
    items_rendered = page.get_by_role("listitem").all_inner_texts()

    print(dom_text, rendered_text)
    print(items_dom, items_rendered)
    browser.close()

In an asynchronous Python test, the method names stay the same and are awaited:

text = await page.get_by_role("button", name="Save").text_content()
visible_text = await page.get_by_role("button", name="Save").inner_text()
texts = await page.get_by_role("listitem").all_text_contents()

Legacy selector methods: why to avoid them

page.textContent(selector) exists, but current Playwright documentation marks it as discouraged. It reads the first matching element when several elements satisfy the selector, which can silently return the wrong item. Replace it with a locator:

// Discouraged
const value = await page.textContent('.message');

// Preferred
const value = await page.locator('.message').textContent();

A locator makes the matching scope explicit and gives you collection methods, auto-waiting and retryable assertions. If multiple matches are expected, use allTextContents() or narrow the locator rather than relying on the first match.

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

Common failures and fixes

“Strict mode violation”

Cause: a locator intended for one element matches several. Fix: make the locator unique with a role name, exact text, a parent scope or a test ID. Use a collection method when several matches are the actual requirement.

Returned text is empty or null

Cause: the element has not received text yet, the locator points at a wrapper, or the content is supplied through an attribute or input value. Fix: locate the element that owns the text, wait through a locator assertion, and use inputValue() for form controls whose value is not a text node.

await expect(page.getByRole('status')).toHaveText(/.+/);
const value = await page.getByLabel('Email').inputValue();

textContent() includes hidden copy

Cause: that is the method’s DOM semantics. Fix: use innerText() or an assertion with useInnerText: true when visibility matters.

Whitespace or line breaks do not compare as expected

Cause: HTML formatting and CSS create different raw strings. Fix: use toHaveText(), which normalizes whitespace for matching, or normalize an extracted value with a deliberate rule such as replace(/s+/g, ' ').trim().

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

Text appears only after an API call

Cause: the locator resolves before the application has finished rendering. Fix: assert the eventual text instead of adding an arbitrary sleep. If the page has a loading state, wait for a meaningful status change or the target locator’s text.

Text is split across descendants

Cause: a label is composed of several nested spans. Fix: locate the containing element and use innerText(), or assert the full string with toHaveText(). Avoid depending on a particular child-span structure unless that structure is the behavior under test.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability and design choices

  • Use one bulk call for a list when you only need strings; it is clearer than issuing a separate call for every item.
  • Keep locators specific. A broad locator increases ambiguity and can force unnecessary retries.
  • Prefer assertions for synchronization. Fixed delays make tests slower and still fail when the application is slower than the chosen delay.
  • Choose textContent for machine-oriented DOM inspection and innerText for user-visible copy; mixing them across tests makes failures harder to interpret.
  • Trim or normalize extracted strings only at the boundary where your application needs normalized data. Do not hide meaningful whitespace when testing formatting.
  • For dynamic values, assert a stable pattern or the relevant substring rather than freezing timestamps, counters or generated IDs.

Or skip the browser setup

If your goal is a clean image or PDF of a page rather than an automated text assertion, ScreenshotNeo provides a website screenshot API and MCP server. A single request captures a URL as PNG, JPEG, WebP or PDF:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for all parameters. The same request in Python is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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}`);
  • Cookie and consent banners, newsletter popups and chat widgets are removed before the shot.
  • Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed; response headers identify the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.
  • The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan.

Create a free ScreenshotNeo account to get the 1,000 monthly screenshots without adding a card.

Frequently Asked Questions

Can I read an input’s current value with textContent()?

No. Form controls keep their current value as a property. Use the locator’s inputValue() method, such as page.getByLabel(‘Email’).inputValue().

Which method should I use for a hidden element?

Use textContent() when you intentionally need DOM text, including hidden descendants. Use innerText() when the requirement is text rendered to a user.

How do I get text from exactly one matching element?

Make the locator unique with a role, accessible name, exact text, parent scope or test ID, then call textContent() or innerText(). A strict-mode error means the locator is still ambiguous.

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

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.