From a91d105d4d051d8d45aa07da1e28e48e2b65dcba Mon Sep 17 00:00:00 2001 From: CloakHQ Date: Mon, 17 Aug 2026 20:48:49 +0200 Subject: [PATCH] fix(humanize): refresh pre-action element checks after click/form navigation (#507) Humanized actions could fail with an element-not-attached error after a navigation driven by a click or form submit, since the checks only refreshed on goto. Now refresh on any main-frame navigation. Python, JS Playwright/Puppeteer, and .NET. Regression from 0.5.6. --- CHANGELOG.md | 1 + cloakbrowser/human/__init__.py | 21 ++++ dotnet/src/CloakBrowser/Human/HumanPage.cs | 7 ++ .../CloakBrowser/Wrappers/HumanizedPage.cs | 7 ++ .../FrameNavigatedInvalidationTests.cs | 101 ++++++++++++++++++ js/src/human-puppeteer/index.ts | 5 + js/src/human/index.ts | 5 + js/tests/humanize.test.ts | 32 +++++- js/tests/stealth.puppeteer.test.ts | 9 +- tests/test_humanize_unit.py | 81 ++++++++++++++ 10 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 dotnet/tests/CloakBrowser.Tests/Wrappers/FrameNavigatedInvalidationTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d64fc9c..0fa3ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi ## [Unreleased] +- **[wrapper]** Fix `humanize=True` actions (`fill`, `click`, `type`) failing with an element-not-attached error after a navigation driven by a click or form submission instead of `goto`. The pre-action element checks could stay bound to the previous document and never recover; they now refresh on every navigation. Regression from 0.5.6. Python, JavaScript Playwright/Puppeteer, and .NET. - **[binary]** Chromium **150.0.7871.114.6** (Pro Stable, Linux x64 + arm64 and Windows x64) — 71 source-level patches. Linux advances from `150.0.7871.114.4`; Windows advances from `150.0.7871.114.3`. macOS remains on Stable `150.0.7871.114.3`. - **Identity and hardware:** expanded the Windows hardware-profile pool and tightened alignment between graphics, CPU, memory, and display characteristics. Fixed seeds may resolve to a different complete hardware identity than on the previous Stable build. - **Windows fidelity:** completed text measurements and font availability across a broader real-world font set, refined platform-specific path handling, improved headed window geometry, and expanded locale coverage including Greek. diff --git a/cloakbrowser/human/__init__.py b/cloakbrowser/human/__init__.py index 8feda2f..7ec272a 100644 --- a/cloakbrowser/human/__init__.py +++ b/cloakbrowser/human/__init__.py @@ -1025,6 +1025,17 @@ def patch_page(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None: cdp_session = None logger.debug("Could not create CDP session — stealth features disabled") + if stealth is not None: + # Invalidate the isolated world on any main-frame nav, not just goto, + # so click/form navigations don't leave it bound to a stale doc (#507). + try: + page.on( + "framenavigated", + lambda frame: stealth.invalidate() if frame == page.main_frame else None, + ) + except Exception: + logger.debug("Could not wire framenavigated invalidation") + raw_mouse: RawMouse = type("_RawMouse", (), { "move": originals.mouse_move, "down": originals.mouse_down, @@ -2095,6 +2106,16 @@ def patch_page_async(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None: cdp_session_holder: list[Any] = [None] # mutable container for closure page._cdp_session_holder = cdp_session_holder # expose for frame-level patching + # Invalidate the isolated world on any main-frame nav, not just goto, so + # click/form navigations don't leave it bound to a stale doc (#507). + try: + page.on( + "framenavigated", + lambda frame: stealth.invalidate() if frame == page.main_frame else None, + ) + except Exception: + logger.debug("Could not wire framenavigated invalidation") + async def _ensure_cdp() -> Any: if cdp_session_holder[0] is None: try: diff --git a/dotnet/src/CloakBrowser/Human/HumanPage.cs b/dotnet/src/CloakBrowser/Human/HumanPage.cs index 097e9d8..4d57aee 100644 --- a/dotnet/src/CloakBrowser/Human/HumanPage.cs +++ b/dotnet/src/CloakBrowser/Human/HumanPage.cs @@ -70,6 +70,13 @@ public sealed class HumanPage _rawKeyboard = new PlaywrightRawKeyboard(page.Keyboard); _scrollPage = new PlaywrightScrollPage(page); _evaluator = new PlaywrightEvaluator(page); + + // Invalidate the isolated world on any main-frame nav, not just goto, so + // click/form navigations don't leave it bound to a stale doc (#507). + _page.FrameNavigated += (_, frame) => + { + if (frame == _page.MainFrame) _stealth?.Invalidate(); + }; } /// diff --git a/dotnet/src/CloakBrowser/Wrappers/HumanizedPage.cs b/dotnet/src/CloakBrowser/Wrappers/HumanizedPage.cs index fce9c7c..3b7df13 100644 --- a/dotnet/src/CloakBrowser/Wrappers/HumanizedPage.cs +++ b/dotnet/src/CloakBrowser/Wrappers/HumanizedPage.cs @@ -35,6 +35,13 @@ public sealed partial class HumanizedPage : IPage _human = new HumanPage(inner, cfg); _mouse = new HumanizedMouse(inner.Mouse, cursor, cfg); _keyboard = new HumanizedKeyboard(inner.Keyboard, cursor, cfg); + + // Invalidate the isolated world on any main-frame nav, not just goto, so + // click/form navigations don't leave it bound to a stale doc (#507). + _inner.FrameNavigated += (_, frame) => + { + if (frame == _inner.MainFrame) _cursor.InvalidateStealth(); + }; } /// The original, un-humanized Playwright page (escape hatch for raw speed). diff --git a/dotnet/tests/CloakBrowser.Tests/Wrappers/FrameNavigatedInvalidationTests.cs b/dotnet/tests/CloakBrowser.Tests/Wrappers/FrameNavigatedInvalidationTests.cs new file mode 100644 index 0000000..c07c549 --- /dev/null +++ b/dotnet/tests/CloakBrowser.Tests/Wrappers/FrameNavigatedInvalidationTests.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using CloakBrowser.Human; +using CloakBrowser.Wrappers; +using Microsoft.Playwright; +using Xunit; + +namespace CloakBrowser.Tests.Wrappers; + +/// +/// Regression #507: a click/form/history navigation (not just page.goto) must +/// invalidate the isolated world. bfcache keeps the previous document's context +/// alive, so a stale eval keeps succeeding against the old document and +/// fill()/click() actionability reads fail. Both page wrappers must subscribe to +/// and invalidate on MAIN-frame navigations only. +/// +public class FrameNavigatedInvalidationTests +{ + private static HumanConfig FastConfig() => new() + { + IdleBetweenActions = false, + InitialCursorX = (100, 100), + InitialCursorY = (100, 100), + }; + + /// Give the world a non-null cached context id so Invalidate() is observable. + private static void PrimeContextId(IsolatedWorld world, int id) => + typeof(IsolatedWorld).GetField("_contextId", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(world, id); + + private static int? ReadContextId(IsolatedWorld world) => + (int?)typeof(IsolatedWorld).GetField("_contextId", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(world); + + [Fact] + public void HumanizedPage_invalidates_world_on_main_frame_nav_only() + { + var (mouse, _) = Fake.Of(); + var (keyboard, _) = Fake.Of(); + var (page, pageRec) = Fake.Of(); + var (mainFrame, _) = Fake.Of