What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
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.
#1 Best Overall
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:
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, 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 minuteRoles 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:
Rank #2
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.
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.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →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.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →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:
Rank #4
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.
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().
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteText 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.
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
textContentfor machine-oriented DOM inspection andinnerTextfor 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:
Recommended Free Tools
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_infoandcapture_pdftools 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.
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.

