stealth-browserctl 0.0.1__py3-none-any.whl

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 (123) hide show
  1. backend/__init__.py +18 -0
  2. backend/constants.py +20 -0
  3. backend/daemon_client.py +109 -0
  4. backend/daemon_server/README.md +143 -0
  5. backend/daemon_server/__init__.py +6 -0
  6. backend/daemon_server/browser/__init__.py +17 -0
  7. backend/daemon_server/browser/ariasnapshot/__init__.py +11 -0
  8. backend/daemon_server/browser/ariasnapshot/aria/__init__.py +22 -0
  9. backend/daemon_server/browser/ariasnapshot/aria/base.py +123 -0
  10. backend/daemon_server/browser/ariasnapshot/aria/generator_dom.py +89 -0
  11. backend/daemon_server/browser/ariasnapshot/aria/types.py +57 -0
  12. backend/daemon_server/browser/ariasnapshot/aria/types_generated.py +60 -0
  13. backend/daemon_server/browser/ariasnapshot/manager.py +231 -0
  14. backend/daemon_server/browser/browser.py +505 -0
  15. backend/daemon_server/browser/session_config.py +381 -0
  16. backend/daemon_server/commands/__init__.py +23 -0
  17. backend/daemon_server/commands/base_handler.py +122 -0
  18. backend/daemon_server/commands/core_commands.py +243 -0
  19. backend/daemon_server/commands/devtools_commands.py +257 -0
  20. backend/daemon_server/commands/dialog_commands.py +35 -0
  21. backend/daemon_server/commands/input_commands.py +153 -0
  22. backend/daemon_server/commands/interaction_commands.py +289 -0
  23. backend/daemon_server/commands/navigation_commands.py +144 -0
  24. backend/daemon_server/commands/proxy_commands.py +119 -0
  25. backend/daemon_server/commands/session_commands.py +72 -0
  26. backend/daemon_server/commands/storage_commands.py +278 -0
  27. backend/daemon_server/commands/web_commands.py +362 -0
  28. backend/daemon_server/errors.py +193 -0
  29. backend/daemon_server/models.py +67 -0
  30. backend/daemon_server/scripts/README.md +73 -0
  31. backend/daemon_server/scripts/__init__.py +0 -0
  32. backend/daemon_server/scripts/compiled/ariaSnapshot.js +1154 -0
  33. backend/daemon_server/scripts/package.json +16 -0
  34. backend/daemon_server/scripts/script_loader.py +115 -0
  35. backend/daemon_server/scripts/tsconfig.json +17 -0
  36. backend/daemon_server/scripts/typescript/ariaSnapshot.ts +1380 -0
  37. backend/daemon_server/server.py +184 -0
  38. backend/daemon_server.py +143 -0
  39. backend/flow/__init__.py +31 -0
  40. backend/flow/mitm/__init__.py +25 -0
  41. backend/flow/mitm/ca.crt +30 -0
  42. backend/flow/mitm/ca.key +52 -0
  43. backend/flow/mitm/config.py +320 -0
  44. backend/flow/mitm/cust_addons.py +418 -0
  45. backend/flow/mitm/custom_upstream_auth.py +57 -0
  46. backend/flow/mitm/inspect.py +290 -0
  47. backend/flow/mitm/modifier.py +348 -0
  48. backend/flow/mitm/request.py +414 -0
  49. backend/flow/mitm/server.py +478 -0
  50. backend/flow/mitm/tab_storage.py +781 -0
  51. backend/flow/mitm/utils.py +138 -0
  52. backend/flow/v2ray/__init__.py +26 -0
  53. backend/flow/v2ray/config.py +1156 -0
  54. backend/flow/v2ray/daemon_process.py +309 -0
  55. backend/flow/v2ray/manager.py +609 -0
  56. backend/flow/v2ray/pool.py +161 -0
  57. backend/flow/v2ray/process.py +327 -0
  58. backend/flow/v2ray/proxy_airport_cli.py +152 -0
  59. backend/flow/v2ray/subscribers.py +120 -0
  60. backend/human/__init__.py +336 -0
  61. backend/human/config.py +114 -0
  62. backend/human/keyboard.py +145 -0
  63. backend/human/mouse.py +172 -0
  64. backend/human/scroll.py +240 -0
  65. backend/undetected/__init__.py +1042 -0
  66. backend/undetected/readme.me +11 -0
  67. backend/undetected/stealth/__init__.py +53 -0
  68. backend/undetected/stealth/compiled/stealth.js +1022 -0
  69. backend/undetected/stealth/typescript/0_utils.ts +277 -0
  70. backend/undetected/stealth/typescript/chrome.app.ts +75 -0
  71. backend/undetected/stealth/typescript/chrome.csi.ts +32 -0
  72. backend/undetected/stealth/typescript/chrome.loadTimes.ts +109 -0
  73. backend/undetected/stealth/typescript/chrome.runtime.ts +234 -0
  74. backend/undetected/stealth/typescript/hairline.fix.ts +13 -0
  75. backend/undetected/stealth/typescript/iframe.contentWindow.ts +77 -0
  76. backend/undetected/stealth/typescript/media.codecs.ts +50 -0
  77. backend/undetected/stealth/typescript/navigator.languages.ts +7 -0
  78. backend/undetected/stealth/typescript/navigator.permissions.ts +23 -0
  79. backend/undetected/stealth/typescript/navigator.plugins.ts +216 -0
  80. backend/undetected/stealth/typescript/navigator.vendor.ts +7 -0
  81. backend/undetected/stealth/typescript/navigator.webdriver.ts +5 -0
  82. backend/undetected/stealth/typescript/navigator.webrtc.ts +11 -0
  83. backend/undetected/stealth/typescript/readme.md +3 -0
  84. backend/undetected/stealth/typescript/stealth.entry.ts +46 -0
  85. backend/undetected/stealth/typescript/tsconfig.json +17 -0
  86. backend/undetected/stealth/typescript/webgl.vendor.ts +24 -0
  87. backend/undetected/stealth/typescript/window.outerdimensions.ts +12 -0
  88. backend/undetected/undetected_chrome/__init__.py +60 -0
  89. backend/undetected/undetected_chrome/patcher.py +425 -0
  90. backend/undetected/undetected_firefox/__init__.py +52 -0
  91. backend/undetected/undetected_firefox/constants.py +49 -0
  92. backend/undetected/undetected_firefox/patcher.py +286 -0
  93. cli/__init__.py +8 -0
  94. frontend/__init__.py +6 -0
  95. frontend/commands/__init__.py +1 -0
  96. frontend/commands/core.py +238 -0
  97. frontend/commands/devtools.py +412 -0
  98. frontend/commands/input.py +121 -0
  99. frontend/commands/interaction.py +218 -0
  100. frontend/commands/navigation.py +128 -0
  101. frontend/commands/proxy.py +227 -0
  102. frontend/commands/session.py +119 -0
  103. frontend/commands/storage.py +243 -0
  104. frontend/commands/web.py +105 -0
  105. frontend/main.py +179 -0
  106. frontend/skill/browserctl/SKILL.md +786 -0
  107. frontend/skill/browserctl/references/session-management.md +200 -0
  108. stealth_browserctl-0.0.1.dist-info/METADATA +427 -0
  109. stealth_browserctl-0.0.1.dist-info/RECORD +123 -0
  110. stealth_browserctl-0.0.1.dist-info/WHEEL +4 -0
  111. stealth_browserctl-0.0.1.dist-info/entry_points.txt +3 -0
  112. webdriver_bidi/README.md +666 -0
  113. webdriver_bidi/__init__.py +597 -0
  114. webdriver_bidi/actions.py +1799 -0
  115. webdriver_bidi/driver.py +244 -0
  116. webdriver_bidi/errors.py +26 -0
  117. webdriver_bidi/local_value.py +172 -0
  118. webdriver_bidi/locator.py +1549 -0
  119. webdriver_bidi/messenger.py +194 -0
  120. webdriver_bidi/models.py +1119 -0
  121. webdriver_bidi/session.py +433 -0
  122. webdriver_bidi/test_bidi.ipynb +268 -0
  123. webdriver_bidi/wait.py +428 -0
