Skip to content

Commit 981e82e

Browse files
author
SentienceDEV
committed
Phase 5: BrowserProtocol PageProtocl for mocking mor unit tests
1 parent 5547d7d commit 981e82e

File tree

8 files changed

+709
-10
lines changed

8 files changed

+709
-10
lines changed

sentience/action_executor.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
"""
77

88
import re
9-
from typing import Any
9+
from typing import Any, Union
1010

1111
from .actions import click, click_async, press, press_async, type_text, type_text_async
1212
from .browser import AsyncSentienceBrowser, SentienceBrowser
1313
from .models import Snapshot
14+
from .protocols import AsyncBrowserProtocol, BrowserProtocol
1415

1516

1617
class ActionExecutor:
@@ -23,15 +24,17 @@ class ActionExecutor:
2324
- Handle action parsing errors consistently
2425
"""
2526

26-
def __init__(self, browser: SentienceBrowser | AsyncSentienceBrowser):
27+
def __init__(self, browser: Union[SentienceBrowser, AsyncSentienceBrowser, BrowserProtocol, AsyncBrowserProtocol]):
2728
"""
2829
Initialize action executor.
2930
3031
Args:
31-
browser: SentienceBrowser or AsyncSentienceBrowser instance
32+
browser: SentienceBrowser, AsyncSentienceBrowser, or protocol-compatible instance
33+
(for testing, can use mock objects that implement BrowserProtocol)
3234
"""
3335
self.browser = browser
34-
self._is_async = isinstance(browser, AsyncSentienceBrowser)
36+
# Check if browser is async - support both concrete types and protocols
37+
self._is_async = isinstance(browser, (AsyncSentienceBrowser, AsyncBrowserProtocol))
3538

3639
def execute(self, action_str: str, snap: Snapshot) -> dict[str, Any]:
3740
"""

sentience/agent.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import asyncio
77
import hashlib
88
import time
9-
from typing import TYPE_CHECKING, Any, Optional
9+
from typing import TYPE_CHECKING, Any, Optional, Union
1010

1111
from .action_executor import ActionExecutor
1212
from .agent_config import AgentConfig
@@ -15,6 +15,7 @@
1515
from .element_filter import ElementFilter
1616
from .llm_interaction_handler import LLMInteractionHandler
1717
from .llm_provider import LLMProvider, LLMResponse
18+
from .protocols import AsyncBrowserProtocol, BrowserProtocol
1819
from .models import (
1920
ActionHistory,
2021
ActionTokenUsage,
@@ -58,7 +59,7 @@ class SentienceAgent(BaseAgent):
5859

5960
def __init__(
6061
self,
61-
browser: SentienceBrowser,
62+
browser: Union[SentienceBrowser, BrowserProtocol],
6263
llm: LLMProvider,
6364
default_snapshot_limit: int = 50,
6465
verbose: bool = True,
@@ -69,7 +70,8 @@ def __init__(
6970
Initialize Sentience Agent
7071
7172
Args:
72-
browser: SentienceBrowser instance
73+
browser: SentienceBrowser instance or BrowserProtocol-compatible object
74+
(for testing, can use mock objects that implement BrowserProtocol)
7375
llm: LLM provider (OpenAIProvider, AnthropicProvider, etc.)
7476
default_snapshot_limit: Default maximum elements to include in context (default: 50)
7577
verbose: Print execution logs (default: True)

sentience/conversational_agent.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
import time
88
from typing import Any
99

10+
from typing import Union
11+
1012
from .agent import SentienceAgent
1113
from .browser import SentienceBrowser
1214
from .llm_provider import LLMProvider
15+
from .protocols import BrowserProtocol
1316
from .models import ExtractionResult, Snapshot, SnapshotOptions, StepExecutionResult
1417
from .snapshot import snapshot
1518

@@ -29,12 +32,15 @@ class ConversationalAgent:
2932
The top result is from amazon.com selling the Apple Magic Mouse 2 for $79."
3033
"""
3134

32-
def __init__(self, browser: SentienceBrowser, llm: LLMProvider, verbose: bool = True):
35+
def __init__(
36+
self, browser: Union[SentienceBrowser, BrowserProtocol], llm: LLMProvider, verbose: bool = True
37+
):
3338
"""
3439
Initialize conversational agent
3540
3641
Args:
37-
browser: SentienceBrowser instance
42+
browser: SentienceBrowser instance or BrowserProtocol-compatible object
43+
(for testing, can use mock objects that implement BrowserProtocol)
3844
llm: LLM provider (OpenAI, Anthropic, LocalLLM, etc.)
3945
verbose: Print step-by-step execution logs (default: True)
4046
"""

