pi-lean-dimension 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/node_modules/pi-lean-portal/AGENTS.md +15 -23
- package/node_modules/pi-lean-portal/README.md +31 -1
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +12 -1
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +263 -1126
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +6 -32
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +1 -1
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +1 -2
- package/node_modules/pi-lean-portal/__tests__/helpers/setup-temp-dir.ts +26 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/plugin-contract.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +65 -69
- package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +1 -14
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +31 -16
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +146 -19
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +3 -3
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +2 -4
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +572 -81
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +13 -43
- package/node_modules/pi-lean-portal/browser-profile.ts +2 -3
- package/node_modules/pi-lean-portal/browser-toggle.ts +168 -385
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +2 -2
- package/node_modules/pi-lean-portal/contributed/README.md +65 -6
- package/node_modules/pi-lean-portal/contributed/camoufox-py/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +108 -14
- package/node_modules/pi-lean-portal/contributed/camoufox-py/pin.json +4 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +10 -30
- package/node_modules/pi-lean-portal/core/guides.ts +14 -18
- package/node_modules/pi-lean-portal/core/router.ts +2 -20
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +19 -30
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +4 -4
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +2 -2
- package/node_modules/pi-lean-portal/core/shared/paths.ts +16 -2
- package/node_modules/pi-lean-portal/core/shared/settings-reader.ts +2 -7
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +1 -1
- package/node_modules/pi-lean-portal/index.ts +8 -1
- package/node_modules/pi-lean-portal/package.json +2 -1
- package/node_modules/pi-lean-portal/tools/web-learn.ts +1 -4
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +87 -0
- package/node_modules/pi-lean-search/index.ts +134 -64
- package/node_modules/pi-lean-search/package.json +2 -1
- package/node_modules/playwright/README.md +3 -3
- package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +2 -2
- package/node_modules/playwright/lib/cli/reportActions.js +2 -2
- package/node_modules/playwright/lib/common/index.js +77 -52
- package/node_modules/playwright/lib/common/index.js.txt +10 -11
- package/node_modules/playwright/lib/errorContext.js +2 -3
- package/node_modules/playwright/lib/index.js +30 -8
- package/node_modules/playwright/lib/matchers/expect.js +4811 -4379
- package/node_modules/playwright/lib/matchers/expect.js.LICENSE +87 -140
- package/node_modules/playwright/lib/matchers/expect.js.txt +30 -35
- package/node_modules/playwright/lib/program.js +19 -0
- package/node_modules/playwright/lib/runner/index.js +309 -153
- package/node_modules/playwright/lib/runner/index.js.txt +13 -12
- package/node_modules/playwright/lib/transform/babelBundle.js +12899 -11654
- package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +379 -354
- package/node_modules/playwright/lib/transform/babelBundle.js.txt +39 -39
- package/node_modules/playwright/lib/transform/esmLoader.js +3129 -1116
- package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +81 -61
- package/node_modules/playwright/lib/transform/esmLoader.js.txt +21 -18
- package/node_modules/playwright/lib/util.js +2 -29
- package/node_modules/playwright/lib/worker/workerProcessEntry.js +14 -32
- package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +7 -8
- package/node_modules/playwright/package.json +3 -6
- package/node_modules/playwright/types/test.d.ts +391 -4
- package/node_modules/playwright/types/testReporter.d.ts +79 -4
- package/node_modules/playwright-core/browsers.json +12 -18
- package/node_modules/playwright-core/lib/bootstrap.js +1 -1
- package/node_modules/playwright-core/lib/coreBundle.js +11344 -9900
- package/node_modules/playwright-core/lib/server/electron/loader.js +5 -2
- package/node_modules/playwright-core/lib/serverRegistry.js +5 -1
- package/node_modules/playwright-core/lib/tools/cli-client/help.json +21 -6
- package/node_modules/playwright-core/lib/tools/cli-client/program.js +1 -1
- package/node_modules/playwright-core/lib/tools/cli-client/session.js +4 -0
- package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +21 -5
- package/node_modules/playwright-core/lib/tools/{cli-client/skill/references/spec-driven-testing.md → skills/playwright-cli/references/test-generation.md} +135 -7
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
- package/node_modules/playwright-core/lib/tools/utils/extension.js +31 -8
- package/node_modules/playwright-core/lib/utilsBundle.js +37698 -33670
- package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +391 -268
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/index.html +2 -2
- package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -1
- package/node_modules/playwright-core/lib/vite/htmlReport/report.js +15 -55
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/index.html +2 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.html +5 -5
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +2 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -4
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +5 -5
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
- package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
- package/node_modules/playwright-core/lib/xdg-open +338 -137
- package/node_modules/playwright-core/package.json +2 -2
- package/node_modules/playwright-core/types/protocol.d.ts +249 -36
- package/node_modules/playwright-core/types/structs.d.ts +3 -1
- package/node_modules/playwright-core/types/types.d.ts +2009 -266
- package/package.json +3 -3
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +0 -188
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +0 -525
- package/node_modules/pi-lean-portal/core/shared/url-safety.ts +0 -164
- package/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json +0 -2739
- package/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md +0 -134
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-BY2S1tHT.css +0 -1
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-C_5TMfeg.js +0 -52
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +0 -1
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +0 -32
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-4ZiSSCmn.css +0 -1
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-Bq-mQf8S.js +0 -193
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +0 -32
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +0 -264
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +0 -9
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/index.DMMX1gXU.js +0 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.v8KI4P3m.js +0 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +0 -6
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +0 -32
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/element-attributes.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/playwright-tests.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/request-mocking.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/running-code.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/session-management.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/storage-state.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/tracing.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{trace → skills/playwright-trace}/SKILL.md +0 -0
package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
PlaywrightBridge — Python browser automation bridge for Playwright-based backends.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Provides JSON-RPC command routing, session lifecycle, element caching,
|
|
5
|
+
and a ``run()`` main loop. ``PlaywrightBridge`` is a concrete base
|
|
6
|
+
class that subclasses override with engine-specific settings:
|
|
6
7
|
|
|
7
8
|
* ``_plugin_name`` — e.g. ``"chromium-py"``, ``"firefox-py"``
|
|
8
9
|
* ``_user_agent`` — fallback UA string (when dynamic probe is disabled or fails)
|
|
@@ -12,6 +13,28 @@ into a parameterized base class. Subclasses override:
|
|
|
12
13
|
|
|
13
14
|
All navigation, interaction, console, cookie, and storage operations
|
|
14
15
|
are shared across engines.
|
|
16
|
+
|
|
17
|
+
Protocol
|
|
18
|
+
--------
|
|
19
|
+
All communication is JSON-RPC 2.0 over stdin/stdout with newline-delimited
|
|
20
|
+
framing. See ``transport.py`` for details.
|
|
21
|
+
|
|
22
|
+
Lifecycle
|
|
23
|
+
---------
|
|
24
|
+
1. The TypeScript ``PythonPluginAdapter`` spawns the Python process.
|
|
25
|
+
2. The bridge starts its ``run()`` loop, waiting for commands.
|
|
26
|
+
3. First command is typically ``browser.navigate``, which calls
|
|
27
|
+
``create_browser_session()`` (if no session exists yet for that taskId)
|
|
28
|
+
and then navigates.
|
|
29
|
+
4. Subsequent commands use the existing session.
|
|
30
|
+
5. ``browser.cleanup`` closes the session for a taskId.
|
|
31
|
+
6. ``browser.shutdown`` (sent via cleanupAll) terminates the process.
|
|
32
|
+
|
|
33
|
+
Named profiles are fully handled by the TypeScript side via
|
|
34
|
+
``core/shared/storage-state.ts`` (disk persistence). The Python
|
|
35
|
+
bridge receives ``storageState`` in the navigate request and applies
|
|
36
|
+
it when creating a new BrowserContext — it does NOT track shared
|
|
37
|
+
contexts across tasks.
|
|
15
38
|
"""
|
|
16
39
|
|
|
17
40
|
import base64
|
|
@@ -20,18 +43,32 @@ import os
|
|
|
20
43
|
import re
|
|
21
44
|
import sys
|
|
22
45
|
import time
|
|
46
|
+
import traceback
|
|
23
47
|
from typing import Any, Optional
|
|
24
48
|
from urllib.parse import unquote as _urlunquote
|
|
25
49
|
|
|
26
|
-
from .bridge import BrowserBridge, SessionNotFoundError
|
|
27
50
|
from .bot_detection import check_bot_detection
|
|
28
|
-
from .accessibility import parse_snapshot, build_locator_args
|
|
51
|
+
from .accessibility import parse_snapshot, build_locator_args, AriaParseResult
|
|
29
52
|
from .browser_data import NAV_SETTLE
|
|
53
|
+
from .transport import (
|
|
54
|
+
read_request,
|
|
55
|
+
write_response,
|
|
56
|
+
make_success_response,
|
|
57
|
+
make_error_response,
|
|
58
|
+
make_parse_error,
|
|
59
|
+
make_invalid_request,
|
|
60
|
+
make_application_error,
|
|
61
|
+
InvalidRequestError,
|
|
62
|
+
METHOD_NOT_FOUND,
|
|
63
|
+
INVALID_PARAMS,
|
|
64
|
+
SESSION_ERROR,
|
|
65
|
+
)
|
|
30
66
|
|
|
31
67
|
# ─── Playwright import (lazy, for better error messages) ──────────────
|
|
32
68
|
|
|
33
69
|
try:
|
|
34
70
|
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout # type: ignore[import-unresolved]
|
|
71
|
+
|
|
35
72
|
HAS_PLAYWRIGHT = True
|
|
36
73
|
except ImportError:
|
|
37
74
|
HAS_PLAYWRIGHT = False
|
|
@@ -39,11 +76,28 @@ except ImportError:
|
|
|
39
76
|
PlaywrightTimeout = TimeoutError # type: ignore[misc]
|
|
40
77
|
|
|
41
78
|
|
|
79
|
+
# ─── Default timeout ──────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
DEFAULT_NAVIGATION_TIMEOUT_MS: int = 30_000
|
|
82
|
+
DEFAULT_INTERACTION_TIMEOUT_MS: int = 10_000
|
|
83
|
+
|
|
84
|
+
|
|
42
85
|
# ─── Nav-settle constants (loaded from shared browser-data.json) ──────
|
|
43
86
|
|
|
44
87
|
_DOM_STABILIZE_JS: str = NAV_SETTLE["domStabilizationJs"]
|
|
45
88
|
|
|
46
89
|
|
|
90
|
+
# ─── Custom exceptions ────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SessionNotFoundError(Exception):
|
|
94
|
+
"""Raised when an operation requires a session but none exists."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class InvalidParamsError(Exception):
|
|
98
|
+
"""Raised when a required parameter is missing or has the wrong type."""
|
|
99
|
+
|
|
100
|
+
|
|
47
101
|
# ─── Shared helper for bridge entry points ───────────────────────────
|
|
48
102
|
|
|
49
103
|
|
|
@@ -64,16 +118,20 @@ def check_playwright_or_exit(browser: str) -> None:
|
|
|
64
118
|
" pip install playwright\n"
|
|
65
119
|
f" playwright install {browser}\n"
|
|
66
120
|
)
|
|
67
|
-
print(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
121
|
+
print(
|
|
122
|
+
json.dumps(
|
|
123
|
+
{
|
|
124
|
+
"jsonrpc": "2.0",
|
|
125
|
+
"id": None,
|
|
126
|
+
"error": {"code": -32000, "message": msg.strip()},
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
)
|
|
72
130
|
sys.stdout.flush()
|
|
73
131
|
sys.exit(1)
|
|
74
132
|
|
|
75
133
|
|
|
76
|
-
class PlaywrightBridge
|
|
134
|
+
class PlaywrightBridge:
|
|
77
135
|
"""Base class for Playwright-based browser bridges.
|
|
78
136
|
|
|
79
137
|
Subclasses must set these class/instance attributes:
|
|
@@ -109,14 +167,14 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
109
167
|
#: Engine-specific install hint (shown when browser executable missing).
|
|
110
168
|
_install_hint: str = ""
|
|
111
169
|
|
|
112
|
-
# ── Stealth quirks
|
|
170
|
+
# ── Stealth quirks ───────────────────────────────────────────────
|
|
113
171
|
#
|
|
114
172
|
# These opt-in flags let stealth subclasses (Camoufox and other stealth engines)
|
|
115
173
|
# disable the base's hard-coded Playwright defaults that would otherwise
|
|
116
174
|
# clobber a fingerprint-managed browser context. All default to ``off``
|
|
117
175
|
# so the shipped ``chromium-py`` / ``firefox-py`` bridges are bit-identical.
|
|
118
176
|
#
|
|
119
|
-
# See
|
|
177
|
+
# See the portal AGENTS.md quirks table for the concrete
|
|
120
178
|
# correctness problem each flag fixes.
|
|
121
179
|
|
|
122
180
|
#: When True, ``create_browser_context()`` does NOT pass ``viewport`` or
|
|
@@ -134,9 +192,14 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
134
192
|
_eval_prefix: str = ""
|
|
135
193
|
|
|
136
194
|
#: When True, ``do_scroll`` uses ``page.mouse.wheel`` instead of
|
|
137
|
-
#: ``page.evaluate("window.scrollBy")``.
|
|
138
|
-
#:
|
|
139
|
-
#:
|
|
195
|
+
#: ``page.evaluate("window.scrollBy")``. On older Camoufox binaries
|
|
196
|
+
#: (``135.0.1-beta.24`` and earlier) the eval-based scroll was a write
|
|
197
|
+
#: that silently no-op'd under the isolated world, so the wheel event
|
|
198
|
+
#: was the only path that moved the page. On ``152.0.4-beta.27+`` the
|
|
199
|
+
#: wheel event itself no-ops (the ``wheel`` listener never fires) while
|
|
200
|
+
#: the eval-based ``window.scrollBy`` now works — so the flag must be
|
|
201
|
+
#: ``False`` on the current binary. Default ``False`` (eval scroll);
|
|
202
|
+
#: only set ``True`` for the legacy Camoufox binary.
|
|
140
203
|
_scroll_via_wheel: bool = False
|
|
141
204
|
|
|
142
205
|
#: When True, ``create_browser_context`` passes ``no_viewport=True`` to
|
|
@@ -235,6 +298,43 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
235
298
|
#: Default ``False`` so shipped bridges stay bit-identical.
|
|
236
299
|
_csp_safe_readonly_via_init_script: bool = False
|
|
237
300
|
|
|
301
|
+
#: When non-zero, ``_wait_for_navigation_settle`` uses this as the
|
|
302
|
+
#: settle poll budget (ms) instead of the default 400. Stealth backends
|
|
303
|
+
#: whose patched browser fires navigation events with higher latency
|
|
304
|
+
#: (e.g. Camoufox's Juggler) set a larger value to avoid settling before
|
|
305
|
+
#: the navigation commit is observable. Default 400 matches the
|
|
306
|
+
#: historical hard-coded value so shipped chromium-py / firefox-py are
|
|
307
|
+
#: unchanged.
|
|
308
|
+
_settle_budget_ms: int = 400
|
|
309
|
+
|
|
310
|
+
#: When True, ``_wait_for_navigation_settle`` switches the no-nav
|
|
311
|
+
#: polling branch from a fixed budget to "URL stable for 150 ms **or**
|
|
312
|
+
#: budget exhausted". Stealth backends whose patched Juggler fires
|
|
313
|
+
#: ``page.url`` updates with higher latency set this along with a wider
|
|
314
|
+
#: :attr:`_settle_budget_ms` so the settle waits for the URL to
|
|
315
|
+
#: stabilize at the target value before declaring no navigation
|
|
316
|
+
#: occurred. Default ``False`` so shipped chromium-py / firefox-py keep
|
|
317
|
+
#: the current tight settle behaviour.
|
|
318
|
+
_url_stability_settle: bool = False
|
|
319
|
+
|
|
320
|
+
# ── Session storage (persists across RPC calls) ─────────────
|
|
321
|
+
|
|
322
|
+
#: Per-taskId session data: {task_id: {...}}.
|
|
323
|
+
#: The dict contents are backend-specific (page, context, etc.).
|
|
324
|
+
sessions: dict[str, dict[str, Any]]
|
|
325
|
+
|
|
326
|
+
#: Per-taskId element cache: {task_id: AriaParseResult}.
|
|
327
|
+
element_caches: dict[str, AriaParseResult]
|
|
328
|
+
|
|
329
|
+
#: Whether the bridge is still running.
|
|
330
|
+
_running: bool
|
|
331
|
+
|
|
332
|
+
#: Plugin configuration dict forwarded from the TypeScript adapter via
|
|
333
|
+
#: the ``browser.init`` RPC. Defaults to ``{}`` for bridges that never
|
|
334
|
+
#: receive an init call (e.g. older adapters, or the shipped
|
|
335
|
+
#: ``chromium-py``/``firefox-py`` when run standalone). Subclasses read
|
|
336
|
+
#: engine-specific options from ``self.plugin_config.get("launch", {})``.
|
|
337
|
+
_plugin_config: dict[str, Any]
|
|
238
338
|
|
|
239
339
|
# ── Shared Playwright state ─────────────────────────────────
|
|
240
340
|
|
|
@@ -242,7 +342,10 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
242
342
|
_browser: Any # Browser instance (lazy, shared)
|
|
243
343
|
|
|
244
344
|
def __init__(self) -> None:
|
|
245
|
-
|
|
345
|
+
self.sessions = {}
|
|
346
|
+
self.element_caches = {}
|
|
347
|
+
self._running = False
|
|
348
|
+
self._plugin_config = {}
|
|
246
349
|
self._pw = None
|
|
247
350
|
self._browser = None
|
|
248
351
|
self._cached_ua: str = ""
|
|
@@ -254,6 +357,52 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
254
357
|
# the extractor's stale result.
|
|
255
358
|
self._readonly_extractor_script: str = ""
|
|
256
359
|
|
|
360
|
+
# ── Plugin config (forwarded via browser.init) ───────────────
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def plugin_config(self) -> dict[str, Any]:
|
|
364
|
+
"""Return the plugin config dict forwarded from the TypeScript adapter.
|
|
365
|
+
|
|
366
|
+
Populated by the ``browser.init`` RPC handler. Always returns a
|
|
367
|
+
dict (empty when no init was received) so subclasses can safely
|
|
368
|
+
call ``self.plugin_config.get("launch", {})``.
|
|
369
|
+
"""
|
|
370
|
+
return self._plugin_config
|
|
371
|
+
|
|
372
|
+
# ── Session helpers ─────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
def get_session(self, task_id: str) -> Optional[dict[str, Any]]:
|
|
375
|
+
"""Get the session data for a task, or None."""
|
|
376
|
+
return self.sessions.get(task_id)
|
|
377
|
+
|
|
378
|
+
def require_session(self, task_id: str) -> dict[str, Any]:
|
|
379
|
+
"""Get the session for a task, raising SESSION_ERROR if absent."""
|
|
380
|
+
session = self.get_session(task_id)
|
|
381
|
+
if session is None:
|
|
382
|
+
raise SessionNotFoundError(
|
|
383
|
+
f"No active session for task '{task_id}'. Call browser.navigate first."
|
|
384
|
+
)
|
|
385
|
+
return session
|
|
386
|
+
|
|
387
|
+
def ensure_session(
|
|
388
|
+
self, task_id: str, config: Optional[dict[str, Any]] = None
|
|
389
|
+
) -> dict[str, Any]:
|
|
390
|
+
"""Get or create a session for the given task."""
|
|
391
|
+
session = self.get_session(task_id)
|
|
392
|
+
if session is not None:
|
|
393
|
+
return session
|
|
394
|
+
new_session = self.create_browser_session(task_id, config or {})
|
|
395
|
+
self.sessions[task_id] = new_session
|
|
396
|
+
return new_session
|
|
397
|
+
|
|
398
|
+
def get_element_cache(self, task_id: str) -> Optional[AriaParseResult]:
|
|
399
|
+
"""Get the cached element parse result for a task, or None."""
|
|
400
|
+
return self.element_caches.get(task_id)
|
|
401
|
+
|
|
402
|
+
def set_element_cache(self, task_id: str, result: AriaParseResult) -> None:
|
|
403
|
+
"""Store a parsed element cache for a task."""
|
|
404
|
+
self.element_caches[task_id] = result
|
|
405
|
+
|
|
257
406
|
# ── Subclass extension point ───────────────────────────────
|
|
258
407
|
|
|
259
408
|
def _launch_browser(self) -> Any:
|
|
@@ -304,7 +453,6 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
304
453
|
except Exception:
|
|
305
454
|
pass
|
|
306
455
|
|
|
307
|
-
|
|
308
456
|
# ── Debug logging ───────────────────────────────────────────
|
|
309
457
|
|
|
310
458
|
@property
|
|
@@ -345,7 +493,11 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
345
493
|
raise RuntimeError(
|
|
346
494
|
"Playwright is not installed. "
|
|
347
495
|
"Run: pip install playwright && "
|
|
348
|
-
+ (
|
|
496
|
+
+ (
|
|
497
|
+
self._install_hint.lower()
|
|
498
|
+
if self._install_hint
|
|
499
|
+
else "playwright install <browser>"
|
|
500
|
+
)
|
|
349
501
|
)
|
|
350
502
|
self._pw = sync_playwright().start() # type: ignore[union-attr]
|
|
351
503
|
try:
|
|
@@ -431,8 +583,12 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
431
583
|
snapshots=True,
|
|
432
584
|
sources=True,
|
|
433
585
|
)
|
|
434
|
-
self._log(
|
|
435
|
-
|
|
586
|
+
self._log(
|
|
587
|
+
"tracing",
|
|
588
|
+
taskId=config.get("_task_id", "shared"),
|
|
589
|
+
action="start",
|
|
590
|
+
dir=_trace_dir,
|
|
591
|
+
)
|
|
436
592
|
except Exception:
|
|
437
593
|
pass # Best-effort
|
|
438
594
|
|
|
@@ -542,14 +698,19 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
542
698
|
|
|
543
699
|
# ── Dialog auto-dismissal ───────────────────────────────
|
|
544
700
|
dialog_log: list[dict[str, str]] = []
|
|
545
|
-
page.on(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
701
|
+
page.on(
|
|
702
|
+
"dialog",
|
|
703
|
+
lambda dialog: (
|
|
704
|
+
dialog_log.append(
|
|
705
|
+
{
|
|
706
|
+
"type": dialog.type,
|
|
707
|
+
"message": dialog.message[:200],
|
|
708
|
+
"handledAs": "accepted",
|
|
709
|
+
}
|
|
710
|
+
),
|
|
711
|
+
dialog.accept(),
|
|
712
|
+
),
|
|
713
|
+
)
|
|
553
714
|
|
|
554
715
|
return {
|
|
555
716
|
"page": page,
|
|
@@ -605,8 +766,7 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
605
766
|
f"trace-{task_id}-{int(time.time() * 1000)}.zip",
|
|
606
767
|
)
|
|
607
768
|
context.tracing.stop(path=_trace_path)
|
|
608
|
-
self._log("tracing", taskId=task_id, action="stop",
|
|
609
|
-
dir=_trace_dir)
|
|
769
|
+
self._log("tracing", taskId=task_id, action="stop", dir=_trace_dir)
|
|
610
770
|
except Exception:
|
|
611
771
|
pass
|
|
612
772
|
|
|
@@ -667,18 +827,22 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
667
827
|
parsed = parse_snapshot(snap_text)
|
|
668
828
|
self.set_element_cache(task_id, parsed)
|
|
669
829
|
|
|
670
|
-
return
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
830
|
+
return (
|
|
831
|
+
parsed.text,
|
|
832
|
+
parsed.count,
|
|
833
|
+
{
|
|
834
|
+
ref: {
|
|
835
|
+
"role": node.role,
|
|
836
|
+
"name": node.name,
|
|
837
|
+
"props": list(node.props),
|
|
838
|
+
"depth": node.depth,
|
|
839
|
+
"raw": node.raw,
|
|
840
|
+
"occurrenceIndex": node.occurrence_index,
|
|
841
|
+
"parentRef": node.parent_ref,
|
|
842
|
+
}
|
|
843
|
+
for ref, node in parsed.elements.items()
|
|
844
|
+
},
|
|
845
|
+
)
|
|
682
846
|
|
|
683
847
|
def _build_interaction_result(
|
|
684
848
|
self, task_id: str, page: Any, **extra: Any
|
|
@@ -705,9 +869,7 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
705
869
|
getattr(node, "name", "unknown") if node else "unknown",
|
|
706
870
|
)
|
|
707
871
|
|
|
708
|
-
def _locate_element(
|
|
709
|
-
self, page: Any, task_id: str, ref: str
|
|
710
|
-
) -> Any:
|
|
872
|
+
def _locate_element(self, page: Any, task_id: str, ref: str) -> Any:
|
|
711
873
|
"""Resolve an @e ref to a Playwright locator.
|
|
712
874
|
|
|
713
875
|
Args:
|
|
@@ -746,7 +908,9 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
746
908
|
# ── Navigation settle helpers ───────────────────────────────
|
|
747
909
|
|
|
748
910
|
@staticmethod
|
|
749
|
-
def _wait_for_page_ready(
|
|
911
|
+
def _wait_for_page_ready(
|
|
912
|
+
page: Any, timeout_ms: int, skip_networkidle: bool = False
|
|
913
|
+
) -> None:
|
|
750
914
|
"""Wait for page readiness after a navigation.
|
|
751
915
|
|
|
752
916
|
Mirrors the TypeScript ``waitForPageReady`` helper — each load
|
|
@@ -772,19 +936,21 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
772
936
|
except Exception:
|
|
773
937
|
pass
|
|
774
938
|
|
|
775
|
-
@staticmethod
|
|
776
939
|
def _wait_for_navigation_settle(
|
|
940
|
+
self,
|
|
777
941
|
page: Any,
|
|
778
942
|
url_before: str,
|
|
779
943
|
nav_timeout_ms: int = 5000,
|
|
780
|
-
settle_timeout_ms: int = 400,
|
|
781
944
|
skip_networkidle: bool = False,
|
|
782
945
|
) -> tuple[bool, str]:
|
|
783
946
|
"""Wait for navigation to settle after a user interaction.
|
|
784
947
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
948
|
+
Listens for the ``framenavigated`` event and waits for page
|
|
949
|
+
readiness only when a navigation has actually started.
|
|
950
|
+
|
|
951
|
+
Uses :attr:`_settle_budget_ms` and :attr:`_url_stability_settle`
|
|
952
|
+
to allow stealth backends with higher event latency (e.g.
|
|
953
|
+
Camoufox's Juggler) to widen the settle window.
|
|
788
954
|
|
|
789
955
|
Args:
|
|
790
956
|
page: Playwright Page.
|
|
@@ -792,8 +958,7 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
792
958
|
nav_timeout_ms: Max time (ms) to wait for each page readiness
|
|
793
959
|
check (load and networkidle each get the full
|
|
794
960
|
budget). Default: 5000.
|
|
795
|
-
|
|
796
|
-
occurs (default: 400).
|
|
961
|
+
skip_networkidle: When True, skip the ``networkidle`` wait.
|
|
797
962
|
|
|
798
963
|
Returns:
|
|
799
964
|
``(navigated, url)`` — whether a main-frame navigation was
|
|
@@ -814,19 +979,53 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
814
979
|
|
|
815
980
|
waited_for_load = False
|
|
816
981
|
if navigated:
|
|
817
|
-
|
|
982
|
+
self._wait_for_page_ready(page, nav_timeout_ms, skip_networkidle)
|
|
818
983
|
waited_for_load = True
|
|
819
984
|
elif page.url != url_before:
|
|
820
985
|
# URL changed without framenavigated event
|
|
821
|
-
|
|
986
|
+
self._wait_for_page_ready(page, nav_timeout_ms, skip_networkidle)
|
|
822
987
|
waited_for_load = True
|
|
823
988
|
else:
|
|
824
|
-
# No navigation —
|
|
825
|
-
|
|
989
|
+
# No navigation yet — poll for a late-starting navigation
|
|
990
|
+
# (e.g. ``setTimeout``-delayed redirects). A fixed blind
|
|
991
|
+
# ``wait_for_timeout`` races the ``framenavigated`` event under
|
|
992
|
+
# load; polling at 50 ms granularity catches the nav as soon
|
|
993
|
+
# as it fires within the settle budget.
|
|
994
|
+
# ponytail: 50 ms poll, budget = self._settle_budget_ms
|
|
995
|
+
# (default 400, Camoufox 2000). Switch to
|
|
996
|
+
# ``page.wait_for_function`` if sub-50 ms latency matters.
|
|
997
|
+
settle_budget = self._settle_budget_ms
|
|
998
|
+
remaining = settle_budget
|
|
999
|
+
|
|
1000
|
+
if self._url_stability_settle:
|
|
1001
|
+
# URL-stability mode: wait for the URL to stabilize at a
|
|
1002
|
+
# new value for 150 ms, or until the budget expires.
|
|
1003
|
+
# Stealth backends with high Juggler event latency (e.g.
|
|
1004
|
+
# Camoufox) benefit from confirming the URL has finished
|
|
1005
|
+
# changing before declaring no navigation occurred.
|
|
1006
|
+
url_stable_since: float | None = None
|
|
1007
|
+
while remaining > 0 and not navigated:
|
|
1008
|
+
page.wait_for_timeout(50)
|
|
1009
|
+
remaining -= 50
|
|
1010
|
+
if navigated:
|
|
1011
|
+
break
|
|
1012
|
+
if page.url != url_before:
|
|
1013
|
+
if url_stable_since is None:
|
|
1014
|
+
url_stable_since = time.monotonic()
|
|
1015
|
+
elif (time.monotonic() - url_stable_since) >= 0.150:
|
|
1016
|
+
break # URL stable for 150 ms
|
|
1017
|
+
else:
|
|
1018
|
+
url_stable_since = None
|
|
1019
|
+
else:
|
|
1020
|
+
# Original tight-poll mode: exit as soon as the URL
|
|
1021
|
+
# changes or the event fires.
|
|
1022
|
+
while remaining > 0 and not navigated and page.url == url_before:
|
|
1023
|
+
page.wait_for_timeout(50)
|
|
1024
|
+
remaining -= 50
|
|
826
1025
|
|
|
827
1026
|
# Late-arrival gate: catch navigations that started during settle
|
|
828
1027
|
if not waited_for_load and (navigated or page.url != url_before):
|
|
829
|
-
|
|
1028
|
+
self._wait_for_page_ready(page, nav_timeout_ms, skip_networkidle)
|
|
830
1029
|
|
|
831
1030
|
finally:
|
|
832
1031
|
page.remove_listener("framenavigated", _on_nav)
|
|
@@ -940,10 +1139,15 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
940
1139
|
title = ""
|
|
941
1140
|
|
|
942
1141
|
if last_error is None:
|
|
943
|
-
self._log(
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1142
|
+
self._log(
|
|
1143
|
+
"navigate",
|
|
1144
|
+
url=url,
|
|
1145
|
+
plugin=self._plugin_name,
|
|
1146
|
+
success=True,
|
|
1147
|
+
botDetected=bot_detected,
|
|
1148
|
+
elementCount=element_count,
|
|
1149
|
+
time=round((time.time() - _t_start) * 1000),
|
|
1150
|
+
)
|
|
947
1151
|
return {
|
|
948
1152
|
"success": True,
|
|
949
1153
|
"url": page.url,
|
|
@@ -955,10 +1159,16 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
955
1159
|
"dialogEvents": self._get_dialog_events(task_id),
|
|
956
1160
|
}
|
|
957
1161
|
else:
|
|
958
|
-
self._log(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1162
|
+
self._log(
|
|
1163
|
+
"navigate",
|
|
1164
|
+
url=url,
|
|
1165
|
+
plugin=self._plugin_name,
|
|
1166
|
+
success=False,
|
|
1167
|
+
botDetected=bot_detected,
|
|
1168
|
+
elementCount=element_count,
|
|
1169
|
+
error=last_error,
|
|
1170
|
+
time=round((time.time() - _t_start) * 1000),
|
|
1171
|
+
)
|
|
962
1172
|
return {
|
|
963
1173
|
"success": False,
|
|
964
1174
|
"url": url,
|
|
@@ -1011,8 +1221,9 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1011
1221
|
try:
|
|
1012
1222
|
locator = self._locate_element(page, task_id, ref)
|
|
1013
1223
|
except RuntimeError as exc:
|
|
1014
|
-
self._log(
|
|
1015
|
-
|
|
1224
|
+
self._log(
|
|
1225
|
+
"click", taskId=task_id, ref=ref, role=_role, name=_name, result="fail"
|
|
1226
|
+
)
|
|
1016
1227
|
return {"success": False, "error": str(exc)}
|
|
1017
1228
|
|
|
1018
1229
|
try:
|
|
@@ -1020,21 +1231,30 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1020
1231
|
locator.click(timeout=5_000)
|
|
1021
1232
|
|
|
1022
1233
|
navigated, _ = self._wait_for_navigation_settle(
|
|
1023
|
-
page,
|
|
1234
|
+
page,
|
|
1235
|
+
url_before,
|
|
1236
|
+
skip_networkidle=self._skip_networkidle,
|
|
1024
1237
|
)
|
|
1025
1238
|
|
|
1026
1239
|
new_url = page.url
|
|
1027
1240
|
new_title = page.title()
|
|
1028
1241
|
|
|
1029
1242
|
result = self._build_interaction_result(
|
|
1030
|
-
task_id,
|
|
1243
|
+
task_id,
|
|
1244
|
+
page,
|
|
1245
|
+
newUrl=new_url,
|
|
1246
|
+
newTitle=new_title,
|
|
1031
1247
|
)
|
|
1032
1248
|
except Exception as exc:
|
|
1033
1249
|
result = {
|
|
1034
1250
|
"success": False,
|
|
1035
1251
|
"error": f"Click failed: {exc}",
|
|
1036
1252
|
}
|
|
1037
|
-
return self._log_op(
|
|
1253
|
+
return self._log_op(
|
|
1254
|
+
"click",
|
|
1255
|
+
{"taskId": task_id, "ref": ref, "role": _role, "name": _name},
|
|
1256
|
+
result,
|
|
1257
|
+
)
|
|
1038
1258
|
|
|
1039
1259
|
def do_type(self, task_id: str, ref: str, text: str) -> dict[str, Any]:
|
|
1040
1260
|
_role, _name = self._ref_debug_info(task_id, ref)
|
|
@@ -1046,8 +1266,9 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1046
1266
|
try:
|
|
1047
1267
|
locator = self._locate_element(page, task_id, ref)
|
|
1048
1268
|
except RuntimeError as exc:
|
|
1049
|
-
self._log(
|
|
1050
|
-
|
|
1269
|
+
self._log(
|
|
1270
|
+
"type", taskId=task_id, ref=ref, role=_role, name=_name, result="fail"
|
|
1271
|
+
)
|
|
1051
1272
|
return {"success": False, "error": str(exc)}
|
|
1052
1273
|
|
|
1053
1274
|
try:
|
|
@@ -1060,7 +1281,11 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1060
1281
|
"success": False,
|
|
1061
1282
|
"error": f"Type failed: {exc}",
|
|
1062
1283
|
}
|
|
1063
|
-
return self._log_op(
|
|
1284
|
+
return self._log_op(
|
|
1285
|
+
"type",
|
|
1286
|
+
{"taskId": task_id, "ref": ref, "role": _role, "name": _name},
|
|
1287
|
+
result,
|
|
1288
|
+
)
|
|
1064
1289
|
|
|
1065
1290
|
def do_scroll(self, task_id: str, direction: str) -> dict[str, Any]:
|
|
1066
1291
|
page, err = self._get_page_or_error(task_id, "scroll")
|
|
@@ -1087,7 +1312,9 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1087
1312
|
"success": False,
|
|
1088
1313
|
"error": f"Scroll failed: {exc}",
|
|
1089
1314
|
}
|
|
1090
|
-
return self._log_op(
|
|
1315
|
+
return self._log_op(
|
|
1316
|
+
"scroll", {"taskId": task_id, "direction": direction}, result
|
|
1317
|
+
)
|
|
1091
1318
|
|
|
1092
1319
|
def do_go_back(self, task_id: str) -> dict[str, Any]:
|
|
1093
1320
|
page, err = self._get_page_or_error(task_id, "goBack")
|
|
@@ -1136,7 +1363,9 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1136
1363
|
page.keyboard.press(key)
|
|
1137
1364
|
|
|
1138
1365
|
navigated, _ = self._wait_for_navigation_settle(
|
|
1139
|
-
page,
|
|
1366
|
+
page,
|
|
1367
|
+
url_before,
|
|
1368
|
+
nav_timeout_ms=3000,
|
|
1140
1369
|
skip_networkidle=self._skip_networkidle,
|
|
1141
1370
|
)
|
|
1142
1371
|
|
|
@@ -1144,7 +1373,10 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1144
1373
|
new_title = page.title()
|
|
1145
1374
|
|
|
1146
1375
|
result = self._build_interaction_result(
|
|
1147
|
-
task_id,
|
|
1376
|
+
task_id,
|
|
1377
|
+
page,
|
|
1378
|
+
newUrl=new_url,
|
|
1379
|
+
newTitle=new_title,
|
|
1148
1380
|
)
|
|
1149
1381
|
except Exception as exc:
|
|
1150
1382
|
result = {
|
|
@@ -1195,9 +1427,7 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1195
1427
|
"success": True,
|
|
1196
1428
|
"messages": [],
|
|
1197
1429
|
}
|
|
1198
|
-
messages: list[dict[str, str]] = session.get(
|
|
1199
|
-
"console_messages", []
|
|
1200
|
-
)
|
|
1430
|
+
messages: list[dict[str, str]] = session.get("console_messages", [])
|
|
1201
1431
|
return {
|
|
1202
1432
|
"success": True,
|
|
1203
1433
|
"messages": messages,
|
|
@@ -1432,3 +1662,264 @@ class PlaywrightBridge(BrowserBridge):
|
|
|
1432
1662
|
"origins": [],
|
|
1433
1663
|
"error": str(exc),
|
|
1434
1664
|
}
|
|
1665
|
+
|
|
1666
|
+
# ── Command handlers ────────────────────────────────────────
|
|
1667
|
+
#
|
|
1668
|
+
# One ``_h_*`` method per JSON-RPC method. Each extracts params and
|
|
1669
|
+
# calls the matching ``do_*`` method. ``_DISPATCH`` (class attribute,
|
|
1670
|
+
# built at the bottom of the class body) maps method name → handler.
|
|
1671
|
+
|
|
1672
|
+
def _h_ping(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1673
|
+
return make_success_response(cmd_id, "pong")
|
|
1674
|
+
|
|
1675
|
+
def _h_init(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1676
|
+
# Forward plugin config from the TypeScript adapter. Sent exactly
|
|
1677
|
+
# once after the ping handshake, before any other RPC.
|
|
1678
|
+
self._plugin_config = params.get("config") or {}
|
|
1679
|
+
return make_success_response(cmd_id, {"ok": True})
|
|
1680
|
+
|
|
1681
|
+
def _h_shutdown(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1682
|
+
self._running = False
|
|
1683
|
+
return make_success_response(cmd_id, "shutting_down")
|
|
1684
|
+
|
|
1685
|
+
def _h_navigate(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1686
|
+
url = self._require_param(params, "url", str)
|
|
1687
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1688
|
+
timeout_ms = params.get("timeoutMs", DEFAULT_NAVIGATION_TIMEOUT_MS)
|
|
1689
|
+
result = self.do_navigate(
|
|
1690
|
+
task_id,
|
|
1691
|
+
url,
|
|
1692
|
+
timeout_ms,
|
|
1693
|
+
storageState=params.get("storageState"),
|
|
1694
|
+
profileName=params.get("profileName"),
|
|
1695
|
+
profileMode=params.get("profileMode"),
|
|
1696
|
+
)
|
|
1697
|
+
return make_success_response(cmd_id, result)
|
|
1698
|
+
|
|
1699
|
+
def _h_snapshot(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1700
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1701
|
+
return make_success_response(cmd_id, self.do_snapshot(task_id))
|
|
1702
|
+
|
|
1703
|
+
def _h_click(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1704
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1705
|
+
ref = self._require_param(params, "ref", str)
|
|
1706
|
+
return make_success_response(cmd_id, self.do_click(task_id, ref))
|
|
1707
|
+
|
|
1708
|
+
def _h_type(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1709
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1710
|
+
ref = self._require_param(params, "ref", str)
|
|
1711
|
+
text = self._require_param(params, "text", str)
|
|
1712
|
+
return make_success_response(cmd_id, self.do_type(task_id, ref, text))
|
|
1713
|
+
|
|
1714
|
+
def _h_scroll(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1715
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1716
|
+
direction = self._require_param(params, "direction", str)
|
|
1717
|
+
if direction not in ("up", "down"):
|
|
1718
|
+
return make_error_response(
|
|
1719
|
+
cmd_id,
|
|
1720
|
+
INVALID_PARAMS,
|
|
1721
|
+
'direction must be "up" or "down"',
|
|
1722
|
+
)
|
|
1723
|
+
return make_success_response(cmd_id, self.do_scroll(task_id, direction))
|
|
1724
|
+
|
|
1725
|
+
def _h_go_back(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1726
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1727
|
+
return make_success_response(cmd_id, self.do_go_back(task_id))
|
|
1728
|
+
|
|
1729
|
+
def _h_press(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1730
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1731
|
+
key = self._require_param(params, "key", str)
|
|
1732
|
+
return make_success_response(cmd_id, self.do_press(task_id, key))
|
|
1733
|
+
|
|
1734
|
+
def _h_screenshot(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1735
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1736
|
+
full_page = params.get("fullPage", False)
|
|
1737
|
+
return make_success_response(cmd_id, self.do_screenshot(task_id, full_page))
|
|
1738
|
+
|
|
1739
|
+
def _h_get_console_messages(
|
|
1740
|
+
self, params: dict[str, Any], cmd_id: Any
|
|
1741
|
+
) -> dict[str, Any]:
|
|
1742
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1743
|
+
return make_success_response(cmd_id, self.do_get_console_messages(task_id))
|
|
1744
|
+
|
|
1745
|
+
def _h_clear_console(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1746
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1747
|
+
return make_success_response(cmd_id, self.do_clear_console(task_id))
|
|
1748
|
+
|
|
1749
|
+
def _h_evaluate(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1750
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1751
|
+
expression = self._require_param(params, "expression", str)
|
|
1752
|
+
read_only = bool(params.get("readOnly", False))
|
|
1753
|
+
result = self.do_evaluate(task_id, expression, read_only=read_only)
|
|
1754
|
+
return make_success_response(cmd_id, result)
|
|
1755
|
+
|
|
1756
|
+
def _h_get_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1757
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1758
|
+
return make_success_response(
|
|
1759
|
+
cmd_id, self.do_get_cookies(task_id, params.get("urls"))
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
def _h_add_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1763
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1764
|
+
cookies = self._require_param(params, "cookies", list)
|
|
1765
|
+
return make_success_response(cmd_id, self.do_add_cookies(task_id, cookies))
|
|
1766
|
+
|
|
1767
|
+
def _h_clear_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1768
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1769
|
+
# No required params beyond taskId — empty call clears ALL cookies
|
|
1770
|
+
result = self.do_clear_cookies(
|
|
1771
|
+
task_id, params.get("name"), params.get("domain"), params.get("path")
|
|
1772
|
+
)
|
|
1773
|
+
return make_success_response(cmd_id, result)
|
|
1774
|
+
|
|
1775
|
+
def _h_get_storage_state(
|
|
1776
|
+
self, params: dict[str, Any], cmd_id: Any
|
|
1777
|
+
) -> dict[str, Any]:
|
|
1778
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1779
|
+
return make_success_response(cmd_id, self.do_get_storage_state(task_id))
|
|
1780
|
+
|
|
1781
|
+
def _h_cleanup(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1782
|
+
task_id = self._require_param(params, "taskId", str)
|
|
1783
|
+
return make_success_response(cmd_id, self.do_cleanup(task_id))
|
|
1784
|
+
|
|
1785
|
+
def _h_describe_quirks(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
|
|
1786
|
+
# Return the bridge's declared quirks flags.
|
|
1787
|
+
return make_success_response(
|
|
1788
|
+
cmd_id,
|
|
1789
|
+
{
|
|
1790
|
+
"fingerprint_managed_context": getattr(
|
|
1791
|
+
self, "_fingerprint_managed_context", False
|
|
1792
|
+
),
|
|
1793
|
+
"eval_prefix": getattr(self, "_eval_prefix", ""),
|
|
1794
|
+
"scroll_via_wheel": getattr(self, "_scroll_via_wheel", False),
|
|
1795
|
+
"skip_default_viewport": getattr(self, "_skip_default_viewport", False),
|
|
1796
|
+
"skip_networkidle": getattr(self, "_skip_networkidle", False),
|
|
1797
|
+
"wrap_mw_eval_in_eval": getattr(self, "_wrap_mw_eval_in_eval", False),
|
|
1798
|
+
"csp_safe_readonly_via_init_script": getattr(
|
|
1799
|
+
self, "_csp_safe_readonly_via_init_script", False
|
|
1800
|
+
),
|
|
1801
|
+
},
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
#: JSON-RPC method name → handler. Built after the handlers are
|
|
1805
|
+
#: defined so the names are in scope. ``handle_command`` looks up
|
|
1806
|
+
#: here and calls ``handler(self, params, cmd_id)``.
|
|
1807
|
+
_DISPATCH = {
|
|
1808
|
+
"ping": _h_ping,
|
|
1809
|
+
"browser.init": _h_init,
|
|
1810
|
+
"shutdown": _h_shutdown,
|
|
1811
|
+
"browser.navigate": _h_navigate,
|
|
1812
|
+
"browser.snapshot": _h_snapshot,
|
|
1813
|
+
"browser.click": _h_click,
|
|
1814
|
+
"browser.type": _h_type,
|
|
1815
|
+
"browser.scroll": _h_scroll,
|
|
1816
|
+
"browser.goBack": _h_go_back,
|
|
1817
|
+
"browser.press": _h_press,
|
|
1818
|
+
"browser.screenshot": _h_screenshot,
|
|
1819
|
+
"browser.getConsoleMessages": _h_get_console_messages,
|
|
1820
|
+
"browser.clearConsole": _h_clear_console,
|
|
1821
|
+
"browser.evaluate": _h_evaluate,
|
|
1822
|
+
"browser.getCookies": _h_get_cookies,
|
|
1823
|
+
"browser.addCookies": _h_add_cookies,
|
|
1824
|
+
"browser.clearCookies": _h_clear_cookies,
|
|
1825
|
+
"browser.getStorageState": _h_get_storage_state,
|
|
1826
|
+
"browser.cleanup": _h_cleanup,
|
|
1827
|
+
"browser.describeQuirks": _h_describe_quirks,
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
# ── Command routing ─────────────────────────────────────────
|
|
1831
|
+
|
|
1832
|
+
def handle_command(
|
|
1833
|
+
self, method: str, params: dict[str, Any], cmd_id: Any
|
|
1834
|
+
) -> dict[str, Any]:
|
|
1835
|
+
"""Route a JSON-RPC method to the appropriate operation handler.
|
|
1836
|
+
|
|
1837
|
+
Returns a JSON-RPC response dict (either result or error).
|
|
1838
|
+
"""
|
|
1839
|
+
try:
|
|
1840
|
+
handler = self._DISPATCH.get(method)
|
|
1841
|
+
if handler is None:
|
|
1842
|
+
return make_error_response(
|
|
1843
|
+
cmd_id, METHOD_NOT_FOUND, f"Method not found: {method}"
|
|
1844
|
+
)
|
|
1845
|
+
return handler(self, params, cmd_id)
|
|
1846
|
+
|
|
1847
|
+
except SessionNotFoundError as exc:
|
|
1848
|
+
return make_error_response(cmd_id, SESSION_ERROR, str(exc))
|
|
1849
|
+
except NotImplementedError as exc:
|
|
1850
|
+
return make_application_error(cmd_id, str(exc))
|
|
1851
|
+
except Exception as exc:
|
|
1852
|
+
tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
1853
|
+
return make_application_error(cmd_id, str(exc), traceback_str=tb)
|
|
1854
|
+
|
|
1855
|
+
# ── Main loop ─────────────────────────────────────────────
|
|
1856
|
+
|
|
1857
|
+
def run(self) -> None:
|
|
1858
|
+
"""Start the main JSON-RPC command loop.
|
|
1859
|
+
|
|
1860
|
+
Reads requests from stdin, dispatches them, and writes responses
|
|
1861
|
+
to stdout. Runs until EOF or a ``shutdown`` command.
|
|
1862
|
+
"""
|
|
1863
|
+
self._running = True
|
|
1864
|
+
while self._running:
|
|
1865
|
+
try:
|
|
1866
|
+
request = read_request()
|
|
1867
|
+
if request is None:
|
|
1868
|
+
break # EOF
|
|
1869
|
+
|
|
1870
|
+
cmd_id = request.get("id")
|
|
1871
|
+
method = request.get("method", "")
|
|
1872
|
+
params = request.get("params", {})
|
|
1873
|
+
|
|
1874
|
+
if not isinstance(params, dict):
|
|
1875
|
+
write_response(
|
|
1876
|
+
make_error_response(
|
|
1877
|
+
cmd_id,
|
|
1878
|
+
INVALID_PARAMS,
|
|
1879
|
+
'"params" must be a JSON object',
|
|
1880
|
+
)
|
|
1881
|
+
)
|
|
1882
|
+
continue
|
|
1883
|
+
|
|
1884
|
+
response = self.handle_command(method, params, cmd_id)
|
|
1885
|
+
write_response(response)
|
|
1886
|
+
|
|
1887
|
+
except InvalidRequestError:
|
|
1888
|
+
# Valid JSON but not a valid JSON-RPC Request object
|
|
1889
|
+
write_response(make_invalid_request(None))
|
|
1890
|
+
except ValueError:
|
|
1891
|
+
# JSON parse error
|
|
1892
|
+
write_response(make_parse_error(None))
|
|
1893
|
+
except EOFError:
|
|
1894
|
+
break
|
|
1895
|
+
except KeyboardInterrupt:
|
|
1896
|
+
break
|
|
1897
|
+
except Exception as exc:
|
|
1898
|
+
tb = "".join(
|
|
1899
|
+
traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
1900
|
+
)
|
|
1901
|
+
write_response(make_application_error(None, str(exc), traceback_str=tb))
|
|
1902
|
+
|
|
1903
|
+
# ── Internal helpers ──────────────────────────────────────
|
|
1904
|
+
|
|
1905
|
+
@staticmethod
|
|
1906
|
+
def _require_param(
|
|
1907
|
+
params: dict[str, Any],
|
|
1908
|
+
key: str,
|
|
1909
|
+
expected_type: type,
|
|
1910
|
+
) -> Any:
|
|
1911
|
+
"""Require a param to exist and be of the expected type.
|
|
1912
|
+
|
|
1913
|
+
Raises ``InvalidParamsError`` if the param is missing or has the
|
|
1914
|
+
wrong type. The caller's ``except Exception`` clause turns this
|
|
1915
|
+
into a JSON-RPC application-error response.
|
|
1916
|
+
"""
|
|
1917
|
+
if key not in params:
|
|
1918
|
+
raise InvalidParamsError(f'Missing required parameter: "{key}"')
|
|
1919
|
+
value = params[key]
|
|
1920
|
+
if not isinstance(value, expected_type):
|
|
1921
|
+
raise InvalidParamsError(
|
|
1922
|
+
f'Parameter "{key}" must be of type {expected_type.__name__}, '
|
|
1923
|
+
f"got {type(value).__name__}"
|
|
1924
|
+
)
|
|
1925
|
+
return value
|