Testing Guide
Playwright Auto-Waiting Migration Guide: Cutting 30% Test Run Time from Selenium
Auto-waiting is the single feature that makes the biggest practical difference when migrating from Selenium to Playwright. Instead of guessing how long to wait, Playwright waits until an element is actually ready for interaction. The result is faster, more reliable tests with dramatically less code.
“Teams migrating from Selenium to Playwright report cutting average test execution time by 30% simply by removing unnecessary sleep and wait calls, thanks to Playwright's built-in auto-waiting.”
Migration benchmarks
1. The Selenium Wait Problem
Every Selenium test suite of significant size has the same problem: waits. The WebDriver protocol sends commands to the browser, but it does not inherently know when the browser has finished processing those commands. Click a button that triggers an AJAX request, and Selenium has no built-in mechanism to wait for the response to arrive and the DOM to update. The browser is asynchronous, but Selenium's interaction model is fundamentally synchronous.
Teams have developed three strategies to deal with this, and all of them have significant downsides. The first is Thread.sleep() or its equivalent: hard-coded pauses that wait a fixed amount of time regardless of whether the application is ready. This is the simplest approach and the worst. A two-second sleep is too long when the application responds in 200 milliseconds (wasting time) and too short when the server is under load (causing flakiness). Every hard-coded sleep in a test suite is either wasted time or a source of intermittent failures.
The second strategy is implicit waits: a global timeout that tells Selenium to poll for elements before failing. Implicit waits reduce some of the explicit sleep calls but create their own problems. They apply globally, so a test that needs to verify that an element does not exist must wait the full implicit timeout before confirming absence. They also interact poorly with explicit waits, creating confusing timeout behavior that is difficult to debug.
The third strategy is explicit waits with expected conditions: WebDriverWait combined with conditions like elementToBeClickable or visibilityOfElementLocated. This is the recommended approach and produces the most reliable tests, but it requires significant boilerplate. Every interaction needs its own wait condition, and choosing the right condition for each interaction requires understanding both the application's behavior and Selenium's condition semantics. The result is verbose, complex test code where wait logic often exceeds the actual test logic in volume.
2. How Playwright Auto-Waiting Actually Works
Playwright takes a fundamentally different approach. Every action method (click(), fill(), check(), and others) automatically waits for the target element to be "actionable" before performing the action. Actionable means different things for different actions. For a click, the element must be visible, stable (not animating), enabled, and not obscured by other elements. For a fill, the element must additionally be editable. For a check, it must be a checkbox or radio input.
This waiting happens automatically on every interaction. You write await page.click('#submit')and Playwright handles the waiting internally. It polls the element's state rapidly (using the browser's own APIs rather than external polling) and proceeds the instant the element is ready. If the element becomes actionable in 50 milliseconds, the click happens at 50 milliseconds. If it takes 3 seconds, the click happens at 3 seconds. There is no wasted time and no guessing.
The auto-waiting extends to navigation as well. When a click triggers a page navigation, Playwright can automatically wait for the navigation to complete before proceeding. Assertions like expect(locator).toBeVisible() also auto-wait, polling until the condition is met or the timeout expires. This means assertions double as wait conditions, eliminating the need for separate wait-then-assert patterns.
Under the hood, Playwright achieves this through its direct communication with the browser via the Chrome DevTools Protocol (or equivalent for Firefox and WebKit). Unlike Selenium's WebDriver protocol, which sends commands to an intermediary driver process, Playwright communicates directly with the browser engine. This gives it real-time visibility into the DOM state, network activity, and rendering pipeline, enabling precise waiting without external polling overhead.
3. Where the 30% Speed Gain Comes From
The 30% reduction in test execution time that teams report after migrating comes from three sources. The largest is the elimination of fixed sleep calls. A Selenium suite with 200 tests might have 500 sleep calls totaling 400 seconds of hard-coded waiting. When these are replaced by Playwright's auto-waiting, the actual wait time drops to whatever the application needs, typically 50 to 200 milliseconds per interaction instead of the 1 to 3 seconds that sleep calls commonly specify.
The second source is the elimination of polling overhead in explicit waits. Selenium's WebDriverWaitpolls at a configurable interval (default 500 milliseconds), which means there is up to 500 milliseconds of unnecessary delay on each wait even when the condition is met immediately. Playwright's internal waiting is event-driven rather than poll-based, so it responds to state changes almost instantly.
The third source is Playwright's parallel execution model. While not directly related to auto-waiting, teams migrating to Playwright often adopt its built-in parallelism at the same time. Playwright runs tests in isolated browser contexts that can execute concurrently, and its worker-based architecture handles parallelism more efficiently than most Selenium Grid setups. The combination of faster individual tests (from auto-waiting) and better parallelism (from the execution model) produces the 30% improvement.
It is worth noting that the speed gain varies by suite. Suites with heavy use of sleep calls see the largest improvements, sometimes exceeding 50%. Suites that already used well-tuned explicit waits see smaller improvements, closer to 10% to 15%. The average across reported migrations lands around 30%, which is significant for teams running tests on every pull request.
4. Common Migration Patterns
The most common pattern during migration is replacing explicit waits with direct actions. In Selenium, you might write a wait for an element to be clickable followed by a click. In Playwright, you write the click directly. The auto-waiting is implicit. This single transformation eliminates roughly half of the wait-related code in most Selenium suites.
Sleep calls before assertions are the second pattern. Selenium tests often include a sleep before checking the result of an action: click a button, sleep for two seconds, then assert the result appeared. In Playwright, the assertion itself handles the waiting. await expect(page.locator('.success-message')).toBeVisible() will wait for the element to appear, up to the configured timeout. No sleep needed.
Page load waits are the third pattern. Selenium tests frequently include explicit waits for page load after navigation. Playwright handles this automatically: await page.goto(url) waits for the load event by default, and you can configure it to wait for networkidle if your page relies on AJAX calls that fire after the initial load.
Element existence checks also simplify dramatically. Selenium's findElements (plural) returning an empty list is a common pattern for checking that something does not exist. In Playwright, await expect(page.locator('.error')).toBeHidden() or await expect(page.locator('.error')).toHaveCount(0) handles this cleanly with auto-waiting for the condition to be true.
5. Edge Cases Where You Still Need Explicit Waits
Auto-waiting handles the majority of waiting scenarios, but there are edge cases where explicit waits remain necessary. The most common is waiting for a network response before proceeding. If your test needs to verify that a specific API call completed (not just that the UI updated), you need page.waitForResponse() to intercept the actual network request. Auto-waiting on UI elements cannot guarantee that the backend operation finished.
Animations and transitions are another edge case. Playwright's auto-waiting checks that an element is "stable" (not moving), but complex CSS animations can confuse this check. An element that is slowly fading in might be considered stable at partial opacity, leading to a screenshot or assertion at an intermediate state. For these cases, either disable animations in the test configuration or add an explicit wait for the animation to complete.
Third-party iframes and embedded content present challenges because Playwright's auto-waiting applies within the current frame context. Waiting for content inside a Stripe payment iframe or a Google Maps embed requires switching to the iframe's frame context first. The auto-waiting works correctly within each frame, but you need to explicitly access the right frame before interacting with its elements.
Server-Sent Events and WebSocket-driven updates are a final category. When the UI updates based on a push from the server (a new chat message, a real-time notification, a live dashboard update), the timing depends on the server's behavior rather than any user interaction. Playwright cannot auto-wait for something that has not been triggered yet. For these scenarios, use page.waitForEvent() or poll-based assertions with appropriate timeouts.
6. A Practical Migration Strategy
Migrating a large Selenium suite to Playwright is best done incrementally. Start with new tests: write all new tests in Playwright while keeping the existing Selenium suite running. This lets your team learn Playwright's patterns and build confidence without risking the existing test coverage. Run both suites in CI during the transition period.
For migrating existing tests, prioritize by pain. Start with the tests that are most flaky (the ones with the most sleep calls and explicit waits), because they benefit the most from auto-waiting. Migrate them to Playwright, remove all the wait logic that auto-waiting handles, and verify that they run faster and more reliably. The early wins from migrating the flakiest tests build momentum and demonstrate the value to the team.
Do not attempt a mechanical, line-by-line translation of Selenium tests to Playwright. The patterns are different enough that a direct translation produces suboptimal Playwright code. Instead, rewrite each test using Playwright idioms: use locators instead of raw selectors, use auto-waiting instead of explicit waits, use Playwright's assertion library instead of generic assertion frameworks. The rewrite takes longer than a translation but produces dramatically better tests.
Tools like Assrt can accelerate the migration by generating fresh Playwright tests from your application's current state. Instead of manually rewriting each Selenium test, run npx @m13v/assrt discover https://your-app.comto generate a new Playwright test suite that covers your application's user flows. Compare the generated tests against your existing Selenium coverage to identify gaps, then supplement as needed. This approach produces cleaner Playwright code than manual migration because the tests are generated from scratch using Playwright best practices, with auto-waiting and self-healing selectors built in from the start.
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.