Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
browser automation

How to Check Whether Specific Text Exists on a Page with Puppeteer

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

Use page.evaluate() for an immediate, whole-page check: evaluate a predicate in the browser and return a Boolean. For text that appears after client-side rendering, wait with page.waitForFunction(). If you need the element that contains the text, use Puppeteer’s text selector or a locator instead. The correct method depends on timing, scope, and matching rules such as case sensitivity and whitespace.

Choose the check that matches your question

Question Recommended API What it proves
Does this string occur in the current page text? page.evaluate() A Boolean result for the chosen DOM text representation at that instant.
Will this string appear after JavaScript finishes rendering? page.waitForFunction() That a page-context predicate becomes truthy before the timeout.
Which element contains this text? Text selector or locator A handle or locator for a minimal matching element, including text in open shadow roots.
Does a known selector exist? page.waitForSelector() That at least one element matches the selector; it does not verify arbitrary text.

These APIs are documented in Puppeteer’s Page.evaluate reference, Frame.waitForFunction reference, Page.waitForSelector reference, and page interactions guide. The documentation pages observed for this topic are labeled Puppeteer 25.10.0 or 25.12.0; your installed version can differ.

Immediate whole-page substring check

This is the smallest useful test. It reads document.body.innerText in the page context and applies JavaScript’s case-sensitive includes():

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});

const target = 'Order confirmed';
const exists = await page.evaluate(
  text => document.body?.innerText.includes(text) ?? false,
  target,
);

console.log({target, exists});
await browser.close();

page.evaluate() serializes the function’s return value back to Node.js. Passing target as an argument is safer and clearer than interpolating it into source code: quotes, backslashes, and user input remain data rather than executable JavaScript.

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.

innerText versus textContent

innerText approximates rendered text and is affected by layout and visibility. textContent reads the DOM text nodes, including text that may not be rendered. Choose deliberately:

const domTextExists = await page.evaluate(
  text => document.body?.textContent?.includes(text) ?? false,
  target,
);

Neither method automatically performs case folding, word-boundary matching, or whitespace normalization. A substring such as Order also matches PreOrder. State the intended rule in the test name and assertion.

Define matching semantics explicitly

Case-insensitive matching

const exists = await page.evaluate((text) => {
  const haystack = document.body?.innerText ?? '';
  return haystack.toLocaleLowerCase().includes(text.toLocaleLowerCase());
}, target);

For predictable machine-oriented checks, use a fixed normalization policy rather than locale-sensitive behavior:

const exists = await page.evaluate((text) => {
  const normalize = value => value.normalize('NFKC').toLowerCase();
  return normalize(document.body?.innerText ?? '').includes(normalize(text));
}, target);

Whitespace-insensitive matching

const compact = value => value.replace(/s+/g, ' ').trim();
const exists = await page.evaluate((text) => {
  const compact = value => value.replace(/s+/g, ' ').trim();
  return compact(document.body?.innerText ?? '').includes(compact(text));
}, target);

Do not normalize whitespace when spaces carry meaning, such as preformatted code or a fixed-format identifier.

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

Exact text instead of a substring

For an exact whole-page comparison, normalize both values and compare with ===. More commonly, exactness applies to one element; locate that element and compare its normalized textContent rather than treating a page-wide substring as exact.

Wait for text rendered asynchronously

A one-time evaluation reports only the current state. Single-page applications may insert the target after an API response, hydration, or a delayed component render. Use a finite predicate wait:

const target = 'Order confirmed';
await page.waitForFunction(
  text => (document.body?.innerText ?? '').includes(text),
  {timeout: 10_000, polling: 'mutation'},
  target,
);

console.log('Text appeared');

waitForFunction() repeatedly runs the predicate in the page context until it returns a truthy value or the timeout expires. Its options support polling, timeout, and cancellation through a signal. A finite timeout turns a missing message into a useful test failure instead of an indefinitely pending run. If your application changes text through timers rather than DOM mutations, use interval polling:

await page.waitForFunction(
  text => (document.body?.innerText ?? '').includes(text),
  {timeout: 15_000, polling: 100},
  target,
);

