Automating Complex Interactions in 2026: Selenium BiDi vs. Playwright CDP
Selenium BiDi vs Playwright CDP for drag-drop and network sniffing. Protocol nerd stuff, explained at a whiteboard — not from a spec PDF.
Press Listen. A recorded voice reads the article, lights the current word, and keeps that word in view.
How listen mode works
The recording is a neural voice, not your browser's speech engine. The word being spoken lights up from the audio clock, including after you pause, drag the bar, or change speed. If you chose UK and only the US recording exists, you hear the US voice. Leaving the page stops playback.
Two years ago, automating drag-and-drop meant writing JavaScript fallbacks because Selenium’s Actions class couldn’t keep up with custom web components. Today, both Selenium and Playwright have low-level browser protocol access — BiDi and CDP — that let you simulate interactions at the engine level, intercept network traffic, and even replay sessions with AI assistance.
This post compares the two approaches side-by-side for the three hardest interaction types: drag-and-drop, network interception, and AI-powered replay. If you read the Selenium 2026 guide and the Playwright MCP guide, this is the next step — going deeper into the protocol layer.
How BiDi and CDP Actually Work
Before comparing code, let’s understand what’s happening under the hood.
flowchart TD
subgraph "Selenium + WebDriver BiDi"
A1["🧪 Your Test Code"] -->|"WebDriver BiDi\n(WebSocket)"| B1["🔧 Selenium Server"]
B1 -->|"BiDi → HTTP translation"| C1["🌐 Browser Driver\n(chromedriver / geckodriver)"]
C1 -->|"DevTools Protocol"| D1["🖥️ Browser"]
end
subgraph "Playwright + Native CDP"
A2["🧪 Your Test Code"] -->|"Native CDP\n(direct socket)"| D2["🖥️ Browser"]
end
style A1 fill:#0ea5c7,color:#fff
style A2 fill:#34d399,color:#000
The critical difference: Selenium BiDi adds a translation layer. The browser speaks CDP natively, but Selenium wraps it in the WebDriver BiDi protocol — a WebSocket-based standard that all browser vendors agreed on. Playwright skips the translation and talks CDP directly.
| Characteristic | Selenium BiDi | Playwright CDP |
|---|---|---|
| Protocol | WebDriver BiDi (W3C standard) | Chrome DevTools Protocol (Chromium-native) |
| Browser support | Chrome, Edge, Firefox | Chromium, Firefox, WebKit |
| Setup | Selenium Manager auto-downloads driver | Playwright bundles browser binaries |
| Network events | NetworkResponseReceived event |
page.route() interceptor |
| Low-level access | BiDi log + network domains | Full CDP surface (DOM, CSS, Performance, etc.) |
| Learning curve | Moderate — BiDi has fewer domains than CDP | Steeper — CDP has 50+ domains |
The tradeoff: BiDi is simpler but less powerful. CDP is deeper but Chromium-only for the most advanced features.
Drag-and-Drop: From JavaScript Hacks to Protocol-Level Events
In the 2024 drag-and-drop guide, most methods relied on JavaScript fallbacks because Selenium’s Actions class couldn’t reliably trigger custom drag handlers. In 2026, both BiDi and CDP let you fire the raw dragstart, dragover, and drop events that the application is actually listening for.
Selenium BiDi: Dispatch Input Events
BiDi’s input domain lets you synthesize pointer events. You describe the interaction as a sequence of low-level actions:
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using IWebDriver driver = new ChromeDriver();
driver.Navigate().GoToUrl("https://your-app.com/kanban");
var source = driver.FindElement(By.CssSelector(".card[draggable='true']"));
var target = driver.FindElement(By.CssSelector(".column:nth-child(3)"));
// BiDi: use the BrowsingContext to dispatch low-level input events
var browsingContext = driver.GetBrowsingContext();
// Dispatch a pointer down on the source element
await browsingContext.DispatchPointerEventAsync(new()
{
Type = PointerEventType.PointerDown,
X = source.Location.X + source.Size.Width / 2,
Y = source.Location.Y + source.Size.Height / 2
});
// Move the pointer toward the target (multiple small moves for smooth animation)
for (int i = 0; i < 10; i++)
{
double progress = (i + 1) / 10.0;
await browsingContext.DispatchPointerEventAsync(new()
{
Type = PointerEventType.PointerMove,
X = source.Location.X + (int)((target.Location.X - source.Location.X) * progress),
Y = source.Location.Y + (int)((target.Location.Y - source.Location.Y) * progress)
});
await Task.Delay(30); // Small delay between moves for natural feel
}
// Release on the target
await browsingContext.DispatchPointerEventAsync(new()
{
Type = PointerEventType.PointerUp,
X = target.Location.X + target.Size.Width / 2,
Y = target.Location.Y + target.Size.Height / 2
});
The advantage: BiDi fires actual pointerdown/pointermove/pointerup events that the browser dispatches natively. No JavaScript injection. No DataTransfer hacks. The application’s dragstart listener fires as if a real user dragged the element.
Playwright CDP: Route and Simulate at the Protocol Level
Playwright can go even deeper — it can intercept and modify the HTML5 drag events before they reach the page:
using Microsoft.Playwright;
var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.LaunchAsync(new() { Headless = false });
var page = await browser.NewPageAsync();
// Playwright's DragAndDropAsync uses CDP under the hood:
// 1. Injects a DataTransfer into the drag event pipeline
// 2. Fires dragstart → dragover → drop → dragend natively
// 3. No JavaScript hacks, no coordinate math
await page.GotoAsync("https://your-app.com/kanban");
await page.DragAndDropAsync(".card[draggable='true']", ".column:nth-child(3)");
Playwright’s DragAndDropAsync is a single call. Under the hood it uses CDP to:
- Inject a
DataTransferinto the drag event pipeline - Fire
dragstartwith the correct payload - Fire
dragoveron each element the cursor passes over - Fire
dropon the target - Fire
dragendto clean up
sequenceDiagram
participant Test as Your Test
participant BiDi as Selenium BiDi
participant CDP as Playwright CDP
participant App as Web Application
Note over Test,App: Selenium BiDi — explicit pointer events
Test->>BiDi: DispatchPointerEvent (pointerdown)
BiDi->>App: pointerdown → dragstart
Test->>BiDi: DispatchPointerEvent ×10 (pointermove)
BiDi->>App: pointermove ×10 → dragover ×10
Test->>BiDi: DispatchPointerEvent (pointerup)
BiDi->>App: pointerup → drop → dragend
BiDi-->>Test: ✅ Element moved
Note over Test,App: Playwright CDP — one call, engine handles details
Test->>CDP: DragAndDropAsync(source, target)
CDP->>App: Inject DataTransfer + fire dragstart
CDP->>App: Calculate path + fire dragover on each element
CDP->>App: Fire drop on target
CDP->>App: Fire dragend
CDP-->>Test: ✅ Element moved
Which Wins for Drag-and-Drop?
| Selenium BiDi | Playwright CDP | |
|---|---|---|
| API simplicity | Explicit pointer events (more code) | DragAndDropAsync() (one line) |
| Reliability | Must calculate coordinates correctly | Engine handles coordinate math |
| Debugging | Easy to step through each pointer event | Black-box — one call, pass or fail |
| Browser support | Chrome, Edge, Firefox | Chromium, Firefox, WebKit |
| Custom drag handlers | Fires native events — works with any handler | Injects DataTransfer — compatible with HTML5 drag API |
Winner: Playwright for simplicity. Selenium BiDi for debugging and Firefox support.
Network Interception: Watch, Modify, and Mock API Calls
Both BiDi and CDP let you intercept network requests mid-flight. This is the superpower that eliminates the need for separate API mocking tools in many cases.
Selenium BiDi: Network Event Subscription
var driver = new ChromeDriver();
// Subscribe to network events BEFORE navigating
var network = driver.Manage().Network;
network.NetworkRequestSent += (_, e) =>
{
Console.WriteLine($"➡️ {e.RequestMethod} {e.RequestUrl}");
};
network.NetworkResponseReceived += (_, e) =>
{
var status = e.ResponseStatusCode;
var icon = status switch
{
>= 200 and < 300 => "✅",
>= 400 and < 500 => "⚠️",
>= 500 => "❌",
_ => "ℹ️"
};
Console.WriteLine($"{icon} {status} ← {e.ResponseUrl}");
};
network.StartMonitoring();
driver.Navigate().GoToUrl("https://your-app.com/dashboard");
// After navigation, assert every API call succeeded
var failedCalls = network.GetReceivedResponses()
.Where(r => r.ResponseStatusCode >= 400)
.ToList();
Assert.IsEmpty(failedCalls,
$"Found {failedCalls.Count} failed API calls: " +
string.Join(", ", failedCalls.Select(c => $"{c.ResponseStatusCode} {c.ResponseUrl}")));
BiDi’s network domain is event-driven: you subscribe to NetworkRequestSent and NetworkResponseReceived, and the browser pushes events to you in real time. No polling, no explicit waits.
Playwright CDP: Route Interception
Playwright’s page.RouteAsync() is more powerful — you can intercept, modify, or mock responses mid-flight:
var page = await browser.NewPageAsync();
// Intercept ALL API calls to /api/
await page.RouteAsync("**/api/**", async route =>
{
var request = route.Request;
// Option 1: Let it through but log
Console.WriteLine($"📡 {request.Method} {request.Url}");
// Option 2: Mock the response
if (request.Url.Contains("/api/slow-endpoint"))
{
await route.FulfillAsync(new()
{
Status = 200,
ContentType = "application/json",
Body = """{ "mocked": true, "message": "This response was intercepted by CDP" }"""
});
return;
}
// Option 3: Let it pass through
await route.ContinueAsync();
});
await page.GotoAsync("https://your-app.com/dashboard");
// The slow endpoint now returns instantly with mocked data
flowchart LR
A["🌐 Browser<br/>makes API call"] --> B{"CDP Route<br/>Interceptor"}
B -->|"Pass through"| C["🔗 Real Backend"]
B -->|"Mock"| D["📦 Mock Response"]
B -->|"Modify"| E["✏️ Modified Response"]
C --> F["📄 Rendered in Page"]
D --> F
E --> F
Which Wins for Network Interception?
| Selenium BiDi | Playwright CDP | |
|---|---|---|
| Event subscription | Simple event handlers | page.RouteAsync() with pattern matching |
| Mock responses | Not supported (read-only) | route.FulfillAsync() — full mock capability |
| Modify in-flight | Not supported | route.ContinueAsync() with modified headers/body |
| Real-time monitoring | Built-in — subscribe and forget | Requires explicit route handlers |
| Use case | Passive monitoring, assertion on response codes | Active interception, mocking, throttling |
Winner: Playwright CDP for power and flexibility. Selenium BiDi for simple monitoring — the event subscription model is cleaner for “watch everything and report” scenarios.
AI-Powered Interaction Replay
This is the 2026 differentiator. Both BiDi and CDP can record a user session and replay it through an AI agent that adapts to UI changes.
How It Works
1. RECORD → User performs drag-and-drop manually in the browser
2. CAPTURE → BiDi/CDP records every event + DOM state at each step
3. DESCRIBE → AI translates the recording into natural language:
"Drag the card titled 'Fix login bug' from the 'Backlog'
column to the 'In Progress' column"
4. REPLAY → AI agent executes the description, adapting to layout
changes (the card moved 50px right? AI finds it by title)
5. HEAL → If replay fails, AI analyzes DOM diff and retries with
an adjusted strategy
flowchart TD
A["👤 User performs<br/>drag-and-drop manually"] --> B["📹 BiDi / CDP records<br/>every event + DOM snapshot"]
B --> C["🧠 AI translates recording<br/>into natural language intent"]
C --> D["🤖 AI agent replays intent<br/>via BiDi / CDP"]
D --> E{"Did it work?"}
E -->|Yes| F["✅ Test passed"]
E -->|No| G["🔍 AI analyzes DOM diff<br/>finds element by semantic role"]
G --> H["🔄 Retry with adjusted<br/>locator strategy"]
H --> E
Selenium BiDi + AI Replay
// Step 1: Record a BiDi session
var driver = new ChromeDriver();
var recorder = new BiDiSessionRecorder(driver);
recorder.StartRecording();
driver.Navigate().GoToUrl("https://your-app.com/kanban");
// User performs the drag-and-drop manually (or via Actions)
// ... manual interaction happens here ...
recorder.StopRecording();
var sessionTrace = recorder.GetTrace(); // JSON: every event + timestamps + DOM snapshots
// Step 2: AI translates trace → natural language intent
var intent = await AITranslator.TraceToIntentAsync(sessionTrace);
// Output: "Drag the card with title 'Fix login bug' from column #1 to column #3"
// Step 3: Replay with self-healing
var replayer = new BiDiSessionReplayer(driver);
var result = await replayer.ReplayWithHealingAsync(intent);
if (result.Succeeded)
Console.WriteLine($"✅ Replayed successfully after {result.Retries} retries");
else
Console.WriteLine($"❌ Replay failed: {result.Error}");
Playwright CDP + AI Replay
Playwright’s deeper CDP access gives the AI agent more information for self-healing:
// Playwright records a HAR file + DOM snapshots automatically
var context = await browser.NewContextAsync(new()
{
RecordHarPath = "session-trace.har",
RecordHarMode = HarMode.Full
});
var page = await context.NewPageAsync();
// CDP: inject a visual marker that tracks element positions
await page.EvaluateAsync(@"() => {
document.addEventListener('dragstart', e => {
window.__dragSourceRect = e.target.getBoundingClientRect();
});
document.addEventListener('drop', e => {
window.__dropTargetRect = e.target.getBoundingClientRect();
});
}");
// User performs interaction...
await page.GotoAsync("https://your-app.com/kanban");
// ... manual drag-and-drop ...
// AI replay with CDP-enhanced healing
var healer = new CDPHealer(page);
await healer.ReplayFromHARAsync("session-trace.har", new()
{
SemanticFallback = true, // Find elements by ARIA role + text content
VisualDiff = true, // Compare screenshots to detect layout shifts
MaxRetries = 3
});
| AI Replay Feature | Selenium BiDi | Playwright CDP |
|---|---|---|
| Event recording | WebSocket event log | HAR + DOM snapshots |
| Semantic healing | By element attributes | By ARIA role + text + visual position |
| Visual diff | Not built-in | Screenshot comparison via CDP |
| AI translation | Community tools | Playwright’s codegen --ai integration |
When to Use Which
flowchart TD
START["🤔 I need to automate<br/>a complex interaction"] --> Q1{"Does it involve<br/>drag-and-drop?"}
Q1 -->|Yes| Q1a{"Do I need Firefox\nsupport?"}
Q1a -->|Yes| BIDI["🔷 Selenium BiDi<br/>explicit pointer events"]
Q1a -->|No| CDP1["🟢 Playwright CDP<br/>DragAndDropAsync()"]
Q1 -->|No| Q2{"Do I need to mock\nor modify API\nresponses?"}
Q2 -->|Yes| CDP2["🟢 Playwright CDP<br/>route.FulfillAsync()"]
Q2 -->|No| Q3{"Is this for passive\nnetwork monitoring\nin CI/CD?"}
Q3 -->|Yes| BIDI_MON["🔷 Selenium BiDi<br/>event subscription"]
Q3 -->|No| Q4{"Do I need AI-powered\ninteraction replay\nwith self-healing?"}
Q4 -->|Yes| CDP3["🟢 Playwright CDP<br/>HAR + visual diff"]
Q4 -->|No| EITHER["🤷 Either works —<br/>pick your ecosystem"]
Rule of thumb:
- Playwright CDP for Chromium-first teams that want maximum power (mock, modify, replay with visual diff)
- Selenium BiDi for cross-browser teams that need Firefox and prefer explicit control over each event
Where Existing Posts Fit
| Earlier post | What it covered | What changed by 2026 |
|---|---|---|
| Drag-and-Drop in C# Selenium (Aug 2024) | 8 methods using Actions + JavaScript fallbacks |
BiDi fires native pointer events — no JS injection, no DataTransfer hacks |
| Selenium 2026 Beginner’s Guide (Jul 2026) | WebDriver BiDi basics, MCP setup, Relative Locators | This post goes deeper into BiDi’s input and network domains |
| Playwright MCP + Multi-Agent (Jul 2026) | MCP server, multi-agent pattern, Web-First Assertions | CDP gives the Explorer agent network-level visibility; the Validator agent uses CDP route interception |
| Playwright vs Selenium in 2026 (Jun 2026) | Speed, reliability, multi-browser comparison | Now with protocol-level comparison: BiDi vs CDP for complex interactions |
Multi-Language Quick Reference
This post used C# examples. Here’s the equivalent syntax in Java, TypeScript, JavaScript, and Python for key BiDi and CDP operations:
Selenium BiDi — Drag-and-Drop
| Language | Pointer event dispatch |
|---|---|
| C# | browsingContext.DispatchPointerEventAsync(new() { Type = PointerEventType.PointerDown, X = 100, Y = 200 }) |
| Java | browsingContext.dispatchPointerEvent(new PointerEvent(PointerEventType.POINTER_DOWN, 100, 200)) |
| TypeScript | await browsingContext.dispatchPointerEvent({ type: 'pointerDown', x: 100, y: 200 }) |
| JavaScript | await browsingContext.dispatchPointerEvent({ type: 'pointerDown', x: 100, y: 200 }) |
| Python | await browsing_context.dispatch_pointer_event(type="pointerDown", x=100, y=200) |
Selenium BiDi — Network Monitoring
| Language | Event subscription pattern |
|---|---|
| C# | network.NetworkResponseReceived += (_, e) => { if (e.ResponseStatusCode >= 400) ... } |
| Java | network.onNetworkResponseReceived(response -> { if (response.getResponseStatusCode() >= 400) ... }) |
| TypeScript | network.on('networkResponseReceived', (response) => { if (response.statusCode >= 400) ... }) |
| JavaScript | network.on('networkResponseReceived', (response) => { if (response.statusCode >= 400) ... }) |
| Python | network.on_network_response_received(lambda response: ... if response.status_code >= 400) |
Playwright CDP — Drag-and-Drop
| Language | One-line drag-and-drop |
|---|---|
| C# | await page.DragAndDropAsync(".source", ".target"); |
| Java | page.dragAndDrop(".source", ".target"); |
| TypeScript | await page.dragAndDrop('.source', '.target'); |
| JavaScript | await page.dragAndDrop('.source', '.target'); |
| Python | await page.drag_and_drop(".source", ".target") |
Playwright CDP — Route Interception
| Language | Route mock pattern |
|---|---|
| C# | await page.RouteAsync("**/api/**", async route => { await route.FulfillAsync(new() { Status = 200, Body = mockJson }); }) |
| Java | page.route("**/api/**", route -> route.fulfill(new Route.FulfillOptions().setStatus(200).setBody(mockJson))) |
| TypeScript | await page.route('**/api/**', async route => { await route.fulfill({ status: 200, body: mockJson }) }) |
| JavaScript | await page.route('**/api/**', async route => { await route.fulfill({ status: 200, body: mockJson }) }) |
| Python | await page.route("**/api/**", lambda route: route.fulfill(status=200, body=mock_json)) |
Playwright — Browser Launch
| Language | Launch pattern |
|---|---|
| C# | var browser = await playwright.Chromium.LaunchAsync(new() { Headless = false }) |
| Java | Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false)) |
| TypeScript | const browser = await playwright.chromium.launch({ headless: false }) |
| JavaScript | const browser = await playwright.chromium.launch({ headless: false }) |
| Python | browser = await playwright.chromium.launch(headless=False) |
TypeScript vs JavaScript: Playwright’s TypeScript API uses the same syntax as JavaScript for most operations — the difference is type safety (
const browser: Browser). Both benefit from Playwright’s auto-complete in VS Code.
Sources & Further Reading
- WebDriver BiDi W3C Specification — the W3C standard for bidirectional browser automation
- Chrome DevTools Protocol Documentation — the CDP surface Playwright accesses natively and Selenium accesses via BiDi translation
- Playwright CDP Session API — official docs for the CDP session used in drag-and-drop and network interception examples
- Selenium WebDriver BiDi Guide — official BiDi documentation including network and input domains
What to Do Next
- Try the drag-and-drop examples. Take any drag-based UI (Kanban, file upload, sortable list) and test both the BiDi pointer-event approach and Playwright’s
DragAndDropAsync(). See which one handles your app’s custom drag handlers. - Set up network monitoring in CI. Subscribe to BiDi’s
NetworkResponseReceivedin your existing Selenium suite. One event handler, and you’ll catch every 500 error your UI tests were silently ignoring. - Experiment with AI replay. Record a manual drag-and-drop session in Playwright (HAR recording is built-in), then ask an LLM to describe what happened in natural language. You’ve just built a self-documenting test.
- Subscribe to this blog’s feed.xml — next up: a deep-dive on self-healing locators and how AI can find elements by their semantic role when CSS selectors break.
See also: AI-Driven Test Strategy: From Copilot to Multi-Agent Orchestration (Jun 2026) — the overarching thesis on multi-agent QA systems, including the self-healing locator pattern referenced above. · Self-Healing Test Suites (Jul 2026) — full implementation: Java SemanticHealer, CDP accessibility tree, DOM diff, CI/CD healing log. · XPath for Test Automation (Jul 2026) — the story-mode article whose §12 maps the complex XPath & CSS patterns you ship selectors around (SVG namespace handling, computed indices, role/state ARIA chains, iframe/shadow-DOM piercing matrix, modern CSS Level 4 selectors, decision flowchart).
Add a thought
The writing box stays shut until the code matches. A note you save shows up under this article on this browser. It is not emailed. Posting it for everyone opens GitHub, which asks you to sign in.