pi-lean-dimension 0.1.0 → 0.2.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 +96 -36
- package/node_modules/pi-lean-portal/AGENTS.md +146 -4
- package/node_modules/pi-lean-portal/README.md +112 -50
- package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
- package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
- package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
- package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
- package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
- package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
- package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
- package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
- package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
- package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
- 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__/accessibility.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.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/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- 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/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
- package/node_modules/pi-lean-portal/contributed/README.md +304 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
- package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
- package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
- package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
- package/node_modules/pi-lean-portal/core/router.ts +59 -98
- package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
- package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
- package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
- package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
- package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
- package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
- package/node_modules/pi-lean-portal/index.ts +42 -7
- package/node_modules/pi-lean-portal/package.json +8 -3
- package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
- package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
- package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
- package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
- package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
- package/node_modules/pi-lean-search/README.md +6 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
- package/node_modules/pi-lean-search/index.ts +4 -21
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
- package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
- package/package.json +4 -4
- package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
- package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
- package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for ``backends/chromium-py/bridge.py``.
|
|
3
|
-
|
|
4
|
-
Pure-logic unit tests that do NOT require a Playwright browser to be
|
|
5
|
-
installed. The bridge module is loaded via ``importlib`` because the
|
|
6
|
-
directory name contains a hyphen.
|
|
7
|
-
|
|
8
|
-
Integration tests requiring an actual Chromium browser live in
|
|
9
|
-
``__tests__/chromium-py.test.ts`` (plugin contract harness).
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import importlib.util
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
# ── Load the bridge module via file path ─────────────────────────────
|
|
19
|
-
|
|
20
|
-
_bridge_path = (
|
|
21
|
-
Path(__file__).resolve().parents[3]
|
|
22
|
-
/ "backends"
|
|
23
|
-
/ "chromium-py"
|
|
24
|
-
/ "bridge.py"
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
assert _bridge_path.exists(), f"bridge.py not found at {_bridge_path}"
|
|
28
|
-
|
|
29
|
-
_spec = importlib.util.spec_from_file_location("chromium_py_bridge", _bridge_path)
|
|
30
|
-
assert _spec is not None, f"Could not create spec for {_bridge_path}"
|
|
31
|
-
_chromium_py_bridge = importlib.util.module_from_spec(_spec)
|
|
32
|
-
_spec.loader.exec_module(_chromium_py_bridge)
|
|
33
|
-
|
|
34
|
-
# Shortcuts to the classes under test
|
|
35
|
-
ChromiumPyBridge = _chromium_py_bridge.ChromiumPyBridge
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
39
|
-
# Test: ChromiumPyBridge construction and base functionality
|
|
40
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class TestChromiumPyBridgeConstruction:
|
|
44
|
-
def test_constructs_without_playwright_browser(self):
|
|
45
|
-
"""ChromiumPyBridge can be instantiated without a Playwright browser."""
|
|
46
|
-
bridge = ChromiumPyBridge()
|
|
47
|
-
assert bridge.sessions == {}
|
|
48
|
-
assert bridge.element_caches == {}
|
|
49
|
-
|
|
50
|
-
def test_ping_response(self):
|
|
51
|
-
"""Ping is handled by the base class."""
|
|
52
|
-
bridge = ChromiumPyBridge()
|
|
53
|
-
result = bridge.handle_command("ping", {}, 1)
|
|
54
|
-
assert result["result"] == "pong"
|
|
55
|
-
|
|
56
|
-
def test_unknown_method_returns_error(self):
|
|
57
|
-
"""Unknown methods return METHOD_NOT_FOUND."""
|
|
58
|
-
bridge = ChromiumPyBridge()
|
|
59
|
-
result = bridge.handle_command("bogus.method", {}, 2)
|
|
60
|
-
assert "error" in result
|
|
61
|
-
assert result["error"]["code"] == -32601
|
|
62
|
-
|
|
63
|
-
def test_missing_session_returns_specific_error(self):
|
|
64
|
-
"""Operations without a session return a SESSION_ERROR."""
|
|
65
|
-
bridge = ChromiumPyBridge()
|
|
66
|
-
result = bridge.handle_command(
|
|
67
|
-
"browser.snapshot", {"taskId": "nonexistent"}, 3
|
|
68
|
-
)
|
|
69
|
-
assert "error" in result
|
|
70
|
-
assert result["error"]["code"] == -32002
|
|
71
|
-
assert "No active session" in result["error"]["message"]
|
|
72
|
-
|
|
73
|
-
def test_invalid_scroll_direction(self):
|
|
74
|
-
"""Invalid scroll direction returns INVALID_PARAMS."""
|
|
75
|
-
bridge = ChromiumPyBridge()
|
|
76
|
-
result = bridge.handle_command(
|
|
77
|
-
"browser.scroll",
|
|
78
|
-
{"taskId": "t", "direction": "sideways"},
|
|
79
|
-
4,
|
|
80
|
-
)
|
|
81
|
-
assert "error" in result
|
|
82
|
-
assert result["error"]["code"] == -32602
|
|
83
|
-
assert 'direction must be "up" or "down"' in result["error"]["message"]
|
|
84
|
-
|
|
85
|
-
def test_not_implemented_returns_application_error(self):
|
|
86
|
-
"""Operations not overridden in subclass return APPLICATION_ERROR."""
|
|
87
|
-
bridge = ChromiumPyBridge()
|
|
88
|
-
# do_navigate is implemented, but the base class requires a session
|
|
89
|
-
# (which fails because create_browser_session is a different error).
|
|
90
|
-
# Use a different operation that doesn't require a session.
|
|
91
|
-
# getConsoleMessages fails with session error for nonexistent tasks.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
95
|
-
# Test: ChromiumPyBridge command dispatch handles all methods
|
|
96
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class TestChromiumPyBridgeDispatch:
|
|
100
|
-
"""Verify that all 13 methods are routed correctly in the handle_command.
|
|
101
|
-
|
|
102
|
-
These tests verify the routing layer only (no browser needed) — they
|
|
103
|
-
confirm that the bridge accepts commands and returns proper error
|
|
104
|
-
shapes for operations that require a session.
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
METHOD_NAMES = [
|
|
108
|
-
"browser.navigate",
|
|
109
|
-
"browser.snapshot",
|
|
110
|
-
"browser.click",
|
|
111
|
-
"browser.type",
|
|
112
|
-
"browser.scroll",
|
|
113
|
-
"browser.goBack",
|
|
114
|
-
"browser.press",
|
|
115
|
-
"browser.screenshot",
|
|
116
|
-
"browser.getConsoleMessages",
|
|
117
|
-
"browser.clearConsole",
|
|
118
|
-
"browser.evaluate",
|
|
119
|
-
"browser.cleanup",
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
@pytest.fixture
|
|
123
|
-
def bridge(self):
|
|
124
|
-
return ChromiumPyBridge()
|
|
125
|
-
|
|
126
|
-
def test_all_methods_have_dispatch_entries(self, bridge):
|
|
127
|
-
"""Every method gets routed (even if it returns a session error)."""
|
|
128
|
-
# Some methods safely return empty results when there's no session.
|
|
129
|
-
# getConsoleMessages/clearConsole use get_session (safe).
|
|
130
|
-
# cleanup is a no-op on missing sessions (returns success).
|
|
131
|
-
no_session_ok = {
|
|
132
|
-
"browser.getConsoleMessages",
|
|
133
|
-
"browser.clearConsole",
|
|
134
|
-
"browser.cleanup",
|
|
135
|
-
}
|
|
136
|
-
for method in self.METHOD_NAMES:
|
|
137
|
-
params = {"taskId": "no-session-test"}
|
|
138
|
-
result = bridge.handle_command(method, params, 99)
|
|
139
|
-
if method in no_session_ok:
|
|
140
|
-
# These safely return success with empty data
|
|
141
|
-
assert "result" in result, f"{method} should return a result"
|
|
142
|
-
continue
|
|
143
|
-
# All others should produce an error
|
|
144
|
-
assert "error" in result, f"{method} did not produce an error"
|
|
145
|
-
code = result["error"]["code"]
|
|
146
|
-
# browser.navigate without 'url' param gets APPLICATION_ERROR (-32000)
|
|
147
|
-
# because the base handle_command routes InvalidParamsError to the
|
|
148
|
-
# generic Exception handler (not the INVALID_PARAMS handler).
|
|
149
|
-
assert code in (-32002, -32602, -32601, -32000), (
|
|
150
|
-
f"{method}: unexpected error code {code}: {result['error']['message'][:60]}"
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
def test_navigate_missing_url(self, bridge):
|
|
154
|
-
"""browser.navigate without url returns an error mentioning url."""
|
|
155
|
-
result = bridge.handle_command(
|
|
156
|
-
"browser.navigate", {"taskId": "t"}, 1
|
|
157
|
-
)
|
|
158
|
-
assert "error" in result
|
|
159
|
-
# Note: the base class maps InvalidParamsError to APPLICATION_ERROR
|
|
160
|
-
# (-32000) rather than INVALID_PARAMS (-32602). We just check the
|
|
161
|
-
# error message mentions the missing param.
|
|
162
|
-
assert "url" in result["error"]["message"]
|
|
163
|
-
|
|
164
|
-
def test_navigate_missing_task_id(self, bridge):
|
|
165
|
-
"""browser.navigate without taskId returns an error mentioning taskId."""
|
|
166
|
-
result = bridge.handle_command(
|
|
167
|
-
"browser.navigate", {"url": "https://example.com"}, 2
|
|
168
|
-
)
|
|
169
|
-
assert "error" in result
|
|
170
|
-
assert "taskId" in result["error"]["message"]
|
|
171
|
-
|
|
172
|
-
def test_click_missing_ref(self, bridge):
|
|
173
|
-
"""browser.click without ref returns INVALID_PARAMS."""
|
|
174
|
-
result = bridge.handle_command(
|
|
175
|
-
"browser.click", {"taskId": "t"}, 3
|
|
176
|
-
)
|
|
177
|
-
assert "error" in result
|
|
178
|
-
assert "ref" in result["error"]["message"]
|
|
179
|
-
|
|
180
|
-
def test_type_missing_text(self, bridge):
|
|
181
|
-
"""browser.type without text returns INVALID_PARAMS."""
|
|
182
|
-
result = bridge.handle_command(
|
|
183
|
-
"browser.type", {"taskId": "t", "ref": "e1"}, 4
|
|
184
|
-
)
|
|
185
|
-
assert "error" in result
|
|
186
|
-
assert "text" in result["error"]["message"]
|
|
187
|
-
|
|
188
|
-
def test_evaluate_missing_expression(self, bridge):
|
|
189
|
-
"""browser.evaluate without expression returns INVALID_PARAMS."""
|
|
190
|
-
result = bridge.handle_command(
|
|
191
|
-
"browser.evaluate", {"taskId": "t"}, 5
|
|
192
|
-
)
|
|
193
|
-
assert "error" in result
|
|
194
|
-
assert "expression" in result["error"]["message"]
|
|
195
|
-
|
|
196
|
-
def test_shutdown_command(self, bridge):
|
|
197
|
-
"""shutdown returns success and sets _running to False."""
|
|
198
|
-
result = bridge.handle_command("shutdown", {}, 6)
|
|
199
|
-
assert result["result"] == "shutting_down"
|
|
200
|
-
assert bridge._running is False
|
|
201
|
-
|
|
202
|
-
def test_cleanup_missing_task_id(self, bridge):
|
|
203
|
-
"""browser.cleanup without taskId returns INVALID_PARAMS."""
|
|
204
|
-
result = bridge.handle_command(
|
|
205
|
-
"browser.cleanup", {}, 7
|
|
206
|
-
)
|
|
207
|
-
assert "error" in result
|
|
208
|
-
assert "taskId" in result["error"]["message"]
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
212
|
-
# Test: Console capture ring-buffer cap
|
|
213
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
class _MockConsolePage:
|
|
217
|
-
"""Mock Playwright page that records ``page.on("console")`` handlers.
|
|
218
|
-
|
|
219
|
-
Lets us invoke captured handlers directly without a real browser.
|
|
220
|
-
"""
|
|
221
|
-
|
|
222
|
-
def __init__(self) -> None:
|
|
223
|
-
self._handlers: dict[str, list] = {}
|
|
224
|
-
|
|
225
|
-
def on(self, event: str, handler) -> None:
|
|
226
|
-
self._handlers.setdefault(event, []).append(handler)
|
|
227
|
-
|
|
228
|
-
def fire_console(self, text: str) -> None:
|
|
229
|
-
"""Simulate a console event by calling all 'console' handlers."""
|
|
230
|
-
for handler in self._handlers.get("console", []):
|
|
231
|
-
handler(_FakeConsoleMessage(text))
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class _FakeConsoleMessage:
|
|
235
|
-
"""Duck-typed substitute for Playwright's ConsoleMessage."""
|
|
236
|
-
|
|
237
|
-
def __init__(self, text: str) -> None:
|
|
238
|
-
self._text = text
|
|
239
|
-
|
|
240
|
-
@property
|
|
241
|
-
def type(self) -> str:
|
|
242
|
-
return "log"
|
|
243
|
-
|
|
244
|
-
@property
|
|
245
|
-
def text(self) -> str:
|
|
246
|
-
return self._text
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
class TestConsoleCap:
|
|
250
|
-
"""Verify the 500-entry ring buffer on console capture."""
|
|
251
|
-
|
|
252
|
-
@pytest.fixture
|
|
253
|
-
def bridge(self):
|
|
254
|
-
return ChromiumPyBridge()
|
|
255
|
-
|
|
256
|
-
def test_console_capped_at_500(self, bridge):
|
|
257
|
-
"""After 501 events, only the last 500 are retained."""
|
|
258
|
-
page = _MockConsolePage()
|
|
259
|
-
session = bridge._setup_page_session(page)
|
|
260
|
-
messages: list[dict[str, str]] = session["console_messages"]
|
|
261
|
-
|
|
262
|
-
# Fire 501 messages
|
|
263
|
-
for i in range(501):
|
|
264
|
-
page.fire_console(f"msg-{i}")
|
|
265
|
-
|
|
266
|
-
assert len(messages) == 500, f"Expected 500, got {len(messages)}"
|
|
267
|
-
assert messages[0]["text"] == "msg-1", "First entry should be msg-1 (msg-0 popped)"
|
|
268
|
-
assert messages[-1]["text"] == "msg-500", "Last entry should be msg-500"
|
|
269
|
-
|
|
270
|
-
def test_console_under_cap_retains_all(self, bridge):
|
|
271
|
-
"""Fewer than 501 events are all retained."""
|
|
272
|
-
page = _MockConsolePage()
|
|
273
|
-
session = bridge._setup_page_session(page)
|
|
274
|
-
messages: list[dict[str, str]] = session["console_messages"]
|
|
275
|
-
|
|
276
|
-
for i in range(10):
|
|
277
|
-
page.fire_console(f"msg-{i}")
|
|
278
|
-
|
|
279
|
-
assert len(messages) == 10, f"Expected 10, got {len(messages)}"
|
|
280
|
-
assert messages[0]["text"] == "msg-0"
|
|
281
|
-
assert messages[-1]["text"] == "msg-9"
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for ``backends/firefox-py/bridge.py``.
|
|
3
|
-
|
|
4
|
-
Pure-logic unit tests that do NOT require a Playwright browser to be
|
|
5
|
-
installed. The bridge module is loaded via ``importlib`` because the
|
|
6
|
-
directory name contains a hyphen.
|
|
7
|
-
|
|
8
|
-
Integration tests requiring an actual Firefox browser live in
|
|
9
|
-
``__tests__/firefox-py.test.ts`` (plugin contract harness).
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import importlib.util
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
# ── Load the bridge module via file path ─────────────────────────────
|
|
19
|
-
|
|
20
|
-
_bridge_path = (
|
|
21
|
-
Path(__file__).resolve().parents[3]
|
|
22
|
-
/ "backends"
|
|
23
|
-
/ "firefox-py"
|
|
24
|
-
/ "bridge.py"
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
assert _bridge_path.exists(), f"bridge.py not found at {_bridge_path}"
|
|
28
|
-
|
|
29
|
-
_spec = importlib.util.spec_from_file_location("firefox_py_bridge", _bridge_path)
|
|
30
|
-
assert _spec is not None, f"Could not create spec for {_bridge_path}"
|
|
31
|
-
|
|
32
|
-
_bridge_module = importlib.util.module_from_spec(_spec)
|
|
33
|
-
_spec.loader.exec_module(_bridge_module) # type: ignore[union-attr]
|
|
34
|
-
|
|
35
|
-
FirefoxPyBridge = _bridge_module.FirefoxPyBridge
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
39
|
-
# Tests
|
|
40
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class TestFirefoxPyBridgeConstruction:
|
|
44
|
-
"""Basic construction and attribute tests."""
|
|
45
|
-
|
|
46
|
-
def test_constructs_without_playwright_browser(self) -> None:
|
|
47
|
-
"""Creating a FirefoxPyBridge instance should work without Playwright/FF installed."""
|
|
48
|
-
bridge = FirefoxPyBridge()
|
|
49
|
-
assert bridge is not None
|
|
50
|
-
assert bridge._plugin_name == "firefox-py"
|
|
51
|
-
assert bridge._user_agent is not None
|
|
52
|
-
assert bridge._install_hint is not None
|
|
53
|
-
assert "playwright install firefox" in bridge._install_hint
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class TestFirefoxPyBridgeDispatch:
|
|
57
|
-
"""Verify that all 13 methods are routed correctly in handle_command."""
|
|
58
|
-
|
|
59
|
-
METHOD_NAMES = [
|
|
60
|
-
"browser.navigate",
|
|
61
|
-
"browser.snapshot",
|
|
62
|
-
"browser.click",
|
|
63
|
-
"browser.type",
|
|
64
|
-
"browser.scroll",
|
|
65
|
-
"browser.goBack",
|
|
66
|
-
"browser.press",
|
|
67
|
-
"browser.screenshot",
|
|
68
|
-
"browser.getConsoleMessages",
|
|
69
|
-
"browser.clearConsole",
|
|
70
|
-
"browser.evaluate",
|
|
71
|
-
"browser.cleanup",
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
@pytest.fixture
|
|
75
|
-
def bridge(self) -> FirefoxPyBridge:
|
|
76
|
-
return FirefoxPyBridge()
|
|
77
|
-
|
|
78
|
-
def test_all_methods_have_dispatch_entries(self, bridge: FirefoxPyBridge) -> None:
|
|
79
|
-
"""Every method gets routed (even if it returns a session error)."""
|
|
80
|
-
no_session_ok = {
|
|
81
|
-
"browser.getConsoleMessages",
|
|
82
|
-
"browser.clearConsole",
|
|
83
|
-
"browser.cleanup",
|
|
84
|
-
}
|
|
85
|
-
for method in self.METHOD_NAMES:
|
|
86
|
-
params = {"taskId": "no-session-test"}
|
|
87
|
-
result = bridge.handle_command(method, params, 99)
|
|
88
|
-
if method in no_session_ok:
|
|
89
|
-
# These safely return success with empty data
|
|
90
|
-
assert "result" in result, f"{method} should return a result"
|
|
91
|
-
continue
|
|
92
|
-
# All others should produce an error
|
|
93
|
-
assert "error" in result, f"{method} did not produce an error"
|
|
94
|
-
code = result["error"]["code"]
|
|
95
|
-
assert code in (-32002, -32602, -32601, -32000), (
|
|
96
|
-
f"{method}: unexpected error code {code}: {result['error']['message'][:60]}"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
def test_navigate_missing_url(self, bridge: FirefoxPyBridge) -> None:
|
|
100
|
-
"""navigate without url returns an error mentioning url."""
|
|
101
|
-
result = bridge.handle_command("browser.navigate", {"taskId": "t"}, 1)
|
|
102
|
-
assert "error" in result
|
|
103
|
-
assert "url" in result["error"]["message"]
|
|
104
|
-
|
|
105
|
-
def test_navigate_missing_task_id(self, bridge: FirefoxPyBridge) -> None:
|
|
106
|
-
"""navigate without taskId returns an error mentioning taskId."""
|
|
107
|
-
result = bridge.handle_command(
|
|
108
|
-
"browser.navigate", {"url": "https://example.com"}, 2
|
|
109
|
-
)
|
|
110
|
-
assert "error" in result
|
|
111
|
-
assert "taskId" in result["error"]["message"]
|
|
112
|
-
|
|
113
|
-
def test_click_missing_ref(self, bridge: FirefoxPyBridge) -> None:
|
|
114
|
-
"""click without ref returns INVALID_PARAMS."""
|
|
115
|
-
result = bridge.handle_command("browser.click", {"taskId": "t"}, 3)
|
|
116
|
-
assert "error" in result
|
|
117
|
-
assert "ref" in result["error"]["message"]
|
|
118
|
-
|
|
119
|
-
def test_type_missing_text(self, bridge: FirefoxPyBridge) -> None:
|
|
120
|
-
"""type without text returns INVALID_PARAMS."""
|
|
121
|
-
result = bridge.handle_command(
|
|
122
|
-
"browser.type", {"taskId": "t", "ref": "e1"}, 4
|
|
123
|
-
)
|
|
124
|
-
assert "error" in result
|
|
125
|
-
assert "text" in result["error"]["message"]
|
|
126
|
-
|
|
127
|
-
def test_evaluate_missing_expression(self, bridge: FirefoxPyBridge) -> None:
|
|
128
|
-
"""evaluate without expression returns INVALID_PARAMS."""
|
|
129
|
-
result = bridge.handle_command("browser.evaluate", {"taskId": "t"}, 5)
|
|
130
|
-
assert "error" in result
|
|
131
|
-
assert "expression" in result["error"]["message"]
|
|
132
|
-
|
|
133
|
-
def test_shutdown_command(self, bridge: FirefoxPyBridge) -> None:
|
|
134
|
-
"""shutdown returns success and sets _running to False."""
|
|
135
|
-
result = bridge.handle_command("shutdown", {}, 6)
|
|
136
|
-
assert result["result"] == "shutting_down"
|
|
137
|
-
assert bridge._running is False
|
|
138
|
-
|
|
139
|
-
def test_cleanup_missing_task_id(self, bridge: FirefoxPyBridge) -> None:
|
|
140
|
-
"""cleanup without taskId returns INVALID_PARAMS."""
|
|
141
|
-
result = bridge.handle_command("browser.cleanup", {}, 7)
|
|
142
|
-
assert "error" in result
|
|
143
|
-
assert "taskId" in result["error"]["message"]
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
147
|
-
# Test: Console capture ring-buffer cap
|
|
148
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
class _MockConsolePage:
|
|
152
|
-
"""Mock Playwright page that records page.on('console') handlers."""
|
|
153
|
-
|
|
154
|
-
def __init__(self) -> None:
|
|
155
|
-
self._handlers: dict[str, list] = {}
|
|
156
|
-
|
|
157
|
-
def on(self, event: str, handler) -> None:
|
|
158
|
-
self._handlers.setdefault(event, []).append(handler)
|
|
159
|
-
|
|
160
|
-
def fire_console(self, text: str) -> None:
|
|
161
|
-
"""Simulate a console event by calling all 'console' handlers."""
|
|
162
|
-
for handler in self._handlers.get("console", []):
|
|
163
|
-
handler(_FakeConsoleMessage(text))
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
class _FakeConsoleMessage:
|
|
167
|
-
"""Duck-typed substitute for Playwright's ConsoleMessage."""
|
|
168
|
-
|
|
169
|
-
def __init__(self, text: str) -> None:
|
|
170
|
-
self._text = text
|
|
171
|
-
|
|
172
|
-
@property
|
|
173
|
-
def type(self) -> str:
|
|
174
|
-
return "log"
|
|
175
|
-
|
|
176
|
-
@property
|
|
177
|
-
def text(self) -> str:
|
|
178
|
-
return self._text
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
class TestConsoleCap:
|
|
182
|
-
"""Verify the 500-entry ring buffer on console capture."""
|
|
183
|
-
|
|
184
|
-
@pytest.fixture
|
|
185
|
-
def bridge(self) -> FirefoxPyBridge:
|
|
186
|
-
return FirefoxPyBridge()
|
|
187
|
-
|
|
188
|
-
def test_console_capped_at_500(self, bridge: FirefoxPyBridge) -> None:
|
|
189
|
-
"""After 501 events, only the last 500 are retained."""
|
|
190
|
-
page = _MockConsolePage()
|
|
191
|
-
session = bridge._setup_page_session(page)
|
|
192
|
-
messages: list[dict[str, str]] = session["console_messages"]
|
|
193
|
-
|
|
194
|
-
for i in range(501):
|
|
195
|
-
page.fire_console(f"msg-{i}")
|
|
196
|
-
|
|
197
|
-
assert len(messages) == 500, f"Expected 500, got {len(messages)}"
|
|
198
|
-
assert messages[0]["text"] == "msg-1", "First should be msg-1 (msg-0 popped)"
|
|
199
|
-
assert messages[-1]["text"] == "msg-500", "Last should be msg-500"
|
|
200
|
-
|
|
201
|
-
def test_console_under_cap_retains_all(self, bridge: FirefoxPyBridge) -> None:
|
|
202
|
-
"""Fewer than 501 events are all retained."""
|
|
203
|
-
page = _MockConsolePage()
|
|
204
|
-
session = bridge._setup_page_session(page)
|
|
205
|
-
messages: list[dict[str, str]] = session["console_messages"]
|
|
206
|
-
|
|
207
|
-
for i in range(10):
|
|
208
|
-
page.fire_console(f"msg-{i}")
|
|
209
|
-
|
|
210
|
-
assert len(messages) == 10, f"Expected 10, got {len(messages)}"
|
|
211
|
-
assert messages[0]["text"] == "msg-0"
|
|
212
|
-
assert messages[-1]["text"] == "msg-9"
|