Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ from sentience import SentienceBrowser, snapshot, find, click

# Start browser with extension
with SentienceBrowser(headless=False) as browser:
browser.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")
browser.goto("https://example.com", wait_until="domcontentloaded")

# Take snapshot - captures all interactive elements
snap = snapshot(browser)
Expand All @@ -44,8 +43,7 @@ import time

with SentienceBrowser(headless=False) as browser:
# Navigate to Amazon Best Sellers
browser.goto("https://www.amazon.com/gp/bestsellers/")
browser.page.wait_for_load_state("networkidle")
browser.goto("https://www.amazon.com/gp/bestsellers/", wait_until="domcontentloaded")
time.sleep(2) # Wait for dynamic content

# Take snapshot and find products
Expand Down Expand Up @@ -146,6 +144,7 @@ first_row = query(snap, "bbox.y<600")

### Actions - Interact with Elements
- **`click(browser, element_id)`** - Click element by ID
- **`click_rect(browser, rect)`** - Click at center of rectangle (coordinate-based)
- **`type_text(browser, element_id, text)`** - Type into input fields
- **`press(browser, key)`** - Press keyboard keys (Enter, Escape, Tab, etc.)

Expand All @@ -160,17 +159,53 @@ print(f"Duration: {result.duration_ms}ms")
print(f"URL changed: {result.url_changed}")
```

**Coordinate-based clicking:**
```python
from sentience import click_rect

# Click at center of rectangle (x, y, width, height)
click_rect(browser, {"x": 100, "y": 200, "w": 50, "h": 30})

# With visual highlight (default: red border for 2 seconds)
click_rect(browser, {"x": 100, "y": 200, "w": 50, "h": 30}, highlight=True, highlight_duration=2.0)

# Using element's bounding box
snap = snapshot(browser)
element = find(snap, "role=button")
if element:
click_rect(browser, {
"x": element.bbox.x,
"y": element.bbox.y,
"w": element.bbox.width,
"h": element.bbox.height
})
```

### Wait & Assertions
- **`wait_for(browser, selector, timeout=5.0)`** - Wait for element to appear
- **`wait_for(browser, selector, timeout=5.0, interval=None, use_api=None)`** - Wait for element to appear
- **`expect(browser, selector)`** - Assertion helper with fluent API

**Examples:**
```python
# Wait for element
# Wait for element (auto-detects optimal interval based on API usage)
result = wait_for(browser, "role=button text='Submit'", timeout=10.0)
if result.found:
print(f"Found after {result.duration_ms}ms")

# Use local extension with fast polling (0.25s interval)
result = wait_for(browser, "role=button", timeout=5.0, use_api=False)

# Use remote API with network-friendly polling (1.5s interval)
result = wait_for(browser, "role=button", timeout=5.0, use_api=True)

# Custom interval override
result = wait_for(browser, "role=button", timeout=5.0, interval=0.5, use_api=False)

# Semantic wait conditions
wait_for(browser, "clickable=true", timeout=5.0) # Wait for clickable element
wait_for(browser, "importance>100", timeout=5.0) # Wait for important element
wait_for(browser, "role=link visible=true", timeout=5.0) # Wait for visible link

# Assertions
expect(browser, "role=button text='Submit'").to_exist(timeout=5.0)
expect(browser, "role=heading").to_be_visible()
Expand Down Expand Up @@ -313,8 +348,7 @@ browser = SentienceBrowser() # headless=True if CI=true, else False

### 1. Wait for Dynamic Content
```python
browser.goto("https://example.com")
browser.page.wait_for_load_state("networkidle")
browser.goto("https://example.com", wait_until="domcontentloaded")
time.sleep(1) # Extra buffer for AJAX/animations
```

Expand Down
80 changes: 80 additions & 0 deletions examples/click_rect_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Example: Using click_rect for coordinate-based clicking with visual feedback
"""

from sentience import SentienceBrowser, snapshot, find, click_rect
import os


def main():
# Get API key from environment variable (optional - uses free tier if not set)
api_key = os.environ.get("SENTIENCE_API_KEY")

with SentienceBrowser(api_key=api_key, headless=False) as browser:
# Navigate to example.com
browser.page.goto("https://example.com", wait_until="domcontentloaded")

print("=== click_rect Demo ===\n")

# Example 1: Click using rect dictionary
print("1. Clicking at specific coordinates (100, 100) with size 50x30")
print(" (You should see a red border highlight for 2 seconds)")
result = click_rect(browser, {"x": 100, "y": 100, "w": 50, "h": 30})
print(f" Result: success={result.success}, outcome={result.outcome}")
print(f" Duration: {result.duration_ms}ms\n")

# Wait a bit
browser.page.wait_for_timeout(1000)

# Example 2: Click using element's bbox
print("2. Clicking using element's bounding box")
snap = snapshot(browser)
link = find(snap, "role=link")

if link:
print(f" Found link: '{link.text}' at ({link.bbox.x}, {link.bbox.y})")
print(" Clicking at center of element's bbox...")
result = click_rect(browser, {
"x": link.bbox.x,
"y": link.bbox.y,
"w": link.bbox.width,
"h": link.bbox.height
})
print(f" Result: success={result.success}, outcome={result.outcome}")
print(f" URL changed: {result.url_changed}\n")

# Navigate back if needed
if result.url_changed:
browser.page.goto("https://example.com", wait_until="domcontentloaded")
browser.page.wait_for_load_state("networkidle")

# Example 3: Click without highlight (for headless/CI)
print("3. Clicking without visual highlight")
result = click_rect(browser, {"x": 200, "y": 200, "w": 40, "h": 20}, highlight=False)
print(f" Result: success={result.success}\n")

# Example 4: Custom highlight duration
print("4. Clicking with custom highlight duration (3 seconds)")
result = click_rect(browser, {"x": 300, "y": 300, "w": 60, "h": 40}, highlight_duration=3.0)
print(f" Result: success={result.success}")
print(" (Red border should stay visible for 3 seconds)\n")

# Example 5: Click with snapshot capture
print("5. Clicking and capturing snapshot after action")
result = click_rect(
browser,
{"x": 150, "y": 150, "w": 50, "h": 30},
take_snapshot=True
)
if result.snapshot_after:
print(f" Snapshot captured: {len(result.snapshot_after.elements)} elements found")
print(f" URL: {result.snapshot_after.url}\n")

print("✅ click_rect demo complete!")
print("\nNote: click_rect uses Playwright's native mouse.click() for realistic")
print("event simulation, triggering hover, focus, mousedown, mouseup sequences.")


if __name__ == "__main__":
main()

114 changes: 114 additions & 0 deletions examples/semantic_wait_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
Example: Semantic wait_for using query DSL
Demonstrates waiting for elements using semantic selectors
"""