Wait for a selector when the selector is the requirement

If the application contract is “the confirmation element is present,” use waitForSelector():

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const confirmation = await page.waitForSelector('[data-testid="confirmation"]', {
  visible: true,
  timeout: 10_000,
});
if (!confirmation) throw new Error('Confirmation element was not found');

This proves selector presence (and, with visible: true, visibility according to Puppeteer’s selector wait). It does not prove that the element contains a particular phrase. Read and test its text separately:

const actual = await page.$eval(
  '[data-testid="confirmation"]',
  element => element.textContent ?? '',
);
if (!actual.includes('Order confirmed')) {
  throw new Error(`Unexpected confirmation text: ${actual}`);
}

The Page.$eval reference describes evaluating against the first matching element. If you retain an element handle from a lower-level wait, dispose of it when finished; the interactions guide warns that unreleased handles can contribute to memory leaks.

Find the element that contains the text

Use a text selector when the next operation is element-oriented—for example, clicking a button whose label is “Continue.” Puppeteer’s interactions guide documents the ::-p-text(...) selector and locators:

const locator = page.locator('::-p-text(Order confirmed)');
await locator.wait();
const text = await locator.map(element => element.textContent).first().wait();
console.log(text);

The text selector targets minimal elements containing the requested text and can match text in open shadow roots. That behavior is different from scanning document.body.innerText. It does not automatically mean exact normalized equality; compare the returned text yourself when exactness matters.

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

Punctuation and selector syntax

Characters that overlap selector syntax can require escaping or a different locator expression. If the target is supplied by a user or external data, prefer a page-context predicate for a literal Boolean check, or validate and escape the selector input before constructing a text selector.

Open and closed shadow roots

Puppeteer’s documented text selector explicitly covers open shadow roots. A body-text snapshot and a text selector may therefore produce different results in component-heavy applications. Closed shadow roots intentionally restrict outside access; test through a public UI signal or application-level hook instead of claiming that a page-wide scan covers them.

Build a reliable reusable helper

export async function pageContainsText(page, text, {
  timeout = 0,
  caseSensitive = true,
  collapseWhitespace = false,
  property = 'innerText',
} = {}) {
  const normalize = value => {
    let result = value;
    if (collapseWhitespace) result = result.replace(/s+/g, ' ').trim();
    if (!caseSensitive) result = result.toLowerCase();
    return result;
  };

  const predicate = (value, options) => {
    const raw = document.body?.[options.property] ?? '';
    const normalizeInPage = input => {
      let result = String(input);
      if (options.collapseWhitespace) result = result.replace(/s+/g, ' ').trim();
      if (!options.caseSensitive) result = result.toLowerCase();
      return result;
    };
    return normalizeInPage(raw).includes(normalizeInPage(value));
  };

  const options = {caseSensitive, collapseWhitespace, property};
  if (timeout > 0) {
    await page.waitForFunction(predicate, {timeout}, text, options);
    return true;
  }
  return page.evaluate(predicate, text, options);
}

Keep the helper’s policy visible in its options. A test that silently changes from case-sensitive to case-insensitive matching can pass while the UI regresses.

Navigation and timing setup

Text checks are only as reliable as the page state they inspect. Choose a navigation wait condition that matches the application:

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.
  • domcontentloaded is useful when the initial DOM is the test target.
  • load waits for the load event and its dependent resources.
  • networkidle0 or networkidle2 can help on pages that finish rendering after requests, but analytics, long polling, and streaming can prevent a true idle state.
await page.goto(url, {
  waitUntil: 'domcontentloaded',
  timeout: 30_000,
});
// Then wait for the specific text or selector your test requires.

Prefer a meaningful application signal over a large arbitrary delay. A fixed waitForTimeout() can be too short on a busy runner and unnecessarily slow on a fast one; a predicate or selector wait expresses the real condition.

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

Common failures and fixes

