ScreenshotNeo

BlogHow-to

How to Interact with Web Pages Using Puppeteer

Learn how to navigate, find elements, fill forms, click controls, wait for results, and handle common Puppeteer interaction problems in JavaScript.

By the ScreenshotNeo team4 October 202610 min read

Puppeteer lets a JavaScript program control a browser page: open a URL, find an element, enter text, click, wait for the resulting state, and inspect what happened. For most interactions, use Puppeteer locators: they wait for the target element to be present and in an appropriate state when performing an action. For navigation-causing clicks, start waiting for navigation before the click can happen.

The standard workflow is: launch or connect to a browser, create a Page, navigate, interact, verify the result, and close the browser if your script launched it. The examples below use Puppeteer’s current documented locator approach. Selectors must match the site you are automating. See the official Getting Started guide, Page interactions guide, and Page API reference for current details.

1. Install Puppeteer and run a basic interaction

Puppeteer is a JavaScript library for browser automation. Its official overview describes automation of Chrome and Firefox using Chrome DevTools Protocol and WebDriver BiDi. The Getting Started guide reviewed for this article is version 25.12.0; check the live documentation when publishing or upgrading because APIs and recommendations can evolve.

In a new project, install the package and create an ES module script:

npm install puppeteer

Save this as interact.mjs. Replace the URL and selectors with ones that match the page and task. The accessible search selector is illustrative; it works only if the page exposes an element with the accessible name “Search.”

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto('https://developer.chrome.com/', {
    waitUntil: 'domcontentloaded',
  });

  await page.setViewport({ width: 1280, height: 800 });
  await page.locator('::-p-aria(Search)').fill('Puppeteer');
  await page.locator('button[type="submit"]').click();

  // Inspect an outcome that makes sense for the page.
  console.log('Current URL:', page.url());
  console.log('Page title:', await page.title());
} finally {
  await browser.close();
}

Run it with node interact.mjs. The try/finally ensures the browser closes even if navigation or an interaction throws. If your program connects to a browser that another process owns, follow the connection lifecycle for that browser instead of closing a browser you do not own.

2. Choose a selector that matches the target

Use a selector that identifies the control the task actually needs. Puppeteer supports CSS selectors and additional selector syntax for accessibility attributes, visible text, XPath, and shadow DOM. These are Puppeteer selector capabilities; the target page still needs to contain a matching element.

Selector approach Example Useful when
Accessible name ::-p-aria(Search) The page exposes a stable, meaningful accessible name.
Text ::-p-text(Continue) The visible wording identifies the control and is unlikely to change.
CSS button[type="submit"] The page has a stable attribute or structural selector.
XPath Use Puppeteer’s XPath selector syntax for the specific target. The target is easier to describe with an XPath expression.
Shadow DOM Use Puppeteer’s shadow-DOM-capable selector syntax. The target is inside a web component’s shadow tree.

For actions, the current guide recommends locators. A locator describes how to find an element when the action runs, and automatically waits for the element to appear and reach an appropriate state. Prefer selectors that describe the intended control rather than brittle generated classes. Accessibility names and text can express user-visible intent; CSS can be concise when the markup is stable. There is no selector type that is best for every page.

3. Fill fields, click buttons, and select options

Fill a text field

await page.locator('input[name="email"]').fill('dev@example.com');

Use the actual field selector. If the form requires a particular sequence of keystrokes or keyboard events, use keyboard input instead of assigning a value in page JavaScript:

await page.locator('input[name="email"]').click();
await page.keyboard.type('dev@example.com');

Click and hover

await page.locator('button[type="submit"]').click();
await page.locator('.account-menu').hover();

Locators are usually the clearest way to perform these actions on a target found by selector. Puppeteer also documents mouse, touch, and keyboard input for tasks that need more control over pointer or key behavior.

Select a dropdown option

await page.locator('select[name="country"]').select('CA');

For a native <select>, pass the option value. A custom dropdown built from ordinary elements may need a click followed by a click on the desired option instead.

Use page JavaScript for page-context work

page.evaluate() runs a function in the page’s JavaScript context. Use it to read page state or perform work that specifically requires that context. For user-like interaction, prefer locators and browser input methods; setting an element’s value directly in page code can bypass behavior that a real interaction would trigger.

const heading = await page.evaluate(() => {
  return document.querySelector('h1')?.textContent?.trim() ?? null;
});
console.log(heading);

4. Wait for the result, not an arbitrary delay

The right wait depends on what the action does. A click may cause full document navigation, update content within the existing document, or make a request without a visible page transition. Wait for the expected outcome rather than assuming all clicks navigate.

When a click causes document navigation

Register the navigation wait before the click. Starting the wait afterward can miss a fast navigation and create a race:

const [response] = await Promise.all([
  page.waitForNavigation(),
  page.locator('a.next-page').click(),
]);
console.log('Navigated to:', page.url());

Use the real link selector. The navigation response may be null in cases such as same-document navigation, so also inspect the resulting URL or another state when that matters to your task.

When content updates without navigation

Wait for a locator that represents the expected result, such as a success message or a row added to a list:

await page.locator('button[type="submit"]').click();
await page.locator('.success-message').wait();
const message = await page.locator('.success-message').textContent();
console.log(message);

For a specific condition, Puppeteer also documents waits for selectors, functions, requests, and responses. Choose a condition tied to the task: for example, wait for a particular response when an API result is the meaningful outcome, or for a selector when the resulting UI is what you need to confirm.

Use waitForSelector when you need an element handle

waitForSelector() is a lower-level option for explicitly waiting for a selector or working with an element handle. Unlike locator actions, it does not automatically retry the action if that action fails. Dispose of element handles when finished to avoid retaining unnecessary references:

