Skip to content

Click actionability probe surfaces raw CDP error when a navigation destroys the execution context #195

Description

@adhami3310

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions