Browser Automation Fundamentals: DOM, Events, and Navigation

Technical Dive
Jul 12
by Idan Raman
DOM, Events & Navigation

Every browser automation script—whether it's clicking through a checkout flow in CI or pulling listings off a page that was never built for scraping—comes down to the same three primitives: reading and changing the DOM, dispatching and waiting on events, and managing navigation from one page state to the next. Playwright and Puppeteer wrap these behind convenient APIs, but the abstractions leak constantly. Stale element errors, races against events that haven't fired yet, and scripts that read a page before it's actually ready are almost never framework bugs—they're a misunderstanding of one of these three fundamentals.

The DOM Is a Tree, Not a String

The HTML a server sends is just the initial paint. The moment JavaScript runs, the browser is working from the DOM—a live tree of nodes that gets mutated constantly by scripts, frameworks, and user interaction. This is why view-source: and a page's actual runtime state routinely disagree, and why regex-matching raw HTML is a losing strategy against any modern site. A selector like page.query_selector(".price") doesn't search a string; it walks the current tree at the instant it runs, which means the same selector can return nothing, one node, or a different node entirely depending on timing.

This is also why locators in Playwright are lazy by design. A Locator object doesn't resolve to an element when it's created—it re-queries the tree every time you act on it, which is what lets auto-waiting retry against a moving target instead of failing on the first stale reference.

Events Are What Actually Happen When You "Click"

Calling a click method doesn't just fire a click event. A real user interaction dispatches a whole sequence—pointerdown, mousedown, focus, mouseup, click—and plenty of front-end code listens for the earlier events in that chain, not just the last one. This is the actual reason element.dispatchEvent() or a raw JS .click() call frequently fails where a driver-level click succeeds: the framework fires the full sequence through the browser's real input pipeline, not a synthetic one-off event injected into the DOM.

It's also why waiting matters more than most scripts assume. An element can be attached to the DOM, technically visible, and still not accept input because a transition is mid-animation or a JS handler hasn't bound yet. Tools built on the Chrome DevTools Protocol get this largely for free because CDP's Input domain dispatches through the same pipeline real hardware events use, rather than trusting a script-injected event that the page's own listeners might ignore.

Navigation Is a Lifecycle, Not a Single Step

"The page loaded" is three or four different browser states wearing one name. Playwright's wait_for_load_state() exposes them directly: domcontentloaded fires once the HTML is parsed, load once every subresource (images, stylesheets, iframes) has finished, and networkidle once the network has gone quiet for a stretch—the one that actually matters for JS-heavy pages that fetch their real content after the initial paint. Scripts that only wait on the first two will reliably scrape or click against a half-rendered page.

import os
from anchorbrowser import Anchorbrowser
from playwright.sync_api import sync_playwright

anchor_client = Anchorbrowser(api_key=os.getenv("ANCHOR_API_KEY"))
session = anchor_client.sessions.create()

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(session.data.cdp_url)
    page = browser.new_page()
    page.goto("https://example.com/products")
    page.wait_for_load_state("networkidle")
    page.wait_for_selector(".product-card")
    price = page.locator(".price").first.text_content()

The connection here is standard CDP over Playwright, so none of this is Anchor-specific syntax—it's the same DOM tree, event pipeline, and load-state model any Playwright script runs against locally. What changes is what's underneath: a cloud session that stays alive for as long as the extraction takes, instead of a local browser process tied to your machine's uptime.

Getting DOM, events, and navigation right is what separates a script that works once from one that survives a hundred runs against a site that keeps changing under it. If you're building that kind of automation and want the browser infrastructure handled for you, Anchor gives you managed cloud sessions you connect to over the same CDP interface. Get an API key and try it against a page you're already automating.

Stay ahead in browser automation

We respect your inbox. Privacy policy

Welcome aboard! Thanks for signing up
Oops! Something went wrong while submitting the form.