backend/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Backend package for browserctl daemon."""
2
+
3
+ from .daemon_server.server import StealthDaemon
4
+ from .flow import(
5
+ CookieAttributes,
6
+ ResponseCookie,
7
+ RequestSummary,
8
+ RequestDetails,Request
9
+ )
10
+ __all__ = [
11
+ "StealthDaemon",
12
+ "CookieAttributes",
13
+ "ResponseCookie",
14
+ "RequestSummary",
15
+ "RequestDetails","Request"
16
+ ]
17
+
18
+
backend/constants.py ADDED
@@ -0,0 +1,20 @@
1
+ """Configuration constants for browserctl daemon."""
2
+
3
+ from pathlib import Path
4
+
5
+ # Base directory for daemon files
6
+ DAEMON_DIR = Path.home() / ".browserctl"
7
+
8
+ # Base directory for v2ray bin
9
+ DEFAULT_V2RAY_INSTALL_PATH = DAEMON_DIR / "v2ray"
10
+
11
+ # Socket path for IPC (cross-platform)
12
+ # Unix: UDS (Unix Domain Socket)
13
+ # Windows: Named Pipe
14
+ DEFAULT_SOCKET_PATH = DAEMON_DIR / "daemon.sock"
15
+
16
+ # PID file for tracking daemon process
17
+ PID_FILE = DAEMON_DIR / "daemon.pid"
18
+
19
+ # Authentication key for IPC connection
20
+ AUTH_KEY = b'browserctl-daemon-v1'
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ module of Stealth CLI Daemon Client, it's for communicating with the daemon server using multiprocessing.connection.
4
+ """
5
+
6
+ import subprocess
7
+ import time
8
+ from multiprocessing.connection import Client
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+ from loguru import logger
13
+ from backend.constants import DEFAULT_SOCKET_PATH, AUTH_KEY
14
+ from backend.daemon_server.models import DaemonRequest, DaemonResponse
15
+
16
+
17
+ class DaemonClient:
18
+ """Client for communicating with the daemon server using multiprocessing.connection (cross-platform)."""
19
+ def __init__(self, socket_path: Optional[Path] = None):
20
+ self.socket_path = socket_path or DEFAULT_SOCKET_PATH
21
+
22
+ def is_daemon_running(self) -> bool:
23
+ """Check if daemon is running."""
24
+ # On Unix, check if socket file exists
25
+ # On Windows, Named Pipes don't create files
26
+ if self.socket_path.exists():
27
+ return True
28
+
29
+ # Try to connect (works on both platforms)
30
+ try:
31
+ client = Client(str(self.socket_path), authkey=AUTH_KEY)
32
+ client.close()
33
+ return True
34
+ except:
35
+ return False
36
+
37
+ def send_request(
38
+ self,
39
+ command: str,
40
+ args: Optional[Dict[str, Any]] = None,
41
+ session: Optional[str] = None,
42
+ timeout: float = 30.0
43
+ ) -> Any:
44
+ """Send a request to the daemon."""
45
+ # Create structured request (defaults handled by Pydantic model)
46
+ request_data = {'command': command}
47
+ if args is not None:
48
+ request_data['args'] = args
49
+ if session is not None:
50
+ request_data['session'] = session
51
+
52
+ request = DaemonRequest(**request_data)
53
+
54
+ try:
55
+ # Connect to daemon (cross-platform)
56
+ client = Client(str(self.socket_path), authkey=AUTH_KEY)
57
+
58
+ # Send request (auto-serialization)
59
+ client.send(request.model_dump())
60
+
61
+ # Receive response (auto-deserialization)
62
+ response = client.recv()
63
+
64
+ client.close()
65
+
66
+ # Parse response
67
+ resp = DaemonResponse(**response)
68
+
69
+ if not resp.success:
70
+ raise RuntimeError(resp.error or 'Unknown error')
71
+
72
+ return resp.result
73
+
74
+ except Exception as e:
75
+ raise RuntimeError(f"Failed from daemon: {e}")
76
+
77
+ def start_daemon(self) -> None:
78
+ """Start the daemon if not running."""
79
+ if self.is_daemon_running():
80
+ logger.debug("Daemon is already running")
81
+ return
82
+
83
+ # Start daemon using subprocess
84
+ import sys
85
+ daemon_script = Path(__file__).parent / "daemon_server.py"
86
+
87
+ subprocess.run(
88
+ [sys.executable, str(daemon_script), "start"],
89
+ check=True
90
+ )
91
+
92
+ # Wait for daemon to start
93
+ for _ in range(20):
94
+ if self.is_daemon_running():
95
+ logger.info("Daemon started")
96
+ return
97
+ time.sleep(0.5)
98
+
99
+ raise RuntimeError("Failed to start daemon")
100
+
101
+ def stop_daemon(self) -> None:
102
+ """Stop the daemon."""
103
+ import sys
104
+ daemon_script = Path(__file__).parent / "daemon_server.py"
105
+
106
+ subprocess.run(
107
+ [sys.executable, str(daemon_script), "stop"],
108
+ check=True
109
+ )
@@ -0,0 +1,143 @@
1
+ # Daemon Server Module Structure
2
+
3
+ ## Overview
4
+
5
+ The daemon server has been refactored from a single 1268-line file into a well-organized module structure for better maintainability and code organization.
6
+
7
+ ## Module Structure
8
+
9
+ ```
10
+ src/backend/daemon_server/
11
+ ├── __init__.py # Module exports
12
+ ├── base_handler.py # Base command handler class
13
+ ├── server.py # Main daemon server
14
+ ├── core_commands.py # Core browser commands (open, close, goto, etc.)
15
+ ├── interaction_commands.py # Interaction commands (click, fill, type, etc.)
16
+ ├── navigation_commands.py # Navigation commands (tabs, back, forward, etc.)
17
+ ├── input_commands.py # Input commands (keyboard, mouse)
18
+ ├── dialog_commands.py # Dialog commands (accept, dismiss)
19
+ ├── storage_commands.py # Storage commands (cookies, localStorage, sessionStorage)
20
+ └── session_commands.py # Session management commands
21
+ ```
22
+
23
+ ## Architecture
24
+
25
+ ### Command Handler Pattern
26
+
27
+ Each command category is implemented as a separate handler class:
28
+
29
+ ```python
30
+ class CommandHandler:
31
+ """Base class for command handlers."""
32
+
33
+ def can_handle(self, command: str) -> bool:
34
+ """Check if this handler can handle the command."""
35
+ raise NotImplementedError
36
+
37
+ def execute(self, session, command: str, args: Dict[str, Any]) -> Any:
38
+ """Execute the command."""
39
+ raise NotImplementedError
40
+ ```
41
+
42
+ ### Handler Categories
43
+
44
+ 1. **CoreCommands** - Basic browser operations
45
+ - `open`, `close`, `goto`, `snapshot`, `screenshot`, `pdf`, `resize`, `eval`
46
+
47
+ 2. **InteractionCommands** - Element interactions
48
+ - `click`, `dblclick`, `type`, `fill`, `press`, `hover`, `drag`, `select`, `upload`, `check`, `uncheck`
49
+
50
+ 3. **NavigationCommands** - Navigation control
51
+ - `go_back`, `go_forward`, `reload`, `new_tab`, `list_tab`, `close_tab`, `select_tab`
52
+
53
+ 4. **InputCommands** - Keyboard and mouse input
54
+ - `keydown`, `keyup`, `mousemove`, `mousedown`, `mouseup`, `mousewheel`
55
+
56
+ 5. **DialogCommands** - Alert/dialog handling
57
+ - `dialog_accept`, `dialog_dismiss`
58
+
59
+ 6. **StorageCommands** - Storage management
60
+ - State: `state_save`, `state_load`
61
+ - Cookies: `cookie_list`, `cookie_get`, `cookie_set`, `cookie_delete`, `cookie_clear`
62
+ - LocalStorage: `localstorage_list`, `localstorage_get`, `localstorage_set`, `localstorage_delete`, `localstorage_clear`
63
+ - SessionStorage: `sessionstorage_list`, `sessionstorage_get`, `sessionstorage_set`, `sessionstorage_delete`, `sessionstorage_clear`
64
+
65
+ 7. **SessionCommands** - Session management
66
+ - `list_sessions`, `close_all`, `kill_all`, `delete_data`
67
+
68
+ ### Main Server
69
+
70
+ The `StealthDaemon` class in `server.py`:
71
+ - Manages the Unix socket server
72
+ - Handles client connections
73
+ - Routes commands to appropriate handlers
74
+ - Manages browser sessions via `BrowserManager`
75
+
76
+ ```python
77
+ class StealthDaemon:
78
+ def __init__(self):
79
+ self.browser_manager = BrowserManager()
80
+ self.handlers = [
81
+ CoreCommands(),
82
+ InteractionCommands(),
83
+ NavigationCommands(),
84
+ InputCommands(),
85
+ DialogCommands(),
86
+ StorageCommands(),
87
+ SessionCommands(self.browser_manager),
88
+ ]
89
+
90
+ def _execute_command(self, session, command: str, args: Dict[str, Any]) -> Any:
91
+ """Execute a command on a browser session."""
92
+ for handler in self.handlers:
93
+ if handler.can_handle(command):
94
+ return handler.execute(session, command, args)
95
+ raise ValueError(f"Unknown command: {command}")
96
+ ```
97
+
98
+ ## Usage
99
+
100
+ The module is used exactly the same way as before:
101
+
102
+ ```python
103
+ from backend.daemon_server import StealthDaemon
104
+
105
+ daemon = StealthDaemon()
106
+ daemon.start()
107
+ ```
108
+
109
+ ## Adding New Commands
110
+
111
+ To add a new command:
112
+
113
+ 1. Identify the appropriate handler category
114
+ 2. Add the command to the `COMMANDS` list
115
+ 3. Implement the `_cmd_<command>` method
116
+ 4. Test the command
117
+
118
+ Example:
119
+
120
+ ```python
121
+ # In interaction_commands.py
122
+
123
+ class InteractionCommands(CommandHandler):
124
+ COMMANDS = [
125
+ 'click', 'dblclick', 'type', 'fill', 'press',
126
+ 'hover', 'drag', 'select', 'upload', 'check', 'uncheck',
127
+ 'new_command' # Add here
128
+ ]
129
+
130
+ def _cmd_new_command(self, session, args: Dict[str, Any]) -> None:
131
+ """New command implementation."""
132
+ # Implementation here
133
+ pass
134
+ ```
135
+
136
+ ## Future Improvements
137
+
138
+ 1. **Add Type Hints**: Add comprehensive type annotations
139
+ 2. **Add Docstrings**: Improve documentation for each method
140
+ 3. **Add Unit Tests**: Create tests for each handler
141
+ 4. **Add Logging**: Add more detailed logging
142
+ 5. **Add Validation**: Add input validation for commands
143
+ 6. **Add Error Handling**: Improve error messages and recovery
@@ -0,0 +1,6 @@
1
+ from .models import DaemonRequest
2
+
3
+ __all__ = [
4
+ 'DaemonRequest',
5
+ ]
6
+
@@ -0,0 +1,17 @@
1
+ from .browser import BrowserManager,BrowserSession
2
+ from .ariasnapshot import AriaSnapshotManager
3
+ from .session_config import (
4
+ SessionConfig,
5
+ BrowserConfig,
6
+ TimeoutsConfig,
7
+ )
8
+
9
+
10
+ __all__ = [
11
+ "BrowserManager","BrowserSession",
12
+ "AriaSnapshotManager",
13
+ "SessionConfig",
14
+ "BrowserConfig",
15
+ "TimeoutsConfig",
16
+
17
+ ]
@@ -0,0 +1,11 @@
1
+ """
2
+ Playwright ariaSnapshot tool - Capture and validate accessibility trees.
3
+ """
4
+
5
+ from .manager import AriaSnapshotManager
6
+
7
+ __version__ = '1.0.0'
8
+
9
+ __all__ = [
10
+ 'AriaSnapshotManager',
11
+ ]
@@ -0,0 +1,22 @@
1
+ """
2
+ ARIA snapshot generation and rendering.
3
+ """
4
+
5
+ from .types import (
6
+ AriaBox, ScrollInfo, AriaExtraProps, AriaNode, AriaSnapshot,
7
+ AriaTreeOptions
8
+ )
9
+ from .base import AriaTreeGenerator, AriaTreeResult
10
+ from .generator_dom import DOMAriaTreeGenerator
11
+
12
+ __all__ = [
13
+ # Types
14
+ 'AriaBox', 'ScrollInfo', 'AriaExtraProps', 'AriaNode', 'AriaSnapshot',
15
+ 'AriaTreeOptions',
16
+
17
+ # Base classes
18
+ 'AriaTreeGenerator', 'AriaTreeResult',
19
+
20
+ # Generators
21
+ 'DOMAriaTreeGenerator'
22
+ ]
@@ -0,0 +1,123 @@
1
+ """
2
+ Base Aria Tree Generator - Abstract interface for accessibility tree generation.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, Union, Optional, Any, List
7
+ from dataclasses import dataclass
8
+ import json
9
+ from .types import AriaTreeOptions, AriaSnapshot, AriaNode
10
+
11
+
12
+ # Helper functions for working with TypedDict
13
+
14
+
15
+ def is_node_interactable(node: AriaNode) -> bool:
16
+ """
17
+ Check if a node is interactable.
18
+
19
+ Args:
20
+ node: The AriaNode to check
21
+
22
+ Returns:
23
+ True if the node is interactable, False otherwise
24
+ """
25
+ # Check if disabled
26
+ if node.get('disabled'):
27
+ return False
28
+
29
+ # Check if visible
30
+ box = node.get('box')
31
+ if box and not box.get('visible', True):
32
+ return False
33
+
34
+ return True
35
+
36
+
37
+ @dataclass
38
+ class AriaTreeResult:
39
+ """
40
+ Result of aria tree generation.
41
+
42
+ Attributes:
43
+ snapshot: The accessibility tree snapshot
44
+ ref_map: Mapping from ref string to XPath, e.g., {'ref1': '/html/body/div[1]'}
45
+ method: Generation method used ('dom' or 'cdp')
46
+ yaml_str: Pre-generated YAML string (from JavaScript)
47
+ """
48
+ snapshot: AriaSnapshot
49
+ ref_map: Dict[str, str]
50
+ method: str
51
+ yaml_str: str = '' # Pre-generated YAML from JavaScript
52
+
53
+ def to_dict(self) -> Dict:
54
+ """Convert to dictionary format."""
55
+ return {
56
+ 'snapshot': self.snapshot,
57
+ 'refMap': self.ref_map,
58
+ 'method': self.method
59
+ }
60
+
61
+ def to_yaml(self) -> str:
62
+ """
63
+ Render an aria tree to YAML string.
64
+
65
+ Returns:
66
+ YAML string representation of the snapshot
67
+ """
68
+ # Use pre-generated YAML if available (from JavaScript)
69
+ return self.yaml_str
70
+
71
+ def to_json(self, compact: bool = False) -> str:
72
+ """
73
+ Render an aria tree to JSON string.
74
+
75
+ Args:
76
+ compact: If True, use compact JSON format (no indentation)
77
+
78
+ Returns:
79
+ JSON string representation of the snapshot
80
+ """
81
+ # Use the original structure directly, no simplification needed
82
+ if compact:
83
+ return json.dumps(self.snapshot['root'], separators=(',', ':'), ensure_ascii=False)
84
+ else:
85
+ return json.dumps(self.snapshot['root'], indent=2, ensure_ascii=False)
86
+
87
+ @staticmethod
88
+ def _normalize_whitespace(text: str) -> str:
89
+ """Normalize whitespace in text."""
90
+ if not text:
91
+ return ''
92
+ return ' '.join(text.split())
93
+
94
+ @staticmethod
95
+ def _escape_yaml(text: str) -> str:
96
+ """Escape text for YAML output."""
97
+ if not text:
98
+ return ''
99
+ text = text.replace('\\', '\\\\')
100
+ text = text.replace('"', '\\"')
101
+ return text
102
+
103
+ class AriaTreeGenerator(ABC):
104
+ """Abstract base class for aria tree generators."""
105
+
106
+ @abstractmethod
107
+ def generate(
108
+ self,
109
+ root_element: Any,
110
+ options: AriaTreeOptions
111
+ ) -> AriaTreeResult:
112
+ """
113
+ Generate an accessibility tree from a DOM element.
114
+
115
+ Args:
116
+ root_element: Root DOM element to start from
117
+ options: Options for tree generation
118
+
119
+ Returns:
120
+ AriaTreeResult containing snapshot, ref_map, and method
121
+ """
122
+ pass
123
+
@@ -0,0 +1,89 @@
1
+ """
2
+ DOM-based Aria Tree Generator.
3
+ Uses JavaScript injection to compute accessibility tree from DOM.
4
+ """
5
+
6
+ from typing import Any, Dict, Optional
7
+
8
+ from .base import AriaTreeGenerator, AriaTreeResult
9
+ from .types import AriaTreeOptions, AriaSnapshot
10
+ from backend.daemon_server.scripts.script_loader import ScriptLoader
11
+ from webdriver_bidi.local_value import LocalValue as V
12
+
13
+ ARIA_SNAPSHOT_SYMBOL_KEY = "stealth.ariaSnapshot.v1"
14
+
15
+
16
+ class DOMAriaTreeGenerator(AriaTreeGenerator):
17
+ """
18
+ DOM-based accessibility tree generator.
19
+
20
+ Uses JavaScript to walk the DOM and compute accessibility properties.
21
+ Fast and works with any browser, but may miss some ARIA edge cases.
22
+ """
23
+
24
+ def __init__(self) -> None:
25
+ """Initialize the DOM generator."""
26
+ self._tool: Optional[ScriptLoader] = None
27
+
28
+ def generate(
29
+ self,
30
+ root_element: Any,
31
+ options: AriaTreeOptions,
32
+ driver: Any = None
33
+ ) -> AriaTreeResult:
34
+ """
35
+ Generate an accessibility tree from a DOM element using JavaScript.
36
+
37
+ Args:
38
+ root_element: Root DOM element to start from (BiDiElement)
39
+ options: Options for tree generation
40
+ driver: BiDi-based driver instance (required)
41
+
42
+ Returns:
43
+ AriaTreeResult containing snapshot, ref_map, and method
44
+ """
45
+ if driver is None:
46
+ raise ValueError("driver must be provided to DOMAriaTreeGenerator.generate()")
47
+
48
+ # Prepare previousXpathToRef for stable refs (invert the map for O(1) lookup)
49
+ previous_ref_map = options.get('previous_ref_map')
50
+ previous_xpath_to_ref = None
51
+ if previous_ref_map:
52
+ previous_xpath_to_ref = {
53
+ xpath: ref for ref, xpath in previous_ref_map.items()
54
+ }
55
+
56
+ # Build options object (AI mode only)
57
+ ai_options = {
58
+ 'depth': options.get('depth'),
59
+ 'boxes': options.get('boxes', False),
60
+ }
61
+
62
+ # Use ScriptLoader to call the API
63
+ if self._tool is None:
64
+ self._tool = ScriptLoader(
65
+ driver,
66
+ ARIA_SNAPSHOT_SYMBOL_KEY,
67
+ 'ariaSnapshot.js'
68
+ )
69
+
70
+ # Build BiDi LocalValue arguments explicitly
71
+ result = self._tool.call(
72
+ 'generateAriaSnapshot',
73
+ V.element(root_element.shared_id), # root element
74
+ V.from_value(ai_options), # options dict
75
+ V.from_value(previous_xpath_to_ref), # previousXpathToRef
76
+ V.boolean(True), # generate_yaml=True
77
+ return_value=True
78
+ )
79
+
80
+ # Extract snapshot, refMap, and YAML from JavaScript result
81
+ snapshot_dict = result.get('snapshot', result)
82
+ yaml_str = result.get('snapshot_yaml', '')
83
+
84
+ return AriaTreeResult(
85
+ snapshot=snapshot_dict,
86
+ ref_map=result.get('refMap', {}),
87
+ method='dom',
88
+ yaml_str=yaml_str
89
+ )
@@ -0,0 +1,57 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ from enum import Enum
18
+ from typing_extensions import TypedDict, NotRequired,Optional
19
+
20
+ # Import auto-generated TypedDict types from TypeScript
21
+ # These are automatically generated by: npm run build:types
22
+ from .types_generated import (
23
+ ScrollInfo,
24
+ AriaBox,
25
+ AriaExtraProps,
26
+ AriaNode,
27
+ AriaTreeOptions as GeneratedAriaTreeOptions,
28
+ AriaTreeResult
29
+ )
30
+
31
+ # Re-export the generated types
32
+ __all__ = [
33
+ 'ScrollInfo', 'AriaBox', 'AriaExtraProps', 'AriaNode',
34
+ 'AriaTreeOptions', 'AriaSnapshot', 'AriaTreeResult',
35
+ ]
36
+
37
+
38
+ # Define AriaSnapshot (wrapper for root node)
39
+ class AriaSnapshot(TypedDict):
40
+ """
41
+ Complete accessibility snapshot.
42
+
43
+ Structure: { root: AriaNode }
44
+ """
45
+ root: AriaNode
46
+
47
+
48
+ # Extend AriaTreeOptions with additional Python-specific fields
49
+ class AriaTreeOptions(GeneratedAriaTreeOptions, total=False):
50
+ """
51
+ Options for generating aria tree for AI/LLM consumption.
52
+
53
+ Extends the auto-generated AriaTreeOptions with Python-specific options.
54
+ """
55
+ ref_prefix: str
56
+ depth: Optional[int]
57
+ previous_ref_map: dict[str, str]
@@ -0,0 +1,60 @@
1
+ from typing_extensions import Dict, List, Literal, NotRequired, TypedDict, Union
2
+
3
+ class ScrollInfo(TypedDict):
4
+ scrollX: float
5
+ scrollY: float
6
+ scrollWidth: float
7
+ scrollHeight: float
8
+ clientWidth: float
9
+ clientHeight: float
10
+ scrollable: bool
11
+
12
+ class AriaBox(TypedDict):
13
+ visible: bool
14
+ inline: bool
15
+ cursor: Union[None,str]
16
+ x: NotRequired[float]
17
+ y: NotRequired[float]
18
+ width: NotRequired[float]
19
+ height: NotRequired[float]
20
+
21
+ class AriaExtraProps(TypedDict):
22
+ url: NotRequired[str]
23
+ placeholder: NotRequired[str]
24
+ src: NotRequired[str]
25
+ size: NotRequired[str]
26
+
27
+ class AriaNode(TypedDict):
28
+ role: str
29
+ name: str
30
+ children: List[Union[str,'AriaNode']]
31
+ props: AriaExtraProps
32
+ box: AriaBox
33
+ receivesPointerEvents: bool
34
+ ref: NotRequired[str]
35
+ scroll: NotRequired[ScrollInfo]
36
+ active: NotRequired[bool]
37
+ checked: NotRequired[Union[Literal[False],Literal[True],Literal["mixed"]]]
38
+ disabled: NotRequired[bool]
39
+ expanded: NotRequired[bool]
40
+ level: NotRequired[float]
41
+ pressed: NotRequired[Union[Literal[False],Literal[True],Literal["mixed"]]]
42
+ selected: NotRequired[bool]
43
+ value: NotRequired[float]
44
+ min: NotRequired[float]
45
+ max: NotRequired[float]
46
+ modal: NotRequired[bool]
47
+
48
+ class AriaTreeOptions(TypedDict):
49
+ refPrefix: NotRequired[str]
50
+ doNotRenderActive: NotRequired[bool]
51
+ depth: NotRequired[float]
52
+ boxes: NotRequired[bool]
53
+
54
+ class Ts2Py_RiRAjt1ZVF(TypedDict):
55
+ root: AriaNode
56
+
57
+ class AriaTreeResult(TypedDict):
58
+ snapshot: Ts2Py_RiRAjt1ZVF
59
+ refMap: Dict[str,str]
60
+ snapshot_yaml: NotRequired[str]