PRACTICAL GUIDE / WebDriver BiDi browsing context events

Track Tabs, Frames, and Navigation with BiDi Browsing Context Events

Track WebDriver BiDi context creation, navigation, frames, and teardown with event correlation, state modeling, diagnostics, and reliable Selenium tests.

By The Testing AcademyUpdated July 11, 20269 min read
All field guides
In this guide11 sections
  1. Model the lifecycle as two related state machines
  2. Open the BiDi channel and subscribe before acting
  3. Snapshot the tree without creating a blind spot
  4. Correlate context IDs and navigation IDs correctly
  5. Recognize tabs and frames from parentage
  6. Reduce events into an inspectable local model
  7. Wait for the milestone the assertion actually needs
  8. Diagnose missing and contradictory events
  9. Balance event precision against harness complexity
  10. Browsing-context implementation checklist
  11. Close with a timeline, not a guess

What you will learn

  • Model the lifecycle as two related state machines
  • Open the BiDi channel and subscribe before acting
  • Snapshot the tree without creating a blind spot
  • Correlate context IDs and navigation IDs correctly

Reliable browsing-context automation begins by treating tabs, windows, and frames as a changing tree, not as a list sampled after the interesting action has already happened. WebDriver BiDi exposes that tree through context lifecycle and navigation events. A test harness can subscribe before an action, correlate events by context and navigation identifiers, and explain exactly which document reached which milestone.

This event model complements classic WebDriver. Element interaction and window switching can remain in Selenium's established APIs, while BiDi supplies chronological evidence. The result is especially useful for popup flows, frame-heavy applications, redirects, and failures where a final URL says too little about how the browser arrived there.

The WebDriver BiDi specification defines browsing-context events including contextCreated, navigationStarted, navigationCommitted, domContentLoaded, load, navigation failures, and contextDestroyed. Context lifecycle and document navigation are related but not identical. A context can survive several navigations, and each navigation has its own identifier.

Animated field map

BiDi Browsing Context Lifecycle

Follow one context from creation through document milestones and child-frame attachment until the context is removed.

  1. 01 / context created

    Context Created

    A tab, window, or frame receives a context id and optional parent id.

  2. 02 / navigation started

    Navigation Started

    A navigation id identifies the new attempt inside the existing context.

  3. 03 / document committed

    Document Committed

    The remote end commits the new document before later readiness events.

  4. 04 / child frame attached

    Child Frame Attached

    A created child context points back to its containing parent context.

  5. 05 / context destroyed

    Context Destroyed

    The context leaves the tree and pending work must be cancelled or closed.

Keep one record keyed by context id and separate navigation records keyed by navigation id. This prevents a second navigation from overwriting the evidence for a failed first attempt. It also prevents a frame removal from being misreported as a navigation failure.

Open the BiDi channel and subscribe before acting

A WebDriver new-session request asks the remote end for a WebSocket URL. The exact Selenium setup differs by binding and browser support, but the wire contract is explicit: request webSocketUrl, read the returned URL from capabilities, connect, and subscribe before the application action.

JSON
{
  "capabilities": {
    "alwaysMatch": {
      "browserName": "firefox",
      "webSocketUrl": true
    }
  }
}

On the connected socket, subscribe to only the events the harness can consume. Add failure and abort events when navigation diagnosis matters; otherwise a timeline can stop at navigationStarted without explaining why.

JSON
{
  "id": 1,
  "method": "session.subscribe",
  "params": {
    "events": [
      "browsingContext.contextCreated",
      "browsingContext.navigationStarted",
      "browsingContext.navigationCommitted",
      "browsingContext.domContentLoaded",
      "browsingContext.load",
      "browsingContext.navigationFailed",
      "browsingContext.navigationAborted",
      "browsingContext.contextDestroyed"
    ]
  }
}

Store the command id until its success response arrives. An event can be delivered independently of command responses, so code must dispatch messages by shape instead of assuming the next socket message acknowledges the subscription.

Snapshot the tree without creating a blind spot

