How ego-lite's Wait Functions Handle Load States and Selectors: A Complete Technical Guide

ego-lite's wait functions use a unified polling architecture that combines CDP (Chrome DevTools Protocol) communication with configurable timeouts to pause execution until pages reach specific load states or selectors become available.

The citrolabs/ego-lite open-source browser automation library provides a Playwright-like waiting API designed specifically for AI agents. This guide examines how waitForLoadState, waitForURL, waitForSelector, and network wait helpers manage different conditions, with full reference to the implementation in src/driver/waits.ts and supporting modules.


The Core Wait Architecture

All wait helpers share a common pattern implemented in src/driver/waits.ts:

  1. Timeout resolution — read user-provided timeout or fall back to state.defaultTimeout
  2. Deadline calculationstate.now() + timeout
  3. Polling loop — repeatedly query the browser via CDP until success or deadline expiry
  4. Resource cleanup — guaranteed release of CDP domains (Network, Runtime, etc.)

This architecture ensures consistent behavior across load states, selectors, URLs, and network events.


How waitForLoadState Handles Different Load States

waitForLoadState accepts either a string ("load", "domcontentloaded", "networkidle") or an options object. The implementation normalizes arguments (lines 58-79) and delegates to specialized handlers:

"load" — Full Document Completion

Calls waitForDocumentLoad with until: "load" from src/driver/load.ts. The helper polls document.readyState until it equals "complete" (lines 165-168).

// Wait for full page load including images and subresources
await waitForLoadState('load');

"domcontentloaded" — DOM Interactive

Calls waitForDocumentLoad with until: "domcontentloaded". Resolves when readyState reaches "interactive" — the DOM is parsed but external resources may still load (lines 165-168).

// Wait only for DOM structure, not full resource loading
await waitForLoadState('domcontentloaded');

"networkidle" — Network Silence

Delegates to the private helper waitForNetworkIdle (lines 346-376). This helper:

  • Enables the CDP Network domain via acquireNetworkEvents
  • Tracks in-flight requests using CDP Network.requestWillBeSent and Network.loadingFinished events
  • Returns true only after zero pending requests for a configurable idleMs window (default behavior, lines 462-473)
// Wait 500ms of network silence before proceeding
await waitForLoadState('networkidle', { idleMs: 500 });

The idleMs parameter lets you tune sensitivity — lower values resolve faster but risk catching transient request gaps; higher values ensure true stability.


How waitForURL Combines URL Matching with Load States

waitForURL polls location.href via Runtime.evaluate until the supplied matcher succeeds (lines 94-104). Matchers support:

  • String — exact match
  • Glob — patterns like **/checkout*
  • RegExp — pattern matching
  • Predicate — custom function (url: string) => boolean

After URL matching, it respects the waitUntil option:

waitUntil value Behavior
commit Resolves immediately when URL matches; useful for about:blank navigation
load (default) Forwards to waitForLoadState("load")
networkidle Forwards to waitForLoadState("networkidle")

Special handling for about:blank (lines 106-110): document-load checks are skipped unless networkidle is explicitly requested, preventing hangs on empty pages.

// Match exact URL and wait for full load
await waitForURL('https://example.com/dashboard', { waitUntil: 'load' });

// Glob match, resolve immediately on navigation commit
await waitForURL('**/checkout*', { waitUntil: 'commit' });

How waitForSelector Handles Selector States

waitForSelector implements two distinct visibility states through different polling strategies (lines 100-108):

"attached" — Element Existence (Default)

Repeatedly calls resolveHandle(selector) to obtain a CDP RemoteObjectId. If resolution fails (element not yet in DOM), sleeps 300ms and retries. Returns immediately upon handle acquisition, regardless of visual properties.

"visible" — Element Visibility

Requires additional JavaScript evaluation via visibilityFn (lines 99-101, 121-124). The injected script verifies:

  • element.offsetParent !== null (not detached from layout)
  • window.getComputedStyle(element).display !== 'none'
  • window.getComputedStyle(element).visibility !== 'hidden'
  • parseFloat(window.getComputedStyle(element).opacity) > 0

