Skip to content

Two loops behind every click

2.1 · Actions · locator.click()

What Playwright does between your click() call and the mouse event.

First, Playwright finds the element 1. If no element matches, it waits and looks again. If two or more elements match, the click fails at once 5.

In your test · strict mode
// ✗ Two "Buy" buttons match
await page
.getByRole('button', { name: 'Buy' })
.click();
// ✓ Find the one in the cart
await page
.getByTestId('cart')
.getByRole('button', { name: 'Buy' })
.click();

Then Playwright tries to click that element 2. Each try checks the element in this order: stable 3, visible, enabled. Then Playwright scrolls the element into view. It also makes sure that no other element covers the click point.

Inside Playwright · injectedScript.ts
if (states.includes('stable')) {
const stableResult = await
this._checkElementIsStable(node);
if (stableResult === false)
return { missingState: 'stable' };
…
}
for (const state of states) {
// visible, enabled
if (state !== 'stable') { … }
}

If a check fails, Playwright waits and tries the same element again 4. It does not find the element again. It does that only if the app removed the element from the page.

Inside Playwright · dom.ts
// We progressively wait longer between
// retries, up to 500ms.
const waitTime = [0, 20, 100, 100, 500];

Both loops stop at the action timeout. Then the last line of the call log tells you which check failed.

In your test · timeout
// ✗ Not needed: the loops already wait
await page.waitForTimeout(1000);
// ✓ Set the deadline for both loops
await page
.getByRole('button', { name: 'Buy' })
.click({ timeout: 5_000 });
Runs in:Playwright serverpage (injected script)browser
Source, v1.64.1: frames.ts · dom.ts · injectedScript.ts