Symptom Likely cause Fix
The result is false, but a user sees the phrase. Checked before client rendering, used a different case, or text is inside a component boundary. Use waitForFunction(), define normalization, and inspect the component’s shadow-root behavior.
waitForSelector() succeeds but the text assertion fails. The selector exists but its content is different or still loading. Read textContent and assert the phrase explicitly, or wait on a text predicate.
The wait times out. The phrase never appears, the wrong frame is active, or the timeout is shorter than the real render time. Capture diagnostics, verify the URL and frame, check the page text, and set a realistic finite timeout.
Text is split by line breaks or nested spans. Whitespace in innerText or textContent differs from the visual phrase. Collapse whitespace deliberately, or assert on the element’s accessible/semantic signal instead.
A text selector returns an unexpected ancestor. The selector chooses a minimal element according to its documented behavior, not necessarily your preferred tag. Inspect the matched element and add a structural selector or explicit text comparison.
The browser process hangs or memory grows. Browser/page instances or element handles are not closed or disposed. Use try/finally to close the browser and dispose retained handles.
The page is in an iframe. page.evaluate() runs in the main frame. Obtain the target frame and call its evaluation or waiting API, then verify the frame URL and lifecycle.

Add diagnostics on failure

try {
  await page.waitForFunction(
    text => (document.body?.innerText ?? '').includes(text),
    {timeout: 8_000},
    target,
  );
} catch (error) {
  console.error('URL:', page.url());
  console.error('Title:', await page.title());
  console.error('Visible text sample:', (await page.evaluate(() => document.body?.innerText ?? '')).slice(0, 2_000));
  throw error;
}

Performance, reliability, and security

  • Evaluate one predicate rather than transferring an entire large DOM to Node.js. The Boolean result is small and avoids unnecessary serialization.
  • Use a selector or scoped element evaluation when the page is large and the requirement concerns one component.
  • Choose mutation polling for DOM-driven rendering and interval polling for timer-driven updates; keep the timeout bounded.
  • Do not put untrusted text into executable code. Pass it as an argument to evaluate(), waitForFunction(), or a locator API.
  • Use stable test hooks such as data-testid when you own the page. Human-facing copy changes more often than an explicit test contract.
  • When matching sensitive text, avoid logging complete page text in CI output; log a short, redacted diagnostic.

Or skip the browser setup

If you only need a rendered screenshot or PDF rather than a Puppeteer assertion, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. It accepts cookie/consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. This is a capture service, not a replacement for a Boolean DOM assertion, but it can remove browser orchestration when your deliverable is visual.

cURL

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

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

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}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const buffer = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', buffer));

See the ScreenshotNeo documentation for request options. It supports full-page capture with lazy images, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF paper settings and page ranges, custom CSS and JavaScript, clicks, selector/delay/network-idle waits, request and resource blocking, headers/cookies/user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen TTL caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Parameter names used by other screenshot APIs are accepted to ease migration.

Every plan includes every feature: Free provides 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to start without a card.

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

Testing strategy

  1. Navigate to the page with an explicit timeout and an appropriate initial wait condition.
  2. State whether the assertion is substring, exact, case-insensitive, or whitespace-normalized.
  3. Use evaluate() for an immediate snapshot, waitForFunction() for asynchronous text, or a text selector/locator for an element operation.
  4. Scope the check to a stable element or frame when a whole-page scan could produce false positives.
  5. On failure, record the URL, title, frame, and a redacted text sample, then close resources in cleanup code.

Frequently Asked Questions

Does Puppeteer have a dedicated contains-text assertion?

Puppeteer supplies browser APIs rather than a built-in assertion library. Evaluate innerText or textContent for a Boolean, or use a text selector/locator to find an element; add assertions from your test framework.

Can a body-text check see text in an iframe?

Not from the main page context. Select the iframe’s frame and run the evaluation or wait in that frame.

Should I use innerText or textContent?

Use innerText when rendered text is the requirement and textContent when DOM text nodes, including hidden content, are the requirement.

What happens when the text never appears?

waitForFunction() rejects after its timeout. Catch the error to add diagnostics, then fail the test rather than using an unbounded wait.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Read next

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.