The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use a resilient Playwright locator, then choose the read method that matches your goal: locator.textContent() for the DOM’s text node content, locator.innerText() for rendered, user-visible text, and allTextContents() or allInnerTexts() when the locator intentionally matches a collection. For test checks, prefer expect(locator).toHaveText() instead of pulling a string into your test.
The basic pattern
A locator is Playwright’s central abstraction for auto-waiting and retryability. Define the element by user-facing meaning where possible, then read its text:
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 domText = await saveButton.textContent();
const renderedText = await saveButton.innerText();
console.log({ domText, renderedText });
});
textContent() and innerText() each operate on the first element matched by that locator. If the locator should match exactly one element, Playwright’s strictness checks help expose an ambiguous selector rather than silently choosing the wrong node.
textContent() versus innerText()
| Method | What it returns | Use it when | Important behavior |
|---|---|---|---|
textContent() |
The DOM node’s text-content string | You need source/DOM text, including text that is not currently rendered | Does not apply visual layout rules such as visibility or line wrapping |
innerText() |
The element’s rendered-text value | You need the text a user effectively sees | Reflects rendering and visible line-break/whitespace behavior; it can differ from textContent() |
Consider an element containing a hidden note, CSS-generated spacing, or line breaks. textContent() is closer to the DOM tree; innerText() is closer to the page’s visual text. Neither is universally “better”—the correct choice depends on what your test or scraper is meant to model.
#1 Best Overall
Choose a locator that expresses the element’s meaning
Interactive controls: use roles
const submit = page.getByRole('button', { name: 'Submit order' });
const label = await submit.innerText();
Role locators follow the accessibility tree and remain readable when classes or generated IDs change. They are usually the best starting point for buttons, links, headings, status messages, checkboxes, and other controls.
Visible copy: use text locators
const exactCopy = page.getByText('Welcome, John', { exact: true });
const dynamicCopy = page.getByText(/welcome, [A-Z a-z]+$/i);
const message = await exactCopy.textContent();
getByText() supports substring matching by default, exact-string matching with exact: true, and regular expressions. During matching, Playwright normalizes whitespace, line breaks, and surrounding whitespace. That normalization affects how an element is found; the returned string still follows the semantics of the method you call.
Scope a locator before reading
const card = page.getByRole('article').filter({ hasText: 'Pro plan' });
const price = card.getByRole('heading', { name: '$15' });
const priceText = await price.innerText();
Scoping avoids accidentally reading a similarly named element elsewhere on the page. If a repeated component has a stable test ID or semantic container, narrow to that container first and then locate its child.
Get text from all matching elements
When a locator intentionally represents a list, use the collection methods rather than looping over individual handles:
const items = page.getByRole('listitem');
const domTexts = await items.allTextContents();
const renderedTexts = await items.allInnerTexts();
console.log(domTexts);
console.log(renderedTexts);
| Collection method | Result |
|---|---|
allTextContents() |
An array containing one textContent string per matched element |
allInnerTexts() |
An array containing one rendered innerText string per matched element |
The array order follows the locator’s document order. If no elements match at the moment the method runs, the result is an empty array; make sure your page has reached the state in which the list is expected before reading it.
Rank #2
Read a specific item
const thirdItem = page.getByRole('listitem').nth(2);
const text = await thirdItem.innerText();
nth(2) selects the third match because indexing is zero-based. Prefer a semantic filter (for example, filter({ hasText: '...' })) when order can change.
Assertions are better than manual extraction for tests
If your purpose is to verify text, keep the value inside Playwright’s web-first assertion system:
import { test, expect } from '@playwright/test';
test('shows the saved status', async ({ page }) => {
await page.goto('https://example.com/settings');
await expect(page.getByRole('status')).toHaveText('Saved');
});
toHaveText() uses text-content semantics by default and automatically waits and retries until the expected state or the assertion timeout is reached. To assert rendered text instead, opt in explicitly:
await expect(page.getByRole('status')).toHaveText('Saved', {
useInnerText: true
});
String expectations normalize whitespace and line breaks before matching. You can also pass a regular expression when portions of the message are dynamic:
await expect(page.getByRole('status')).toHaveText(/saved at d{2}:d{2}/i);
Use direct extraction when you need to transform, store, compare, or send the text elsewhere. Use an assertion when the text itself is the test’s pass/fail condition.
Python Playwright equivalents
The Python binding exposes the same concepts with snake_case method names:
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →from playwright.sync_api import Page
def read_text(page: Page) -> tuple[str | None, str]:
button = page.get_by_role("button", name="Save")
dom_text = button.text_content()
rendered_text = button.inner_text()
return dom_text, rendered_text
items = page.get_by_role("listitem")
dom_texts = items.all_text_contents()
rendered_texts = items.all_inner_texts()
With the asynchronous Python API, add await to each locator operation:
button = page.get_by_role("button", name="Save")
dom_text = await button.text_content()
rendered_text = await button.inner_text()
texts = await page.get_by_role("listitem").all_text_contents()
Python assertions follow the same rule: use a locator assertion for checks, and choose the rendered-text option only when visual semantics matter.
Why older selector calls can surprise you
page.textContent(selector) exists for compatibility but is discouraged in current Playwright guidance. It reads only the first match when several nodes satisfy the selector, which can hide an accidental broad match. Replace it with an explicit locator:
// Discouraged
const text = await page.textContent('.notice');
// Preferred
const text = await page.locator('.notice').textContent();
The locator form makes the selection independently inspectable, composes with role and text locators, and gives you collection methods when multiple matches are expected.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Rank #4
Dynamic, hidden, and empty text: practical edge cases
Wait for the state you actually need
Locators auto-wait for actionable interactions and retry assertions, but direct reads should still be placed after the application state they depend on. For a status that appears after a save, wait for the status assertion rather than adding an arbitrary sleep:
const status = page.getByRole('status');
await expect(status).toBeVisible();
const message = await status.innerText();
Hidden descendants
A hidden child can contribute to textContent() while not contributing to the rendered result you expect from innerText(). If hidden content is intentionally part of your data, use textContent(); if the requirement is user-visible copy, use innerText() and assert visibility where appropriate.
Null versus an empty string
textContent() can return null for a node without text content, whereas an element with no characters commonly yields an empty string. Type your code accordingly (for example, string | null in TypeScript) instead of calling string methods without checking.
Inputs are not read through element text
An input’s current value is a property, not child text. Read it with inputValue():
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
const email = await page.getByLabel('Email').inputValue();
Troubleshooting text reads
- “Locator resolved to multiple elements.” Make the locator unique with a role name,
exact: true, a container scope, or a semantic filter. UseallTextContents()only when multiple matches are intended. - An empty string is returned. The content may be injected later, located in a different frame, or represented by an input value. Wait for the relevant state, switch to the correct frame, or use
inputValue(). innerText()differs from what you expected. Check visibility, CSS layout, line breaks, and hidden descendants. If you need raw DOM text, switch totextContent().- The test is flaky. Replace fixed delays with locator assertions such as
toHaveText(), and choose a locator based on role or meaningful text instead of unstable classes. - The list is incomplete. Virtualized lists may render only the visible window. Scroll or use the application’s data interface if you need every record; Playwright can only read nodes currently present in the DOM.
- The element is inside an iframe. Enter the frame with
page.frameLocator('iframe-selector'), then create the locator from that frame. - Whitespace comparisons fail. Prefer
toHaveText(), which normalizes whitespace for matching, or normalize deliberately before comparing extracted strings.
Performance and reliability choices
- One locator read is usually clearer and cheaper than creating element handles for the same purpose.
- For a collection, one
allTextContents()orallInnerTexts()call avoids repetitive locator queries and preserves order. - Use
textContent()when layout is irrelevant; rendered-text calculation withinnerText()can require the browser to account for layout. - Keep assertions close to the action that changes the text. This gives Playwright a bounded retry window and produces a useful failure location.
- Do not use a screenshot or OCR to read ordinary HTML text; the DOM APIs are more deterministic and retain exact string semantics.
Or skip the browser setup
If your actual goal is to capture a page image or PDF rather than inspect a string, ScreenshotNeo provides a single HTTP request. It accepts consent banners before capture, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets you disable each cleanup step. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the result with X-Page-Verdict and X-Billed headers.
See the complete parameter reference in the ScreenshotNeo API documentation. A cURL request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The equivalent Python request:
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 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 exposes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every feature is available on every plan; 1,000 screenshots per month are free with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.
A concise decision guide
- Need raw DOM characters? Use
locator.textContent(). - Need what is rendered to a user? Use
locator.innerText(). - Need every match? Use
allTextContents()orallInnerTexts(). - Need a pass/fail check? Use
expect(locator).toHaveText(), addinguseInnerText: trueonly for rendered semantics. - Need an input’s current value? Use
inputValue(), not text extraction.
Frequently Asked Questions
Does Playwright wait automatically when calling textContent()?
Locators provide auto-waiting and retryability, but a direct text read is not a substitute for a state-specific assertion. For changing content, wait with an assertion such as toHaveText() or toBeVisible() before reading.
How do I preserve line breaks in extracted text?
Use innerText() when rendered line-break behavior is part of the requirement. For DOM text without layout semantics, use textContent(); then apply your own normalization if your output format requires it.
Can I get text from a shadow DOM element?
Use a locator that reaches the shadow-hosted element; Playwright locators generally pierce open shadow DOM. Closed shadow roots remain inaccessible through ordinary page locators.
Quick Recap
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.