from sentience import SentienceBrowser, wait_for, click
import os


def main():
# Get API key from environment variable (optional - uses free tier if not set)
api_key = os.environ.get("SENTIENCE_API_KEY")

with SentienceBrowser(api_key=api_key, headless=False) as browser:
# Navigate to example.com
browser.page.goto("https://example.com", wait_until="domcontentloaded")

print("=== Semantic wait_for Demo ===\n")

# Example 1: Wait for element by role
print("1. Waiting for link element (role=link)")
wait_result = wait_for(browser, "role=link", timeout=5.0)
if wait_result.found:
print(f" ✅ Found after {wait_result.duration_ms}ms")
print(f" Element: '{wait_result.element.text}' (id: {wait_result.element.id})")
else:
print(f" ❌ Not found (timeout: {wait_result.timeout})")
print()

# Example 2: Wait for element by role and text
print("2. Waiting for link with specific text")
wait_result = wait_for(browser, "role=link text~'Example'", timeout=5.0)
if wait_result.found:
print(f" ✅ Found after {wait_result.duration_ms}ms")
print(f" Element text: '{wait_result.element.text}'")
else:
print(" ❌ Not found")
print()

# Example 3: Wait for clickable element
print("3. Waiting for clickable element")
wait_result = wait_for(browser, "clickable=true", timeout=5.0)
if wait_result.found:
print(f" ✅ Found clickable element after {wait_result.duration_ms}ms")
print(f" Role: {wait_result.element.role}")
print(f" Text: '{wait_result.element.text}'")
print(f" Is clickable: {wait_result.element.visual_cues.is_clickable}")
else:
print(" ❌ Not found")
print()

# Example 4: Wait for element with importance threshold
print("4. Waiting for important element (importance > 100)")
wait_result = wait_for(browser, "importance>100", timeout=5.0)
if wait_result.found:
print(f" ✅ Found important element after {wait_result.duration_ms}ms")
print(f" Importance: {wait_result.element.importance}")
print(f" Role: {wait_result.element.role}")
else:
print(" ❌ Not found")
print()

# Example 5: Wait and then click
print("5. Wait for element, then click it")
wait_result = wait_for(browser, "role=link", timeout=5.0)
if wait_result.found:
print(" ✅ Found element, clicking...")
click_result = click(browser, wait_result.element.id)
print(f" Click result: success={click_result.success}, outcome={click_result.outcome}")
if click_result.url_changed:
print(f" ✅ Navigation occurred: {browser.page.url}")
else:
print(" ❌ Element not found, cannot click")
print()

# Example 6: Using local extension (fast polling)
print("6. Using local extension with auto-optimized interval")
print(" When use_api=False, interval auto-adjusts to 0.25s (fast)")
wait_result = wait_for(browser, "role=link", timeout=5.0, use_api=False)
if wait_result.found:
print(f" ✅ Found after {wait_result.duration_ms}ms")
print(" (Used local extension, polled every 0.25 seconds)")
print()

# Example 7: Using remote API (slower polling)
print("7. Using remote API with auto-optimized interval")
print(" When use_api=True, interval auto-adjusts to 1.5s (network-friendly)")
if api_key:
wait_result = wait_for(browser, "role=link", timeout=5.0, use_api=True)
if wait_result.found:
print(f" ✅ Found after {wait_result.duration_ms}ms")
print(" (Used remote API, polled every 1.5 seconds)")
else:
print(" ⚠️ Skipped (no API key set)")
print()

# Example 8: Custom interval override
print("8. Custom interval override (manual control)")
print(" You can still specify custom interval if needed")
wait_result = wait_for(browser, "role=link", timeout=5.0, interval=0.5, use_api=False)
if wait_result.found:
print(f" ✅ Found after {wait_result.duration_ms}ms")
print(" (Custom interval: 0.5 seconds)")
print()

print("✅ Semantic wait_for demo complete!")
print("\nNote: wait_for uses the semantic query DSL to find elements.")
print("This is more robust than CSS selectors because it understands")
print("the semantic meaning of elements (role, text, clickability, etc.).")


if __name__ == "__main__":
main()

3 changes: 2 additions & 1 deletion sentience/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .models import Snapshot, Element, BBox, Viewport, ActionResult, WaitResult
from .snapshot import snapshot
from .query import query, find
from .actions import click, type_text, press
from .actions import click, type_text, press, click_rect
from .wait import wait_for
from .expect import expect
from .inspector import Inspector, inspect
Expand All @@ -31,6 +31,7 @@
"click",
"type_text",
"press",
"click_rect",
"wait_for",
"expect",
"Inspector",
Expand Down
Loading