pi-lean-dimension 0.1.0 → 0.2.1

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.
Files changed (130) hide show
  1. package/README.md +96 -36
  2. package/node_modules/pi-lean-portal/AGENTS.md +146 -4
  3. package/node_modules/pi-lean-portal/README.md +112 -50
  4. package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
  5. package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
  6. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
  7. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
  8. package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
  9. package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
  10. package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
  11. package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
  12. package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
  13. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
  15. package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
  16. package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
  17. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  18. package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
  19. package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
  20. package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
  21. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
  22. package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
  23. package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
  24. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
  25. package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
  26. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
  27. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
  28. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
  29. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
  30. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
  31. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
  32. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
  33. package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
  34. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
  35. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
  36. package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
  37. package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  38. package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
  39. package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
  40. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  41. package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
  42. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
  43. package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
  44. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
  45. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
  48. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
  49. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
  50. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
  51. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  52. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  53. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
  54. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
  55. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
  56. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  57. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  58. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  59. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  60. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  61. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
  62. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
  63. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
  64. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
  65. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
  66. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
  67. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  68. 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
  69. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  70. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  71. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  72. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  74. 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
  75. 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
  76. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  77. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  79. 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
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
  84. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
  85. package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
  87. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
  88. package/node_modules/pi-lean-portal/contributed/README.md +304 -0
  89. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
  90. package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
  91. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
  92. package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
  93. package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
  94. package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
  95. package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
  96. package/node_modules/pi-lean-portal/core/router.ts +59 -98
  97. package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
  98. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
  99. package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
  100. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
  101. package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
  102. package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
  103. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
  104. package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
  105. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
  106. package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
  107. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
  108. package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
  109. package/node_modules/pi-lean-portal/index.ts +42 -7
  110. package/node_modules/pi-lean-portal/package.json +8 -3
  111. package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
  112. package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
  113. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
  114. package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
  115. package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
  116. package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
  117. package/node_modules/pi-lean-search/README.md +6 -2
  118. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
  119. package/node_modules/pi-lean-search/index.ts +4 -21
  120. package/node_modules/pi-lean-search/package.json +2 -2
  121. package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
  122. package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
  123. package/package.json +4 -4
  124. package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
  125. package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
  126. package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
  127. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
  128. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
  129. package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
  130. 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"