sentience/element_filter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def filter_by_importance(
6565
def filter_by_goal(
6666
snapshot: Snapshot,
6767
goal: str | None,
68-
max_elements: int = 50,
68+
max_elements: int = 100,
6969
) -> list[Element]:
7070
"""
7171
Filter elements from snapshot based on goal context.

sentience/protocols.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
"""
2+
Protocol definitions for testability and dependency injection.
3+
4+
These protocols define the minimal interface required by agent classes,
5+
enabling better testability through mocking while maintaining type safety.
6+
"""
7+
8+
from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
9+
10+
if TYPE_CHECKING:
11+
from playwright.async_api import Page as AsyncPage
12+
from playwright.sync_api import Page
13+
14+
from .models import Snapshot
15+
16+
17+
@runtime_checkable
18+
class PageProtocol(Protocol):
19+
"""
20+
Protocol for Playwright Page operations used by agents.
21+
22+
This protocol defines the minimal interface required from Playwright's Page object.
23+
Agents use this interface to interact with the browser page.
24+
"""
25+
26+
@property
27+
def url(self) -> str:
28+
"""Current page URL."""
29+
...
30+
31+
def evaluate(self, script: str, *args: Any, **kwargs: Any) -> Any:
32+
"""
33+
Evaluate JavaScript in the page context.
34+
35+
Args:
36+
script: JavaScript code to evaluate
37+
*args: Arguments to pass to the script
38+
**kwargs: Keyword arguments to pass to the script
39+
40+
Returns:
41+
Result of the JavaScript evaluation
42+
"""
43+
...
44+
45+
def goto(self, url: str, **kwargs: Any) -> Optional[Any]:
46+
"""
47+
Navigate to a URL.
48+
49+
Args:
50+
url: URL to navigate to
51+
**kwargs: Additional navigation options
52+
53+
Returns:
54+
Response object or None
55+
"""
56+
...
57+
58+
def wait_for_timeout(self, timeout: int) -> None:
59+
"""
60+
Wait for a specified timeout.
61+
62+
Args:
63+
timeout: Timeout in milliseconds
64+
"""
65+
...
66+
67+
def wait_for_load_state(self, state: str = "load", timeout: Optional[int] = None) -> None:
68+
"""
69+
Wait for page load state.
70+
71+
Args:
72+
state: Load state to wait for (e.g., "load", "domcontentloaded", "networkidle")
73+
timeout: Optional timeout in milliseconds
74+
"""
75+
...
76+
77+
78+
@runtime_checkable
79+
class BrowserProtocol(Protocol):
80+
"""
81+
Protocol for browser operations used by agents.
82+
83+
This protocol defines the minimal interface required from SentienceBrowser.
84+
Agents use this interface to interact with the browser and take snapshots.
85+
86+
Note: SentienceBrowser naturally implements this protocol, so no changes
87+
are required to existing code. This protocol enables better testability
88+
through mocking.
89+
"""
90+
91+
@property
92+
def page(self) -> Optional[PageProtocol]:
93+
"""
94+
Current Playwright Page object.
95+
96+
Returns:
97+
Page object if browser is started, None otherwise
98+
"""
99+
...
100+
101+
def start(self) -> None:
102+
"""Start the browser session."""
103+
...
104+
105+
def close(self, output_path: Optional[str] = None) -> Optional[str]:
106+
"""
107+
Close the browser session.
108+
109+
Args:
110+
output_path: Optional path to save browser state/output
111+
112+
Returns:
113+
Path to saved output or None
114+
"""
115+
...
116+
117+
def goto(self, url: str) -> None:
118+
"""
119+
Navigate to a URL.
120+
121+
Args:
122+
url: URL to navigate to
123+
"""
124+
...
125+
126+
127+
@runtime_checkable
128+
class AsyncPageProtocol(Protocol):
129+
"""
130+
Protocol for async Playwright Page operations.
131+
132+
Similar to PageProtocol but for async operations.
133+
"""
134+
135+
@property
136+
def url(self) -> str:
137+
"""Current page URL."""
138+
...
139+
140+
async def evaluate(self, script: str, *args: Any, **kwargs: Any) -> Any:
141+
"""
142+
Evaluate JavaScript in the page context (async).
143+
144+
Args:
145+
script: JavaScript code to evaluate
146+
*args: Arguments to pass to the script
147+
**kwargs: Keyword arguments to pass to the script
148+
149+
Returns:
150+
Result of the JavaScript evaluation
151+
"""
152+
...
153+
154+
async def goto(self, url: str, **kwargs: Any) -> Optional[Any]:
155+
"""
156+
Navigate to a URL (async).
157+
158+
Args:
159+
url: URL to navigate to
160+
**kwargs: Additional navigation options
161+
162+
Returns:
163+
Response object or None
164+
"""
165+
...
166+
167+
async def wait_for_timeout(self, timeout: int) -> None:
168+
"""
169+
Wait for a specified timeout (async).
170+
171+
Args:
172+
timeout: Timeout in milliseconds
173+
"""
174+
...
175+
176+
async def wait_for_load_state(
177+
self, state: str = "load", timeout: Optional[int] = None
178+
) -> None:
179+
"""
180+
Wait for page load state (async).
181+
182+
Args:
183+
state: Load state to wait for (e.g., "load", "domcontentloaded", "networkidle")
184+
timeout: Optional timeout in milliseconds
185+
"""
186+
...
187+
188+
189+
@runtime_checkable
190+
class AsyncBrowserProtocol(Protocol):
191+
"""
192+
Protocol for async browser operations.
193+
194+
Similar to BrowserProtocol but for async operations.
195+
"""
196+
197+
@property
198+
def page(self) -> Optional[AsyncPageProtocol]:
199+
"""
200+
Current Playwright AsyncPage object.
201+
202+
Returns:
203+
AsyncPage object if browser is started, None otherwise
204+
"""
205+
...
206+
207+
async def start(self) -> None:
208+
"""Start the browser session (async)."""
209+
...
210+
211+
async def close(self, output_path: Optional[str] = None) -> Optional[str]:
212+
"""
213+
Close the browser session (async).
214+
215+
Args:
216+
output_path: Optional path to save browser state/output
217+
218+
Returns:
219+
Path to saved output or None
220+
"""
221+
...
222+
223+
async def goto(self, url: str) -> None:
224+
"""
225+
Navigate to a URL (async).
226+
227+
Args:
228+
url: URL to navigate to
229+
"""
230+
...
231+

tests/integration/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Integration tests for Sentience SDK.
3+
4+
These tests use real browser instances to test end-to-end functionality
5+
and catch real-world bugs that mocks might miss.
6+
"""
7+

tests/unit/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Unit tests for Sentience SDK.
3+
4+
These tests use mocks and protocols to test logic in isolation,
5+
without requiring real browser instances.
6+
"""
7+

0 commit comments

Comments
 (0)