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.
In this guide11 sections
- 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
- Recognize tabs and frames from parentage
- Reduce events into an inspectable local model
- Wait for the milestone the assertion actually needs
- Diagnose missing and contradictory events
- Balance event precision against harness complexity
- Browsing-context implementation checklist
- 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.
Model the lifecycle as two related state machines
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.
01 / context created
Context Created
A tab, window, or frame receives a context id and optional parent id.
02 / navigation started
Navigation Started
A navigation id identifies the new attempt inside the existing context.
03 / document committed
Document Committed
The remote end commits the new document before later readiness events.
04 / child frame attached
Child Frame Attached
A created child context points back to its containing parent context.
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.
{
"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.
{
"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.
{
"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.
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.
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.
- 01Selenium documentation
Selenium Project
Canonical WebDriver, Grid, waits, element, and browser automation guidance.
- 02WebDriver 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.
RELATED GUIDES
Continue the learning route
GUIDE 01
A Deterministic Window and Tab State Machine for Selenium Tests
Build deterministic Selenium window handle management with set-delta waits, semantic validation, safe close-and-restore cleanup, and popup diagnostics.
GUIDE 02
Migrate Selenium CDP Hooks to WebDriver BiDi
Migrate Selenium CDP hooks to WebDriver BiDi with a capability inventory, event adapters, parity tests, cross-browser rollout, and failure diagnostics.
GUIDE 03
Page Load Strategy and Navigation Readiness in Selenium 4
Choose a Selenium page load strategy, separate document milestones from app readiness, and diagnose navigation timeouts without hiding slow failures.
GUIDE 04
Handle Iframes in Selenium: Switch Frames Without Flaky Tests
Handle iframes in Selenium with practical examples for switching frames, locating nested content, waits, errors, third-party widgets, and stable tests.