β WebDriver BiDi: from spec to implementation
β’ 1040 words β’ 5 min β’ updated
Disclaimer: Some LLM magic was used in this blog post1.
From mid-2022 to late 2023, I was one of the core contributors to chromium-bidi, an implementation of the WebDriver BiDi protocol for Chromium. This was before the agentic LLM days.
Think of it as the next-generation browser automation protocol β the successor to WebDriver Classic (Selenium-era) and a complement to CDP (Chrome DevTools Protocol), used by Puppeteer.
The key innovation: bidirectional communication between the test client and the browser, enabling event-driven automation instead of request-response polling.
With classic WebDriver, if you wanted to listen for a console log or a network request, you couldn’t β the client sends commands, the server responds, end of story. You’d need to poll for changes. With BiDi, the browser can push events to the client as they happen: DOM mutations, network interceptions, script errors, all in real time.
The project is a TypeScript translation layer that sits between BiDi clients and
CDP, running inside a Chrome Chromium tab.
Our
small
team at Google built it from the
ground up.
By the numbers #
Disclaimer: The LLM fetched these numbers, using the GitHub CLI. I am confident they are reasonably correct2.
Over roughly two years, my contributions stood as follows:
- 426 PRs authored (371 merged)
- 442 PRs reviewed from teammates
- 29 issues filed
- 42 PRs upstream to web-platform-tests/wpt (the shared browser test suite)
- 34 PRs upstream to w3c/webdriver-bidi (the W3C specification itself)
At peak velocity (May–June 2023), I was opening 3–4 PRs per day.
Infrastructure first #
My first contribution (#221, July 2022) was adding Terser minification to the mapper, reducing its size by 43%. Not fancy, but representative of my approach: fix the foundation before building on it.
The first ~100 PRs were heavily focused on developer experience (#devex):
- Introduced pre-commit.com with ESLint, Prettier, shellcheck, and codespell (#373). That’s when I would eventually fall in love with it.
- Added macOS E2E tests, headful WPT (Web Platform Tests β the shared test suite across browser vendors) runs in GitHub Actions, Chrome for Testing in CI (#427, #466, #671)
- Integrated Wireit build system (#603, +751/-337)
- Enforced conventional commits (#900). From that day I started to adopt a light (loose) version of it in my own repositories.
- Parallelized WPT test runs (#828)
- Added Codecov integration (#918). It was for free.
- Automated browser version pinning and WPT submodule updates (#941)
There was also a coordinated effort in January 2023 to remove static state from
the codebase β about 10 PRs converting EventManager, SubscriptionManager,
LogManager, ScriptEvaluator, BrowsingContextStorage, and RealmStorage
from static classes to proper instances. This was the kind of refactoring that
makes everything that comes after easier: better testability, less global
coupling.
Features #
With the infrastructure solid, features came fast(er).
browsingContext.print
(#526):
Print-to-PDF, including shrinkToFit support and golden image comparison
testing.
browsingContext.captureScreenshot
(#515): Full
screenshot implementation including OOPIF (out-of-process iframe) support and
headful mode.
script.addPreloadScript
(#582): Preload
scripts that run before page content, with channel and sandbox support added
incrementally over several months.
Channel mechanism / script.MessageEvent
(#319): A
prototype for safe page-to-client communication. This one took ~4 months from
first draft to merge β the longest-lived PR I had.
browsingContext.reload
(#654): Including
the wait parameter and ignoreCache support.
Network Interception woes #
The largest body of work was implementing the full network request interception subsystem. This was the kind of feature where you feel the weight of the entire spec behind every line of code. And, again, this was before agentic LLMs.
It started in June 2023 with scaffolding (#845) and stretched through November. The plot arc:
- Parse URL patterns for
network.addIntercept(#1186, +461/-526) - Handle
Fetch.requestPausedCDP events (#1304) - Implement
network.failRequest(#1318, 20 review rounds) - Implement
network.continueRequest(#1331, 28 review rounds) - Unblock event queue when network events are blocked (#1409) – co-authored with Maksim, this one was tricky
- Implement
network.continueResponse(#1443, 35 review rounds) - Implement
network.provideResponse(#1457) - Implement
network.continueWithAuth(#1470)
My takeaway: networking is flaky.
All five BiDi network interception commands, fully implemented. The
continueResponse PR alone went through 35 rounds of review. This was
painstaking, detail-oriented work β matching the spec precisely while dealing
with CDP’s quirks underneath.
Again, this was before LLMs (how many times did I already mention that?). It would have been soooooo much easier, had they existed back then. They are great at implementing specs and matching patterns, after all.
Full-stack, spec to tests #
What I enjoyed the most about this work was the feedback loop across layers. I wasn’t just writing TypeScript in one repo. The workflow often resembled the following:
- Find an ambiguity or bug in the W3C spec while implementing a feature
- Send a fix upstream to the spec (34 PRs)
- Write the implementation in chromium-bidi
- Write or fix tests in web-platform-tests (42 PRs)
This is the kind of work where you see the entire stack β from the standards document to the test suite shared across browser vendors.
The WPT work was also a first-class concern in CI: I set up auto-committing WPT expectations, parallelized test runs, and maintained a local bad SSL server for testing (#1521).
Test Reorganization #
One PR worth calling out: #1110 (+2586/-2385). This reorganized all E2E tests by domain β the largest single diff I landed. Not fancy but necessary. A well-organized test suite is how you keep a project maintainable as it grows.
Fin #
This was one of the most rewarding projects I worked on at Google. Building a browser automation protocol from scratch, contributing to W3C specs, and seeing it all come together in real test infrastructure used by other browser vendors – it’s the kind of work that makes you appreciate the craft of software engineering at the standards level.
The project is still active and has since been adopted as the default WebDriver BiDi implementation in Chromium.
-
As it’s explicitly noted that I do not use AI to craft blog posts in this site, and this is one exception, I feel that it must be disclosed upfront. You’re free to skip it if it doesn’t suit you. That said, you can tell I edited the entire post by hand because there are various personal touches all over the place. ↩︎
-
Because, initially, it wasn’t…by a lot. You really need to double-check numbers made up by LLMs. ↩︎