For AI agents: a documentation index is available at /llms.txt
Skip to main content

Interacting with pages in BAP

BAP's interaction methods run inside the browser, so a click is a real click at real coordinates rather than a dispatched DOM event. This guide covers filling and submitting a form, the wait behavior every interaction method shares, and scrolling to trigger lazy-loaded content.

Prerequisites

Fill out and submit a form

type() enters text character by character with a randomized delay, which is what makes it look human to bot detection. click() submits.

import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
token: TOKEN,
});

try {
const page = await browser.newPage();
await page.goto("https://example.com/login");

await page.type("#username", "user@example.com");

// A delay range makes typing look human to bot detection.
await page.type("#password", "secret", { delay: [100, 200] });

// Start the wait before the click, or a fast navigation finishes first.
const navigated = page.waitForNavigation({ waitUntil: "networkIdle" });
await page.click("#submit");
await navigated;

console.log(await page.url());
} finally {
await browser.close();
}

Response:

https://example.com/dashboard

There is no fill(). type() is the only entry point for text, because instant value-setting is one of the signals bot detection scores. Pass delay: [0, 0] when you're on a trusted site and want the speed instead.

Other input types

Dropdowns, checkboxes, and hovers each have a method. select() takes the option values as varargs, so one value or several both work.

// Single or multiple select.
await page.select("#country", "CA");
await page.select("#toppings", "cheese", "basil");

// Checkboxes and radios.
await page.check("#terms");
await page.uncheck("#newsletter");

// Hover, which is how menus that open on mouseover are reached.
await page.hover("#account-menu");

Wait behavior every method shares

Interaction methods take the same four options, and the defaults mean you rarely need explicit waits:

OptionDefaultWhat it does
waittrueWait for the selector to exist in the DOM before acting. Set false to fail immediately when it's absent
timeout30000How long that wait may take, in milliseconds
scrolltrueScroll the element into view first, the way a person would have to
visiblefalseAct only if the element is actually rendered, not merely present

visible is the one worth reaching for. A selector can match a node that's hidden behind a modal or collapsed in a menu, and clicking it succeeds without doing anything the user would see. visible: true turns that into a timeout you can catch instead of a silent no-op.

// Fail fast rather than waiting 30 seconds for an element that may not exist.
await page.click(".cookie-accept", { wait: false });

// Only click the button a user could actually see.
await page.click("#checkout", { visible: true, timeout: 10_000 });

Scroll, including for lazy-loaded content

scroll() targets a selector or explicit coordinates. Its throughPage option walks the whole document in viewport-sized steps and returns to the top, which is how you trigger lazy-loaded images and infinite-scroll content before capturing or extracting.

// Scroll to an element, or to a coordinate.
await page.scroll({ selector: "#footer" });
await page.scroll({ x: 0, y: 2000 });

// Trigger everything that loads on scroll, then return to the top.
await page.scroll({ throughPage: true });
await page.screenshot({ path: "full.png", fullPage: true });

goBack(), goForward(), and reload() drive the browser's own history, so a flow that depends on back-button behavior doesn't need re-navigation. All three take the same waitUntil and timeout options as goto().

await page.goto("https://example.com/results");
await page.click(".result:first-child");

// Back to the results, without re-running the search.
await page.goBack({ waitUntil: "domContentLoaded" });
await page.reload();

Neither throws when there's nothing in that direction. goBack() and goForward() resolve to null in TypeScript and return None in Python instead.

What isn't here

BAP has no page.mouse, page.keyboard, or page.touchscreen, because those need a live CDP session and BrowserQL has no equivalent. Arbitrary drag gestures, key chords, and multi-touch are out of reach. When a flow needs them, hand the session to Puppeteer or Playwright for that step and keep the rest on BAP.

FAQ & Troubleshooting

My click succeeded but nothing happened on the page

The selector probably matched a hidden element, such as a duplicate in a collapsed menu. Add visible: true so the call fails instead of silently clicking something invisible, and tighten the selector.

Reading the page after a click gives me the old content

The click started a navigation that hadn't finished. Wait for an element unique to the destination with waitForSelector(), which is the option that works identically in both SDKs. waitForNavigation() also works in TypeScript, but it has to be started before the click, as waiting for things explains.

Why is there no fill()?

Setting a field's value instantly is a bot-detection signal, so BAP exposes only type() with its randomized per-character delay. Pass delay: [0, 0] when human-like timing doesn't matter.

Images below the fold are missing from my screenshot

They're lazy-loaded and never entered the viewport. Call scroll({ throughPage: true }) first, which steps through the document and returns to the top, then capture.

Why does addScriptTag fail with SyntaxError: Invalid or unexpected token?

Inline content must be a single line, because the BrowserQL server rejects multi-line content. Host the script and pass url instead, or collapse the source to one line first, which only works when it has no newline-sensitive syntax such as // comments or multi-line template literals. The Python method is add_script_tag and behaves the same way.

Next steps

Was this page helpful?