The Generational Shift in E2E Testing — From Selenium to Playwright
Browser automation, beginning with Jason Huggins's Selenium in 2004, has gone through generational shifts: integration with WebDriver, Puppeteer, Cypress, and Playwright. We compare how each generation answered the challenges of waiting, instability, and cross-browser support.
Property-based testing and fuzzing (Article 7) were techniques that increased test coverage by automatically generating input at the level of a function or module. From here, we shift perspective entirely, to the history of E2E (End-to-End) testing — actually operating a browser to verify a system from end to end. For roughly twenty years, this field has been a running battle against three challenges: waiting, instability (flakiness), and cross-browser support.
The Birth of Selenium — 2004
In 2004, at ThoughtWorks’s Chicago office, Jason Huggins — struggling to test an internal time-and-expenses application — wrote a tool that used JavaScript running inside the browser to automate UI operations. Initially called “JavaScriptTestRunner,” this tool was open-sourced later that year and came to be known as Selenium (Selenium Core).
The early implementation (Selenium Core / Selenium RC, “Remote Control”) injected test JavaScript into the same origin as the page under test, simulating DOM operations from inside the browser. This approach had the following constraints:
- Same-origin policy constraints: operations crossing different domains were hard to perform
- Only JavaScript-mediated operations were possible: it could not fully reproduce native browser operations (real mouse clicks, real key input)
- The complex setup of the Remote Control (RC) server: running tests required standing up a local proxy server
The Arrival of WebDriver and Integration into Selenium 2
In response to these constraints, Simon Stewart, also at ThoughtWorks, began developing an entirely different approach from late 2006 onward: WebDriver. WebDriver operates the browser directly through the native APIs and drivers provided by browser vendors, achieving operation much closer to the real thing, unconstrained by JavaScript.
At the 2007 Google Test Automation Conference (GTAC), Huggins and Stewart debated merging the two projects — in a session now known as the “Steel Cage Knife Fight” — and decided to unify them. The work began in August 2009; the first merged version (alpha1) was released that December, and Selenium 2.0 was officially released as a stable version in July 2011. Selenium 2 arrived offering both Selenium RC’s usability and WebDriver’s native control.
Subsequently, the WebDriver protocol itself was standardized across browsers through the W3C’s Browser Testing and Tools Working Group, and on May 31, 2018, WebDriver became a W3C Recommendation. This led major browser vendors to provide their own official WebDriver implementations (ChromeDriver, geckodriver, and so on), easing the problem of behavior differing from driver to driver.
Selenium IDE — Another Branch of the Lineage
The early Selenium era also had Selenium IDE, a Firefox extension that could record and replay browser operations directly. Its ease of use — recording operations with just a click, with no code required — made it popular even among many non-engineers, but development once stalled following a change in Firefox’s extension architecture, and it was later reimplemented as a WebExtensions version. “Record-and-playback” tools have long been criticized for their poor maintainability — they break easily under UI changes and obscure the intent of a test — a tension that continues to this day.
The Long-Standing Affliction of Waiting and Instability
The single biggest challenge that persisted through the Selenium/WebDriver generation was the problem of waiting. If you try to operate an element before page loading, Ajax communication, or an animation has finished, the test fails because the element doesn’t yet exist or can’t be clicked. Techniques such as fixed waits via sleep(), explicit waits via WebDriverWait, and implicit waits have all been used, but each required empirically tuning “how many seconds is enough,” and this remained a breeding ground for flaky tests (see Article 14).
Puppeteer — Direct Control via CDP (2017)
In 2017, Google’s Chrome DevTools team released Puppeteer, a library for controlling Chrome directly from Node.js. Rather than going through a cross-browser protocol like WebDriver, Puppeteer directly uses the low-level debugging protocol that Chrome itself provides, CDP (Chrome DevTools Protocol), to control the browser.
- Operating at a lower level than WebDriver’s HTTP-based protocol, it enables faster, more precise control
- It’s strong at automation uses beyond testing as well — intercepting network requests, measuring performance, generating PDFs, taking screenshots
- On the other hand, it was initially Chrome (Chromium) only, lagging behind Selenium in cross-browser support
Cypress — A Shift in Thinking to In-Browser Execution (circa 2017)
Cypress began development in 2014, when Brian Mann grew frustrated with the awkwardness of existing tools (Selenium in particular); it entered public beta in October 2017 and launched commercially in October 2018. Its architecture is fundamentally different from Selenium/WebDriver.
- Test code runs inside the browser, on the same event loop as the application under test (control is mediated through a Node.js proxy server, but command execution itself happens inside the browser)
- This lets the test code directly and immediately observe DOM state changes, network calls, and timers, in real time
- Automatic waiting: it automatically detects the appearance of elements, the completion of animations, and the completion of XHR/fetch calls, making explicit
sleeporwaitstatements largely unnecessary - Time-travel debugging: it records a DOM snapshot at the point of each command’s execution during a test run, letting you go back and inspect that state in the browser’s developer tools when a test fails
Its weaknesses were that architectural constraints made scenarios spanning multiple tabs or multiple browsers hard to write, and that cross-browser support (particularly for WebKit/Safari) remained weak for a long time.
Playwright — Unifying Cross-Browser Support and Auto-Wait (2020)
In January 2020, Microsoft announced Playwright (the 1.0 release followed in May of the same year). Its development team included engineers who had worked on Puppeteer at Google, and its design carries forward Puppeteer’s idea of direct CDP control while aiming to overcome both Puppeteer’s weakness — Chrome-only — and Cypress’s weakness — poor cross-browser support.
- Cross-browser support: not just Chromium-based browsers, but also Firefox and WebKit (Safari’s rendering engine) can be operated through a single API
- Auto-wait: a mechanism built in at the command level that automatically waits until an element is actually in an operable state
- Parallel execution and browser-context isolation: multiple lightweight “browser contexts” can be spawned within a single browser process, letting independent sessions (with no shared cookies or cache) run in parallel per test at low cost
- Built-in traces, video, and screenshots: debugging information on failure is included as a standard feature
Generational Differences, in Code
Even for the same operation — click a login button, wait for the result, then verify it — the shape of the code differs greatly across generations.
// Selenium WebDriver: you have to write explicit waits yourself
WebElement button = driver.findElement(By.id("login"));
button.click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("welcome")));
// Cypress: the command chain automatically waits for elements to appear and settle
cy.get("#login").click();
cy.get("#welcome").should("be.visible");
// Playwright: auto-wait is built in by default, and the same code runs across browsers
await page.click("#login");
await expect(page.locator("#welcome")).toBeVisible();
Where the Selenium generation required explicitly writing “code that waits,” in the Cypress and Playwright generations the commands themselves automatically wait until the target becomes operable. This succinctly captures how the design philosophy evolved to “eliminate the main cause of flaky tests at the mechanism level.”
What Each Generation Solved
| Tool (year) | Control method | Main problem solved | Problem left unsolved |
|---|---|---|---|
| Selenium Core/RC (2004) | In-browser JS injection | Made automating manual testing possible at all | Same-origin constraints, incomplete native operations |
| WebDriver / Selenium 2 (2007–2011) | Via native browser API | Escape from RC’s proxy setup and JS constraints | Waiting still required explicit code |
| WebDriver’s W3C standardization (2018) | Standardized HTTP protocol | Differences and incompatibilities across vendor implementations | The waiting problem remained unsolved |
| Puppeteer (2017) | Direct CDP control | Fast, precise, low-level control | Chromium-only, no cross-browser support |
| Cypress (2017) | In-browser execution, shared event loop | Automated waiting, debugging experience | Constraints on multi-tab/cross-browser scenarios |
| Playwright (2020) | CDP-family plus each browser’s own protocol | Both cross-browser support and auto-wait, plus parallel execution | The relative youth of its ecosystem |
WebDriver BiDi, the Next Standard
Beyond Selenium and Playwright, tools tailored to various ecosystems have also emerged, such as WebdriverIO, a Node.js wrapper around WebDriver, and TestCafe, aimed at .NET/JS. All of them pursue the same underlying goals: automating waits, offering a readable API, and running stably in CI environments.
The WebDriver protocol itself also continues to evolve. Traditional WebDriver (now called WebDriver Classic) is HTTP-based, one-way request/response, which made it poorly suited to receiving network events or console logs in real time. To resolve this weakness, the W3C is standardizing a next-generation specification, WebDriver BiDi (BiDirectional), that enables two-way communication over WebSockets. It aims to combine the strengths of WebDriver Classic — cross-browser support and a neutral W3C standard — with the strengths of CDP — fast, two-way communication and low-level control — and the standardization of browser automation remains an ongoing, evolving theme.
From This Article to the Next
Selenium, WebDriver, Puppeteer, Cypress, Playwright — the thread running through the generational shift in browser testing has consistently been the pursuit of “accuracy and speed of control” and “automating the waiting and timing problem.” What we turn to next is another stage that ran alongside this history: the ecosystem of JavaScript itself as the language under test. We trace the evolution of test runners such as Jasmine, Mocha, Jest, and Vitest.
References
- Selenium official documentation and project history (selenium.dev)
- W3C, “WebDriver” Recommendation (May 31, 2018)
- Puppeteer official documentation (pptr.dev)
- Cypress official documentation (docs.cypress.io)
- Playwright official documentation (playwright.dev)
- W3C, “WebDriver BiDi” specification (draft)