Subscriptions describe changes after they become active. browsingContext.getTree describes the current tree. A robust collector subscribes first, requests the tree second, and merges both sources idempotently. If a context appears in an event and the snapshot, the same context id should update one record rather than create two.

JSON
{
  "id": 2,
  "method": "browsingContext.getTree",
  "params": {}
}

There is still a reconciliation problem: an event may arrive while the tree command is in flight. Preserve event timestamps and local receive order, but do not invent a total browser-wide order when separate contexts are active concurrently. The safe invariant is per context and per navigation. After merging, every child should either reference a known parent or remain explicitly marked as awaiting its parent event.

Correlate context IDs and navigation IDs correctly

The context id answers "where did this happen?" The navigation id answers "which document transition was this?" Events such as navigationStarted, navigationCommitted, domContentLoaded, and load include navigation information that lets the collector group milestones from the same attempt.

A redirect chain and a replacement navigation can produce sequences that are more complex than one start followed by one load. Never mark a context ready merely because any load event arrived. Match the expected context, navigation id, and readiness milestone. If the application intentionally triggers another navigation, supersede the earlier wait with an explicit reason and retain both timelines.

Fragment navigation and history updates deserve separate treatment because they do not imply a new document. A route change in a single-page application may require an application-level condition after the protocol event. BiDi tells the harness what the browser did; it does not know when a framework's data and widgets satisfy the test's business contract.

Recognize tabs and frames from parentage

contextCreated carries context information, including a parent relationship. A context without a parent is top level. A context with a parent is an embedded child, which allows a reducer to build an explicit frame tree even when frames are added dynamically.

Classic Selenium window handles remain the correct input for switchTo().window(...), as described in the official windows and tabs guide. Do not assume that a BiDi context id is interchangeable with a WebDriver window handle unless the binding explicitly documents that relationship. Keep a mapping established from supported APIs and observed metadata.

Frame events are valuable even when interaction still uses switchTo().frame(...). If an element lookup fails, the timeline can show that the expected child context was never created, was replaced during a rerender, or was destroyed before the command. That is much more actionable than repeating the lookup with a longer timeout.

Reduce events into an inspectable local model

The reducer should be deterministic and side-effect free. Event handlers enqueue normalized messages; one reducer updates state and emits higher-level signals. This design avoids races between callbacks that independently mutate maps or resolve promises.

TypeScript
type Phase = "created" | "started" | "committed" | "dom-ready" | "loaded" | "destroyed";

type ContextRecord = {
  id: string;
  parent: string | null;
  url: string;
  phase: Phase;
  activeNavigation: string | null;
};

type BidiEvent = {
  method: string;
  params: {
    context: string;
    parent?: string | null;
    navigation?: string | null;
    url?: string;
  };
};

export function applyEvent(
  contexts: Map<string, ContextRecord>,
  event: BidiEvent,
): void {
  const p = event.params;
  const current = contexts.get(p.context) ?? {
    id: p.context,
    parent: p.parent ?? null,
    url: p.url ?? "about:blank",
    phase: "created" as const,
    activeNavigation: null,
  };

  const phases: Record<string, Phase> = {
    "browsingContext.contextCreated": "created",
    "browsingContext.navigationStarted": "started",
    "browsingContext.navigationCommitted": "committed",
    "browsingContext.domContentLoaded": "dom-ready",
    "browsingContext.load": "loaded",
    "browsingContext.contextDestroyed": "destroyed",
  };

  contexts.set(p.context, {
    ...current,
    parent: p.parent ?? current.parent,
    url: p.url ?? current.url,
    phase: phases[event.method] ?? current.phase,
    activeNavigation: p.navigation ?? current.activeNavigation,
  });
}

Production code should keep aborted and failed navigation records instead of forcing them into the happy-path phase list. It should also validate message shapes before reduction and bound retained history so a long-lived session does not accumulate unlimited events.

Wait for the milestone the assertion actually needs

