sentienceapi 0.90.16__py3-none-any.whl → 0.98.0__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.

Potentially problematic release.


This version of sentienceapi might be problematic. Click here for more details.

Files changed (90) hide show
  1. sentience/__init__.py +120 -6
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +217 -0
  4. sentience/actions.py +758 -30
  5. sentience/agent.py +806 -293
  6. sentience/agent_config.py +3 -0
  7. sentience/agent_runtime.py +840 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +89 -1141
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +372 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +483 -0
  21. sentience/base_agent.py +95 -0
  22. sentience/browser.py +678 -39
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cloud_tracing.py +507 -42
  26. sentience/constants.py +6 -0
  27. sentience/conversational_agent.py +77 -43
  28. sentience/cursor_policy.py +142 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +98 -2
  31. sentience/extension/background.js +56 -185
  32. sentience/extension/content.js +150 -287
  33. sentience/extension/injected_api.js +1088 -1368
  34. sentience/extension/manifest.json +1 -1
  35. sentience/extension/pkg/sentience_core.d.ts +22 -22
  36. sentience/extension/pkg/sentience_core.js +275 -433
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/release.json +47 -47
  39. sentience/failure_artifacts.py +241 -0
  40. sentience/formatting.py +9 -53
  41. sentience/inspector.py +183 -1
  42. sentience/integrations/__init__.py +6 -0
  43. sentience/integrations/langchain/__init__.py +12 -0
  44. sentience/integrations/langchain/context.py +18 -0
  45. sentience/integrations/langchain/core.py +326 -0
  46. sentience/integrations/langchain/tools.py +180 -0
  47. sentience/integrations/models.py +46 -0
  48. sentience/integrations/pydanticai/__init__.py +15 -0
  49. sentience/integrations/pydanticai/deps.py +20 -0
  50. sentience/integrations/pydanticai/toolset.py +468 -0
  51. sentience/llm_interaction_handler.py +191 -0
  52. sentience/llm_provider.py +765 -66
  53. sentience/llm_provider_utils.py +120 -0
  54. sentience/llm_response_builder.py +153 -0
  55. sentience/models.py +595 -3
  56. sentience/ordinal.py +280 -0
  57. sentience/overlay.py +109 -2
  58. sentience/protocols.py +228 -0
  59. sentience/query.py +67 -5
  60. sentience/read.py +95 -3
  61. sentience/recorder.py +223 -3
  62. sentience/schemas/trace_v1.json +128 -9
  63. sentience/screenshot.py +48 -2
  64. sentience/sentience_methods.py +86 -0
  65. sentience/snapshot.py +599 -55
  66. sentience/snapshot_diff.py +126 -0
  67. sentience/text_search.py +120 -5
  68. sentience/trace_event_builder.py +148 -0
  69. sentience/trace_file_manager.py +197 -0
  70. sentience/trace_indexing/index_schema.py +95 -7
  71. sentience/trace_indexing/indexer.py +105 -48
  72. sentience/tracer_factory.py +120 -9
  73. sentience/tracing.py +172 -8
  74. sentience/utils/__init__.py +40 -0
  75. sentience/utils/browser.py +46 -0
  76. sentience/{utils.py → utils/element.py} +3 -42
  77. sentience/utils/formatting.py +59 -0
  78. sentience/verification.py +618 -0
  79. sentience/visual_agent.py +2058 -0
  80. sentience/wait.py +68 -2
  81. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
  82. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  83. sentience/extension/test-content.js +0 -4
  84. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  85. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  86. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  87. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  88. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  89. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  90. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,211 @@
