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.
- 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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Shared fixtures and helpers for Python bridge unit tests."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ── Bridge loading ─────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
_BACKEND_CLASSES = {
|
|
12
|
+
"chromium-py": "ChromiumPyBridge",
|
|
13
|
+
"firefox-py": "FirefoxPyBridge",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_bridge_class(backend_name: str):
|
|
18
|
+
"""Load a bridge class by backend name (chromium-py, firefox-py)."""
|
|
19
|
+
dir_name = backend_name.replace("-", "_")
|
|
20
|
+
bridge_path = (
|
|
21
|
+
Path(__file__).resolve().parents[3]
|
|
22
|
+
/ "backends"
|
|
23
|
+
/ backend_name
|
|
24
|
+
/ "bridge.py"
|
|
25
|
+
)
|
|
26
|
+
assert bridge_path.exists(), f"bridge.py not found at {bridge_path}"
|
|
27
|
+
spec = importlib.util.spec_from_file_location(
|
|
28
|
+
f"{dir_name}_bridge", bridge_path
|
|
29
|
+
)
|
|
30
|
+
assert spec is not None, f"Could not create spec for {bridge_path}"
|
|
31
|
+
module = importlib.util.module_from_spec(spec)
|
|
32
|
+
spec.loader.exec_module(module) # type: ignore[union-attr]
|
|
33
|
+
return getattr(module, _BACKEND_CLASSES[backend_name])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture(params=["chromium-py", "firefox-py"])
|
|
37
|
+
def bridge_factory(request):
|
|
38
|
+
"""Parametrized fixture yielding (backend_name, bridge_instance)."""
|
|
39
|
+
backend_name = request.param
|
|
40
|
+
cls = _load_bridge_class(backend_name)
|
|
41
|
+
return backend_name, cls()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── Shared constants ──────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
METHOD_NAMES = [
|
|
47
|
+
"browser.navigate",
|
|
48
|
+
"browser.snapshot",
|
|
49
|
+
"browser.click",
|
|
50
|
+
"browser.type",
|
|
51
|
+
"browser.scroll",
|
|
52
|
+
"browser.goBack",
|
|
53
|
+
"browser.press",
|
|
54
|
+
"browser.screenshot",
|
|
55
|
+
"browser.getConsoleMessages",
|
|
56
|
+
"browser.clearConsole",
|
|
57
|
+
"browser.evaluate",
|
|
58
|
+
"browser.cleanup",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── Mock objects ──────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _MockConsolePage:
|
|
66
|
+
"""Mock Playwright page that records ``page.on("console")`` handlers.
|
|
67
|
+
|
|
68
|
+
Lets us invoke captured handlers directly without a real browser.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self) -> None:
|
|
72
|
+
self._handlers: dict[str, list] = {}
|
|
73
|
+
|
|
74
|
+
def on(self, event: str, handler) -> None:
|
|
75
|
+
self._handlers.setdefault(event, []).append(handler)
|
|
76
|
+
|
|
77
|
+
def fire_console(self, text: str) -> None:
|
|
78
|
+
"""Simulate a console event by calling all 'console' handlers."""
|
|
79
|
+
for handler in self._handlers.get("console", []):
|
|
80
|
+
handler(_FakeConsoleMessage(text))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _FakeConsoleMessage:
|
|
84
|
+
"""Duck-typed substitute for Playwright's ConsoleMessage."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, text: str) -> None:
|
|
87
|
+
self._text = text
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def type(self) -> str:
|
|
91
|
+
return "log"
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def text(self) -> str:
|
|
95
|
+
return self._text
|
|
@@ -16,39 +16,11 @@ from pi_browser_bridge.accessibility import (
|
|
|
16
16
|
parse_snapshot,
|
|
17
17
|
build_locator_args,
|
|
18
18
|
_parse_line,
|
|
19
|
-
_count_leading_spaces,
|
|
20
|
-
_role_icon,
|
|
21
|
-
_truncate,
|
|
22
19
|
INTERACTIVE_ROLES,
|
|
23
20
|
INFORMATIONAL_ROLES,
|
|
24
21
|
)
|
|
25
22
|
|
|
26
23
|
|
|
27
|
-
# ═════════════════════════════════════════════════════════════════════
|
|
28
|
-
# _count_leading_spaces
|
|
29
|
-
# ═════════════════════════════════════════════════════════════════════
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class TestCountLeadingSpaces:
|
|
33
|
-
def test_zero(self) -> None:
|
|
34
|
-
assert _count_leading_spaces("foo") == 0
|
|
35
|
-
|
|
36
|
-
def test_two_spaces(self) -> None:
|
|
37
|
-
assert _count_leading_spaces(" foo") == 2
|
|
38
|
-
|
|
39
|
-
def test_four_spaces(self) -> None:
|
|
40
|
-
assert _count_leading_spaces(" foo") == 4
|
|
41
|
-
|
|
42
|
-
def test_tab(self) -> None:
|
|
43
|
-
assert _count_leading_spaces("\tfoo") == 1
|
|
44
|
-
|
|
45
|
-
def test_empty(self) -> None:
|
|
46
|
-
assert _count_leading_spaces("") == 0
|
|
47
|
-
|
|
48
|
-
def test_only_spaces(self) -> None:
|
|
49
|
-
assert _count_leading_spaces(" ") == 3
|
|
50
|
-
|
|
51
|
-
|
|
52
24
|
# ═════════════════════════════════════════════════════════════════════
|
|
53
25
|
# _parse_line
|
|
54
26
|
# ═════════════════════════════════════════════════════════════════════
|
|
@@ -411,7 +383,7 @@ class TestBuildLocatorArgs:
|
|
|
411
383
|
role, kwargs = build_locator_args(node)
|
|
412
384
|
assert role == "button"
|
|
413
385
|
assert kwargs["name"] == "Click"
|
|
414
|
-
assert kwargs["exact"]
|
|
386
|
+
assert kwargs["exact"] == True
|
|
415
387
|
assert kwargs["occurrenceIndex"] == 0
|
|
416
388
|
|
|
417
389
|
def test_heading_with_level(self) -> None:
|
|
@@ -437,7 +409,7 @@ class TestBuildLocatorArgs:
|
|
|
437
409
|
raw='- checkbox "Agree" [checked]',
|
|
438
410
|
)
|
|
439
411
|
role, kwargs = build_locator_args(node)
|
|
440
|
-
assert kwargs["checked"]
|
|
412
|
+
assert kwargs["checked"] == True
|
|
441
413
|
|
|
442
414
|
def test_mixed_checkbox(self) -> None:
|
|
443
415
|
node = AriaCachedNode(
|
|
@@ -461,7 +433,7 @@ class TestBuildLocatorArgs:
|
|
|
461
433
|
raw='- treeitem "Node" [expanded=true]',
|
|
462
434
|
)
|
|
463
435
|
role, kwargs = build_locator_args(node)
|
|
464
|
-
assert kwargs["expanded"]
|
|
436
|
+
assert kwargs["expanded"] == True
|
|
465
437
|
|
|
466
438
|
def test_collapsed(self) -> None:
|
|
467
439
|
node = AriaCachedNode(
|
|
@@ -473,8 +445,8 @@ class TestBuildLocatorArgs:
|
|
|
473
445
|
raw='- treeitem "Node" [expanded=false, selected]',
|
|
474
446
|
)
|
|
475
447
|
role, kwargs = build_locator_args(node)
|
|
476
|
-
assert kwargs["expanded"]
|
|
477
|
-
assert kwargs["selected"]
|
|
448
|
+
assert kwargs["expanded"] == False
|
|
449
|
+
assert kwargs["selected"] == True
|
|
478
450
|
|
|
479
451
|
def test_disabled(self) -> None:
|
|
480
452
|
node = AriaCachedNode(
|
|
@@ -486,7 +458,7 @@ class TestBuildLocatorArgs:
|
|
|
486
458
|
raw='- button "Save" [disabled]',
|
|
487
459
|
)
|
|
488
460
|
role, kwargs = build_locator_args(node)
|
|
489
|
-
assert kwargs["disabled"]
|
|
461
|
+
assert kwargs["disabled"] == True
|
|
490
462
|
|
|
491
463
|
def test_pressed(self) -> None:
|
|
492
464
|
node = AriaCachedNode(
|
|
@@ -498,7 +470,7 @@ class TestBuildLocatorArgs:
|
|
|
498
470
|
raw='- button "Toggle" [pressed]',
|
|
499
471
|
)
|
|
500
472
|
role, kwargs = build_locator_args(node)
|
|
501
|
-
assert kwargs["pressed"]
|
|
473
|
+
assert kwargs["pressed"] == True
|
|
502
474
|
|
|
503
475
|
def test_pressed_mixed(self) -> None:
|
|
504
476
|
node = AriaCachedNode(
|
|
@@ -524,7 +496,7 @@ class TestBuildLocatorArgs:
|
|
|
524
496
|
)
|
|
525
497
|
role, kwargs = build_locator_args(node)
|
|
526
498
|
assert kwargs["name"] == long
|
|
527
|
-
assert kwargs["exact"]
|
|
499
|
+
assert kwargs["exact"] == False
|
|
528
500
|
|
|
529
501
|
def test_includes_occurrence_index_unique(self) -> None:
|
|
530
502
|
"""build_locator_args includes occurrenceIndex=0 for unique elements."""
|
|
@@ -641,99 +613,3 @@ class TestParseSnapshotOccurrenceIndex:
|
|
|
641
613
|
assert result.elements["e1"].occurrence_index == 0 # first link
|
|
642
614
|
assert result.elements["e2"].occurrence_index == 1 # second link
|
|
643
615
|
|
|
644
|
-
|
|
645
|
-
# ═════════════════════════════════════════════════════════════════════
|
|
646
|
-
# _role_icon
|
|
647
|
-
# ═════════════════════════════════════════════════════════════════════
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
class TestRoleIcon:
|
|
651
|
-
def test_known_role(self) -> None:
|
|
652
|
-
assert _role_icon("button") == "\U0001f518 "
|
|
653
|
-
|
|
654
|
-
def test_unknown_role(self) -> None:
|
|
655
|
-
assert _role_icon("bogus") == ""
|
|
656
|
-
|
|
657
|
-
def test_all_interactive_have_icons(self) -> None:
|
|
658
|
-
for role in sorted(INTERACTIVE_ROLES):
|
|
659
|
-
icon = _role_icon(role)
|
|
660
|
-
assert icon != "", f"{role} is missing an icon"
|
|
661
|
-
assert icon.endswith(" "), f"{role} icon should end with space"
|
|
662
|
-
assert "\ufe0f " not in icon or icon.endswith(" "), (
|
|
663
|
-
f"{role} icon '{icon}' should end with plain space after \\ufe0f"
|
|
664
|
-
)
|
|
665
|
-
|
|
666
|
-
def test_all_informational_have_icons(self) -> None:
|
|
667
|
-
for role in sorted(INFORMATIONAL_ROLES):
|
|
668
|
-
icon = _role_icon(role)
|
|
669
|
-
assert icon != "", f"{role} is missing an icon"
|
|
670
|
-
assert icon.endswith(" "), f"{role} icon should end with space"
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
# ═════════════════════════════════════════════════════════════════════
|
|
674
|
-
# _truncate
|
|
675
|
-
# ═════════════════════════════════════════════════════════════════════
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
class TestTruncate:
|
|
679
|
-
def test_short_string(self) -> None:
|
|
680
|
-
assert _truncate("hello", 10) == "hello"
|
|
681
|
-
|
|
682
|
-
def test_exact_length(self) -> None:
|
|
683
|
-
assert _truncate("hello", 5) == "hello"
|
|
684
|
-
|
|
685
|
-
def test_long_string(self) -> None:
|
|
686
|
-
result = _truncate("hello world", 8)
|
|
687
|
-
assert len(result) == 8
|
|
688
|
-
assert result.endswith("\u2026") # ellipsis
|
|
689
|
-
|
|
690
|
-
def test_empty_string(self) -> None:
|
|
691
|
-
assert _truncate("", 5) == ""
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
# ═════════════════════════════════════════════════════════════════════
|
|
695
|
-
# Role set completeness — every role has a classification
|
|
696
|
-
# ═════════════════════════════════════════════════════════════════════
|
|
697
|
-
|
|
698
|
-
# Common ARIA roles that should be classified (not an exhaustive list)
|
|
699
|
-
COMMON_ARIA_ROLES = {
|
|
700
|
-
"button", "link", "textbox", "searchbox", "combobox",
|
|
701
|
-
"checkbox", "radio", "heading", "listbox", "option",
|
|
702
|
-
"menuitem", "tab", "switch", "slider", "spinbutton",
|
|
703
|
-
"progressbar", "meter", "scrollbar", "gridcell", "cell",
|
|
704
|
-
"columnheader", "rowheader", "tabpanel", "img", "figure",
|
|
705
|
-
"listitem", "dialog", "alertdialog", "tooltip", "navigation",
|
|
706
|
-
"banner", "form", "search", "toolbar", "menu", "menubar",
|
|
707
|
-
"note", "alert", "status", "list", "table", "grid",
|
|
708
|
-
"treegrid", "article", "section", "blockquote", "code",
|
|
709
|
-
"paragraph", "text", "group", "region", "main",
|
|
710
|
-
"complementary", "contentinfo", "definition", "term",
|
|
711
|
-
"math", "marquee", "timer", "log", "deletion", "insertion",
|
|
712
|
-
"mark", "suggestion", "comment",
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
class TestRoleSets:
|
|
717
|
-
def test_no_role_in_both_sets(self) -> None:
|
|
718
|
-
overlap = INTERACTIVE_ROLES & INFORMATIONAL_ROLES
|
|
719
|
-
assert overlap == set(), f"Roles in both sets: {overlap}"
|
|
720
|
-
|
|
721
|
-
def test_all_known_roles_classified(self) -> None:
|
|
722
|
-
"""Every role we know about should be in exactly one set."""
|
|
723
|
-
union = INTERACTIVE_ROLES | INFORMATIONAL_ROLES
|
|
724
|
-
unclassified = COMMON_ARIA_ROLES - union
|
|
725
|
-
assert unclassified == set(), f"Unclassified roles: {unclassified}"
|
|
726
|
-
|
|
727
|
-
def test_intersection_with_typescript_roles(self) -> None:
|
|
728
|
-
"""The INTERACTIVE_ROLES set should match the TypeScript one.
|
|
729
|
-
|
|
730
|
-
If this test fails, run the TypeScript test to see what changed.
|
|
731
|
-
"""
|
|
732
|
-
# As of Phase A / B, the Python and TS sets should be identical.
|
|
733
|
-
# Count is the canonical cross-check:
|
|
734
|
-
assert len(INTERACTIVE_ROLES) == 50, (
|
|
735
|
-
f"Expected 50 interactive roles, got {len(INTERACTIVE_ROLES)}"
|
|
736
|
-
)
|
|
737
|
-
assert len(INFORMATIONAL_ROLES) == 18, (
|
|
738
|
-
f"Expected 18 informational roles, got {len(INFORMATIONAL_ROLES)}"
|
|
739
|
-
)
|
|
@@ -41,27 +41,17 @@ class TestCheckBotDetection:
|
|
|
41
41
|
def test_normal_page_not_detected(self):
|
|
42
42
|
"""A normal page with no bot signals returns False."""
|
|
43
43
|
page = _MockPage(title="Example Domain", body_text="Welcome to Example")
|
|
44
|
-
assert check_bot_detection(page)
|
|
44
|
+
assert check_bot_detection(page) == False
|
|
45
45
|
|
|
46
46
|
def test_cloudflare_in_title(self):
|
|
47
47
|
"""'cloudflare' in the title triggers bot detection."""
|
|
48
48
|
page = _MockPage(title="Just a moment... | Cloudflare")
|
|
49
|
-
assert check_bot_detection(page)
|
|
50
|
-
|
|
51
|
-
def test_just_a_moment_in_title(self):
|
|
52
|
-
"""'just a moment' in the title triggers bot detection."""
|
|
53
|
-
page = _MockPage(title="Just a moment...")
|
|
54
|
-
assert check_bot_detection(page) is True
|
|
55
|
-
|
|
56
|
-
def test_checking_browser_in_title(self):
|
|
57
|
-
"""'checking your browser' in the title triggers bot detection."""
|
|
58
|
-
page = _MockPage(title="Checking your browser before accessing")
|
|
59
|
-
assert check_bot_detection(page) is True
|
|
49
|
+
assert check_bot_detection(page) == True
|
|
60
50
|
|
|
61
51
|
def test_verify_human_in_title(self):
|
|
62
52
|
"""'verify you are human' in the title triggers bot detection."""
|
|
63
53
|
page = _MockPage(title="Verify you are human")
|
|
64
|
-
assert check_bot_detection(page)
|
|
54
|
+
assert check_bot_detection(page) == True
|
|
65
55
|
|
|
66
56
|
def test_captcha_in_title_not_detected(self):
|
|
67
57
|
"""'captcha' in the title does NOT trigger bot detection.
|
|
@@ -71,20 +61,7 @@ class TestCheckBotDetection:
|
|
|
71
61
|
CAPTCHAs in passing (e.g. Wikipedia, tech blogs).
|
|
72
62
|
"""
|
|
73
63
|
page = _MockPage(title="CAPTCHA Challenge")
|
|
74
|
-
assert check_bot_detection(page)
|
|
75
|
-
|
|
76
|
-
def test_cf_ray_in_body_not_detected(self):
|
|
77
|
-
"""'cf-ray' in body does NOT trigger bot detection.
|
|
78
|
-
|
|
79
|
-
'cf-ray' is an HTTP response header that rarely appears in
|
|
80
|
-
rendered body text. The TypeScript reference (bot-detection.ts)
|
|
81
|
-
does not include this signal.
|
|
82
|
-
"""
|
|
83
|
-
page = _MockPage(
|
|
84
|
-
title="Access Denied",
|
|
85
|
-
body_text="cf-ray: abc123\nserver: cloudflare",
|
|
86
|
-
)
|
|
87
|
-
assert check_bot_detection(page) is False
|
|
64
|
+
assert check_bot_detection(page) == False
|
|
88
65
|
|
|
89
66
|
def test_akamai_reference_in_body(self):
|
|
90
67
|
"""Akamai-style reference codes in body trigger bot detection."""
|
|
@@ -92,7 +69,7 @@ class TestCheckBotDetection:
|
|
|
92
69
|
title="Error",
|
|
93
70
|
body_text="Access denied. Reference #18.abc.def",
|
|
94
71
|
)
|
|
95
|
-
assert check_bot_detection(page)
|
|
72
|
+
assert check_bot_detection(page) == True
|
|
96
73
|
|
|
97
74
|
def test_recaptcha_in_html(self):
|
|
98
75
|
"""'recaptcha' in HTML triggers bot detection."""
|
|
@@ -101,89 +78,7 @@ class TestCheckBotDetection:
|
|
|
101
78
|
body_text="Submit the form",
|
|
102
79
|
html='<div><script src="recaptcha/api.js"></script></div>',
|
|
103
80
|
)
|
|
104
|
-
assert check_bot_detection(page)
|
|
105
|
-
|
|
106
|
-
def test_hcaptcha_in_html(self):
|
|
107
|
-
"""'hcaptcha' in HTML triggers bot detection."""
|
|
108
|
-
page = _MockPage(
|
|
109
|
-
title="Login",
|
|
110
|
-
body_text="Login form",
|
|
111
|
-
html='<div data-sitekey="abc" class="hcaptcha"></div>',
|
|
112
|
-
)
|
|
113
|
-
assert check_bot_detection(page) is True
|
|
114
|
-
|
|
115
|
-
def test_turnstile_in_html(self):
|
|
116
|
-
"""Cloudflare challenge markers in body trigger bot detection."""
|
|
117
|
-
page = _MockPage(
|
|
118
|
-
title="Challenge",
|
|
119
|
-
body_text="_cf_chl_opt",
|
|
120
|
-
)
|
|
121
|
-
assert check_bot_detection(page) is True
|
|
122
|
-
|
|
123
|
-
def test_privacy_pass_in_title_not_detected(self):
|
|
124
|
-
"""'privacy pass' in title does NOT trigger detection.
|
|
125
|
-
|
|
126
|
-
This signal is not present in the TypeScript reference
|
|
127
|
-
(bot-detection.ts) and was removed to align with it.
|
|
128
|
-
"""
|
|
129
|
-
page = _MockPage(title="Privacy Pass Challenge")
|
|
130
|
-
assert check_bot_detection(page) is False
|
|
131
|
-
|
|
132
|
-
def test_automated_access_in_title_not_detected(self):
|
|
133
|
-
"""'automated access' in title does NOT trigger detection.
|
|
134
|
-
|
|
135
|
-
This signal is not present in the TypeScript reference
|
|
136
|
-
(bot-detection.ts) and was removed to align with it.
|
|
137
|
-
"""
|
|
138
|
-
page = _MockPage(title="Automated Access Blocked")
|
|
139
|
-
assert check_bot_detection(page) is False
|
|
140
|
-
|
|
141
|
-
def test_browser_check_in_title_not_detected(self):
|
|
142
|
-
"""'browser check' in title does NOT trigger detection.
|
|
143
|
-
|
|
144
|
-
This signal is not present in the TypeScript reference
|
|
145
|
-
(bot-detection.ts) and was removed to align with it.
|
|
146
|
-
"""
|
|
147
|
-
page = _MockPage(title="Browser Check")
|
|
148
|
-
assert check_bot_detection(page) is False
|
|
149
|
-
|
|
150
|
-
def test_wikipedia_captcha_not_false_positive(self):
|
|
151
|
-
"""Wikipedia's 'captcha' mention in body text is NOT detected.
|
|
152
|
-
|
|
153
|
-
This test validates that bot detection is title-level only for
|
|
154
|
-
the CAPTCHA signal, preventing false positives from sites that
|
|
155
|
-
simply mention CAPTCHA in their content.
|
|
156
|
-
"""
|
|
157
|
-
page = _MockPage(
|
|
158
|
-
title="Wikipedia - The Free Encyclopedia",
|
|
159
|
-
body_text=(
|
|
160
|
-
"CAPTCHA is a type of challenge-response test used in computing"
|
|
161
|
-
),
|
|
162
|
-
)
|
|
163
|
-
# 'captcha' is not in the title, so it should NOT trigger
|
|
164
|
-
assert check_bot_detection(page) is False
|
|
165
|
-
|
|
166
|
-
def test_blank_page_not_detected(self):
|
|
167
|
-
"""An empty or blank page does not trigger bot detection."""
|
|
168
|
-
page = _MockPage(title="", body_text="", html="")
|
|
169
|
-
assert check_bot_detection(page) is False
|
|
170
|
-
|
|
171
|
-
def test_bot_signal_in_body_not_mistaken_if_not_high_specificity(self):
|
|
172
|
-
"""Generic phrases in body don't trigger unless high-specificity."""
|
|
173
|
-
page = _MockPage(
|
|
174
|
-
title="Blog",
|
|
175
|
-
body_text="This page requires JavaScript to work properly. "
|
|
176
|
-
"Please enable JavaScript.",
|
|
177
|
-
)
|
|
178
|
-
assert check_bot_detection(page) is False
|
|
179
|
-
|
|
180
|
-
def test_data_sitekey_in_html(self):
|
|
181
|
-
"""'data-sitekey' (reCAPTCHA) in HTML triggers bot detection."""
|
|
182
|
-
page = _MockPage(
|
|
183
|
-
title="Contact",
|
|
184
|
-
html='<div class="g-recaptcha" data-sitekey="abc123"></div>',
|
|
185
|
-
)
|
|
186
|
-
assert check_bot_detection(page) is True
|
|
81
|
+
assert check_bot_detection(page) == True
|
|
187
82
|
|
|
188
83
|
def test_exception_during_title_safe(self):
|
|
189
84
|
"""If title() raises, bot detection still checks body and HTML."""
|
|
@@ -199,7 +94,7 @@ class TestCheckBotDetection:
|
|
|
199
94
|
return ""
|
|
200
95
|
return ""
|
|
201
96
|
|
|
202
|
-
assert check_bot_detection(_BrokenPage())
|
|
97
|
+
assert check_bot_detection(_BrokenPage()) == True
|
|
203
98
|
|
|
204
99
|
def test_exception_during_evaluate_safe(self):
|
|
205
100
|
"""If evaluate() raises, bot detection returns False gracefully."""
|
|
@@ -211,38 +106,4 @@ class TestCheckBotDetection:
|
|
|
211
106
|
def evaluate(self, expression: str, *args: Any) -> str:
|
|
212
107
|
raise RuntimeError("evaluate failed")
|
|
213
108
|
|
|
214
|
-
assert check_bot_detection(_BrokenEvalPage())
|
|
215
|
-
|
|
216
|
-
def test_cloudflare_nginx_in_body_not_detected(self):
|
|
217
|
-
"""'cloudflare-nginx' in body does NOT trigger detection.
|
|
218
|
-
|
|
219
|
-
This signal is not present in the TypeScript reference
|
|
220
|
-
(bot-detection.ts) and was removed to align with it.
|
|
221
|
-
"""
|
|
222
|
-
page = _MockPage(
|
|
223
|
-
title="Error 503",
|
|
224
|
-
body_text="Server: cloudflare-nginx",
|
|
225
|
-
)
|
|
226
|
-
assert check_bot_detection(page) is False
|
|
227
|
-
|
|
228
|
-
def test_reference_hash_in_body(self):
|
|
229
|
-
"""'reference #' in body triggers bot detection."""
|
|
230
|
-
page = _MockPage(
|
|
231
|
-
title="Blocked",
|
|
232
|
-
body_text="Reference #18.abcde12345",
|
|
233
|
-
)
|
|
234
|
-
assert check_bot_detection(page) is True
|
|
235
|
-
|
|
236
|
-
def test_blocked_request_in_body_not_detected(self):
|
|
237
|
-
"""'blocked request' in body does NOT trigger detection.
|
|
238
|
-
|
|
239
|
-
This signal is not present in the TypeScript reference
|
|
240
|
-
(bot-detection.ts) — more specific variants like
|
|
241
|
-
'you have been blocked' and 'your request has been blocked'
|
|
242
|
-
are used instead.
|
|
243
|
-
"""
|
|
244
|
-
page = _MockPage(
|
|
245
|
-
title="403 Forbidden",
|
|
246
|
-
body_text="Blocked request",
|
|
247
|
-
)
|
|
248
|
-
assert check_bot_detection(page) is False
|
|
109
|
+
assert check_bot_detection(_BrokenEvalPage()) == False
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for shared ``browser-data.json`` loading.
|
|
3
|
+
|
|
4
|
+
Verifies that the shared data file is valid JSON and that the Python
|
|
5
|
+
loaders in ``bot_detection``, ``accessibility``, and ``playwright_base``
|
|
6
|
+
can parse it correctly. A break here means both the TS and Python
|
|
7
|
+
sides are broken in the same way.
|
|
8
|
+
|
|
9
|
+
Pure-logic tests — no Playwright browser required.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from pi_browser_bridge.bot_detection import _BLOCK_SIGNALS, _BODY_ONLY_SIGNALS, _BODY_ONLY_PATTERNS, _HTML_SIGNALS
|
|
17
|
+
from pi_browser_bridge.accessibility import INTERACTIVE_ROLES, INFORMATIONAL_ROLES, _role_icon
|
|
18
|
+
|
|
19
|
+
# Path to the shared JSON file (relative to this test file)
|
|
20
|
+
# tests/ -> python-base/ -> backends/ -> pi-lean-portal/ -> core/shared/browser-data.json
|
|
21
|
+
_SHARED_JSON = os.path.join(
|
|
22
|
+
os.path.dirname(__file__),
|
|
23
|
+
"..", "..", "..",
|
|
24
|
+
"core", "shared", "browser-data.json",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(scope="module")
|
|
29
|
+
def shared_data() -> dict:
|
|
30
|
+
"""Load and parse browser-data.json once per module."""
|
|
31
|
+
try:
|
|
32
|
+
with open(_SHARED_JSON, "r") as f:
|
|
33
|
+
return json.load(f)
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
pytest.fail(f"Shared data file not found: {_SHARED_JSON}")
|
|
36
|
+
except json.JSONDecodeError as exc:
|
|
37
|
+
pytest.fail(f"Shared data file is not valid JSON: {exc}")
|
|
38
|
+
except OSError as exc:
|
|
39
|
+
pytest.fail(f"Failed to read shared data file: {exc}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
43
|
+
# JSON file validity
|
|
44
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestSharedJsonValidity:
|
|
48
|
+
def test_json_is_valid(self, shared_data: dict) -> None:
|
|
49
|
+
"""The file parses as valid JSON with a version field."""
|
|
50
|
+
assert isinstance(shared_data, dict)
|
|
51
|
+
assert shared_data["version"] == 1
|
|
52
|
+
|
|
53
|
+
def test_has_expected_top_level_keys(self, shared_data: dict) -> None:
|
|
54
|
+
"""All three top-level sections are present."""
|
|
55
|
+
assert "botSignals" in shared_data
|
|
56
|
+
assert "accessibility" in shared_data
|
|
57
|
+
assert "navSettle" in shared_data
|
|
58
|
+
|
|
59
|
+
def test_bot_signals_has_expected_keys(self, shared_data: dict) -> None:
|
|
60
|
+
bot = shared_data["botSignals"]
|
|
61
|
+
assert "blockSignals" in bot
|
|
62
|
+
assert "bodyOnlySignals" in bot
|
|
63
|
+
assert "bodyOnlyPatterns" in bot
|
|
64
|
+
assert "htmlSignals" in bot
|
|
65
|
+
|
|
66
|
+
def test_accessibility_has_expected_keys(self, shared_data: dict) -> None:
|
|
67
|
+
a11y = shared_data["accessibility"]
|
|
68
|
+
assert "interactiveRoles" in a11y
|
|
69
|
+
assert "informationalRoles" in a11y
|
|
70
|
+
assert "roleIcons" in a11y
|
|
71
|
+
|
|
72
|
+
def test_nav_settle_has_expected_keys(self, shared_data: dict) -> None:
|
|
73
|
+
ns = shared_data["navSettle"]
|
|
74
|
+
assert "navTimeoutMs" in ns
|
|
75
|
+
assert "settleTimeoutMs" in ns
|
|
76
|
+
assert "settleRaceMs" in ns
|
|
77
|
+
assert "domStabilizationJs" in ns
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
81
|
+
# Python-side loaders match the JSON data
|
|
82
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestBotDetectionLoader:
|
|
86
|
+
"""Verify bot_detection.py loaded its signals from the shared JSON."""
|
|
87
|
+
|
|
88
|
+
def test_block_signals_match_json(self, shared_data: dict) -> None:
|
|
89
|
+
expected = tuple(shared_data["botSignals"]["blockSignals"])
|
|
90
|
+
assert _BLOCK_SIGNALS == expected
|
|
91
|
+
|
|
92
|
+
def test_body_only_signals_match_json(self, shared_data: dict) -> None:
|
|
93
|
+
expected = tuple(shared_data["botSignals"]["bodyOnlySignals"])
|
|
94
|
+
assert _BODY_ONLY_SIGNALS == expected
|
|
95
|
+
|
|
96
|
+
def test_html_signals_match_json(self, shared_data: dict) -> None:
|
|
97
|
+
expected = tuple(shared_data["botSignals"]["htmlSignals"])
|
|
98
|
+
assert _HTML_SIGNALS == expected
|
|
99
|
+
|
|
100
|
+
def test_body_only_patterns_count(self, shared_data: dict) -> None:
|
|
101
|
+
"""The number of compiled patterns matches the source strings."""
|
|
102
|
+
expected_count = len(shared_data["botSignals"]["bodyOnlyPatterns"])
|
|
103
|
+
assert len(_BODY_ONLY_PATTERNS) == expected_count
|
|
104
|
+
|
|
105
|
+
def test_reference_code_pattern_matches(self) -> None:
|
|
106
|
+
"""The compiled reference# pattern matches expected input."""
|
|
107
|
+
assert len(_BODY_ONLY_PATTERNS) >= 1
|
|
108
|
+
pattern = _BODY_ONLY_PATTERNS[0]
|
|
109
|
+
assert pattern.search("reference #abc123.def") is not None
|
|
110
|
+
assert pattern.search("Reference#abc.def") is not None
|
|
111
|
+
assert pattern.search("normal text") is None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestAccessibilityLoader:
|
|
115
|
+
"""Verify accessibility.py loaded its data from the shared JSON."""
|
|
116
|
+
|
|
117
|
+
def test_interactive_roles_match_json(self, shared_data: dict) -> None:
|
|
118
|
+
expected = set(shared_data["accessibility"]["interactiveRoles"])
|
|
119
|
+
assert INTERACTIVE_ROLES == expected
|
|
120
|
+
|
|
121
|
+
def test_informational_roles_match_json(self, shared_data: dict) -> None:
|
|
122
|
+
expected = set(shared_data["accessibility"]["informationalRoles"])
|
|
123
|
+
assert INFORMATIONAL_ROLES == expected
|
|
124
|
+
|
|
125
|
+
def test_role_icons_match_json(self, shared_data: dict) -> None:
|
|
126
|
+
icons = shared_data["accessibility"]["roleIcons"]
|
|
127
|
+
for role, expected_icon in icons.items():
|
|
128
|
+
assert _role_icon(role) == expected_icon, f"Icon mismatch for role '{role}'"
|
|
129
|
+
|
|
130
|
+
def test_role_icon_unknown_returns_empty(self) -> None:
|
|
131
|
+
assert _role_icon("nonexistent_role") == ""
|