Skip to content

Commit dd6b8eb

Browse files
committed
phase 2B completed
1 parent 14b0f4d commit dd6b8eb

File tree

8 files changed

+477
-35
lines changed

8 files changed

+477
-35
lines changed

sentience/_extension_loader.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,3 @@ def find_extension_path() -> Path:
3838
f"2. {dev_ext_path}\n"
3939
"Make sure the extension is built and 'sentience/extension' directory exists."
4040
)
41-

sentience/async_api.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,41 @@
2121
from sentience.actions import click_async
2222
"""
2323

24+
# ========== Actions (Phase 1) ==========
25+
# Re-export async action functions from actions.py
26+
from sentience.actions import click_async, click_rect_async, press_async, type_text_async
27+
2428
# ========== Browser ==========
2529
# Re-export AsyncSentienceBrowser from browser.py (moved there for better organization)
2630
from sentience.browser import AsyncSentienceBrowser
2731

28-
# ========== Snapshot (Phase 1) ==========
29-
# Re-export async snapshot functions from snapshot.py
30-
from sentience.snapshot import snapshot_async
32+
# Re-export async expect functions from expect.py
33+
from sentience.expect import ExpectationAsync, expect_async
3134

32-
# ========== Actions (Phase 1) ==========
33-
# Re-export async action functions from actions.py
34-
from sentience.actions import (
35-
click_async,
36-
type_text_async,
37-
press_async,
38-
click_rect_async,
39-
)
35+
# Re-export async overlay functions from overlay.py
36+
from sentience.overlay import clear_overlay_async, show_overlay_async
4037

41-
# ========== Phase 2A: Core Utilities ==========
42-
# Re-export async wait function from wait.py
43-
from sentience.wait import wait_for_async
38+
# ========== Query Functions (Pure Functions - No Async Needed) ==========
39+
# Re-export query functions (pure functions, no async needed)
40+
from sentience.query import find, query
41+
42+
# ========== Phase 2B: Supporting Utilities ==========
43+
# Re-export async read function from read.py
44+
from sentience.read import read_async
4445

4546
# Re-export async screenshot function from screenshot.py
4647
from sentience.screenshot import screenshot_async
4748

49+
# ========== Snapshot (Phase 1) ==========
50+
# Re-export async snapshot functions from snapshot.py
51+
from sentience.snapshot import snapshot_async
52+
4853
# Re-export async text search function from text_search.py
4954
from sentience.text_search import find_text_rect_async
5055

51-
# ========== Phase 2B: Supporting Utilities (Future) ==========
52-
# TODO: Re-export when implemented
53-
# from sentience.read import read_async
54-
# from sentience.overlay import show_overlay_async, clear_overlay_async
55-
# from sentience.expect import expect_async, ExpectationAsync
56+
# ========== Phase 2A: Core Utilities ==========
57+
# Re-export async wait function from wait.py
58+
from sentience.wait import wait_for_async
5659

5760
# ========== Phase 2C: Agent Layer (Future) ==========
5861
# TODO: Re-export when implemented
@@ -64,9 +67,6 @@
6467
# from sentience.recorder import RecorderAsync
6568
# from sentience.inspector import InspectorAsync
6669

67-
# ========== Query Functions (Pure Functions - No Async Needed) ==========
68-
# Re-export query functions (pure functions, no async needed)
69-
from sentience.query import find, query
7070

7171
__all__ = [
7272
# Browser
@@ -82,12 +82,12 @@
8282
"wait_for_async", # Re-exported from wait.py
8383
"screenshot_async", # Re-exported from screenshot.py
8484
"find_text_rect_async", # Re-exported from text_search.py
85-
# Phase 2B: Supporting Utilities (Future - uncomment when implemented)
86-
# "read_async",
87-
# "show_overlay_async",
88-
# "clear_overlay_async",
89-
# "expect_async",
90-
# "ExpectationAsync",
85+
# Phase 2B: Supporting Utilities
86+
"read_async", # Re-exported from read.py
87+
"show_overlay_async", # Re-exported from overlay.py
88+
"clear_overlay_async", # Re-exported from overlay.py
89+
"expect_async", # Re-exported from expect.py
90+
"ExpectationAsync", # Re-exported from expect.py
9191
# Phase 2C: Agent Layer (Future - uncomment when implemented)
9292
# "SentienceAgentAsync",
9393
# "BaseAgentAsync",

sentience/browser.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
from pathlib import Path
1111
from urllib.parse import urlparse
1212

13-
from playwright.async_api import BrowserContext as AsyncBrowserContext, Page as AsyncPage, Playwright as AsyncPlaywright, async_playwright
13+
from playwright.async_api import BrowserContext as AsyncBrowserContext
14+
from playwright.async_api import Page as AsyncPage
15+
from playwright.async_api import Playwright as AsyncPlaywright
16+
from playwright.async_api import async_playwright
1417
from playwright.sync_api import BrowserContext, Page, Playwright, sync_playwright
1518

1619
from sentience._extension_loader import find_extension_path

sentience/expect.py

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
Expect/Assert functionality
33
"""
44