1
+ """
2
+ Custom exceptions for Sentience backends.
3
+
4
+ These exceptions provide clear, actionable error messages when things go wrong
5
+ during browser-use integration or backend operations.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+
12
+ class SentienceBackendError(Exception):
13
+ """Base exception for all Sentience backend errors."""
14
+
15
+ pass
16
+
17
+
18
+ @dataclass
19
+ class ExtensionDiagnostics:
20
+ """Diagnostics collected when extension loading fails."""
21
+
22
+ sentience_defined: bool = False
23
+ sentience_snapshot: bool = False
24
+ url: str = ""
25
+ error: str | None = None
26
+
27
+ @classmethod
28
+ def from_dict(cls, data: dict[str, Any]) -> "ExtensionDiagnostics":
29
+ """Create from diagnostic dict returned by browser eval."""
30
+ return cls(
31
+ sentience_defined=data.get("sentience_defined", False),
32
+ sentience_snapshot=data.get("sentience_snapshot", False),
33
+ url=data.get("url", ""),
34
+ error=data.get("error"),
35
+ )
36
+
37
+ def to_dict(self) -> dict[str, Any]:
38
+ """Convert to dict for serialization."""
39
+ return {
40
+ "sentience_defined": self.sentience_defined,
41
+ "sentience_snapshot": self.sentience_snapshot,
42
+ "url": self.url,
43
+ "error": self.error,
44
+ }
45
+
46
+
47
+ class ExtensionNotLoadedError(SentienceBackendError):
48
+ """
49
+ Raised when the Sentience extension is not loaded in the browser.
50
+
51
+ This typically means:
52
+ 1. Browser was launched without --load-extension flag
53
+ 2. Extension path is incorrect
54
+ 3. Extension failed to initialize
55
+
56
+ Example fix for browser-use:
57
+ from sentience import get_extension_dir
58
+ from browser_use import BrowserSession, BrowserProfile
59
+
60
+ profile = BrowserProfile(
61
+ args=[f"--load-extension={get_extension_dir()}"],
62
+ )
63
+ session = BrowserSession(browser_profile=profile)
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ message: str,
69
+ timeout_ms: int | None = None,
70
+ diagnostics: ExtensionDiagnostics | None = None,
71
+ ) -> None:
72
+ self.timeout_ms = timeout_ms
73
+ self.diagnostics = diagnostics
74
+ super().__init__(message)
75
+
76
+ @classmethod
77
+ def from_timeout(
78
+ cls,
79
+ timeout_ms: int,
80
+ diagnostics: ExtensionDiagnostics | None = None,
81
+ ) -> "ExtensionNotLoadedError":
82
+ """Create error from timeout during extension wait."""
83
+ diag_info = ""
84
+ if diagnostics:
85
+ if diagnostics.error:
86
+ diag_info = f"\n Error: {diagnostics.error}"
87
+ else:
88
+ diag_info = (
89
+ f"\n window.sentience defined: {diagnostics.sentience_defined}"
90
+ f"\n window.sentience.snapshot available: {diagnostics.sentience_snapshot}"
91
+ f"\n Page URL: {diagnostics.url}"
92
+ )
93
+
94
+ message = (
95
+ f"Sentience extension not loaded after {timeout_ms}ms.{diag_info}\n\n"
96
+ "To fix this, ensure the extension is loaded when launching the browser:\n\n"
97
+ " from sentience import get_extension_dir\n"
98
+ " from browser_use import BrowserSession, BrowserProfile\n\n"
99
+ " profile = BrowserProfile(\n"
100
+ f' args=[f"--load-extension={{get_extension_dir()}}"],\n'
101
+ " )\n"
102
+ " session = BrowserSession(browser_profile=profile)\n"
103
+ )
104
+ return cls(message, timeout_ms=timeout_ms, diagnostics=diagnostics)
105
+
106
+
107
+ class ExtensionInjectionError(SentienceBackendError):
108
+ """
109
+ Raised when window.sentience API is not available on the page.
110
+
111
+ This can happen when:
112
+ 1. Page loaded before extension could inject
113
+ 2. Page has Content Security Policy blocking extension
114
+ 3. Extension crashed or was disabled
115
+
116
+ Call snapshot() with a longer timeout or wait for page load.
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ message: str,
122
+ url: str | None = None,
123
+ ) -> None:
124
+ self.url = url
125
+ super().__init__(message)
126
+
127
+ @classmethod
128
+ def from_page(cls, url: str) -> "ExtensionInjectionError":
129
+ """Create error for a specific page."""
130
+ message = (
131
+ f"window.sentience API not available on page: {url}\n\n"
132
+ "Possible causes:\n"
133
+ " 1. Page loaded before extension could inject (try increasing timeout)\n"
134
+ " 2. Page has Content Security Policy blocking the extension\n"
135
+ " 3. Extension was disabled or crashed\n\n"
136
+ "Try:\n"
137
+ " snap = await snapshot(backend, options=SnapshotOptions(timeout_ms=10000))"
138
+ )
139
+ return cls(message, url=url)
140
+
141
+
142
+ class BackendEvalError(SentienceBackendError):
143
+ """
144
+ Raised when JavaScript evaluation fails in the browser.
145
+
146
+ This wraps underlying CDP or Playwright errors with context.
147
+ """
148
+
149
+ def __init__(
150
+ self,
151
+ message: str,
152
+ expression: str | None = None,
153
+ original_error: Exception | None = None,
154
+ ) -> None:
155
+ self.expression = expression
156
+ self.original_error = original_error
157
+ super().__init__(message)
158
+
159
+
160
+ class SnapshotError(SentienceBackendError):
161
+ """
162
+ Raised when taking a snapshot fails.
163
+
164
+ This can happen when:
165
+ 1. Extension returned null or invalid data
166
+ 2. Page is in an invalid state
167
+ 3. Extension threw an error
168
+ """
169
+
170
+ def __init__(
171
+ self,
172
+ message: str,
173
+ url: str | None = None,
174
+ raw_result: Any = None,
175
+ ) -> None:
176
+ self.url = url
177
+ self.raw_result = raw_result
178
+ super().__init__(message)
179
+
180
+ @classmethod
181
+ def from_null_result(cls, url: str | None = None) -> "SnapshotError":
182
+ """Create error for null snapshot result."""
183
+ message = (
184
+ "window.sentience.snapshot() returned null.\n\n"
185
+ "Possible causes:\n"
186
+ " 1. Extension is not properly initialized\n"
187
+ " 2. Page DOM is in an invalid state\n"
188
+ " 3. Extension encountered an internal error\n\n"
189
+ "Try refreshing the page and taking a new snapshot."
190
+ )
191
+ if url:
192
+ message = f"{message}\n Page URL: {url}"
193
+ return cls(message, url=url, raw_result=None)
194
+
195
+
196
+ class ActionError(SentienceBackendError):
197
+ """
198
+ Raised when a browser action (click, type, scroll) fails.
199
+ """
200
+
201
+ def __init__(
202
+ self,
203
+ action: str,
204
+ message: str,
205
+ coordinates: tuple[float, float] | None = None,
206
+ original_error: Exception | None = None,
207
+ ) -> None:
208
+ self.action = action
209
+ self.coordinates = coordinates
210
+ self.original_error = original_error
211
+ super().__init__(f"{action} failed: {message}")
@@ -0,0 +1,194 @@
1
+ """
2
+ Playwright backend implementation for BrowserBackend protocol.
3
+
4
+ This wraps existing SentienceBrowser/AsyncSentienceBrowser to provide
5
+ a unified interface, enabling code that works with both browser-use
6
+ (CDPBackendV0) and native Playwright (PlaywrightBackend).
7
+
8
+ Usage:
9
+ from sentience import SentienceBrowserAsync
10
+ from sentience.backends import PlaywrightBackend, snapshot_from_backend
11
+
12
+ browser = SentienceBrowserAsync()
13
+ await browser.start()
14
+ await browser.goto("https://example.com")
15
+
16
+ # Create backend from existing browser
17
+ backend = PlaywrightBackend(browser.page)
18
+
19
+ # Use backend-agnostic functions
20
+ snap = await snapshot_from_backend(backend)
21
+ await click(backend, element.bbox)
22
+ """
23
+
24
+ import asyncio
25
+ import base64
26
+ import time
27
+ from typing import TYPE_CHECKING, Any, Literal
28
+
29
+ from .protocol import BrowserBackend, LayoutMetrics, ViewportInfo
30
+
31
+ if TYPE_CHECKING:
32
+ from playwright.async_api import Page as AsyncPage
33
+
34
+
35
+ class PlaywrightBackend:
36
+ """
37
+ Playwright-based implementation of BrowserBackend.
38
+
39
+ Wraps a Playwright async Page to provide the standard backend interface.
40
+ This enables using backend-agnostic actions with existing SentienceBrowser code.
41
+ """
42
+
43
+ def __init__(self, page: "AsyncPage") -> None:
44
+ """
45
+ Initialize Playwright backend.
46
+
47
+ Args:
48
+ page: Playwright async Page object
49
+ """
50
+ self._page = page
51
+ self._cached_viewport: ViewportInfo | None = None
52
+
53
+ @property
54
+ def page(self) -> "AsyncPage":
55
+ """Access the underlying Playwright page."""
56
+ return self._page
57
+
58
+ async def refresh_page_info(self) -> ViewportInfo:
59
+ """Cache viewport + scroll offsets; cheap & safe to call often."""
60
+ result = await self._page.evaluate(
61
+ """
62
+ (() => ({
63
+ width: window.innerWidth,
64
+ height: window.innerHeight,
65
+ scroll_x: window.scrollX,
66
+ scroll_y: window.scrollY,
67
+ content_width: document.documentElement.scrollWidth,
68
+ content_height: document.documentElement.scrollHeight
69
+ }))()
70
+ """
71
+ )
72
+
73
+ self._cached_viewport = ViewportInfo(
74
+ width=result.get("width", 0),
75
+ height=result.get("height", 0),
76
+ scroll_x=result.get("scroll_x", 0),
77
+ scroll_y=result.get("scroll_y", 0),
78
+ content_width=result.get("content_width"),
79
+ content_height=result.get("content_height"),
80
+ )
81
+ return self._cached_viewport
82
+
83
+ async def eval(self, expression: str) -> Any:
84
+ """Evaluate JavaScript expression in page context."""
85
+ return await self._page.evaluate(expression)
86
+
87
+ async def call(
88
+ self,
89
+ function_declaration: str,
90
+ args: list[Any] | None = None,
91
+ ) -> Any:
92
+ """Call JavaScript function with arguments."""
93
+ if args:
94
+ return await self._page.evaluate(function_declaration, *args)
95
+ return await self._page.evaluate(f"({function_declaration})()")
96
+
97
+ async def get_layout_metrics(self) -> LayoutMetrics:
98
+ """Get page layout metrics."""
99
+ # Playwright doesn't expose CDP directly in the same way,
100
+ # so we approximate using JavaScript
101
+ result = await self._page.evaluate(
102
+ """
103
+ (() => ({
104
+ viewport_x: window.scrollX,
105
+ viewport_y: window.scrollY,
106
+ viewport_width: window.innerWidth,
107
+ viewport_height: window.innerHeight,
108
+ content_width: document.documentElement.scrollWidth,
109
+ content_height: document.documentElement.scrollHeight,
110
+ device_scale_factor: window.devicePixelRatio || 1
111
+ }))()
112
+ """
113
+ )
114
+
115
+ return LayoutMetrics(
116
+ viewport_x=result.get("viewport_x", 0),
117
+ viewport_y=result.get("viewport_y", 0),
118
+ viewport_width=result.get("viewport_width", 0),
119
+ viewport_height=result.get("viewport_height", 0),
120
+ content_width=result.get("content_width", 0),
121
+ content_height=result.get("content_height", 0),
122
+ device_scale_factor=result.get("device_scale_factor", 1.0),
123
+ )
124
+
125
+ async def screenshot_png(self) -> bytes:
126
+ """Capture viewport screenshot as PNG bytes."""
127
+ return await self._page.screenshot(type="png")
128
+
129
+ async def mouse_move(self, x: float, y: float) -> None:
130
+ """Move mouse to viewport coordinates."""
131
+ await self._page.mouse.move(x, y)
132
+
133
+ async def mouse_click(
134
+ self,
135
+ x: float,
136
+ y: float,
137
+ button: Literal["left", "right", "middle"] = "left",
138
+ click_count: int = 1,
139
+ ) -> None:
140
+ """Click at viewport coordinates."""
141
+ await self._page.mouse.click(x, y, button=button, click_count=click_count)
142
+
143
+ async def wheel(
144
+ self,
145
+ delta_y: float,
146
+ x: float | None = None,
147
+ y: float | None = None,
148
+ ) -> None:
149
+ """Scroll using mouse wheel."""
150
+ # Get viewport center if coordinates not provided
151
+ if x is None or y is None:
152
+ if self._cached_viewport is None:
153
+ await self.refresh_page_info()
154
+ assert self._cached_viewport is not None
155
+ x = x if x is not None else self._cached_viewport.width / 2
156
+ y = y if y is not None else self._cached_viewport.height / 2
157
+
158
+ await self._page.mouse.wheel(0, delta_y)
159
+
160
+ async def type_text(self, text: str) -> None:
161
+ """Type text using keyboard input."""
162
+ await self._page.keyboard.type(text)
163
+
164
+ async def wait_ready_state(
165
+ self,
166
+ state: Literal["interactive", "complete"] = "interactive",
167
+ timeout_ms: int = 15000,
168
+ ) -> None:
169
+ """Wait for document.readyState to reach target state."""
170
+ acceptable_states = {"complete"} if state == "complete" else {"interactive", "complete"}
171
+
172
+ start = time.monotonic()
173
+ timeout_sec = timeout_ms / 1000.0
174
+
175
+ while True:
176
+ elapsed = time.monotonic() - start
177
+ if elapsed >= timeout_sec:
178
+ raise TimeoutError(
179
+ f"Timed out waiting for document.readyState='{state}' " f"after {timeout_ms}ms"
180
+ )
181
+
182
+ current_state = await self._page.evaluate("document.readyState")
183
+ if current_state in acceptable_states:
184
+ return
185
+
186
+ await asyncio.sleep(0.1)
187
+
188
+ async def get_url(self) -> str:
189
+ """Get current page URL."""
190
+ return self._page.url
191
+
192
+
193
+ # Verify protocol compliance at import time
194
+ assert isinstance(PlaywrightBackend.__new__(PlaywrightBackend), BrowserBackend)
@@ -0,0 +1,216 @@
1
+ """
2
+ v0 BrowserBackend Protocol - Minimal interface for browser-use integration.
3
+
4
+ This protocol defines the minimal interface required to:
5
+ - Take Sentience snapshots (DOM/geometry via extension)
6
+ - Compute viewport-coord clicks
7
+ - Scroll + re-snapshot + click
8
+ - Stabilize after action
9
+
10
+ No navigation API required (browser-use already handles navigation).
11
+
12
+ Design principle: Keep it so small that nothing can break.
13
+ """
14
+
15
+ from typing import Any, Literal, Protocol, runtime_checkable
16
+
17
+ from pydantic import BaseModel
18
+
19
+
20
+ class ViewportInfo(BaseModel):
21
+ """Viewport and scroll position information."""
22
+
23
+ width: int
24
+ height: int
25
+ scroll_x: float = 0.0
26
+ scroll_y: float = 0.0
27
+ content_width: float | None = None
28
+ content_height: float | None = None
29
+
30
+
31
+ class LayoutMetrics(BaseModel):
32
+ """Page layout metrics from CDP Page.getLayoutMetrics."""
33
+
34
+ # Viewport dimensions
35
+ viewport_x: float = 0.0
36
+ viewport_y: float = 0.0
37
+ viewport_width: float = 0.0
38
+ viewport_height: float = 0.0
39
+
40
+ # Content dimensions (scrollable area)
41
+ content_width: float = 0.0
42
+ content_height: float = 0.0
43
+
44
+ # Device scale factor
45
+ device_scale_factor: float = 1.0
46
+
47
+
48
+ @runtime_checkable
49
+ class BrowserBackend(Protocol):
50
+ """
51
+ Minimal backend protocol for v0 proof-of-concept.
52
+
53
+ This is enough to:
54
+ - Take Sentience snapshots (DOM/geometry via extension)
55
+ - Execute JavaScript for element interaction
56
+ - Perform mouse operations (move, click, scroll)
57
+ - Wait for page stability
58
+
59
+ Implementers:
60
+ - CDPBackendV0: For browser-use integration via CDP
61
+ - PlaywrightBackend: Wrapper around existing SentienceBrowser (future)
62
+ """
63
+
64
+ async def refresh_page_info(self) -> ViewportInfo:
65
+ """
66
+ Cache viewport + scroll offsets + url; cheap & safe to call often.
67
+
68
+ Returns:
69
+ ViewportInfo with current viewport state
70
+ """
71
+ ...
72
+
73
+ async def eval(self, expression: str) -> Any:
74
+ """
75
+ Evaluate JavaScript expression in page context.
76
+
77
+ Uses CDP Runtime.evaluate with returnByValue=True.
78
+
79
+ Args:
80
+ expression: JavaScript expression to evaluate
81
+
82
+ Returns:
83
+ Result value (JSON-serializable)
84
+ """
85
+ ...
86
+
87
+ async def call(
88
+ self,
89
+ function_declaration: str,
90
+ args: list[Any] | None = None,
91
+ ) -> Any:
92
+ """
93
+ Call a JavaScript function with arguments.
94
+
95
+ Uses CDP Runtime.callFunctionOn for safe argument passing.
96
+ Safer than eval() for passing complex arguments.
97
+
98
+ Args:
99
+ function_declaration: JavaScript function body, e.g., "(x, y) => x + y"
100
+ args: Arguments to pass to the function
101
+
102
+ Returns:
103
+ Result value (JSON-serializable)
104
+ """
105
+ ...
106
+
107
+ async def get_layout_metrics(self) -> LayoutMetrics:
108
+ """
109
+ Get page layout metrics.
110
+
111
+ Uses CDP Page.getLayoutMetrics to get viewport and content dimensions.
112
+
113
+ Returns:
114
+ LayoutMetrics with viewport and content size info
115
+ """
116
+ ...
117
+
118
+ async def screenshot_png(self) -> bytes:
119
+ """
120
+ Capture viewport screenshot as PNG bytes.
121
+
122
+ Uses CDP Page.captureScreenshot.
123
+
124
+ Returns:
125
+ PNG image bytes
126
+ """
127
+ ...
128
+
129
+ async def mouse_move(self, x: float, y: float) -> None:
130
+ """
131
+ Move mouse to viewport coordinates.
132
+
133
+ Uses CDP Input.dispatchMouseEvent with type="mouseMoved".
134
+
135
+ Args:
136
+ x: X coordinate in viewport
137
+ y: Y coordinate in viewport
138
+ """
139
+ ...
140
+
141
+ async def mouse_click(
142
+ self,
143
+ x: float,
144
+ y: float,
145
+ button: Literal["left", "right", "middle"] = "left",
146
+ click_count: int = 1,
147
+ ) -> None:
148
+ """
149
+ Click at viewport coordinates.
150
+
151
+ Uses CDP Input.dispatchMouseEvent with mousePressed + mouseReleased.
152
+
153
+ Args:
154
+ x: X coordinate in viewport
155
+ y: Y coordinate in viewport
156
+ button: Mouse button to click
157
+ click_count: Number of clicks (1 for single, 2 for double)
158
+ """
159
+ ...
160
+
161
+ async def wheel(
162
+ self,
163
+ delta_y: float,
164
+ x: float | None = None,
165
+ y: float | None = None,
166
+ ) -> None:
167
+ """
168
+ Scroll using mouse wheel.
169
+
170
+ Uses CDP Input.dispatchMouseEvent with type="mouseWheel".
171
+
172
+ Args:
173
+ delta_y: Scroll amount (positive = down, negative = up)
174
+ x: X coordinate for scroll (default: viewport center)
175
+ y: Y coordinate for scroll (default: viewport center)
176
+ """
177
+ ...
178
+
179
+ async def type_text(self, text: str) -> None:
180
+ """
181
+ Type text using keyboard input.
182
+
183
+ Uses CDP Input.dispatchKeyEvent for each character.
184
+
185
+ Args:
186
+ text: Text to type
187
+ """
188
+ ...
189
+
190
+ async def wait_ready_state(
191
+ self,
192
+ state: Literal["interactive", "complete"] = "interactive",
193
+ timeout_ms: int = 15000,
194
+ ) -> None:
195
+ """
196
+ Wait for document.readyState to reach target state.
197
+
198
+ Uses polling instead of CDP events (no leak from unregistered listeners).
199
+
200
+ Args:
201
+ state: Target state ("interactive" or "complete")
202
+ timeout_ms: Maximum time to wait in milliseconds
203
+
204
+ Raises:
205
+ TimeoutError: If state not reached within timeout
206
+ """
207
+ ...
208
+
209
+ async def get_url(self) -> str:
210
+ """
211
+ Get current page URL.
212
+
213
+ Returns:
214
+ Current page URL (window.location.href)
215
+ """
216
+ ...