Playwright’s auto-wait is actually a form of explicit waiting, just managed by the framework, and it often makes explicit waiters unnecessary.
Let’s see it in action. Imagine a simple page with a button that appears after a short delay.
<!DOCTYPE html>
<html>
<head>
<title>Delayed Button</title>
<script>
setTimeout(() => {
const button = document.createElement('button');
button.id = 'delayedButton';
button.textContent = 'Click Me';
document.body.appendChild(button);
}, 2000); // Button appears after 2 seconds
</script>
</head>
<body>
<h1>Waiting for button...</h1>
</body>
</html>
Now, let’s try to interact with this button using Playwright.
Scenario 1: Relying on Auto-Wait
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("file:///path/to/your/delayed_button.html") # Replace with your file path
# Playwright automatically waits for the button to be visible and enabled
page.click("#delayedButton")
print("Clicked the button using auto-wait!")
browser.close()
When you run this, Playwright doesn’t immediately throw an error trying to find #delayedButton. It waits for the element to appear in the DOM, become visible, and be actionable before attempting the click. This is the magic of auto-wait. The default timeout for these waits is 30 seconds, but it can be configured globally or per-action.
Scenario 2: Explicitly Waiting (Less Common with Auto-Wait)
While Playwright’s auto-wait covers most scenarios, you might still encounter situations where you need more fine-grained control or want to wait for a condition before an element is even present.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("file:///path/to/your/delayed_button.html") # Replace with your file path
# Explicitly waiting for the button to be attached to the DOM
page.wait_for_selector("#delayedButton", state="attached")
print("Button is now attached to the DOM.")
# Then, you might click it (though auto-wait would handle this too)
page.click("#delayedButton")
print("Clicked the button after explicit wait.")
browser.close()
Here, page.wait_for_selector("#delayedButton", state="attached") explicitly tells Playwright to pause execution until an element with the ID delayedButton exists in the DOM. The state parameter can be one of "attached", "detached", "hidden", or "visible".
The core problem Playwright’s auto-wait solves is the flakiness of automated tests due to unpredictable network latency and JavaScript execution times. Traditional automation frameworks often required developers to manually insert sleep() calls or complex WebDriverWait constructs, leading to brittle tests that either failed intermittently or ran far too slowly. Playwright’s approach abstracts this away for most common interactions.
Internally, when you perform an action like page.click(), Playwright doesn’t just look for the element once. It has a robust waiting mechanism that checks for several conditions by default:
- Attached: The element is present in the DOM.
- Visible: The element is not hidden by CSS (
display: none,visibility: hidden,opacity: 0, etc.) and has a computed size greater than 0x0. - Enabled: The element is not disabled (e.g., an
<input>withdisabledattribute, or a<button>withdisabled).
Playwright polls for these conditions until the action’s timeout is reached. If all conditions are met within the timeout, the action proceeds. If not, an error is thrown.
The page.goto() action also has an auto-wait. By default, it waits for the "load" event. You can configure it to wait for "domcontentloaded" or "networkidle" using the wait_until option: page.goto("url", wait_until="networkidle"). Waiting for "networkidle" means Playwright will wait until there are no more than 0 network connections for at least 500 ms. This is useful for pages that load resources asynchronously after the initial load event.
The one thing most people don’t know is that the default timeout for most Playwright actions (like click, fill, waitForSelector) is a generous 30 seconds. This is configurable globally via browser_context_options or launch_options (timeout=60000 for 60 seconds), or per-action: page.click("#myButton", timeout=10000) for a 10-second timeout on that specific click. This long default is a deliberate choice to make tests more robust out-of-the-box, but it can mask underlying performance issues if you’re not careful.
The next concept to explore is how to handle more complex synchronization scenarios, such as waiting for specific network requests to complete or for a particular piece of text to appear on the page.