Skip to content

fix(executor): humanized click reads borders without Playwright's private _expect - #141

Merged
broven merged 1 commit into
mainfrom
fix/140-border-width-keyerror
Oct 18, 2026
Merged

broven merged 1 commit into
mainfrom
fix/140-border-width-keyerror

Conversation

@broven

@broven broven commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Fixes #140

Symptom

With Playwright >= 1.63, every humanized locator.click() on a normally sized visible element raises KeyError: 'received' in repl/human_input.py _border_width. Humanized fill() clicks first, so it fails too. A plain <button> is enough to trigger it.

Root cause

_border_width read computed CSS through Playwright's private Locator._impl_obj._expect('to.have.css', {regex '.*'}) and took result['received']. In Playwright 1.63, Frame._expect returns only {"matches": ...} when the expectation passes. received is filled in only on the failure path, and the .* regex always passes. CI and the dev env never hit this because uv.lock pinned 1.60.0, while pyproject says >=1.60.0, so a fresh install resolves to a newer version.

Fix

  • human_input.py: the left/top border widths now come from the existing _browserwright_scroll selector engine (a contentScript engine, so it runs in Playwright's utility world and answers through a detached transport node). The no-main-world-JS guarantee stays intact, and nothing depends on a private result shape anymore. _border_width(locator, prop) (two _expect round trips) is replaced by _borders(locator), which gets both widths from one read. They are parsed with parseInt to match Playwright's own padding-relative offset, as before.
  • uv.lock: playwright 1.60.0 -> 1.64.0, so the dev env and CI resolve the same Playwright a fresh install does. pyproject is unchanged. No new test: the existing tests/daemon/e2e/test_human_input.py already fails with this exact KeyError on 1.63 against the old code (shown below), so the lock bump turns it into the regression guard.

I did not add a fallback to native click on a failed read. The read now goes through the same transport that _wheel_into_view already depends on a moment earlier in the same click, so a fallback would only hide real faults.

Evidence

Issue repro (the exact two scripts from #140, 3 rounds each, --backend=cdp --create, run through this checkout on an isolated daemon: own facade/relay/CDP ports, XDG_RUNTIME_DIR, BS_HOME, BD_CONFIG="", uv run --with playwright==<v>):

main (4b54676), playwright 1.63.0:

  File "/private/tmp/bw-140-base/src/browserwright/repl/human_input.py", line 383, in _click
    border_x = _border_width(locator, 'border-left-width', budget)
  File "/private/tmp/bw-140-base/src/browserwright/repl/human_input.py", line 248, in _border_width
    received = result['received']
KeyError: 'received'
exit=1

this branch:

=== playwright 1.64.0
clicked button
clicked Learn more -> https://www.iana.org/help/example-domains
(x3)
exit=0
=== playwright 1.63.0
clicked button
clicked Learn more -> https://www.iana.org/help/example-domains
(x3)
exit=0

It also passed on 1.60.0, run before the lock bump.

Real-Chrome E2E, human input (tests/daemon/e2e/run.sh tests/daemon/e2e/test_human_input.py tests/daemon/e2e/test_human_input_no_focus.py -v):

main this branch
test_human_input.py, playwright 1.63.0 FAIL: KeyError: 'received' (via fill -> click -> _border_width) PASS
both files, playwright 1.64.0 (new lock) n/a 2 passed in 69.56s
both files, playwright 1.60.0 2 passed 2 passed

test_human_input.py covers bordered/positioned targets and asserts the real event coordinates, including the border offset, so the new border read is checked against the page's own getComputedStyle.

Note: test_human_input_no_focus.py is headful, and on a busy desktop it sometimes fails because another macOS app takes focus. Its focus log then shows windows.onFocusChanged [-1] (WINDOW_ID_NONE), never the session window. This happened on both main and this branch, and the test passes on rerun.

Fast gate (mise run test, on the 1.64.0 lock):

[test:evals]   ✓ 13/13 passed
[test:pi] ℹ pass 5
[test:pi] ℹ fail 0
[test:skill] 338 passed in 16.87s
[test:daemon] 981 passed, 105 skipped in 55.97s

mise run lint: All checks passed!

Not run: the full real-Chrome E2E suite (only the human-input files), and the extension backend against a daily Chrome (the human-input E2E uses the extension backend in Chrome for Testing).

🤖 Generated with Claude Code

…vate _expect

Every human-input click raised KeyError: 'received' on Playwright >= 1.63.
_border_width read computed CSS through Locator._impl_obj._expect('to.have.css')
and took result['received'], but from 1.63 a passing expectation returns only
{"matches": ...}; 'received' is filled in on the failure path. The '.*' regex
always passes, so every normally sized click failed.

The left/top border widths now come from the _browserwright_scroll selector
engine that already plans wheel scrolling. It runs in Playwright's utility
world and answers through a detached transport node, so the read is still
never visible to the page's main world, and it no longer depends on a private
result shape. Both widths come from one read, parsed with parseInt like
Playwright's own padding-relative offset.

uv.lock moves playwright 1.60.0 -> 1.64.0 so the dev env and CI resolve the
same Playwright a fresh install does (pyproject keeps >=1.60.0). The existing
tests/daemon/e2e/test_human_input.py fails with this KeyError on 1.63 against
the old code, so the lock bump makes it the regression guard.

Fixes #140

Co-Authored-By: Claude <noreply@anthropic.com>
@broven
broven merged commit 230f667 into main Oct 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[agent-bug] Every humanized locator.click() raises KeyError: 'received' in _border_width with Playwright 1.63 (cdp backend)

1 participant