From 9500bd1537dfd66e0d0647051193262f9905a11d Mon Sep 17 00:00:00 2001 From: Randy Date: Sun, 18 Oct 2026 15:38:36 +0800 Subject: [PATCH] fix(executor): humanized click reads borders without Playwright's private _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 --- src/browserwright/repl/human_input.py | 45 +++++++++++++-------------- uv.lock | 18 +++++------ 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/browserwright/repl/human_input.py b/src/browserwright/repl/human_input.py index 812c9b7..3eb611c 100644 --- a/src/browserwright/repl/human_input.py +++ b/src/browserwright/repl/human_input.py @@ -110,10 +110,13 @@ } } } + const style=getComputedStyle(root); const active=root.getRootNode().activeElement || root.ownerDocument.activeElement; out.textContent=JSON.stringify({rect:r,jobs,focused:active === root || root.contains(active), viewport:{width:innerWidth,height:innerHeight, dpr:devicePixelRatio || 1},clientLeft:root.clientLeft,clientTop:root.clientTop, + // Playwright offsets padding-relative positions by parseInt(border). + border:{x:parseInt(style.borderLeftWidth,10) || 0,y:parseInt(style.borderTopWidth,10) || 0}, offsetWidth:root.offsetWidth,offsetHeight:root.offsetHeight,caret}); // A detached transport node never changes the document or its styles. return [out]; @@ -238,24 +241,20 @@ def _approach(page: Any, x: float, y: float, budget: _Budget, metrics: dict[str, budget.pause(_RNG.uniform(0.008, 0.015)) -def _border_width(locator: Any, property_name: str, budget: _Budget) -> float: - # Read computed CSS via Playwright's utility-world expectation transport. - # Locator.evaluate runs in the main world and would expose internal reads. - result = locator._sync(locator._impl_obj._expect('to.have.css', { - 'expressionArg': property_name, 'expectedText': [{'regexSource': '.*', 'regexFlags': ''}], - 'isNot': False, 'timeout': budget.remaining(), - })) - received = result['received'] - if isinstance(received, dict) and 'value' in received: - received = received['value'] - if isinstance(received, dict): - # Older Playwright releases return the wire-format value here. - from playwright._impl._js_handle import parse_value - received = parse_value(received) - if not isinstance(received, str): - raise ValueError(f'Unexpected computed CSS result for {property_name}') - # Match Playwright's parseInt border offset, including scaled screens. - return int(float(received.removesuffix('px'))) +def _borders(locator: Any, budget: _Budget) -> tuple[int, int]: + """Computed left/top border widths, read in Playwright's utility world. + + Locator.evaluate runs in the main world and would expose internal reads, + so this uses the same isolated selector-engine transport as scrolling. + (Playwright's private ``_expect('to.have.css')`` result shape is not + stable: from 1.63 a passing expectation omits ``received`` entirely.) + """ + handle = locator.element_handle(timeout=budget.remaining()) + try: + border = _scroll_plan(handle, budget)['border'] + finally: + handle.dispose() + return border['x'], border['y'] def _scroll_plan(handle: Any, budget: _Budget, position: Any = None) -> dict[str, Any]: @@ -380,8 +379,7 @@ def _click(native: Any, locator: Any, kwargs: dict[str, Any], *, generated_posit return native(locator, **{**kwargs, "timeout": budget.remaining()}) # A comfortable interior point, deliberately away from dead center. fraction_x = _RNG.uniform(0.23, 0.40) if _RNG.random() < .5 else _RNG.uniform(.60, .77) - border_x = _border_width(locator, 'border-left-width', budget) - border_y = _border_width(locator, 'border-top-width', budget) + border_x, border_y = _borders(locator, budget) ratio = metrics['dpr'] # Native click truncates to hundredths of a CSS pixel. Choose points # representable on both that grid and the physical pixel grid (e.g. @@ -399,10 +397,9 @@ def _click(native: Any, locator: Any, kwargs: dict[str, Any], *, generated_posit position = {'x': x - box['x'] - border_x + .00001, 'y': y - box['y'] - border_y + .00001} else: - x = math.trunc((box['x'] + _border_width(locator, 'border-left-width', budget) - + position['x']) * 100) / 100 - y = math.trunc((box['y'] + _border_width(locator, 'border-top-width', budget) - + position['y']) * 100) / 100 + border_x, border_y = _borders(locator, budget) + x = math.trunc((box['x'] + border_x + position['x']) * 100) / 100 + y = math.trunc((box['y'] + border_y + position['y']) * 100) / 100 _approach(page, x, y, budget, metrics) # Native actionability can observe a newer target box than the approach. # Correct its final absolute coordinates at the dispatch boundary instead diff --git a/uv.lock b/uv.lock index ad8bf4a..13b390f 100644 --- a/uv.lock +++ b/uv.lock @@ -447,21 +447,21 @@ wheels = [ [[package]] name = "playwright" -version = "1.60.0" +version = "1.64.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/21/f0/832bd9677194908da118064eef20082f2791e3d18215cc6d9391ee2c5a67/playwright-1.60.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:6a8cd0fec171fb3089e95e898c8bc8a6f35dea0b78b399e12fcc19427e91b1d7", size = 43474635, upload-time = "2026-05-18T12:00:31.969Z" }, - { url = "https://files.pythonhosted.org/packages/59/7b/e1d32ae8a3ed937ec2be3721c5f728b13d731a0b7c6442e0b3bec5094ac0/playwright-1.60.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:39b5420ba6145045b69ced4c5c47d4d9fe5bddfc8ff816c518913afcb25ec7a5", size = 42261327, upload-time = "2026-05-18T12:00:35.638Z" }, - { url = "https://files.pythonhosted.org/packages/d7/bc/23de499ded6411c188a20c5a0dea6f0cd4ed5d2b3cc6042a5dbd3ed609aa/playwright-1.60.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:2581d0e6a3392c71f91b27460c7fd093356818dc430f48153896c8aeeaef7705", size = 43474636, upload-time = "2026-05-18T12:00:39.294Z" }, - { url = "https://files.pythonhosted.org/packages/22/7b/1d679f4fced4ea94efadd17103856d8c565384f68382a1681264e46f5925/playwright-1.60.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:1c2bfae7884fb3fb05b853290eab8f343d524e5016f2f1def702acbbdf14c93e", size = 47467220, upload-time = "2026-05-18T12:00:43.179Z" }, - { url = "https://files.pythonhosted.org/packages/84/c2/1528d267d4442bd2c6b8eaeab819dd52c2030bf80e89293f0ba1f687473b/playwright-1.60.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43e66564125ee31b07a58cefb21e256d62d67d8d1713e6858df7a3019d8ed353", size = 47154856, upload-time = "2026-05-18T12:00:46.715Z" }, - { url = "https://files.pythonhosted.org/packages/bb/4e/b008b6440a7a1624378041da94829956d4b8f7ab9ef5aad22d0dc3f2e26d/playwright-1.60.0-py3-none-win32.whl", hash = "sha256:ec94e416ea320711e0ad4bf185dcbf41833672961e90773e1885255d7db7b7e7", size = 37902157, upload-time = "2026-05-18T12:00:50.374Z" }, - { url = "https://files.pythonhosted.org/packages/55/f0/0541524133104f9cc20bf900870ff4a736b76a23483f3a55295ddfa58409/playwright-1.60.0-py3-none-win_amd64.whl", hash = "sha256:9566821ce6030a1f9e7146a24e19355ab0d98805fd0f9be50bb3d8fef1750c02", size = 37902159, upload-time = "2026-05-18T12:00:53.728Z" }, - { url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/24e5c283694e7a63aa376ddde395031decd7d627c77b34a58953176e8a19/playwright-1.64.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d76a501c9930b5a097b00e2448cda2200122a1e8e4be762ff535c1b076277737", size = 44347913, upload-time = "2026-10-13T23:35:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b3/99f6c07a7f59adb3830e42cda2e2093dc1c2cbd1f69fe4393ead30c88257/playwright-1.64.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8de42430e9a7c8b04ec963856d484d36ffd452882318ebad2df3e6fb49a8197a", size = 43014183, upload-time = "2026-10-13T23:35:24.195Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f4/e0ddecd32342cac462777ef9123de833486743709bd437458c49e004d750/playwright-1.64.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:61e4e0801bfd76b30e04635aaec45647df707881ccf14382471fcb0eaeb1d16f", size = 44347915, upload-time = "2026-10-13T23:35:27.476Z" }, + { url = "https://files.pythonhosted.org/packages/65/8b/78c19b805d52323122385b9c3f000144608deac38635455978af6d14807b/playwright-1.64.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:5a59af1b230b234008524a5d42b613b233d4256f73bc1dd25bf3f11db0c81b75", size = 48286664, upload-time = "2026-10-13T23:35:30.981Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7e/c100ee3c59ff42e3199880d77e8f59f3230fdb0dd1ecfd21a0a203bb5a68/playwright-1.64.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d20be6a0884e946b2471774dd960ec95519ba533e61329038526d9aea9a23", size = 47972215, upload-time = "2026-10-13T23:35:34.976Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6e/3746bca44a77ad29fb4bf1722ad3345d5e936d5363d8daffbf23b226d3a4/playwright-1.64.0-py3-none-win32.whl", hash = "sha256:8b9f18dc1c23143ac0a5b3c59015db30e9413cc52e1ddddfc2836a7fbad7165a", size = 38675586, upload-time = "2026-10-13T23:35:38.665Z" }, + { url = "https://files.pythonhosted.org/packages/e9/be/1dd8a65e3713c50c64b2580f50494ece90a4a9ede6e23788ef120e54c1f2/playwright-1.64.0-py3-none-win_amd64.whl", hash = "sha256:2c14d105548876b15bea5e7eca77bf0d8ff4ba0c607c1ae931067f3d1b010369", size = 38675591, upload-time = "2026-10-13T23:35:42.078Z" }, + { url = "https://files.pythonhosted.org/packages/f1/53/683ee3eb28902208d1d0c1d2eb3d6ddd99bc0184063babc6a2a7886c5799/playwright-1.64.0-py3-none-win_arm64.whl", hash = "sha256:97a5c247f1130f3343f097caf3bb1e79358d6b6cfa3550d97ecd721d1905911a", size = 34593061, upload-time = "2026-10-13T23:35:45.768Z" }, ] [[package]]