Diagnosing Flaky Element Interactions in Playwright: A Step‑by‑Step Auto‑Wait Checklist
When Playwright tests intermittently fail on element actions, the culprit is often auto‑wait mis‑application. This guide walks through the condition, diagnostics, ordered checks, fixes, and escalation steps to stabilize flaky <code>click</code> and <code>fill</code> interactions.
21 Jul 2026, 03:09 UTC

Recognizable Condition
During a Playwright test run you notice that locator.click() or locator.fill() occasionally fails with a TimeoutError even though the element is clearly visible in the browser. The failure appears random and is hard to reproduce locally.
Auto‑Wait Behavior in Playwright
- Playwright automatically waits for elements to be attached, visible, and enabled before performing actions such as
click,fill, andhover. - Direct queries like
locator.textContent()orlocator.isVisible()do not trigger an auto‑wait; they return immediately. - Elements obscured by overlays (e.g., modal backdrops, loading screens) are considered not visible for the purpose of auto‑wait, unless the overlay is removed first.
Diagnostic Table
| Condition | Likely Cause |
|---|---|
| Element appears visible, but action times out | Auto‑wait not triggered because selector is used in a non‑action method or overlay is present. |
| Action succeeds on some runs, fails on others | Network latency or slow rendering causes element to appear after the default timeout. |
Explicit await expect(locator).toBeVisible() passes, but locator.click() still fails | Custom locator logic or stale reference; the element reference changed between the check and the action. |
Ordered Checks
- Verify Playwright Version
Auto‑wait rules changed in 1.30. Ensure you’re running
playwright --versionand upgrade if older:npm install -D @playwright/test@latest - Run with Debug Logging
Enable detailed logs to see when Playwright waits for a selector:
playwright test --debugLook for lines like
Waiting for selector 'button#save' to be visiblebefore the failing action. - Inspect the Trace Viewer
Generate a trace with
--trace onand opennpx playwright show-trace. Verify that awaitForSelectorevent precedes the action. - Check for Overlays
Use
await page.waitForSelector('.modal-backdrop', { state: 'detached' })before interacting with elements inside a modal. - Validate Custom Locators
If you use
page.locator('data-test-id=submit'), confirm that the selector is unique and does not return multiple elements. Multiple matches can cause Playwright to pick the wrong one. - Increase Timeout Temporarily
Run the test with a higher timeout to rule out network slowness:
await locator.click({ timeout: 15000 })
Fixes Tied to Findings
- No Auto‑Wait Triggered
- Wrap the action in an explicit
await expect(locator).toBeEnabled()before the click:
await expect(locator).toBeEnabled() await locator.click() - Wrap the action in an explicit
- Or use
locator.waitFor({ state: 'visible' })explicitly.
- Wait for the overlay to disappear before interacting:
await page.waitForSelector('.modal-backdrop', { state: 'detached' })
await locator.click()
- Re‑evaluate the locator right before the action:
const button = page.locator('button#save')
await button.click()
- Increase the global timeout in
playwright.config.ts:
export default defineConfig({
timeout: 20000,
// other config
})
Escalation Criteria
- If the failure persists after applying the above fixes and you’re on the latest Playwright release, file a reproducible issue on the Playwright GitHub repo with a trace and minimal test case.
- Consider isolating the test in a separate project to rule out global configuration interference.
- If the flaky element is part of a third‑party library, check their issue tracker for similar reports.
Practical Verification Checklist
- Run the test with
--debugand confirm the auto‑wait log appears before the action. - Open the trace viewer and see a
waitForSelectorevent matching the failing selector. - After applying a fix, rerun the test in headless mode and ensure the failure is no longer reported.
- If the test still fails, compare the output of
await locator.isVisible()before and after the action to confirm the element’s state.
Concrete Example
Suppose a test clicks a Save button inside a modal that appears after an API call:
test('save data', async ({ page }) => {
await page.goto('/editor')
await page.click('button#create')
// Modal opens asynchronously
await page.waitForSelector('.modal-backdrop', { state: 'attached' })
// Wait for the button to be enabled
const saveBtn = page.locator('button#save')
await expect(saveBtn).toBeEnabled()
await saveBtn.click()
})
Running this test with --debug will show a clear auto‑wait for button#save after the modal appears, preventing flaky failures.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.