5+
import asyncio
56
import time
67

7-
from .browser import SentienceBrowser
8+
from .browser import AsyncSentienceBrowser, SentienceBrowser
89
from .models import Element
910
from .query import query
10-
from .wait import wait_for
11+
from .wait import wait_for, wait_for_async
1112

1213

1314
class Expectation:
@@ -90,3 +91,98 @@ def expect(browser: SentienceBrowser, selector: str | dict) -> Expectation:
9091
Expectation helper
9192
"""
9293
return Expectation(browser, selector)
94+
95+
96+
class ExpectationAsync:
97+
"""Assertion helper for element expectations (async)"""
98+
99+
def __init__(self, browser: AsyncSentienceBrowser, selector: str | dict):
100+
self.browser = browser
101+
self.selector = selector
102+
103+
async def to_be_visible(self, timeout: float = 10.0) -> Element:
104+
"""Assert element is visible (exists and in viewport)"""
105+
result = await wait_for_async(self.browser, self.selector, timeout=timeout)
106+
107+
if not result.found:
108+
raise AssertionError(f"Element not found: {self.selector} (timeout: {timeout}s)")
109+
110+
element = result.element
111+
if not element.in_viewport:
112+
raise AssertionError(f"Element found but not visible in viewport: {self.selector}")
113+
114+
return element
115+
116+
async def to_exist(self, timeout: float = 10.0) -> Element:
117+
"""Assert element exists"""
118+
result = await wait_for_async(self.browser, self.selector, timeout=timeout)
119+
120+
if not result.found:
121+
raise AssertionError(f"Element does not exist: {self.selector} (timeout: {timeout}s)")
122+
123+
return result.element
124+
125+
async def to_have_text(self, expected_text: str, timeout: float = 10.0) -> Element:
126+
"""Assert element has specific text"""
127+
result = await wait_for_async(self.browser, self.selector, timeout=timeout)
128+
129+
if not result.found:
130+
raise AssertionError(f"Element not found: {self.selector} (timeout: {timeout}s)")
131+
132+
element = result.element
133+
if not element.text or expected_text not in element.text:
134+
raise AssertionError(
135+
f"Element text mismatch. Expected '{expected_text}', got '{element.text}'"
136+
)
137+
138+
return element
139+
140+
async def to_have_count(self, expected_count: int, timeout: float = 10.0) -> None:
141+
"""Assert selector matches exactly N elements"""
142+
from .snapshot import snapshot_async
143+
144+
start_time = time.time()
145+
while time.time() - start_time < timeout:
146+
snap = await snapshot_async(self.browser)
147+
matches = query(snap, self.selector)
148+
149+
if len(matches) == expected_count:
150+
return
151+
152+
await asyncio.sleep(0.25)
153+
154+
# Final check
155+
snap = await snapshot_async(self.browser)
156+
matches = query(snap, self.selector)
157+
actual_count = len(matches)
158+
159+
raise AssertionError(
160+
f"Element count mismatch. Expected {expected_count}, got {actual_count}"
161+
)
162+
163+
164+
def expect_async(browser: AsyncSentienceBrowser, selector: str | dict) -> ExpectationAsync:
165+
"""
166+
Create expectation helper for assertions (async)
167+
168+
Args:
169+
browser: AsyncSentienceBrowser instance
170+
selector: String DSL or dict query
171+
172+
Returns:
173+
ExpectationAsync helper
174+
175+
Example:
176+
# Assert element is visible
177+
element = await expect_async(browser, "role=button").to_be_visible()
178+
179+
# Assert element has text
180+
element = await expect_async(browser, "h1").to_have_text("Welcome")
181+
182+
# Assert element exists
183+
element = await expect_async(browser, "role=link").to_exist()
184+
185+
# Assert count
186+
await expect_async(browser, "role=button").to_have_count(5)
187+
"""
188+
return ExpectationAsync(browser, selector)

sentience/overlay.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from typing import Any
66

7-
from .browser import SentienceBrowser
7+
from .browser import AsyncSentienceBrowser, SentienceBrowser
88
from .models import Element, Snapshot
99

1010

@@ -113,3 +113,110 @@ def clear_overlay(browser: SentienceBrowser) -> None:
113113
}
114114
"""
115115
)
116+
117+
118+
async def show_overlay_async(
119+
browser: AsyncSentienceBrowser,
120+
elements: list[Element] | list[dict[str, Any]] | Snapshot,
121+
target_element_id: int | None = None,
122+
) -> None:
123+
"""
124+
Display visual overlay highlighting elements in the browser (async)
125+
126+
This function shows a Shadow DOM overlay with color-coded borders around
127+
detected elements. Useful for debugging, learning, and validating element detection.
128+
129+
Args:
130+
browser: AsyncSentienceBrowser instance
131+
elements: Can be:
132+
- List of Element objects (from snapshot.elements)
133+
- List of raw element dicts (from snapshot result or API response)
134+
- Snapshot object (will use snapshot.elements)
135+
target_element_id: Optional ID of element to highlight in red (default: None)
136+
137+
Color Coding:
138+
- Red: Target element (when target_element_id is specified)
139+
- Blue: Primary elements (is_primary=true)
140+
- Green: Regular interactive elements
141+
142+
Visual Indicators:
143+
- Border thickness and opacity scale with importance score
144+
- Semi-transparent fill for better visibility
145+
- Importance badges showing scores
146+
- Star icon for primary elements
147+
- Target emoji for the target element
148+
149+
Auto-clear: Overlay automatically disappears after 5 seconds
150+
151+
Example:
152+
# Show overlay from snapshot
153+
snap = await snapshot_async(browser)
154+
await show_overlay_async(browser, snap)
155+
156+
# Show overlay with custom elements
157+
elements = [{"id": 1, "bbox": {"x": 100, "y": 100, "width": 200, "height": 50}, ...}]
158+
await show_overlay_async(browser, elements)
159+
160+
# Show overlay with target element highlighted in red
161+
await show_overlay_async(browser, snap, target_element_id=42)
162+
163+
# Clear overlay manually before 5 seconds
164+
await clear_overlay_async(browser)
165+
"""
166+
if not browser.page:
167+
raise RuntimeError("Browser not started. Call await browser.start() first.")
168+
169+
# Handle different input types
170+
if isinstance(elements, Snapshot):
171+
# Extract elements from Snapshot object
172+
elements_list = [el.model_dump() for el in elements.elements]
173+
elif isinstance(elements, list) and len(elements) > 0:
174+
# Check if it's a list of Element objects or dicts
175+
if hasattr(elements[0], "model_dump"):
176+
# List of Element objects
177+
elements_list = [el.model_dump() for el in elements]
178+
else:
179+
# Already a list of dicts
180+
elements_list = elements
181+
else:
182+
raise ValueError("elements must be a Snapshot, list of Element objects, or list of dicts")
183+
184+
# Call extension API
185+
await browser.page.evaluate(
186+
"""
187+
(args) => {
188+
if (window.sentience && window.sentience.showOverlay) {
189+
window.sentience.showOverlay(args.elements, args.targetId);
190+
} else {
191+
console.warn('[Sentience SDK] showOverlay not available - is extension loaded?');
192+
}
193+
}
194+
""",
195+
{"elements": elements_list, "targetId": target_element_id},
196+
)
197+
198+
199+
async def clear_overlay_async(browser: AsyncSentienceBrowser) -> None:
200+
"""
201+
Clear the visual overlay manually (before 5-second auto-clear) (async)
202+
203+
Args:
204+
browser: AsyncSentienceBrowser instance
205+
206+
Example:
207+
await show_overlay_async(browser, snap)
208+
# ... inspect overlay ...
209+
await clear_overlay_async(browser) # Remove immediately
210+
"""
211+
if not browser.page:
212+
raise RuntimeError("Browser not started. Call await browser.start() first.")
213+
214+
await browser.page.evaluate(
215+
"""
216+
() => {
217+
if (window.sentience && window.sentience.clearOverlay) {
218+
window.sentience.clearOverlay();
219+
}
220+
}
221+
"""
222+
)

0 commit comments

Comments
 (0)