navigationCommitted is useful when the test needs the authoritative destination early, but commitment does not mean the DOM is usable. domContentLoaded means parsing reached that browser milestone; deferred scripts and application requests may still be active. load waits for the document load event, but a dashboard can continue fetching data afterward.

Choose one browser milestone and one business condition. For example, wait for navigationCommitted on the popup's context, then use Selenium to wait for the signed-in account heading. This separates browser transport progress from application readiness. A timeout report can then say which boundary failed.

Avoid a generic "wait until quiet" rule. Persistent connections, analytics, and polling can make quietness undefined. An event stream should narrow synchronization to a known context and transition, not replace product-specific assertions with another broad heuristic.

Diagnose missing and contradictory events

If creation is missing, verify that subscription succeeded before the click and that no context filter excluded children. If start appears without commit, inspect navigationFailed and navigationAborted, browser logs, certificate policy, and the destination. If commit appears without DOM readiness, suspect document execution, renderer failure, or a test that closed the context too early.

An unexpected contextDestroyed is terminal for waits tied to that context. Reject them immediately with the last URL, parent, active navigation, and recent events. Letting those waits run to a generic timeout hides the strongest evidence available.

Duplicate-looking events often indicate two navigations or two contexts, not a protocol defect. Print both identifiers in every diagnostic line. Conversely, a reused context with a new navigation id is normal; creating a new local context record for every URL will corrupt parentage and leak state.

Balance event precision against harness complexity

BiDi adds a live connection, subscription ownership, message validation, and teardown obligations. For a simple page transition, Selenium navigation plus an explicit wait is often sufficient. Use lifecycle collection where chronology changes diagnosis or synchronization, such as popups, frames, redirects, downloads, and multi-page workflows.

Keep collection session-scoped and assertions test-scoped. One socket may serve the WebDriver session, but each test wait should register its expected context and remove its listener on success, failure, or cancellation. Unsubscribe when a feature is no longer needed, and close the socket as part of driver teardown.

Browsing-context implementation checklist

  • Request and verify a returned WebSocket URL for the session.
  • Subscribe before the action that can create or navigate a context.
  • Merge a post-subscription tree snapshot with live events by context id.
  • Store context lifecycle separately from per-navigation history.
  • Use the parent field to classify top-level and child contexts.
  • Match waits by context, navigation id, and a deliberate milestone.
  • Record failure and abort events, not only successful load events.
  • Cancel waits immediately when their context is destroyed.
  • Bound timeline retention and remove test-scoped listeners.
  • Keep classic WebDriver switching and BiDi identifiers conceptually separate.

Close with a timeline, not a guess

A dependable browser harness can answer which context appeared, who its parent was, which navigation began, whether a document committed, which readiness milestone arrived, and when the context disappeared. Subscribe early, reduce events into an explicit tree, and attach the final timeline to failures. That turns tab and frame synchronization from post-failure sampling into a protocol-backed account of what the browser actually did.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Selenium documentation

    Selenium Project

    Canonical WebDriver, Grid, waits, element, and browser automation guidance.

  2. 02
    WebDriver standard

    W3C

    The browser automation protocol specification behind WebDriver implementations.

FAQ / QUICK ANSWERS

Questions testers ask

What is a browsing context in WebDriver BiDi?

A browsing context is a navigable document environment represented by a stable context identifier. Top-level contexts model tabs or windows, while child contexts model embedded frames and carry a parent identifier.

Does navigation create a new BiDi browsing context?

Usually no. A cross-document navigation keeps the context identifier and receives a new navigation identifier. The context ends only when the tab, window, or frame context is destroyed.

Which BiDi event proves that a new document was committed?

The `browsingContext.navigationCommitted` event marks document commitment. It is earlier than DOMContentLoaded or load, so it should not be treated as proof that application content is ready.

How can a listener distinguish a child frame from a new tab?

Inspect the context information's `parent` field. A top-level tab or window has no parent, while a frame context refers to the context that contains it.

Why should a test subscribe before triggering navigation?

BiDi events are live notifications, not a historical event log. Subscribing after the click or navigation can miss context creation or early navigation events and leave the local model incomplete.