const handle = await page.waitForSelector('.result');
if (handle) {
  try {
    console.log(await handle.evaluate(element => element.textContent?.trim()));
  } finally {
    await handle.dispose();
  }
}
Approach Waiting and retries Choose it for
Locator Waits for the element to be present and in a suitable state for its action. Most clicks, fills, and other element actions.
waitForSelector() and element handle Explicit wait; you manage subsequent action behavior and handle lifecycle. Lower-level control or code that needs an element handle.

5. Verify what happened and capture diagnostics

After an action, check an observable outcome: the URL, page title, visible message, selected value, or other state relevant to the task. A screenshot can help diagnose rendering, but it does not by itself prove the action succeeded.

console.log('URL:', page.url());
console.log('Title:', await page.title());

const screenshot = await page.screenshot({ path: 'after-action.png' });

The Page API also documents page screenshots and PDF output. Use them as supporting diagnostics or outputs where appropriate, alongside checking the actual result state.

6. Complete interaction example with a form and result check

This example shows the shape of a robust interaction script. Replace the URL, selectors, field values, and expected result with the target site’s actual page behavior. It does not assume that a particular public site has this form.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });
  await page.goto('https://example.com/form', {
    waitUntil: 'domcontentloaded',
  });

  await page.locator('input[name="email"]').fill('dev@example.com');

  // If submission navigates, set up the wait before clicking.
  const [response] = await Promise.all([
    page.waitForNavigation(),
    page.locator('button[type="submit"]').click(),
  ]);

  console.log('Result URL:', page.url());
  console.log('Title:', await page.title());

  // Capture a diagnostic artifact when useful.
  await page.screenshot({ path: 'result.png' });
} finally {
  await browser.close();
}

If the form updates in place, replace the navigation wait with a wait for the actual success state, such as page.locator('.success-message').wait(). Choose the wait based on observed site behavior.

7. Common problems and fixes

Symptom Likely cause Fix
Locator times out or cannot find an element The selector or accessible name does not match the current page, or the element has not appeared. Inspect the page and confirm the exact accessible name, text, attributes, and DOM location. Use a selector suited to the actual markup.
Click appears to do nothing The control may be disabled, covered, outside the intended state, or may update the page without navigation. Wait for the control’s usable state through a locator action, then wait for and verify the expected UI or response.
Navigation wait hangs or races The click does not navigate, or the wait began after a fast navigation. For navigation, pair waitForNavigation() and the click in Promise.all(). For in-page changes, wait for a selector, function, request, or response that represents the outcome.
Script reads stale content The script reads immediately after an action while the page is still updating. Wait for the specific new content or state before reading it; avoid fixed sleeps as a substitute for a condition.
Text entry does not trigger expected site behavior Direct page-context value assignment may not reproduce keyboard input events. Use locator fill() for normal field entry or keyboard input when the task depends on key events.
Element handle resources accumulate Handles returned by lower-level APIs are retained after use. Dispose of handles when finished, or use locators when a handle is not needed.
Screenshot looks right but task is wrong A rendered image alone does not establish that the desired action succeeded. Check URL, title, result text, selected value, or another task-specific state as well.
Browser remains open after an error Cleanup was skipped on an exception path. Put browser closure in a finally block when the script owns the launched browser.

8. Performance, reliability, and cost considerations

Performance

  • Wait for the page condition your task needs. A narrower condition can let the script proceed sooner than waiting for unrelated activity to finish.
  • Reuse a browser for multiple pages or tasks when the process design permits it, while creating pages with the state and isolation each task needs.
  • Do not use long fixed delays as a general synchronization strategy. They can waste time on fast pages and still fail on slow ones.
  • Capture screenshots and PDFs only when they are part of the output or useful diagnostics; image capture adds work beyond a simple interaction.

Reliability

  • Use selectors tied to meaningful names, text, or stable attributes where available, and revisit them when the site changes.
  • Register navigation waits before actions that navigate. For in-page updates, wait for the resulting state instead.
  • Verify the outcome explicitly. A click completing is not the same as the intended task succeeding.
  • Close resources the script owns in a finally block, and dispose of lower-level handles.

Cost

Puppeteer is a software library; the cited documentation does not specify a per-interaction service price or a browser hosting charge. Your actual operating cost depends on where and how you run the browser. If you need a managed screenshot rather than a browser-interaction workflow, ScreenshotNeo’s stated plans are free for 1,000 shots a month, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan.

9. Or skip the browser setup

If your task is to capture a page rather than interact with its controls, ScreenshotNeo can return a screenshot or PDF through one GET request. See the ScreenshotNeo API documentation for its request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
  writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. ScreenshotNeo is a website screenshot API and MCP server by Yorker Media; its capture request does not replace Puppeteer when you need to fill a form or click through a workflow.

Sign up for ScreenshotNeo’s free plan to capture up to 1,000 screenshots a month with no card.

10. Frequently asked questions

Can Puppeteer interact with pages that update without reloading?

Yes. Wait for the expected updated state, such as a result element or response, rather than waiting for document navigation.

Can Puppeteer interact with content inside a web component?

Puppeteer supports selector capabilities for shadow DOM. The selector must still identify the target in the component’s actual structure.

Should I use a screenshot to confirm a click worked?

A screenshot can help inspect what rendered, but confirm success through the URL or a task-specific page state as well.

When is ScreenshotNeo a better fit than Puppeteer?

Use ScreenshotNeo when you need a page screenshot or PDF without running your own browser workflow. Use Puppeteer when you need to interact with controls, navigate a multi-step UI, or inspect browser state.