Summary
When a navigation commits while Locator.click() is probing actionability, the execution context is torn down and the raw CDP error surfaces to the caller:
Error: Protocol error (DOM.getBoxModel): Cannot find context with specified id
Error: Protocol error (Runtime.callFunctionOn): Cannot find context with specified id
Which CDP method appears depends on which probe step is in flight when the navigation lands. Playwright re-arms and completes the click.
This is the same failure class as #79, fixed in #85 for the selector-wait path by adding is_execution_context_destroyed_error and retrying the evaluate against a re-resolved frame. The click actionability probe (_target_state / locator_probe_state) does not appear to go through that recovery.
Note the raised type is Error, not TimeoutError, so callers that only tolerate timeouts around a click do not catch it.
Repro
Deterministic, local, no network. The page navigates a configurable number of ms after load, and the click is attempted across a sweep of delays so some attempts land inside the probe window.
import asyncio, http.server, socket, sys, threading
PAGE_A = b"""<!doctype html><html><body>
<button id="go">Log out</button>
<script>setTimeout(function(){ location.href = '/b'; }, %d);</script>
</body></html>"""
PAGE_B = b"""<!doctype html><html><body><button id="go">Log out</button>done</body></html>"""
class H(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if self.path.startswith("/b"):
body = PAGE_B
else:
delay = 60
try:
delay = int(self.path.split("d=")[1])
except Exception:
pass
body = PAGE_A % delay
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a): pass
def serve():
s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close()
srv = http.server.ThreadingHTTPServer(("127.0.0.1", p), H); srv.daemon_threads = True
threading.Thread(target=srv.serve_forever, daemon=True).start()
return p
async def run(module, port):
api = __import__(f"{module}.async_api", fromlist=["async_playwright"])
async with api.async_playwright() as pw:
b = await pw.chromium.launch(headless=True)
page = await b.new_page()
errors = {}
for delay in (0, 5, 10, 20, 30, 50, 80, 120):
for _ in range(8):
await page.goto(f"http://127.0.0.1:{port}/a?d={delay}")
try:
await page.get_by_text("Log out").click(timeout=3000)
except Exception as exc:
key = f"{type(exc).__name__}: {str(exc)[:90]}"
errors[key] = errors.get(key, 0) + 1
print(f"[{module:11s}] errors over 64 attempts: {errors or 'none'}")
await b.close()
PORT = serve()
for m in sys.argv[1:] or ["playwright", "rustwright"]:
asyncio.run(run(m, PORT))
Result
[playwright ] errors over 64 attempts: none
[rustwright ] errors over 64 attempts: {
'Error: Protocol error (DOM.getBoxModel): Cannot find context with specified id': 4,
'Error: Error: No element matches locator\n at <anonymous>:910:19': 1,
}
Expected: rustwright matches playwright and completes the click across the navigation.
Environment
- rustwright 0.1.1 (PyPI) and git
ec130a64 — both reproduce
- playwright 1.61.0 for comparison
- Chromium headless shell 1228, Linux x86_64, Python 3.14
Why it matters
Found while evaluating rustwright as a drop-in for a Playwright suite. It shows up on any control whose own click navigates — in our case a logout button, where the traceback is:
Locator.click -> _click_impl -> _wait_for_single -> _target_state
-> locator_probe_state('{"kind":"text","text":"Log out","exact":false}', ...)
-> Protocol error (Runtime.callFunctionOn): Cannot find context with specified id
It is timing-dependent, so it reads as a flaky test rather than a reproducible defect: locally it failed ~8% of attempts, and in CI it appeared only under parallel load.
Summary
When a navigation commits while
Locator.click()is probing actionability, the execution context is torn down and the raw CDP error surfaces to the caller:Which CDP method appears depends on which probe step is in flight when the navigation lands. Playwright re-arms and completes the click.
This is the same failure class as #79, fixed in #85 for the selector-wait path by adding
is_execution_context_destroyed_errorand retrying the evaluate against a re-resolved frame. The click actionability probe (_target_state/locator_probe_state) does not appear to go through that recovery.Note the raised type is
Error, notTimeoutError, so callers that only tolerate timeouts around a click do not catch it.Repro
Deterministic, local, no network. The page navigates a configurable number of ms after load, and the click is attempted across a sweep of delays so some attempts land inside the probe window.
Result
Expected: rustwright matches playwright and completes the click across the navigation.
Environment
ec130a64— both reproduceWhy it matters
Found while evaluating rustwright as a drop-in for a Playwright suite. It shows up on any control whose own click navigates — in our case a logout button, where the traceback is:
It is timing-dependent, so it reads as a flaky test rather than a reproducible defect: locally it failed ~8% of attempts, and in CI it appeared only under parallel load.