chore: import upstream snapshot with attribution
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
# Browser Actor
|
||||
|
||||
Browser Actor is a web automation library built on CDP (Chrome DevTools Protocol) that provides low-level browser automation capabilities within the browser-use ecosystem.
|
||||
|
||||
## Usage
|
||||
|
||||
### Integrated with Browser (Recommended)
|
||||
```python
|
||||
from browser_use import Browser # Alias for BrowserSession
|
||||
|
||||
# Create and start browser session
|
||||
browser = Browser()
|
||||
await browser.start()
|
||||
|
||||
# Create new tabs and navigate
|
||||
page = await browser.new_page("https://example.com")
|
||||
pages = await browser.get_pages()
|
||||
current_page = await browser.get_current_page()
|
||||
```
|
||||
|
||||
### Direct Page Access (Advanced)
|
||||
```python
|
||||
from browser_use.actor import Page, Element, Mouse
|
||||
|
||||
# Create page with existing browser session
|
||||
page = Page(browser_session, target_id, session_id)
|
||||
```
|
||||
|
||||
## Basic Operations
|
||||
|
||||
```python
|
||||
# Tab Management
|
||||
page = await browser.new_page() # Create blank tab
|
||||
page = await browser.new_page("https://example.com") # Create tab with URL
|
||||
pages = await browser.get_pages() # Get all existing tabs
|
||||
await browser.close_page(page) # Close specific tab
|
||||
|
||||
# Navigation
|
||||
await page.goto("https://example.com")
|
||||
await page.go_back()
|
||||
await page.go_forward()
|
||||
await page.reload()
|
||||
```
|
||||
|
||||
## Element Operations
|
||||
|
||||
```python
|
||||
# Find elements by CSS selector
|
||||
elements = await page.get_elements_by_css_selector("input[type='text']")
|
||||
buttons = await page.get_elements_by_css_selector("button.submit")
|
||||
|
||||
# Get element by backend node ID
|
||||
element = await page.get_element(backend_node_id=12345)
|
||||
|
||||
# AI-powered element finding (requires LLM)
|
||||
element = await page.get_element_by_prompt("search button", llm=your_llm)
|
||||
element = await page.must_get_element_by_prompt("login form", llm=your_llm)
|
||||
```
|
||||
|
||||
> **Note**: `get_elements_by_css_selector` returns immediately without waiting for visibility.
|
||||
|
||||
## Element Interactions
|
||||
|
||||
```python
|
||||
# Element actions
|
||||
await element.click(button='left', click_count=1, modifiers=['Control'])
|
||||
await element.fill("Hello World") # Clears first, then types
|
||||
await element.hover()
|
||||
await element.focus()
|
||||
await element.check() # Toggle checkbox/radio
|
||||
await element.select_option(["option1", "option2"]) # For dropdown/select
|
||||
await element.drag_to(target_element) # Drag and drop
|
||||
|
||||
# Element properties
|
||||
value = await element.get_attribute("value")
|
||||
box = await element.get_bounding_box() # Returns BoundingBox or None
|
||||
info = await element.get_basic_info() # Comprehensive element info
|
||||
screenshot_b64 = await element.screenshot(format='png')
|
||||
|
||||
# Execute JavaScript on element (this context is the element)
|
||||
text = await element.evaluate("() => this.textContent")
|
||||
await element.evaluate("(color) => this.style.backgroundColor = color", "yellow")
|
||||
classes = await element.evaluate("() => Array.from(this.classList)")
|
||||
```
|
||||
|
||||
## Mouse Operations
|
||||
|
||||
```python
|
||||
# Mouse operations
|
||||
mouse = await page.mouse
|
||||
await mouse.click(x=100, y=200, button='left', click_count=1)
|
||||
await mouse.move(x=300, y=400, steps=1)
|
||||
await mouse.down(button='left') # Press button
|
||||
await mouse.up(button='left') # Release button
|
||||
await mouse.scroll(x=0, y=100, delta_x=0, delta_y=-500) # Scroll at coordinates
|
||||
```
|
||||
|
||||
## Page Operations
|
||||
|
||||
```python
|
||||
# JavaScript evaluation
|
||||
result = await page.evaluate('() => document.title') # Must use arrow function format
|
||||
result = await page.evaluate('(x, y) => x + y', 10, 20) # With arguments
|
||||
|
||||
# Keyboard input
|
||||
await page.press("Control+A") # Key combinations supported
|
||||
await page.press("Escape") # Single keys
|
||||
|
||||
# Page controls
|
||||
await page.set_viewport_size(width=1920, height=1080)
|
||||
page_screenshot = await page.screenshot() # PNG by default
|
||||
page_png = await page.screenshot(format="png", quality=90)
|
||||
|
||||
# Page information
|
||||
url = await page.get_url()
|
||||
title = await page.get_title()
|
||||
```
|
||||
|
||||
## AI-Powered Features
|
||||
|
||||
```python
|
||||
# Content extraction using LLM
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ProductInfo(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
description: str
|
||||
|
||||
# Extract structured data from current page
|
||||
products = await page.extract_content(
|
||||
"Find all products with their names, prices and descriptions",
|
||||
ProductInfo,
|
||||
llm=your_llm
|
||||
)
|
||||
```
|
||||
|
||||
## Core Classes
|
||||
|
||||
- **BrowserSession** (aliased as **Browser**): Main browser session manager with tab operations
|
||||
- **Page**: Represents a single browser tab or iframe for page-level operations
|
||||
- **Element**: Individual DOM element for interactions and property access
|
||||
- **Mouse**: Mouse operations within a page (click, move, scroll)
|
||||
|
||||
## API Reference
|
||||
|
||||
### BrowserSession Methods (Tab Management)
|
||||
- `start()` - Initialize and start the browser session
|
||||
- `stop()` - Stop the browser session (keeps browser alive)
|
||||
- `kill()` - Kill the browser process and reset all state
|
||||
- `new_page(url=None)` → `Page` - Create blank tab or navigate to URL
|
||||
- `get_pages()` → `list[Page]` - Get all available pages
|
||||
- `get_current_page()` → `Page | None` - Get the currently focused page
|
||||
- `close_page(page: Page | str)` - Close page by object or ID
|
||||
- Session management and CDP client operations
|
||||
|
||||
### Page Methods (Page Operations)
|
||||
- `get_elements_by_css_selector(selector: str)` → `list[Element]` - Find elements by CSS selector
|
||||
- `get_element(backend_node_id: int)` → `Element` - Get element by backend node ID
|
||||
- `get_element_by_prompt(prompt: str, llm)` → `Element | None` - AI-powered element finding
|
||||
- `must_get_element_by_prompt(prompt: str, llm)` → `Element` - AI element finding (raises if not found)
|
||||
- `extract_content(prompt: str, structured_output: type[T], llm)` → `T` - Extract structured data using LLM
|
||||
- `goto(url: str)` - Navigate this page to URL
|
||||
- `go_back()`, `go_forward()` - Navigate history (with error handling)
|
||||
- `reload()` - Reload the current page
|
||||
- `evaluate(page_function: str, *args)` → `str` - Execute JavaScript (MUST use (...args) => format)
|
||||
- `press(key: str)` - Press key on page (supports "Control+A" format)
|
||||
- `set_viewport_size(width: int, height: int)` - Set viewport dimensions
|
||||
- `screenshot(format='png', quality=None)` → `str` - Take page screenshot, return base64
|
||||
- `get_url()` → `str`, `get_title()` → `str` - Get page information
|
||||
- `mouse` → `Mouse` - Get mouse interface for this page
|
||||
|
||||
### Element Methods (DOM Interactions)
|
||||
- `click(button='left', click_count=1, modifiers=None)` - Click element with advanced fallbacks
|
||||
- `fill(text: str, clear=True)` - Fill input with text (clears first by default)
|
||||
- `hover()` - Hover over element
|
||||
- `focus()` - Focus the element
|
||||
- `check()` - Toggle checkbox/radio button (clicks to change state)
|
||||
- `select_option(values: str | list[str])` - Select dropdown options
|
||||
- `drag_to(target_element: Element | Position, source_position=None, target_position=None)` - Drag to target element
|
||||
- `evaluate(page_function: str, *args)` → `str` - Execute JavaScript on element (this = element)
|
||||
- `get_attribute(name: str)` → `str | None` - Get attribute value
|
||||
- `get_bounding_box()` → `BoundingBox | None` - Get element position/size
|
||||
- `screenshot(format='png', quality=None)` → `str` - Take element screenshot, return base64
|
||||
- `get_basic_info()` → `ElementInfo` - Get comprehensive element information
|
||||
|
||||
|
||||
### Mouse Methods (Coordinate-Based Operations)
|
||||
- `click(x: int, y: int, button='left', click_count=1)` - Click at coordinates
|
||||
- `move(x: int, y: int, steps=1)` - Move to coordinates
|
||||
- `down(button='left', click_count=1)`, `up(button='left', click_count=1)` - Press/release button
|
||||
- `scroll(x=0, y=0, delta_x=None, delta_y=None)` - Scroll page at coordinates
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### Position
|
||||
```python
|
||||
class Position(TypedDict):
|
||||
x: float
|
||||
y: float
|
||||
```
|
||||
|
||||
### BoundingBox
|
||||
```python
|
||||
class BoundingBox(TypedDict):
|
||||
x: float
|
||||
y: float
|
||||
width: float
|
||||
height: float
|
||||
```
|
||||
|
||||
### ElementInfo
|
||||
```python
|
||||
class ElementInfo(TypedDict):
|
||||
backendNodeId: int # CDP backend node ID
|
||||
nodeId: int | None # CDP node ID
|
||||
nodeName: str # HTML tag name (e.g., "DIV", "INPUT")
|
||||
nodeType: int # DOM node type
|
||||
nodeValue: str | None # Text content for text nodes
|
||||
attributes: dict[str, str] # HTML attributes
|
||||
boundingBox: BoundingBox | None # Element position and size
|
||||
error: str | None # Error message if info retrieval failed
|
||||
```
|
||||
|
||||
## Important Usage Notes
|
||||
|
||||
**This is browser-use actor, NOT Playwright or Selenium.** Only use the methods documented above.
|
||||
|
||||
### Critical JavaScript Rules
|
||||
- `page.evaluate()` and `element.evaluate()` MUST use `(...args) => {}` arrow function format
|
||||
- Always returns string (objects are JSON-stringified automatically)
|
||||
- Use single quotes around the function: `page.evaluate('() => document.title')`
|
||||
- For complex selectors in JS: `'() => document.querySelector("input[name=\\"email\\"]")'`
|
||||
- `element.evaluate()`: `this` context is bound to the element automatically
|
||||
|
||||
### Method Restrictions
|
||||
- `get_elements_by_css_selector()` returns immediately (no automatic waiting)
|
||||
- For dropdowns: use `element.select_option()`, NOT `element.fill()`
|
||||
- Form submission: click submit button or use `page.press("Enter")`
|
||||
- No methods like: `element.submit()`, `element.dispatch_event()`, `element.get_property()`
|
||||
|
||||
### Error Prevention
|
||||
- Always verify page state changes with `page.get_url()`, `page.get_title()`
|
||||
- Use `element.get_attribute()` to check element properties
|
||||
- Validate CSS selectors before use
|
||||
- Handle navigation timing with appropriate `asyncio.sleep()` calls
|
||||
|
||||
### AI Features
|
||||
- `get_element_by_prompt()` and `extract_content()` require an LLM instance
|
||||
- These methods use DOM analysis and structured output parsing
|
||||
- Best for complex page understanding and data extraction tasks
|
||||
@@ -0,0 +1,11 @@
|
||||
"""CDP-Use High-Level Library
|
||||
|
||||
A Playwright-like library built on top of CDP (Chrome DevTools Protocol).
|
||||
"""
|
||||
|
||||
from .element import Element
|
||||
from .mouse import Mouse
|
||||
from .page import Page
|
||||
from .utils import Utils
|
||||
|
||||
__all__ = ['Page', 'Element', 'Mouse', 'Utils']
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
||||
"""Mouse class for mouse operations."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cdp_use.cdp.input.commands import DispatchMouseEventParameters, SynthesizeScrollGestureParameters
|
||||
from cdp_use.cdp.input.types import MouseButton
|
||||
|
||||
from browser_use.browser.session import BrowserSession
|
||||
|
||||
|
||||
class Mouse:
|
||||
"""Mouse operations for a target."""
|
||||
|
||||
def __init__(self, browser_session: 'BrowserSession', session_id: str | None = None, target_id: str | None = None):
|
||||
self._browser_session = browser_session
|
||||
self._client = browser_session.cdp_client
|
||||
self._session_id = session_id
|
||||
self._target_id = target_id
|
||||
|
||||
async def click(self, x: int, y: int, button: 'MouseButton' = 'left', click_count: int = 1) -> None:
|
||||
"""Click at the specified coordinates."""
|
||||
# Mouse press
|
||||
press_params: 'DispatchMouseEventParameters' = {
|
||||
'type': 'mousePressed',
|
||||
'x': x,
|
||||
'y': y,
|
||||
'button': button,
|
||||
'clickCount': click_count,
|
||||
}
|
||||
await self._client.send.Input.dispatchMouseEvent(
|
||||
press_params,
|
||||
session_id=self._session_id,
|
||||
)
|
||||
|
||||
# Mouse release
|
||||
release_params: 'DispatchMouseEventParameters' = {
|
||||
'type': 'mouseReleased',
|
||||
'x': x,
|
||||
'y': y,
|
||||
'button': button,
|
||||
'clickCount': click_count,
|
||||
}
|
||||
await self._client.send.Input.dispatchMouseEvent(
|
||||
release_params,
|
||||
session_id=self._session_id,
|
||||
)
|
||||
|
||||
async def down(self, button: 'MouseButton' = 'left', click_count: int = 1) -> None:
|
||||
"""Press mouse button down."""
|
||||
params: 'DispatchMouseEventParameters' = {
|
||||
'type': 'mousePressed',
|
||||
'x': 0, # Will use last mouse position
|
||||
'y': 0,
|
||||
'button': button,
|
||||
'clickCount': click_count,
|
||||
}
|
||||
await self._client.send.Input.dispatchMouseEvent(
|
||||
params,
|
||||
session_id=self._session_id,
|
||||
)
|
||||
|
||||
async def up(self, button: 'MouseButton' = 'left', click_count: int = 1) -> None:
|
||||
"""Release mouse button."""
|
||||
params: 'DispatchMouseEventParameters' = {
|
||||
'type': 'mouseReleased',
|
||||
'x': 0, # Will use last mouse position
|
||||
'y': 0,
|
||||
'button': button,
|
||||
'clickCount': click_count,
|
||||
}
|
||||
await self._client.send.Input.dispatchMouseEvent(
|
||||
params,
|
||||
session_id=self._session_id,
|
||||
)
|
||||
|
||||
async def move(self, x: int, y: int, steps: int = 1) -> None:
|
||||
"""Move mouse to the specified coordinates."""
|
||||
# TODO: Implement smooth movement with multiple steps if needed
|
||||
_ = steps # Acknowledge parameter for future use
|
||||
|
||||
params: 'DispatchMouseEventParameters' = {'type': 'mouseMoved', 'x': x, 'y': y}
|
||||
await self._client.send.Input.dispatchMouseEvent(params, session_id=self._session_id)
|
||||
|
||||
async def scroll(self, x: int = 0, y: int = 0, delta_x: int | None = None, delta_y: int | None = None) -> None:
|
||||
"""Scroll the page using robust CDP methods."""
|
||||
if not self._session_id:
|
||||
raise RuntimeError('Session ID is required for scroll operations')
|
||||
|
||||
# Method 1: Try mouse wheel event (most reliable)
|
||||
try:
|
||||
# Get viewport dimensions
|
||||
layout_metrics = await self._client.send.Page.getLayoutMetrics(session_id=self._session_id)
|
||||
viewport_width = layout_metrics['layoutViewport']['clientWidth']
|
||||
viewport_height = layout_metrics['layoutViewport']['clientHeight']
|
||||
|
||||
# Use provided coordinates or center of viewport
|
||||
scroll_x = x if x > 0 else viewport_width / 2
|
||||
scroll_y = y if y > 0 else viewport_height / 2
|
||||
|
||||
# Calculate scroll deltas (positive = down/right)
|
||||
scroll_delta_x = delta_x or 0
|
||||
scroll_delta_y = delta_y or 0
|
||||
|
||||
# Dispatch mouse wheel event
|
||||
await self._client.send.Input.dispatchMouseEvent(
|
||||
params={
|
||||
'type': 'mouseWheel',
|
||||
'x': scroll_x,
|
||||
'y': scroll_y,
|
||||
'deltaX': scroll_delta_x,
|
||||
'deltaY': scroll_delta_y,
|
||||
},
|
||||
session_id=self._session_id,
|
||||
)
|
||||
return
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Method 2: Fallback to synthesizeScrollGesture
|
||||
try:
|
||||
params: 'SynthesizeScrollGestureParameters' = {'x': x, 'y': y, 'xDistance': delta_x or 0, 'yDistance': delta_y or 0}
|
||||
await self._client.send.Input.synthesizeScrollGesture(
|
||||
params,
|
||||
session_id=self._session_id,
|
||||
)
|
||||
except Exception:
|
||||
# Method 3: JavaScript fallback
|
||||
scroll_js = f'window.scrollBy({delta_x or 0}, {delta_y or 0})'
|
||||
await self._client.send.Runtime.evaluate(
|
||||
params={'expression': scroll_js, 'returnByValue': True},
|
||||
session_id=self._session_id,
|
||||
)
|
||||
@@ -0,0 +1,564 @@
|
||||
"""Page class for page-level operations."""
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from browser_use import logger
|
||||
from browser_use.actor.utils import get_key_info
|
||||
from browser_use.dom.serializer.serializer import DOMTreeSerializer
|
||||
from browser_use.dom.service import DomService
|
||||
from browser_use.llm.messages import SystemMessage, UserMessage
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cdp_use.cdp.dom.commands import (
|
||||
DescribeNodeParameters,
|
||||
QuerySelectorAllParameters,
|
||||
)
|
||||
from cdp_use.cdp.emulation.commands import SetDeviceMetricsOverrideParameters
|
||||
from cdp_use.cdp.input.commands import (
|
||||
DispatchKeyEventParameters,
|
||||
)
|
||||
from cdp_use.cdp.page.commands import CaptureScreenshotParameters, NavigateParameters, NavigateToHistoryEntryParameters
|
||||
from cdp_use.cdp.runtime.commands import EvaluateParameters
|
||||
from cdp_use.cdp.target.commands import (
|
||||
AttachToTargetParameters,
|
||||
GetTargetInfoParameters,
|
||||
)
|
||||
from cdp_use.cdp.target.types import TargetInfo
|
||||
|
||||
from browser_use.browser.session import BrowserSession
|
||||
from browser_use.llm.base import BaseChatModel
|
||||
|
||||
from .element import Element
|
||||
from .mouse import Mouse
|
||||
|
||||
|
||||
class Page:
|
||||
"""Page operations (tab or iframe)."""
|
||||
|
||||
def __init__(
|
||||
self, browser_session: 'BrowserSession', target_id: str, session_id: str | None = None, llm: 'BaseChatModel | None' = None
|
||||
):
|
||||
self._browser_session = browser_session
|
||||
self._client = browser_session.cdp_client
|
||||
self._target_id = target_id
|
||||
self._session_id: str | None = session_id
|
||||
self._mouse: 'Mouse | None' = None
|
||||
|
||||
self._llm = llm
|
||||
|
||||
async def _ensure_session(self) -> str:
|
||||
"""Ensure we have a session ID for this target."""
|
||||
if not self._session_id:
|
||||
params: 'AttachToTargetParameters' = {'targetId': self._target_id, 'flatten': True}
|
||||
result = await self._client.send.Target.attachToTarget(params)
|
||||
self._session_id = result['sessionId']
|
||||
|
||||
# Enable necessary domains
|
||||
import asyncio
|
||||
|
||||
await asyncio.gather(
|
||||
self._client.send.Page.enable(session_id=self._session_id),
|
||||
self._client.send.DOM.enable(session_id=self._session_id),
|
||||
self._client.send.Runtime.enable(session_id=self._session_id),
|
||||
self._client.send.Network.enable(session_id=self._session_id),
|
||||
)
|
||||
|
||||
return self._session_id
|
||||
|
||||
@property
|
||||
async def session_id(self) -> str:
|
||||
"""Get the session ID for this target.
|
||||
|
||||
@dev Pass this to an arbitrary CDP call
|
||||
"""
|
||||
return await self._ensure_session()
|
||||
|
||||
@property
|
||||
async def mouse(self) -> 'Mouse':
|
||||
"""Get the mouse interface for this target."""
|
||||
if not self._mouse:
|
||||
session_id = await self._ensure_session()
|
||||
from .mouse import Mouse
|
||||
|
||||
self._mouse = Mouse(self._browser_session, session_id, self._target_id)
|
||||
return self._mouse
|
||||
|
||||
async def reload(self) -> None:
|
||||
"""Reload the target."""
|
||||
session_id = await self._ensure_session()
|
||||
await self._client.send.Page.reload(session_id=session_id)
|
||||
|
||||
async def get_element(self, backend_node_id: int) -> 'Element':
|
||||
"""Get an element by its backend node ID."""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
from .element import Element as Element_
|
||||
|
||||
return Element_(self._browser_session, backend_node_id, session_id)
|
||||
|
||||
async def evaluate(self, page_function: str, *args) -> str:
|
||||
"""Execute JavaScript in the target.
|
||||
|
||||
Args:
|
||||
page_function: JavaScript code that MUST start with (...args) => format
|
||||
*args: Arguments to pass to the function
|
||||
|
||||
Returns:
|
||||
String representation of the JavaScript execution result.
|
||||
Objects and arrays are JSON-stringified.
|
||||
"""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
# Clean and fix common JavaScript string parsing issues
|
||||
page_function = self._fix_javascript_string(page_function)
|
||||
|
||||
# Enforce arrow function format
|
||||
if not (page_function.startswith('(') and '=>' in page_function):
|
||||
raise ValueError(f'JavaScript code must start with (...args) => format. Got: {page_function[:50]}...')
|
||||
|
||||
# Build the expression - call the arrow function with provided args
|
||||
if args:
|
||||
# Convert args to JSON representation for safe passing
|
||||
import json
|
||||
|
||||
arg_strs = [json.dumps(arg) for arg in args]
|
||||
expression = f'({page_function})({", ".join(arg_strs)})'
|
||||
else:
|
||||
expression = f'({page_function})()'
|
||||
|
||||
# Debug: log the actual expression being evaluated
|
||||
logger.debug(f'Evaluating JavaScript: {repr(expression)}')
|
||||
|
||||
params: 'EvaluateParameters' = {'expression': expression, 'returnByValue': True, 'awaitPromise': True}
|
||||
result = await self._client.send.Runtime.evaluate(
|
||||
params,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
if 'exceptionDetails' in result:
|
||||
raise RuntimeError(f'JavaScript evaluation failed: {result["exceptionDetails"]}')
|
||||
|
||||
value = result.get('result', {}).get('value')
|
||||
|
||||
# Always return string representation
|
||||
if value is None:
|
||||
return ''
|
||||
elif isinstance(value, str):
|
||||
return value
|
||||
else:
|
||||
# Convert objects, numbers, booleans to string
|
||||
import json
|
||||
|
||||
try:
|
||||
return json.dumps(value) if isinstance(value, (dict, list)) else str(value)
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
|
||||
def _fix_javascript_string(self, js_code: str) -> str:
|
||||
"""Fix common JavaScript string parsing issues when written as Python string."""
|
||||
|
||||
# Just do minimal, safe cleaning
|
||||
js_code = js_code.strip()
|
||||
|
||||
# Only fix the most common and safe issues:
|
||||
|
||||
# 1. Remove obvious Python string wrapper quotes if they exist
|
||||
if (js_code.startswith('"') and js_code.endswith('"')) or (js_code.startswith("'") and js_code.endswith("'")):
|
||||
# Check if it's a wrapped string (not part of JS syntax)
|
||||
inner = js_code[1:-1]
|
||||
if inner.count('"') + inner.count("'") == 0 or '() =>' in inner:
|
||||
js_code = inner
|
||||
|
||||
# 2. Only fix clearly escaped quotes that shouldn't be
|
||||
# But be very conservative - only if we're sure it's a Python string artifact
|
||||
if '\\"' in js_code and js_code.count('\\"') > js_code.count('"'):
|
||||
js_code = js_code.replace('\\"', '"')
|
||||
if "\\'" in js_code and js_code.count("\\'") > js_code.count("'"):
|
||||
js_code = js_code.replace("\\'", "'")
|
||||
|
||||
# 3. Basic whitespace normalization only
|
||||
js_code = js_code.strip()
|
||||
|
||||
# Final validation - ensure it's not empty
|
||||
if not js_code:
|
||||
raise ValueError('JavaScript code is empty after cleaning')
|
||||
|
||||
return js_code
|
||||
|
||||
async def screenshot(self, format: str = 'png', quality: int | None = None) -> str:
|
||||
"""Take a screenshot and return base64 encoded image.
|
||||
|
||||
Args:
|
||||
format: Image format ('jpeg', 'png', 'webp')
|
||||
quality: Quality 0-100 for JPEG format
|
||||
|
||||
Returns:
|
||||
Base64-encoded image data
|
||||
"""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
params: 'CaptureScreenshotParameters' = {'format': format}
|
||||
|
||||
if quality is not None and format.lower() == 'jpeg':
|
||||
params['quality'] = quality
|
||||
|
||||
result = await self._client.send.Page.captureScreenshot(params, session_id=session_id)
|
||||
|
||||
return result['data']
|
||||
|
||||
async def press(self, key: str) -> None:
|
||||
"""Press a key on the page (sends keyboard input to the focused element or page)."""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
# Handle key combinations like "Control+A"
|
||||
if '+' in key:
|
||||
parts = key.split('+')
|
||||
modifiers = parts[:-1]
|
||||
main_key = parts[-1]
|
||||
|
||||
# Calculate modifier bitmask
|
||||
modifier_value = 0
|
||||
modifier_map = {'Alt': 1, 'Control': 2, 'Meta': 4, 'Shift': 8}
|
||||
for mod in modifiers:
|
||||
modifier_value |= modifier_map.get(mod, 0)
|
||||
|
||||
# Press modifier keys
|
||||
for mod in modifiers:
|
||||
code, vk_code = get_key_info(mod)
|
||||
params: 'DispatchKeyEventParameters' = {'type': 'keyDown', 'key': mod, 'code': code}
|
||||
if vk_code is not None:
|
||||
params['windowsVirtualKeyCode'] = vk_code
|
||||
await self._client.send.Input.dispatchKeyEvent(params, session_id=session_id)
|
||||
|
||||
# Press main key with modifiers bitmask
|
||||
main_code, main_vk_code = get_key_info(main_key)
|
||||
main_down_params: 'DispatchKeyEventParameters' = {
|
||||
'type': 'keyDown',
|
||||
'key': main_key,
|
||||
'code': main_code,
|
||||
'modifiers': modifier_value,
|
||||
}
|
||||
if main_vk_code is not None:
|
||||
main_down_params['windowsVirtualKeyCode'] = main_vk_code
|
||||
await self._client.send.Input.dispatchKeyEvent(main_down_params, session_id=session_id)
|
||||
|
||||
main_up_params: 'DispatchKeyEventParameters' = {
|
||||
'type': 'keyUp',
|
||||
'key': main_key,
|
||||
'code': main_code,
|
||||
'modifiers': modifier_value,
|
||||
}
|
||||
if main_vk_code is not None:
|
||||
main_up_params['windowsVirtualKeyCode'] = main_vk_code
|
||||
await self._client.send.Input.dispatchKeyEvent(main_up_params, session_id=session_id)
|
||||
|
||||
# Release modifier keys
|
||||
for mod in reversed(modifiers):
|
||||
code, vk_code = get_key_info(mod)
|
||||
release_params: 'DispatchKeyEventParameters' = {'type': 'keyUp', 'key': mod, 'code': code}
|
||||
if vk_code is not None:
|
||||
release_params['windowsVirtualKeyCode'] = vk_code
|
||||
await self._client.send.Input.dispatchKeyEvent(release_params, session_id=session_id)
|
||||
else:
|
||||
# Simple key press
|
||||
code, vk_code = get_key_info(key)
|
||||
key_down_params: 'DispatchKeyEventParameters' = {'type': 'keyDown', 'key': key, 'code': code}
|
||||
if vk_code is not None:
|
||||
key_down_params['windowsVirtualKeyCode'] = vk_code
|
||||
await self._client.send.Input.dispatchKeyEvent(key_down_params, session_id=session_id)
|
||||
|
||||
key_up_params: 'DispatchKeyEventParameters' = {'type': 'keyUp', 'key': key, 'code': code}
|
||||
if vk_code is not None:
|
||||
key_up_params['windowsVirtualKeyCode'] = vk_code
|
||||
await self._client.send.Input.dispatchKeyEvent(key_up_params, session_id=session_id)
|
||||
|
||||
async def set_viewport_size(self, width: int, height: int) -> None:
|
||||
"""Set the viewport size."""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
params: 'SetDeviceMetricsOverrideParameters' = {
|
||||
'width': width,
|
||||
'height': height,
|
||||
'deviceScaleFactor': 1.0,
|
||||
'mobile': False,
|
||||
}
|
||||
await self._client.send.Emulation.setDeviceMetricsOverride(
|
||||
params,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
# Target properties (from CDP getTargetInfo)
|
||||
async def get_target_info(self) -> 'TargetInfo':
|
||||
"""Get target information."""
|
||||
params: 'GetTargetInfoParameters' = {'targetId': self._target_id}
|
||||
result = await self._client.send.Target.getTargetInfo(params)
|
||||
return result['targetInfo']
|
||||
|
||||
async def get_url(self) -> str:
|
||||
"""Get the current URL."""
|
||||
info = await self.get_target_info()
|
||||
return info.get('url', '')
|
||||
|
||||
async def get_title(self) -> str:
|
||||
"""Get the current title."""
|
||||
info = await self.get_target_info()
|
||||
return info.get('title', '')
|
||||
|
||||
async def goto(self, url: str) -> None:
|
||||
"""Navigate this target to a URL."""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
params: 'NavigateParameters' = {'url': url}
|
||||
await self._client.send.Page.navigate(params, session_id=session_id)
|
||||
|
||||
async def navigate(self, url: str) -> None:
|
||||
"""Alias for goto."""
|
||||
await self.goto(url)
|
||||
|
||||
async def go_back(self) -> None:
|
||||
"""Navigate back in history."""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
try:
|
||||
# Get navigation history
|
||||
history = await self._client.send.Page.getNavigationHistory(session_id=session_id)
|
||||
current_index = history['currentIndex']
|
||||
entries = history['entries']
|
||||
|
||||
# Check if we can go back
|
||||
if current_index <= 0:
|
||||
raise RuntimeError('Cannot go back - no previous entry in history')
|
||||
|
||||
# Navigate to the previous entry
|
||||
previous_entry_id = entries[current_index - 1]['id']
|
||||
params: 'NavigateToHistoryEntryParameters' = {'entryId': previous_entry_id}
|
||||
await self._client.send.Page.navigateToHistoryEntry(params, session_id=session_id)
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'Failed to navigate back: {e}')
|
||||
|
||||
async def go_forward(self) -> None:
|
||||
"""Navigate forward in history."""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
try:
|
||||
# Get navigation history
|
||||
history = await self._client.send.Page.getNavigationHistory(session_id=session_id)
|
||||
current_index = history['currentIndex']
|
||||
entries = history['entries']
|
||||
|
||||
# Check if we can go forward
|
||||
if current_index >= len(entries) - 1:
|
||||
raise RuntimeError('Cannot go forward - no next entry in history')
|
||||
|
||||
# Navigate to the next entry
|
||||
next_entry_id = entries[current_index + 1]['id']
|
||||
params: 'NavigateToHistoryEntryParameters' = {'entryId': next_entry_id}
|
||||
await self._client.send.Page.navigateToHistoryEntry(params, session_id=session_id)
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'Failed to navigate forward: {e}')
|
||||
|
||||
# Element finding methods (these would need to be implemented based on DOM queries)
|
||||
async def get_elements_by_css_selector(self, selector: str) -> list['Element']:
|
||||
"""Get elements by CSS selector."""
|
||||
session_id = await self._ensure_session()
|
||||
|
||||
# Get document first
|
||||
doc_result = await self._client.send.DOM.getDocument(session_id=session_id)
|
||||
document_node_id = doc_result['root']['nodeId']
|
||||
|
||||
# Query selector all
|
||||
query_params: 'QuerySelectorAllParameters' = {'nodeId': document_node_id, 'selector': selector}
|
||||
result = await self._client.send.DOM.querySelectorAll(query_params, session_id=session_id)
|
||||
|
||||
elements = []
|
||||
from .element import Element as Element_
|
||||
|
||||
# Convert node IDs to backend node IDs
|
||||
for node_id in result['nodeIds']:
|
||||
# Get backend node ID
|
||||
describe_params: 'DescribeNodeParameters' = {'nodeId': node_id}
|
||||
node_result = await self._client.send.DOM.describeNode(describe_params, session_id=session_id)
|
||||
backend_node_id = node_result['node']['backendNodeId']
|
||||
elements.append(Element_(self._browser_session, backend_node_id, session_id))
|
||||
|
||||
return elements
|
||||
|
||||
# AI METHODS
|
||||
|
||||
@property
|
||||
def dom_service(self) -> 'DomService':
|
||||
"""Get the DOM service for this target."""
|
||||
return DomService(self._browser_session)
|
||||
|
||||
async def get_element_by_prompt(self, prompt: str, llm: 'BaseChatModel | None' = None) -> 'Element | None':
|
||||
"""Get an element by a prompt."""
|
||||
await self._ensure_session()
|
||||
llm = llm or self._llm
|
||||
|
||||
if not llm:
|
||||
raise ValueError('LLM not provided')
|
||||
|
||||
dom_service = self.dom_service
|
||||
|
||||
# Lazy fetch all_frames inside get_dom_tree if needed (for cross-origin iframes)
|
||||
enhanced_dom_tree, _ = await dom_service.get_dom_tree(target_id=self._target_id, all_frames=None)
|
||||
|
||||
session_id = self._browser_session.id
|
||||
serialized_dom_state, _ = DOMTreeSerializer(
|
||||
enhanced_dom_tree, None, paint_order_filtering=True, session_id=session_id
|
||||
).serialize_accessible_elements()
|
||||
|
||||
llm_representation = serialized_dom_state.llm_representation()
|
||||
|
||||
system_message = SystemMessage(
|
||||
content="""You are an AI created to find an element on a page by a prompt.
|
||||
|
||||
<browser_state>
|
||||
Interactive Elements: All interactive elements will be provided in format as [index]<type>text</type> where
|
||||
- index: Numeric identifier for interaction
|
||||
- type: HTML element type (button, input, etc.)
|
||||
- text: Element description
|
||||
|
||||
Examples:
|
||||
[33]<div>User form</div>
|
||||
[35]<button aria-label='Submit form'>Submit</button>
|
||||
|
||||
Note that:
|
||||
- Only elements with numeric indexes in [] are interactive
|
||||
- (stacked) indentation (with \t) is important and means that the element is a (html) child of the element above (with a lower index)
|
||||
- Pure text elements without [] are not interactive.
|
||||
</browser_state>
|
||||
|
||||
Your task is to find an element index (if any) that matches the prompt (written in <prompt> tag).
|
||||
|
||||
If non of the elements matches the, return None.
|
||||
|
||||
Before you return the element index, reason about the state and elements for a sentence or two."""
|
||||
)
|
||||
|
||||
state_message = UserMessage(
|
||||
content=f"""
|
||||
<browser_state>
|
||||
{llm_representation}
|
||||
</browser_state>
|
||||
|
||||
<prompt>
|
||||
{prompt}
|
||||
</prompt>
|
||||
"""
|
||||
)
|
||||
|
||||
class ElementResponse(BaseModel):
|
||||
# thinking: str
|
||||
element_highlight_index: int | None
|
||||
|
||||
llm_response = await llm.ainvoke(
|
||||
[
|
||||
system_message,
|
||||
state_message,
|
||||
],
|
||||
output_format=ElementResponse,
|
||||
)
|
||||
|
||||
element_highlight_index = llm_response.completion.element_highlight_index
|
||||
|
||||
if element_highlight_index is None or element_highlight_index not in serialized_dom_state.selector_map:
|
||||
return None
|
||||
|
||||
element = serialized_dom_state.selector_map[element_highlight_index]
|
||||
|
||||
from .element import Element as Element_
|
||||
|
||||
return Element_(self._browser_session, element.backend_node_id, self._session_id)
|
||||
|
||||
async def must_get_element_by_prompt(self, prompt: str, llm: 'BaseChatModel | None' = None) -> 'Element':
|
||||
"""Get an element by a prompt.
|
||||
|
||||
@dev LLM can still return None, this just raises an error if the element is not found.
|
||||
"""
|
||||
element = await self.get_element_by_prompt(prompt, llm)
|
||||
if element is None:
|
||||
raise ValueError(f'No element found for prompt: {prompt}')
|
||||
|
||||
return element
|
||||
|
||||
async def extract_content(self, prompt: str, structured_output: type[T], llm: 'BaseChatModel | None' = None) -> T:
|
||||
"""Extract structured content from the current page using LLM.
|
||||
|
||||
Extracts clean markdown from the page and sends it to LLM for structured data extraction.
|
||||
|
||||
Args:
|
||||
prompt: Description of what content to extract
|
||||
structured_output: Pydantic BaseModel class defining the expected output structure
|
||||
llm: Language model to use for extraction
|
||||
|
||||
Returns:
|
||||
The structured BaseModel instance with extracted content
|
||||
"""
|
||||
llm = llm or self._llm
|
||||
|
||||
if not llm:
|
||||
raise ValueError('LLM not provided')
|
||||
|
||||
# Extract clean markdown using the same method as in tools/service.py
|
||||
try:
|
||||
content, content_stats = await self._extract_clean_markdown()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'Could not extract clean markdown: {type(e).__name__}')
|
||||
|
||||
# System prompt for structured extraction
|
||||
system_prompt = """
|
||||
You are an expert at extracting structured data from the markdown of a webpage.
|
||||
|
||||
<input>
|
||||
You will be given a query and the markdown of a webpage that has been filtered to remove noise and advertising content.
|
||||
</input>
|
||||
|
||||
<instructions>
|
||||
- You are tasked to extract information from the webpage that is relevant to the query.
|
||||
- You should ONLY use the information available in the webpage to answer the query. Do not make up information or provide guess from your own knowledge.
|
||||
- If the information relevant to the query is not available in the page, your response should mention that.
|
||||
- If the query asks for all items, products, etc., make sure to directly list all of them.
|
||||
- Return the extracted content in the exact structured format specified.
|
||||
</instructions>
|
||||
|
||||
<output>
|
||||
- Your output should present ALL the information relevant to the query in the specified structured format.
|
||||
- Do not answer in conversational format - directly output the relevant information in the structured format.
|
||||
</output>
|
||||
""".strip()
|
||||
|
||||
# Build prompt with just query and content
|
||||
prompt_content = f'<query>\n{prompt}\n</query>\n\n<webpage_content>\n{content}\n</webpage_content>'
|
||||
|
||||
# Send to LLM with structured output
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
llm.ainvoke(
|
||||
[SystemMessage(content=system_prompt), UserMessage(content=prompt_content)], output_format=structured_output
|
||||
),
|
||||
timeout=120.0,
|
||||
)
|
||||
|
||||
# Return the structured output BaseModel instance
|
||||
return response.completion
|
||||
except Exception as e:
|
||||
raise RuntimeError(str(e))
|
||||
|
||||
async def _extract_clean_markdown(self, extract_links: bool = False) -> tuple[str, dict]:
|
||||
"""Extract clean markdown from the current page using enhanced DOM tree.
|
||||
|
||||
Uses the shared markdown extractor for consistency with tools/service.py.
|
||||
"""
|
||||
from browser_use.dom.markdown_extractor import extract_clean_markdown
|
||||
|
||||
dom_service = self.dom_service
|
||||
return await extract_clean_markdown(dom_service=dom_service, target_id=self._target_id, extract_links=extract_links)
|
||||
@@ -0,0 +1,41 @@
|
||||
import asyncio
|
||||
|
||||
from browser_use import Agent, Browser, ChatOpenAI
|
||||
|
||||
llm = ChatOpenAI('gpt-4.1-mini')
|
||||
|
||||
|
||||
async def main():
|
||||
"""
|
||||
Main function demonstrating mixed automation with Browser-Use and Playwright.
|
||||
"""
|
||||
print('🚀 Mixed Automation with Browser-Use and Actor API')
|
||||
|
||||
browser = Browser(keep_alive=True)
|
||||
await browser.start()
|
||||
|
||||
page = await browser.get_current_page() or await browser.new_page()
|
||||
|
||||
# Go to apple wikipedia page
|
||||
await page.goto('https://www.google.com/travel/flights')
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
round_trip_button = await page.must_get_element_by_prompt('round trip button', llm)
|
||||
await round_trip_button.click()
|
||||
|
||||
one_way_button = await page.must_get_element_by_prompt('one way button', llm)
|
||||
await one_way_button.click()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
agent = Agent(task='Find the cheapest flight from London to Paris on 2025-10-15', llm=llm, browser_session=browser)
|
||||
await agent.run()
|
||||
|
||||
input('Press Enter to continue...')
|
||||
|
||||
await browser.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from browser_use import Browser, ChatOpenAI
|
||||
|
||||
TASK = """
|
||||
On the current wikipedia page, find the latest huge edit and tell me what is was about.
|
||||
"""
|
||||
|
||||
|
||||
class LatestEditFinder(BaseModel):
|
||||
"""Find the latest huge edit on the current wikipedia page."""
|
||||
|
||||
latest_edit: str
|
||||
edit_time: str
|
||||
edit_author: str
|
||||
edit_summary: str
|
||||
edit_url: str
|
||||
|
||||
|
||||
llm = ChatOpenAI('gpt-4.1-mini')
|
||||
|
||||
|
||||
async def main():
|
||||
"""
|
||||
Main function demonstrating mixed automation with Browser-Use and Playwright.
|
||||
"""
|
||||
print('🚀 Mixed Automation with Browser-Use and Actor API')
|
||||
|
||||
browser = Browser(keep_alive=True)
|
||||
await browser.start()
|
||||
|
||||
page = await browser.get_current_page() or await browser.new_page()
|
||||
|
||||
# Go to apple wikipedia page
|
||||
await page.goto('https://browser-use.github.io/stress-tests/challenges/angularjs-form.html')
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
element = await page.get_element_by_prompt('zip code input', llm)
|
||||
|
||||
print('Element found', element)
|
||||
|
||||
if element:
|
||||
await element.click()
|
||||
else:
|
||||
print('No element found')
|
||||
|
||||
await browser.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
Executable
+236
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Playground script to test the browser-use actor API.
|
||||
|
||||
This script demonstrates:
|
||||
- Starting a browser session
|
||||
- Using the actor API to navigate and interact
|
||||
- Finding elements, clicking, scrolling, JavaScript evaluation
|
||||
- Testing most of the available methods
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
from browser_use import Browser
|
||||
|
||||
# Configure logging to see what's happening
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main playground function."""
|
||||
logger.info('🚀 Starting browser actor playground')
|
||||
|
||||
# Create browser session
|
||||
browser = Browser()
|
||||
|
||||
try:
|
||||
# Start the browser
|
||||
await browser.start()
|
||||
logger.info('✅ Browser session started')
|
||||
|
||||
# Navigate to Wikipedia using integrated methods
|
||||
logger.info('📖 Navigating to Wikipedia...')
|
||||
page = await browser.new_page('https://en.wikipedia.org')
|
||||
|
||||
# Get basic page info
|
||||
url = await page.get_url()
|
||||
title = await page.get_title()
|
||||
logger.info(f'📄 Page loaded: {title} ({url})')
|
||||
|
||||
# Take a screenshot
|
||||
logger.info('📸 Taking initial screenshot...')
|
||||
screenshot_b64 = await page.screenshot()
|
||||
logger.info(f'📸 Screenshot captured: {len(screenshot_b64)} bytes')
|
||||
|
||||
# Set viewport size
|
||||
logger.info('🖥️ Setting viewport to 1920x1080...')
|
||||
await page.set_viewport_size(1920, 1080)
|
||||
|
||||
# Execute some JavaScript to count links
|
||||
logger.info('🔍 Counting article links using JavaScript...')
|
||||
js_code = """() => {
|
||||
// Find all article links on the page
|
||||
const links = Array.from(document.querySelectorAll('a[href*="/wiki/"]:not([href*=":"])'))
|
||||
.filter(link => !link.href.includes('Main_Page') && !link.href.includes('Special:'));
|
||||
|
||||
return {
|
||||
total: links.length,
|
||||
sample: links.slice(0, 3).map(link => ({
|
||||
href: link.href,
|
||||
text: link.textContent.trim()
|
||||
}))
|
||||
};
|
||||
}"""
|
||||
|
||||
link_info = json.loads(await page.evaluate(js_code))
|
||||
logger.info(f'🔗 Found {link_info["total"]} article links')
|
||||
# Try to find and interact with links using CSS selector
|
||||
try:
|
||||
# Find article links on the page
|
||||
links = await page.get_elements_by_css_selector('a[href*="/wiki/"]:not([href*=":"])')
|
||||
|
||||
if links:
|
||||
logger.info(f'📋 Found {len(links)} wiki links via CSS selector')
|
||||
|
||||
# Pick the first link
|
||||
link_element = links[0]
|
||||
|
||||
# Get link info using available methods
|
||||
basic_info = await link_element.get_basic_info()
|
||||
link_href = await link_element.get_attribute('href')
|
||||
|
||||
logger.info(f'🎯 Selected element: <{basic_info["nodeName"]}>')
|
||||
logger.info(f'🔗 Link href: {link_href}')
|
||||
|
||||
if basic_info['boundingBox']:
|
||||
bbox = basic_info['boundingBox']
|
||||
logger.info(f'📏 Position: ({bbox["x"]}, {bbox["y"]}) Size: {bbox["width"]}x{bbox["height"]}')
|
||||
|
||||
# Test element interactions with robust implementations
|
||||
logger.info('👆 Hovering over the element...')
|
||||
await link_element.hover()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
logger.info('🔍 Focusing the element...')
|
||||
await link_element.focus()
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Click the link using robust click method
|
||||
logger.info('🖱️ Clicking the link with robust fallbacks...')
|
||||
await link_element.click()
|
||||
|
||||
# Wait for navigation
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# Get new page info
|
||||
new_url = await page.get_url()
|
||||
new_title = await page.get_title()
|
||||
logger.info(f'📄 Navigated to: {new_title}')
|
||||
logger.info(f'🌐 New URL: {new_url}')
|
||||
else:
|
||||
logger.warning('❌ No links found to interact with')
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f'⚠️ Link interaction failed: {e}')
|
||||
|
||||
# Scroll down the page
|
||||
logger.info('📜 Scrolling down the page...')
|
||||
mouse = await page.mouse
|
||||
await mouse.scroll(x=0, y=100, delta_y=500)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Test mouse operations
|
||||
logger.info('🖱️ Testing mouse operations...')
|
||||
await mouse.move(x=100, y=200)
|
||||
await mouse.click(x=150, y=250)
|
||||
|
||||
# Execute more JavaScript examples
|
||||
logger.info('🧪 Testing JavaScript evaluation...')
|
||||
|
||||
# Simple expressions
|
||||
page_height = await page.evaluate('() => document.body.scrollHeight')
|
||||
current_scroll = await page.evaluate('() => window.pageYOffset')
|
||||
logger.info(f'📏 Page height: {page_height}px, current scroll: {current_scroll}px')
|
||||
|
||||
# JavaScript with arguments
|
||||
result = await page.evaluate('(x) => x * 2', 21)
|
||||
logger.info(f'🧮 JavaScript with args: 21 * 2 = {result}')
|
||||
|
||||
# More complex JavaScript
|
||||
page_stats = json.loads(
|
||||
await page.evaluate("""() => {
|
||||
return {
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
links: document.querySelectorAll('a').length,
|
||||
images: document.querySelectorAll('img').length,
|
||||
scrollTop: window.pageYOffset,
|
||||
viewportHeight: window.innerHeight
|
||||
};
|
||||
}""")
|
||||
)
|
||||
logger.info(f'📊 Page stats: {page_stats}')
|
||||
|
||||
# Get page title using different methods
|
||||
title_via_js = await page.evaluate('() => document.title')
|
||||
title_via_api = await page.get_title()
|
||||
logger.info(f'📝 Title via JS: "{title_via_js}"')
|
||||
logger.info(f'📝 Title via API: "{title_via_api}"')
|
||||
|
||||
# Take a final screenshot
|
||||
logger.info('📸 Taking final screenshot...')
|
||||
final_screenshot = await page.screenshot()
|
||||
logger.info(f'📸 Final screenshot: {len(final_screenshot)} bytes')
|
||||
|
||||
# Test browser navigation with error handling
|
||||
logger.info('⬅️ Testing browser back navigation...')
|
||||
try:
|
||||
await page.go_back()
|
||||
await asyncio.sleep(2)
|
||||
|
||||
back_url = await page.get_url()
|
||||
back_title = await page.get_title()
|
||||
logger.info(f'📄 After going back: {back_title}')
|
||||
logger.info(f'🌐 Back URL: {back_url}')
|
||||
except RuntimeError as e:
|
||||
logger.info(f'ℹ️ Navigation back failed as expected: {e}')
|
||||
|
||||
# Test creating new page
|
||||
logger.info('🆕 Creating new blank page...')
|
||||
new_page = await browser.new_page()
|
||||
new_page_url = await new_page.get_url()
|
||||
logger.info(f'🆕 New page created with URL: {new_page_url}')
|
||||
|
||||
# Get all pages
|
||||
all_pages = await browser.get_pages()
|
||||
logger.info(f'📑 Total pages: {len(all_pages)}')
|
||||
|
||||
# Test form interaction if we can find a form
|
||||
try:
|
||||
# Look for search input on the page
|
||||
search_inputs = await page.get_elements_by_css_selector('input[type="search"], input[name*="search"]')
|
||||
|
||||
if search_inputs:
|
||||
search_input = search_inputs[0]
|
||||
logger.info('🔍 Found search input, testing form interaction...')
|
||||
|
||||
await search_input.focus()
|
||||
await search_input.fill('test search query')
|
||||
await page.press('Enter')
|
||||
|
||||
logger.info('✅ Form interaction test completed')
|
||||
else:
|
||||
logger.info('ℹ️ No search inputs found for form testing')
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f'ℹ️ Form interaction test skipped: {e}')
|
||||
|
||||
# wait 2 seconds before closing the new page
|
||||
logger.info('🕒 Waiting 2 seconds before closing the new page...')
|
||||
await asyncio.sleep(2)
|
||||
logger.info('🗑️ Closing new page...')
|
||||
await browser.close_page(new_page)
|
||||
|
||||
logger.info('✅ Playground completed successfully!')
|
||||
|
||||
input('Press Enter to continue...')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Error in playground: {e}', exc_info=True)
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
logger.info('🧹 Cleaning up...')
|
||||
try:
|
||||
await browser.stop()
|
||||
logger.info('✅ Browser session stopped')
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Error stopping browser: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Utility functions for actor operations."""
|
||||
|
||||
|
||||
class Utils:
|
||||
"""Utility functions for actor operations."""
|
||||
|
||||
@staticmethod
|
||||
def get_key_info(key: str) -> tuple[str, int | None]:
|
||||
"""Get the code and windowsVirtualKeyCode for a key.
|
||||
|
||||
Args:
|
||||
key: Key name (e.g., 'Enter', 'ArrowUp', 'a', 'A')
|
||||
|
||||
Returns:
|
||||
Tuple of (code, windowsVirtualKeyCode)
|
||||
|
||||
Reference: Windows Virtual Key Codes
|
||||
https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
"""
|
||||
# Complete mapping of key names to (code, virtualKeyCode)
|
||||
# Based on standard Windows Virtual Key Codes
|
||||
key_map = {
|
||||
# Navigation keys
|
||||
'Backspace': ('Backspace', 8),
|
||||
'Tab': ('Tab', 9),
|
||||
'Enter': ('Enter', 13),
|
||||
'Escape': ('Escape', 27),
|
||||
'Space': ('Space', 32),
|
||||
' ': ('Space', 32),
|
||||
'PageUp': ('PageUp', 33),
|
||||
'PageDown': ('PageDown', 34),
|
||||
'End': ('End', 35),
|
||||
'Home': ('Home', 36),
|
||||
'ArrowLeft': ('ArrowLeft', 37),
|
||||
'ArrowUp': ('ArrowUp', 38),
|
||||
'ArrowRight': ('ArrowRight', 39),
|
||||
'ArrowDown': ('ArrowDown', 40),
|
||||
'Insert': ('Insert', 45),
|
||||
'Delete': ('Delete', 46),
|
||||
# Modifier keys
|
||||
'Shift': ('ShiftLeft', 16),
|
||||
'ShiftLeft': ('ShiftLeft', 16),
|
||||
'ShiftRight': ('ShiftRight', 16),
|
||||
'Control': ('ControlLeft', 17),
|
||||
'ControlLeft': ('ControlLeft', 17),
|
||||
'ControlRight': ('ControlRight', 17),
|
||||
'Alt': ('AltLeft', 18),
|
||||
'AltLeft': ('AltLeft', 18),
|
||||
'AltRight': ('AltRight', 18),
|
||||
'Meta': ('MetaLeft', 91),
|
||||
'MetaLeft': ('MetaLeft', 91),
|
||||
'MetaRight': ('MetaRight', 92),
|
||||
# Function keys F1-F24
|
||||
'F1': ('F1', 112),
|
||||
'F2': ('F2', 113),
|
||||
'F3': ('F3', 114),
|
||||
'F4': ('F4', 115),
|
||||
'F5': ('F5', 116),
|
||||
'F6': ('F6', 117),
|
||||
'F7': ('F7', 118),
|
||||
'F8': ('F8', 119),
|
||||
'F9': ('F9', 120),
|
||||
'F10': ('F10', 121),
|
||||
'F11': ('F11', 122),
|
||||
'F12': ('F12', 123),
|
||||
'F13': ('F13', 124),
|
||||
'F14': ('F14', 125),
|
||||
'F15': ('F15', 126),
|
||||
'F16': ('F16', 127),
|
||||
'F17': ('F17', 128),
|
||||
'F18': ('F18', 129),
|
||||
'F19': ('F19', 130),
|
||||
'F20': ('F20', 131),
|
||||
'F21': ('F21', 132),
|
||||
'F22': ('F22', 133),
|
||||
'F23': ('F23', 134),
|
||||
'F24': ('F24', 135),
|
||||
# Numpad keys
|
||||
'NumLock': ('NumLock', 144),
|
||||
'Numpad0': ('Numpad0', 96),
|
||||
'Numpad1': ('Numpad1', 97),
|
||||
'Numpad2': ('Numpad2', 98),
|
||||
'Numpad3': ('Numpad3', 99),
|
||||
'Numpad4': ('Numpad4', 100),
|
||||
'Numpad5': ('Numpad5', 101),
|
||||
'Numpad6': ('Numpad6', 102),
|
||||
'Numpad7': ('Numpad7', 103),
|
||||
'Numpad8': ('Numpad8', 104),
|
||||
'Numpad9': ('Numpad9', 105),
|
||||
'NumpadMultiply': ('NumpadMultiply', 106),
|
||||
'NumpadAdd': ('NumpadAdd', 107),
|
||||
'NumpadSubtract': ('NumpadSubtract', 109),
|
||||
'NumpadDecimal': ('NumpadDecimal', 110),
|
||||
'NumpadDivide': ('NumpadDivide', 111),
|
||||
# Lock keys
|
||||
'CapsLock': ('CapsLock', 20),
|
||||
'ScrollLock': ('ScrollLock', 145),
|
||||
# OEM/Punctuation keys (US keyboard layout)
|
||||
'Semicolon': ('Semicolon', 186),
|
||||
';': ('Semicolon', 186),
|
||||
'Equal': ('Equal', 187),
|
||||
'=': ('Equal', 187),
|
||||
'Comma': ('Comma', 188),
|
||||
',': ('Comma', 188),
|
||||
'Minus': ('Minus', 189),
|
||||
'-': ('Minus', 189),
|
||||
'Period': ('Period', 190),
|
||||
'.': ('Period', 190),
|
||||
'Slash': ('Slash', 191),
|
||||
'/': ('Slash', 191),
|
||||
'Backquote': ('Backquote', 192),
|
||||
'`': ('Backquote', 192),
|
||||
'BracketLeft': ('BracketLeft', 219),
|
||||
'[': ('BracketLeft', 219),
|
||||
'Backslash': ('Backslash', 220),
|
||||
'\\': ('Backslash', 220),
|
||||
'BracketRight': ('BracketRight', 221),
|
||||
']': ('BracketRight', 221),
|
||||
'Quote': ('Quote', 222),
|
||||
"'": ('Quote', 222),
|
||||
# Media/Browser keys
|
||||
'AudioVolumeMute': ('AudioVolumeMute', 173),
|
||||
'AudioVolumeDown': ('AudioVolumeDown', 174),
|
||||
'AudioVolumeUp': ('AudioVolumeUp', 175),
|
||||
'MediaTrackNext': ('MediaTrackNext', 176),
|
||||
'MediaTrackPrevious': ('MediaTrackPrevious', 177),
|
||||
'MediaStop': ('MediaStop', 178),
|
||||
'MediaPlayPause': ('MediaPlayPause', 179),
|
||||
'BrowserBack': ('BrowserBack', 166),
|
||||
'BrowserForward': ('BrowserForward', 167),
|
||||
'BrowserRefresh': ('BrowserRefresh', 168),
|
||||
'BrowserStop': ('BrowserStop', 169),
|
||||
'BrowserSearch': ('BrowserSearch', 170),
|
||||
'BrowserFavorites': ('BrowserFavorites', 171),
|
||||
'BrowserHome': ('BrowserHome', 172),
|
||||
# Additional common keys
|
||||
'Clear': ('Clear', 12),
|
||||
'Pause': ('Pause', 19),
|
||||
'Select': ('Select', 41),
|
||||
'Print': ('Print', 42),
|
||||
'Execute': ('Execute', 43),
|
||||
'PrintScreen': ('PrintScreen', 44),
|
||||
'Help': ('Help', 47),
|
||||
'ContextMenu': ('ContextMenu', 93),
|
||||
}
|
||||
|
||||
if key in key_map:
|
||||
return key_map[key]
|
||||
|
||||
# Handle alphanumeric keys dynamically
|
||||
if len(key) == 1:
|
||||
if key.isalpha():
|
||||
# Letter keys: A-Z have VK codes 65-90
|
||||
return (f'Key{key.upper()}', ord(key.upper()))
|
||||
elif key.isdigit():
|
||||
# Digit keys: 0-9 have VK codes 48-57 (same as ASCII)
|
||||
return (f'Digit{key}', ord(key))
|
||||
|
||||
# Fallback: use the key name as code, no virtual key code
|
||||
return (key, None)
|
||||
|
||||
|
||||
# Backward compatibility: provide standalone function
|
||||
def get_key_info(key: str) -> tuple[str, int | None]:
|
||||
"""Get the code and windowsVirtualKeyCode for a key.
|
||||
|
||||
Args:
|
||||
key: Key name (e.g., 'Enter', 'ArrowUp', 'a', 'A')
|
||||
|
||||
Returns:
|
||||
Tuple of (code, windowsVirtualKeyCode)
|
||||
|
||||
Reference: Windows Virtual Key Codes
|
||||
https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
"""
|
||||
return Utils.get_key_info(key)
|
||||
Reference in New Issue
Block a user