Failed visibility checks are treated as "not ready yet" — the polling loop continues rather than throwing.

// Wait for element to exist in DOM (fastest)
await waitForSelector('#submit-button');

// Wait for element to be visually rendered (slower, more reliable)
await waitForSelector('#submit-button', { state: 'visible' });

The 300ms polling interval balances responsiveness with CDP overhead. Unlike Playwright's rAF-based waiting, ego-lite uses fixed intervals for predictable timing in agent workflows.


Network-Level Waiting: waitForRequest and waitForResponse

Both functions call waitForNetworkMatch (lines 125-135, 250-267), which:

  1. Ensures Network domain enabled via acquireNetworkEvents
  2. Listens for CDP events (Network.requestWillBeSent, Network.responseReceived)
  3. Applies networkMatches logic to compare against matcher (lines 330-337)

Matcher types:

  • String — URL contains substring
  • RegExp — pattern test against full URL
  • Predicate — custom (request|response) => boolean

Important restriction: async predicates throw a clear error (lines 330-337) — only synchronous functions are supported due to CDP event handling constraints.

// Wait for any PNG image request
const pngReq = await waitForRequest(/\.png$/);

// Wait for successful API response using predicate
const apiResp = await waitForResponse(r => 
  r.status() === 200 && r.url().includes('/api/')
);

Network waiting shares timeout handling (networkTimeout, browserEventTimeout) and cleanup logic with other waits (lines 373-382), ensuring the Network domain is always released after completion or failure.


Timeout and Resource Management

All waits integrate with src/state.ts for timing utilities:

  • state.now() — high-resolution timestamp for deadline calculation
  • state.sleep(ms) — async delay between polling iterations
  • state.defaultTimeout — fallback when user timeout unspecified

Critical cleanup guarantees (lines 373-382):

  • Network domain disabled after waitForNetworkIdle, waitForRequest, waitForResponse
  • JavaScript handles released after waitForSelector visibility checks
  • Event listeners drained via drainEvents from src/driver/observe.ts

This prevents resource leaks during long-running agent sessions.


Summary

  • Unified polling architecture — all waits use state.now() + timeout deadlines with CDP-based condition checking
  • waitForLoadState delegates to waitForDocumentLoad (load/DOMContentLoaded) or waitForNetworkIdle (networkidle)
  • waitForURL combines flexible URL matching with optional post-navigation load state waiting
  • waitForSelector distinguishes "attached" (DOM presence) from "visible" (rendered, interactive) via JavaScript evaluation
  • Network waits (waitForRequest/waitForResponse) leverage CDP Network domain with synchronous matcher constraints
  • Resource safety — guaranteed cleanup of CDP domains and handles via shared timeout/cleanup logic

Frequently Asked Questions

What is the default timeout for ego-lite wait functions?

Wait functions fall back to state.defaultTimeout when no explicit timeout is provided. According to src/state.ts, this provides a consistent baseline across all operations, though the exact millisecond value depends on the state configuration at initialization.

Why does waitForSelector have a 300ms polling interval?

The 300ms fixed interval in waitForSelector (lines 100-108) balances responsiveness with CDP communication overhead. Unlike Playwright's requestAnimationFrame-based approach, this predictable timing helps AI agents reason about execution timing without browser frame-layer complexity.

Can I use async predicates with waitForRequest or waitForResponse?

No. The networkMatches helper in src/driver/waits.ts explicitly detects and throws on async predicates (lines 330-337). This restriction exists because CDP event handlers execute synchronously — use synchronous functions that inspect request/response properties directly.

How does networkidle detection differ from Playwright?

ego-lite's waitForNetworkIdle (lines 462-473) uses CDP Network domain events to track in-flight requests, similar to Playwright, but exposes idleMs for direct configuration. The implementation in src/driver/observe.ts provides the drainEvents utility for event queue management, tailored for agent automation rather than heavy testing workloads.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →