[Add] browser-use and main.py
This commit is contained in:
parent
08e64bdf45
commit
96914d44ac
30 changed files with 30952 additions and 1 deletions
59
browser-use/tests/conftest.py
Normal file
59
browser-use/tests/conftest.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""
|
||||
Test configuration for browser-use.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import SecretStr
|
||||
|
||||
# Ensure the project root is in the Python path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
# Load environment variables
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
from browser_use.browser.context import BrowserContext
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def llm():
|
||||
"""
|
||||
Fixture to provide a ChatOpenAI instance or a mock for testing.
|
||||
Uses a mock if OPENAI_API_KEY is not set.
|
||||
"""
|
||||
api_key = os.getenv('OPENAI_API_KEY')
|
||||
logger.debug(f'API Key present: {bool(api_key)}')
|
||||
logger.debug('Using actual ChatOpenAI model')
|
||||
return ChatOpenAI(model='gpt-4o', api_key=SecretStr(api_key) if api_key else None)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def browser():
|
||||
"""
|
||||
Fixture to provide a Browser instance for testing.
|
||||
"""
|
||||
logger.debug('Creating Browser instance for testing')
|
||||
return Browser(config=BrowserConfig(headless=True, disable_security=True))
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
async def browser_context(browser):
|
||||
"""
|
||||
Fixture to provide a BrowserContext instance for testing.
|
||||
"""
|
||||
logger.debug('Creating BrowserContext instance for testing')
|
||||
context = BrowserContext(browser=browser)
|
||||
yield context
|
||||
await context.close()
|
||||
305
browser-use/tests/test_action_filters.py
Normal file
305
browser-use/tests/test_action_filters.py
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import Page
|
||||
from pydantic import BaseModel
|
||||
|
||||
from browser_use.controller.registry.service import Registry
|
||||
from browser_use.controller.registry.views import ActionRegistry, RegisteredAction
|
||||
|
||||
|
||||
class EmptyParamModel(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class TestActionFilters:
|
||||
def test_get_prompt_description_no_filters(self):
|
||||
"""Test that system prompt only includes actions with no filters"""
|
||||
registry = ActionRegistry()
|
||||
|
||||
# Add actions with and without filters
|
||||
no_filter_action = RegisteredAction(
|
||||
name='no_filter_action',
|
||||
description='Action with no filters',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=None,
|
||||
page_filter=None,
|
||||
)
|
||||
|
||||
page_filter_action = RegisteredAction(
|
||||
name='page_filter_action',
|
||||
description='Action with page filter',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=None,
|
||||
page_filter=lambda page: True,
|
||||
)
|
||||
|
||||
domain_filter_action = RegisteredAction(
|
||||
name='domain_filter_action',
|
||||
description='Action with domain filter',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=['example.com'],
|
||||
page_filter=None,
|
||||
)
|
||||
|
||||
registry.actions = {
|
||||
'no_filter_action': no_filter_action,
|
||||
'page_filter_action': page_filter_action,
|
||||
'domain_filter_action': domain_filter_action,
|
||||
}
|
||||
|
||||
# System prompt (no page) should only include actions with no filters
|
||||
system_description = registry.get_prompt_description()
|
||||
assert 'no_filter_action' in system_description
|
||||
assert 'page_filter_action' not in system_description
|
||||
assert 'domain_filter_action' not in system_description
|
||||
|
||||
def test_page_filter_matching(self):
|
||||
"""Test that page filters work correctly"""
|
||||
registry = ActionRegistry()
|
||||
|
||||
# Create a mock page
|
||||
mock_page = MagicMock(spec=Page)
|
||||
mock_page.url = 'https://example.com/page'
|
||||
|
||||
# Create actions with different page filters
|
||||
matching_action = RegisteredAction(
|
||||
name='matching_action',
|
||||
description='Action with matching page filter',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=None,
|
||||
page_filter=lambda page: 'example.com' in page.url,
|
||||
)
|
||||
|
||||
non_matching_action = RegisteredAction(
|
||||
name='non_matching_action',
|
||||
description='Action with non-matching page filter',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=None,
|
||||
page_filter=lambda page: 'other.com' in page.url,
|
||||
)
|
||||
|
||||
registry.actions = {'matching_action': matching_action, 'non_matching_action': non_matching_action}
|
||||
|
||||
# Page-specific description should only include matching actions
|
||||
page_description = registry.get_prompt_description(mock_page)
|
||||
assert 'matching_action' in page_description
|
||||
assert 'non_matching_action' not in page_description
|
||||
|
||||
def test_domain_filter_matching(self):
|
||||
"""Test that domain filters work correctly with glob patterns"""
|
||||
registry = ActionRegistry()
|
||||
|
||||
# Create actions with different domain patterns
|
||||
actions = {
|
||||
'exact_match': RegisteredAction(
|
||||
name='exact_match',
|
||||
description='Exact domain match',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=['example.com'],
|
||||
page_filter=None,
|
||||
),
|
||||
'subdomain_match': RegisteredAction(
|
||||
name='subdomain_match',
|
||||
description='Subdomain wildcard match',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=['*.example.com'],
|
||||
page_filter=None,
|
||||
),
|
||||
'prefix_match': RegisteredAction(
|
||||
name='prefix_match',
|
||||
description='Prefix wildcard match',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=['example*'],
|
||||
page_filter=None,
|
||||
),
|
||||
'non_matching': RegisteredAction(
|
||||
name='non_matching',
|
||||
description='Non-matching domain',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=['other.com'],
|
||||
page_filter=None,
|
||||
),
|
||||
}
|
||||
|
||||
registry.actions = actions
|
||||
|
||||
# Test exact domain match
|
||||
mock_page = MagicMock(spec=Page)
|
||||
mock_page.url = 'https://example.com/page'
|
||||
|
||||
exact_match_description = registry.get_prompt_description(mock_page)
|
||||
assert 'exact_match' in exact_match_description
|
||||
assert 'non_matching' not in exact_match_description
|
||||
|
||||
# Test subdomain match
|
||||
mock_page.url = 'https://sub.example.com/page'
|
||||
subdomain_match_description = registry.get_prompt_description(mock_page)
|
||||
assert 'subdomain_match' in subdomain_match_description
|
||||
assert 'exact_match' not in subdomain_match_description
|
||||
|
||||
# Test prefix match
|
||||
mock_page.url = 'https://example123.org/page'
|
||||
prefix_match_description = registry.get_prompt_description(mock_page)
|
||||
assert 'prefix_match' in prefix_match_description
|
||||
|
||||
def test_domain_and_page_filter_together(self):
|
||||
"""Test that actions can be filtered by both domain and page filter"""
|
||||
registry = ActionRegistry()
|
||||
|
||||
# Create a mock page
|
||||
mock_page = MagicMock(spec=Page)
|
||||
mock_page.url = 'https://example.com/admin'
|
||||
|
||||
# Actions with different combinations of filters
|
||||
actions = {
|
||||
'domain_only': RegisteredAction(
|
||||
name='domain_only',
|
||||
description='Domain filter only',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=['example.com'],
|
||||
page_filter=None,
|
||||
),
|
||||
'page_only': RegisteredAction(
|
||||
name='page_only',
|
||||
description='Page filter only',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=None,
|
||||
page_filter=lambda page: 'admin' in page.url,
|
||||
),
|
||||
'both_matching': RegisteredAction(
|
||||
name='both_matching',
|
||||
description='Both filters matching',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=['example.com'],
|
||||
page_filter=lambda page: 'admin' in page.url,
|
||||
),
|
||||
'both_one_fail': RegisteredAction(
|
||||
name='both_one_fail',
|
||||
description='One filter fails',
|
||||
function=lambda: None,
|
||||
param_model=EmptyParamModel,
|
||||
domains=['other.com'],
|
||||
page_filter=lambda page: 'admin' in page.url,
|
||||
),
|
||||
}
|
||||
|
||||
registry.actions = actions
|
||||
|
||||
# Check that only actions with matching filters are included
|
||||
description = registry.get_prompt_description(mock_page)
|
||||
assert 'domain_only' in description # Domain matches
|
||||
assert 'page_only' in description # Page filter matches
|
||||
assert 'both_matching' in description # Both filters match
|
||||
assert 'both_one_fail' not in description # Domain filter fails
|
||||
|
||||
# Test with different URL where page filter fails
|
||||
mock_page.url = 'https://example.com/dashboard'
|
||||
description = registry.get_prompt_description(mock_page)
|
||||
assert 'domain_only' in description # Domain matches
|
||||
assert 'page_only' not in description # Page filter fails
|
||||
assert 'both_matching' not in description # Page filter fails
|
||||
assert 'both_one_fail' not in description # Domain filter fails
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_registry_action_decorator(self):
|
||||
"""Test the action decorator with filters"""
|
||||
registry = Registry()
|
||||
|
||||
# Define actions with different filters
|
||||
@registry.action(
|
||||
description='No filter action',
|
||||
)
|
||||
def no_filter_action():
|
||||
pass
|
||||
|
||||
@registry.action(description='Domain filter action', domains=['example.com'])
|
||||
def domain_filter_action():
|
||||
pass
|
||||
|
||||
@registry.action(description='Page filter action', page_filter=lambda page: 'admin' in page.url)
|
||||
def page_filter_action():
|
||||
pass
|
||||
|
||||
# Check that system prompt only includes the no_filter_action
|
||||
system_description = registry.get_prompt_description()
|
||||
assert 'No filter action' in system_description
|
||||
assert 'Domain filter action' not in system_description
|
||||
assert 'Page filter action' not in system_description
|
||||
|
||||
# Check that page-specific prompt includes the right actions
|
||||
mock_page = MagicMock(spec=Page)
|
||||
mock_page.url = 'https://example.com/admin'
|
||||
|
||||
page_description = registry.get_prompt_description(mock_page)
|
||||
assert 'Domain filter action' in page_description
|
||||
assert 'Page filter action' in page_description
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_action_model_creation(self):
|
||||
"""Test that action models are created correctly with filters"""
|
||||
registry = Registry()
|
||||
|
||||
# Define actions with different filters
|
||||
@registry.action(
|
||||
description='No filter action',
|
||||
)
|
||||
def no_filter_action():
|
||||
pass
|
||||
|
||||
@registry.action(description='Domain filter action', domains=['example.com'])
|
||||
def domain_filter_action():
|
||||
pass
|
||||
|
||||
@registry.action(description='Page filter action', page_filter=lambda page: 'admin' in page.url)
|
||||
def page_filter_action():
|
||||
pass
|
||||
|
||||
@registry.action(description='Both filters action', domains=['example.com'], page_filter=lambda page: 'admin' in page.url)
|
||||
def both_filters_action():
|
||||
pass
|
||||
|
||||
# Initial action model should only include no_filter_action
|
||||
initial_model = registry.create_action_model()
|
||||
assert 'no_filter_action' in initial_model.model_fields
|
||||
assert 'domain_filter_action' not in initial_model.model_fields
|
||||
assert 'page_filter_action' not in initial_model.model_fields
|
||||
assert 'both_filters_action' not in initial_model.model_fields
|
||||
|
||||
# Action model with matching page should include all matching actions
|
||||
mock_page = MagicMock(spec=Page)
|
||||
mock_page.url = 'https://example.com/admin'
|
||||
|
||||
page_model = registry.create_action_model(page=mock_page)
|
||||
assert 'no_filter_action' in page_model.model_fields
|
||||
assert 'domain_filter_action' in page_model.model_fields
|
||||
assert 'page_filter_action' in page_model.model_fields
|
||||
assert 'both_filters_action' in page_model.model_fields
|
||||
|
||||
# Action model with non-matching domain should exclude domain-filtered actions
|
||||
mock_page.url = 'https://other.com/admin'
|
||||
non_matching_domain_model = registry.create_action_model(page=mock_page)
|
||||
assert 'no_filter_action' in non_matching_domain_model.model_fields
|
||||
assert 'domain_filter_action' not in non_matching_domain_model.model_fields
|
||||
assert 'page_filter_action' in non_matching_domain_model.model_fields
|
||||
assert 'both_filters_action' not in non_matching_domain_model.model_fields
|
||||
|
||||
# Action model with non-matching page filter should exclude page-filtered actions
|
||||
mock_page.url = 'https://example.com/dashboard'
|
||||
non_matching_page_model = registry.create_action_model(page=mock_page)
|
||||
assert 'no_filter_action' in non_matching_page_model.model_fields
|
||||
assert 'domain_filter_action' in non_matching_page_model.model_fields
|
||||
assert 'page_filter_action' not in non_matching_page_model.model_fields
|
||||
assert 'both_filters_action' not in non_matching_page_model.model_fields
|
||||
220
browser-use/tests/test_agent_actions.py
Normal file
220
browser-use/tests/test_agent_actions.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import asyncio
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from langchain_openai import AzureChatOpenAI
|
||||
from pydantic import BaseModel, SecretStr
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
from browser_use.browser.views import BrowserState
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llm():
|
||||
"""Initialize language model for testing"""
|
||||
|
||||
# return ChatAnthropic(model_name='claude-3-5-sonnet-20240620', timeout=25, stop=None)
|
||||
return AzureChatOpenAI(
|
||||
model='gpt-4o',
|
||||
api_version='2024-10-21',
|
||||
azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
|
||||
api_key=SecretStr(os.getenv('AZURE_OPENAI_KEY', '')),
|
||||
)
|
||||
# return ChatOpenAI(model='gpt-4o-mini')
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for each test case."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
async def browser(event_loop):
|
||||
browser_instance = Browser(
|
||||
config=BrowserConfig(
|
||||
headless=True,
|
||||
)
|
||||
)
|
||||
yield browser_instance
|
||||
await browser_instance.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def context(browser):
|
||||
async with await browser.new_context() as context:
|
||||
yield context
|
||||
# Clean up automatically happens with __aexit__
|
||||
|
||||
|
||||
# pytest tests/test_agent_actions.py -v -k "test_ecommerce_interaction" --capture=no
|
||||
# @pytest.mark.asyncio
|
||||
@pytest.mark.skip(reason='Kinda expensive to run')
|
||||
async def test_ecommerce_interaction(llm, context):
|
||||
"""Test complex ecommerce interaction sequence"""
|
||||
agent = Agent(
|
||||
task="Go to amazon.com, search for 'laptop', filter by 4+ stars, and find the price of the first result",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
save_conversation_path='tmp/test_ecommerce_interaction/conversation',
|
||||
)
|
||||
|
||||
history: AgentHistoryList = await agent.run(max_steps=20)
|
||||
|
||||
# Verify sequence of actions
|
||||
action_sequence = []
|
||||
for action in history.model_actions():
|
||||
action_name = list(action.keys())[0]
|
||||
if action_name in ['go_to_url', 'open_tab']:
|
||||
action_sequence.append('navigate')
|
||||
elif action_name == 'input_text':
|
||||
action_sequence.append('input')
|
||||
# Check that the input is 'laptop'
|
||||
inp = action['input_text']['text'].lower() # type: ignore
|
||||
if inp == 'laptop':
|
||||
action_sequence.append('input_exact_correct')
|
||||
elif 'laptop' in inp:
|
||||
action_sequence.append('correct_in_input')
|
||||
else:
|
||||
action_sequence.append('incorrect_input')
|
||||
elif action_name == 'click_element':
|
||||
action_sequence.append('click')
|
||||
|
||||
# Verify essential steps were performed
|
||||
assert 'navigate' in action_sequence # Navigated to Amazon
|
||||
assert 'input' in action_sequence # Entered search term
|
||||
assert 'click' in action_sequence # Clicked search/filter
|
||||
assert 'input_exact_correct' in action_sequence or 'correct_in_input' in action_sequence
|
||||
|
||||
|
||||
# @pytest.mark.asyncio
|
||||
async def test_error_recovery(llm, context):
|
||||
"""Test agent's ability to recover from errors"""
|
||||
agent = Agent(
|
||||
task='Navigate to nonexistent-site.com and then recover by going to google.com ',
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
|
||||
history: AgentHistoryList = await agent.run(max_steps=10)
|
||||
|
||||
actions_names = history.action_names()
|
||||
actions = history.model_actions()
|
||||
assert 'go_to_url' in actions_names or 'open_tab' in actions_names, f'{actions_names} does not contain go_to_url or open_tab'
|
||||
for action in actions:
|
||||
if 'go_to_url' in action:
|
||||
assert 'url' in action['go_to_url'], 'url is not in go_to_url'
|
||||
assert action['go_to_url']['url'].endswith('google.com'), 'url does not end with google.com'
|
||||
break
|
||||
|
||||
|
||||
# @pytest.mark.asyncio
|
||||
async def test_find_contact_email(llm, context):
|
||||
"""Test agent's ability to find contact email on a website"""
|
||||
agent = Agent(
|
||||
task='Go to https://browser-use.com/ and find out the contact email',
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
|
||||
history: AgentHistoryList = await agent.run(max_steps=10)
|
||||
|
||||
# Verify the agent found the contact email
|
||||
extracted_content = history.extracted_content()
|
||||
email = 'info@browser-use.com'
|
||||
for content in extracted_content:
|
||||
if email in content:
|
||||
break
|
||||
else:
|
||||
pytest.fail(f'{extracted_content} does not contain {email}')
|
||||
|
||||
|
||||
# @pytest.mark.asyncio
|
||||
async def test_agent_finds_installation_command(llm, context):
|
||||
"""Test agent's ability to find the pip installation command for browser-use on the web"""
|
||||
agent = Agent(
|
||||
task='Find the pip installation command for the browser-use repo',
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
|
||||
history: AgentHistoryList = await agent.run(max_steps=10)
|
||||
|
||||
# Verify the agent found the correct installation command
|
||||
extracted_content = history.extracted_content()
|
||||
install_command = 'pip install browser-use'
|
||||
for content in extracted_content:
|
||||
if install_command in content:
|
||||
break
|
||||
else:
|
||||
pytest.fail(f'{extracted_content} does not contain {install_command}')
|
||||
|
||||
|
||||
class CaptchaTest(BaseModel):
|
||||
name: str
|
||||
url: str
|
||||
success_text: str
|
||||
additional_text: str | None = None
|
||||
|
||||
|
||||
# run 3 test: python -m pytest tests/test_agent_actions.py -v -k "test_captcha_solver" --capture=no --log-cli-level=INFO
|
||||
# pytest tests/test_agent_actions.py -v -k "test_captcha_solver" --capture=no --log-cli-level=INFO
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
'captcha',
|
||||
[
|
||||
CaptchaTest(
|
||||
name='Text Captcha',
|
||||
url='https://2captcha.com/demo/text',
|
||||
success_text='Captcha is passed successfully!',
|
||||
),
|
||||
CaptchaTest(
|
||||
name='Basic Captcha',
|
||||
url='https://captcha.com/demos/features/captcha-demo.aspx',
|
||||
success_text='Correct!',
|
||||
),
|
||||
CaptchaTest(
|
||||
name='Rotate Captcha',
|
||||
url='https://2captcha.com/demo/rotatecaptcha',
|
||||
success_text='Captcha is passed successfully',
|
||||
additional_text='Use multiple clicks at once. click done when image is exact correct position.',
|
||||
),
|
||||
CaptchaTest(
|
||||
name='MT Captcha',
|
||||
url='https://2captcha.com/demo/mtcaptcha',
|
||||
success_text='Verified Successfully',
|
||||
additional_text='Stop when you solved it successfully.',
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_captcha_solver(llm, context, captcha: CaptchaTest):
|
||||
"""Test agent's ability to solve different types of captchas"""
|
||||
agent = Agent(
|
||||
task=f'Go to {captcha.url} and solve the captcha. {captcha.additional_text}',
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
|
||||
history: AgentHistoryList = await agent.run(max_steps=7)
|
||||
|
||||
state: BrowserState = await context.get_state()
|
||||
|
||||
all_text = state.element_tree.get_all_text_till_next_clickable_element()
|
||||
|
||||
if not all_text:
|
||||
all_text = ''
|
||||
|
||||
if not isinstance(all_text, str):
|
||||
all_text = str(all_text)
|
||||
|
||||
solved = captcha.success_text in all_text
|
||||
assert solved, f'Failed to solve {captcha.name}'
|
||||
|
||||
# python -m pytest tests/test_agent_actions.py -v --capture=no
|
||||
|
||||
# pytest tests/test_agent_actions.py -v -k "test_captcha_solver" --capture=no --log-cli-level=INFO
|
||||
69
browser-use/tests/test_attach_chrome.py
Normal file
69
browser-use/tests/test_attach_chrome.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import asyncio
|
||||
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
|
||||
async def test_full_screen(start_fullscreen: bool, maximize: bool):
|
||||
async with async_playwright() as p:
|
||||
try:
|
||||
print('Attempting to connect to Chrome...')
|
||||
# run in terminal: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --no-first-run
|
||||
browser = await p.chromium.connect_over_cdp(
|
||||
'http://localhost:9222',
|
||||
timeout=20000, # 20 second timeout for connection
|
||||
)
|
||||
print('Connected to Chrome successfully')
|
||||
|
||||
# Get the first context and page, or create new ones if needed
|
||||
if len(browser.contexts) == 0:
|
||||
context = await browser.new_context(ignore_https_errors=True)
|
||||
else:
|
||||
context = browser.contexts[0]
|
||||
|
||||
if len(context.pages) == 0:
|
||||
page = await context.new_page()
|
||||
else:
|
||||
page = context.pages[0]
|
||||
|
||||
print('Attempting to navigate to Gmail...')
|
||||
try:
|
||||
# First try with a shorter timeout
|
||||
await page.goto(
|
||||
'https://mail.google.com',
|
||||
wait_until='load', # Changed from domcontentloaded
|
||||
timeout=10000,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'First navigation attempt failed: {e}')
|
||||
print('Trying again with different settings...')
|
||||
# If that fails, try again with different settings
|
||||
await page.goto(
|
||||
'https://mail.google.com',
|
||||
wait_until='commit', # Less strict wait condition
|
||||
timeout=30000,
|
||||
)
|
||||
|
||||
# Wait for the page to stabilize
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print(f'Current page title: {await page.title()}')
|
||||
|
||||
# Optional: wait for specific Gmail elements
|
||||
try:
|
||||
await page.wait_for_selector('div[role="main"]', timeout=5000)
|
||||
print('Gmail interface detected')
|
||||
except Exception as e:
|
||||
print(f'Note: Gmail interface not detected: {e}')
|
||||
|
||||
await asyncio.sleep(30)
|
||||
except Exception as e:
|
||||
print(f'An error occurred: {e}')
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
await browser.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(test_full_screen(False, False))
|
||||
578
browser-use/tests/test_browser.py
Normal file
578
browser-use/tests/test_browser.py
Normal file
|
|
@ -0,0 +1,578 @@
|
|||
import asyncio
|
||||
import subprocess
|
||||
|
||||
import psutil
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from browser_use.browser.browser import Browser, BrowserConfig, ProxySettings
|
||||
from browser_use.browser.context import BrowserContext, BrowserContextConfig
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_builtin_browser_launch(monkeypatch):
|
||||
"""
|
||||
Test that the standard browser is launched correctly:
|
||||
When no remote (cdp or wss) or chrome instance is provided, the Browser class uses _setup_builtin_browser.
|
||||
This test monkeypatches async_playwright to return dummy objects, and asserts that get_playwright_browser returns the expected DummyBrowser.
|
||||
"""
|
||||
|
||||
class DummyBrowser:
|
||||
pass
|
||||
|
||||
class DummyChromium:
|
||||
async def launch(self, headless, args, proxy=None, handle_sigterm=False, handle_sigint=False):
|
||||
return DummyBrowser()
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
config = BrowserConfig(headless=True, disable_security=False, extra_browser_args=['--test'])
|
||||
browser_obj = Browser(config=config)
|
||||
result_browser = await browser_obj.get_playwright_browser()
|
||||
assert isinstance(result_browser, DummyBrowser), 'Expected DummyBrowser from _setup_builtin_browser'
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cdp_browser_launch(monkeypatch):
|
||||
"""
|
||||
Test that when a CDP URL is provided in the configuration, the Browser uses _setup_cdp
|
||||
and returns the expected DummyBrowser.
|
||||
"""
|
||||
|
||||
class DummyBrowser:
|
||||
pass
|
||||
|
||||
class DummyChromium:
|
||||
async def connect_over_cdp(self, endpoint_url, timeout=20000):
|
||||
assert endpoint_url == 'ws://dummy-cdp-url', 'The endpoint URL should match the configuration.'
|
||||
return DummyBrowser()
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
config = BrowserConfig(cdp_url='ws://dummy-cdp-url')
|
||||
browser_obj = Browser(config=config)
|
||||
result_browser = await browser_obj.get_playwright_browser()
|
||||
assert isinstance(result_browser, DummyBrowser), 'Expected DummyBrowser from _setup_cdp'
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wss_browser_launch(monkeypatch):
|
||||
"""
|
||||
Test that when a WSS URL is provided in the configuration,
|
||||
the Browser uses setup_wss and returns the expected DummyBrowser.
|
||||
"""
|
||||
|
||||
class DummyBrowser:
|
||||
pass
|
||||
|
||||
class DummyChromium:
|
||||
async def connect(self, wss_url):
|
||||
assert wss_url == 'ws://dummy-wss-url', 'WSS URL should match the configuration.'
|
||||
return DummyBrowser()
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
config = BrowserConfig(wss_url='ws://dummy-wss-url')
|
||||
browser_obj = Browser(config=config)
|
||||
result_browser = await browser_obj.get_playwright_browser()
|
||||
assert isinstance(result_browser, DummyBrowser), 'Expected DummyBrowser from _setup_wss'
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_provided_browser_launch(monkeypatch):
|
||||
"""
|
||||
Test that when a browser_binary_path is provided the Browser class uses
|
||||
_setup_user_provided_browser branch and returns the expected DummyBrowser object
|
||||
by reusing an existing Chrome instance.
|
||||
"""
|
||||
|
||||
# Dummy response for requests.get when checking chrome debugging endpoint.
|
||||
class DummyResponse:
|
||||
status_code = 200
|
||||
|
||||
def dummy_get(url, timeout):
|
||||
if url == 'http://localhost:9222/json/version':
|
||||
return DummyResponse()
|
||||
raise requests.ConnectionError('Connection failed')
|
||||
|
||||
monkeypatch.setattr(requests, 'get', dummy_get)
|
||||
|
||||
class DummyBrowser:
|
||||
pass
|
||||
|
||||
class DummyChromium:
|
||||
async def connect_over_cdp(self, endpoint_url, timeout=20000):
|
||||
assert endpoint_url == 'http://localhost:9222', "Endpoint URL must be 'http://localhost:9222'"
|
||||
return DummyBrowser()
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
config = BrowserConfig(browser_binary_path='dummy/chrome', extra_browser_args=['--dummy-arg'])
|
||||
browser_obj = Browser(config=config)
|
||||
result_browser = await browser_obj.get_playwright_browser()
|
||||
assert isinstance(result_browser, DummyBrowser), 'Expected DummyBrowser from _setup_user_provided_browser'
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_provided_browser_launch_on_custom_chrome_remote_debugging_port(monkeypatch):
|
||||
"""
|
||||
Test that when a browser_binary_path and chrome_remote_debugging_port are provided, the Browser class uses
|
||||
_setup_user_provided_browser branch and returns the expected DummyBrowser object
|
||||
by launching a new Chrome instance with --remote-debugging-port=chrome_remote_debugging_port argument.
|
||||
"""
|
||||
|
||||
# Custom remote debugging port
|
||||
custom_chrome_remote_debugging_port = 9223
|
||||
|
||||
# Dummy response for requests.get when checking chrome debugging endpoint.
|
||||
class DummyResponse:
|
||||
status_code = 200
|
||||
|
||||
def dummy_get(url, timeout):
|
||||
if url == f'http://localhost:{custom_chrome_remote_debugging_port}/json/version':
|
||||
return DummyResponse()
|
||||
raise requests.ConnectionError('Connection failed')
|
||||
|
||||
monkeypatch.setattr(requests, 'get', dummy_get)
|
||||
|
||||
class DummyProcess:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
class DummySubProcess:
|
||||
pid = 1234
|
||||
|
||||
async def dummy_create_subprocess_exec(browser_binary_path, *args, **kwargs):
|
||||
assert f'--remote-debugging-port={custom_chrome_remote_debugging_port}' in args, (
|
||||
f'Chrome must be started with with --remote-debugging-port={custom_chrome_remote_debugging_port} argument'
|
||||
)
|
||||
|
||||
return DummySubProcess()
|
||||
|
||||
monkeypatch.setattr(asyncio, 'create_subprocess_exec', dummy_create_subprocess_exec)
|
||||
monkeypatch.setattr(psutil, 'Process', DummyProcess)
|
||||
|
||||
class DummyBrowser:
|
||||
pass
|
||||
|
||||
class DummyChromium:
|
||||
async def connect_over_cdp(self, endpoint_url, timeout=20000):
|
||||
assert endpoint_url == f'http://localhost:{custom_chrome_remote_debugging_port}', (
|
||||
f"Endpoint URL must be 'http://localhost:{custom_chrome_remote_debugging_port}'"
|
||||
)
|
||||
return DummyBrowser()
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
|
||||
config = BrowserConfig(
|
||||
browser_binary_path='dummy/chrome',
|
||||
chrome_remote_debugging_port=custom_chrome_remote_debugging_port,
|
||||
extra_browser_args=['--dummy-arg'],
|
||||
)
|
||||
|
||||
browser_obj = Browser(config=config)
|
||||
result_browser = await browser_obj.get_playwright_browser()
|
||||
assert isinstance(result_browser, DummyBrowser), (
|
||||
f'Expected DummyBrowser with remote debugging port {custom_chrome_remote_debugging_port} from _setup_user_provided_browser'
|
||||
)
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_builtin_browser_disable_security_args(monkeypatch):
|
||||
"""
|
||||
Test that the standard browser launch includes disable-security arguments when disable_security is True.
|
||||
This verifies that _setup_builtin_browser correctly appends the security disabling arguments along with
|
||||
the base arguments and any extra arguments provided.
|
||||
"""
|
||||
# These are the base arguments defined in _setup_builtin_browser.
|
||||
base_args = [
|
||||
'--no-sandbox',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--disable-infobars',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-window-activation',
|
||||
'--disable-focus-on-load',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--no-startup-window',
|
||||
'--window-position=0,0',
|
||||
]
|
||||
# When disable_security is True, these arguments should be added.
|
||||
disable_security_args = [
|
||||
'--disable-web-security',
|
||||
'--disable-site-isolation-trials',
|
||||
'--disable-features=IsolateOrigins,site-per-process',
|
||||
]
|
||||
# Additional arbitrary argument for testing extra args
|
||||
extra_args = ['--dummy-extra']
|
||||
|
||||
class DummyBrowser:
|
||||
pass
|
||||
|
||||
class DummyChromium:
|
||||
async def launch(self, headless, args, proxy=None, handle_sigterm=False, handle_sigint=False):
|
||||
# Expected args is the base args plus disable security args and the extra args.
|
||||
expected_args = base_args + disable_security_args + extra_args
|
||||
assert headless is True, 'Expected headless to be True'
|
||||
assert args == expected_args, f'Expected args {expected_args}, but got {args}'
|
||||
assert proxy is None, 'Expected proxy to be None'
|
||||
return DummyBrowser()
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
config = BrowserConfig(headless=True, disable_security=True, extra_browser_args=extra_args)
|
||||
browser_obj = Browser(config=config)
|
||||
result_browser = await browser_obj.get_playwright_browser()
|
||||
assert isinstance(result_browser, DummyBrowser), (
|
||||
'Expected DummyBrowser from _setup_builtin_browser with disable_security active'
|
||||
)
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_context_creation():
|
||||
"""
|
||||
Test that the new_context method returns a BrowserContext with the correct attributes.
|
||||
This verifies that the BrowserContext is initialized with the provided Browser instance and configuration.
|
||||
"""
|
||||
config = BrowserConfig()
|
||||
browser_obj = Browser(config=config)
|
||||
custom_context_config = BrowserContextConfig()
|
||||
context = await browser_obj.new_context(custom_context_config)
|
||||
assert isinstance(context, BrowserContext), 'Expected new_context to return an instance of BrowserContext'
|
||||
assert context.browser is browser_obj, "Expected the context's browser attribute to be the Browser instance"
|
||||
assert context.config == custom_context_config, "Expected the context's config attribute to be the provided config"
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_provided_browser_launch_failure(monkeypatch):
|
||||
"""
|
||||
Test that when a Chrome instance cannot be started or connected to,
|
||||
the Browser._setup_user_provided_browser branch eventually raises a RuntimeError.
|
||||
We simulate failure by:
|
||||
- Forcing requests.get to always raise a ConnectionError (so no existing instance is found).
|
||||
- Monkeypatching subprocess.Popen to do nothing.
|
||||
- Replacing asyncio.sleep to avoid delays.
|
||||
- Having the dummy playwright's connect_over_cdp method always raise an Exception.
|
||||
"""
|
||||
|
||||
def dummy_get(url, timeout):
|
||||
raise requests.ConnectionError('Simulated connection failure')
|
||||
|
||||
monkeypatch.setattr(requests, 'get', dummy_get)
|
||||
monkeypatch.setattr(subprocess, 'Popen', lambda args, stdout, stderr: None)
|
||||
|
||||
async def fake_sleep(seconds):
|
||||
return
|
||||
|
||||
monkeypatch.setattr(asyncio, 'sleep', fake_sleep)
|
||||
|
||||
class DummyChromium:
|
||||
async def connect_over_cdp(self, endpoint_url, timeout=20000):
|
||||
raise Exception('Connection failed simulation')
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
config = BrowserConfig(browser_binary_path='dummy/chrome', extra_browser_args=['--dummy-arg'])
|
||||
browser_obj = Browser(config=config)
|
||||
with pytest.raises(RuntimeError, match='To start chrome in Debug mode'):
|
||||
await browser_obj.get_playwright_browser()
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_playwright_browser_caching(monkeypatch):
|
||||
"""
|
||||
Test that get_playwright_browser returns a cached browser instance.
|
||||
On the first call, the browser is initialized; on subsequent calls,
|
||||
the same instance is returned.
|
||||
"""
|
||||
|
||||
class DummyBrowser:
|
||||
pass
|
||||
|
||||
class DummyChromium:
|
||||
async def launch(self, headless, args, proxy=None, handle_sigterm=False, handle_sigint=False):
|
||||
return DummyBrowser()
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
config = BrowserConfig(headless=True, disable_security=False, extra_browser_args=['--test'])
|
||||
browser_obj = Browser(config=config)
|
||||
first_browser = await browser_obj.get_playwright_browser()
|
||||
second_browser = await browser_obj.get_playwright_browser()
|
||||
assert first_browser is second_browser, 'Expected the browser to be cached and reused across calls.'
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_error_handling(monkeypatch):
|
||||
"""
|
||||
Test that the close method properly handles exceptions thrown by
|
||||
playwright_browser.close() and playwright.stop(), ensuring that the
|
||||
browser's attributes are set to None even if errors occur.
|
||||
"""
|
||||
|
||||
class DummyBrowserWithError:
|
||||
async def close(self):
|
||||
raise Exception('Close error simulation')
|
||||
|
||||
class DummyPlaywrightWithError:
|
||||
async def stop(self):
|
||||
raise Exception('Stop error simulation')
|
||||
|
||||
config = BrowserConfig()
|
||||
browser_obj = Browser(config=config)
|
||||
browser_obj.playwright_browser = DummyBrowserWithError()
|
||||
browser_obj.playwright = DummyPlaywrightWithError()
|
||||
await browser_obj.close()
|
||||
assert browser_obj.playwright_browser is None, 'Expected playwright_browser to be None after close'
|
||||
assert browser_obj.playwright is None, 'Expected playwright to be None after close'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_standard_browser_launch_with_proxy(monkeypatch):
|
||||
"""
|
||||
Test that when a proxy is provided in the BrowserConfig, the _setup_builtin_browser method
|
||||
correctly passes the proxy parameter to the playwright.chromium.launch method.
|
||||
This test sets up a dummy async_playwright context and verifies that the dummy proxy is received.
|
||||
"""
|
||||
|
||||
class DummyBrowser:
|
||||
pass
|
||||
|
||||
# Create a dummy proxy settings instance.
|
||||
dummy_proxy = ProxySettings(server='http://dummy.proxy')
|
||||
|
||||
class DummyChromium:
|
||||
async def launch(self, headless, args, proxy=None, handle_sigterm=False, handle_sigint=False):
|
||||
# Assert that the proxy passed equals the dummy proxy provided in the configuration.
|
||||
assert isinstance(proxy, dict) and proxy['server'] == 'http://dummy.proxy', (
|
||||
f'Expected proxy {dummy_proxy} but got {proxy}'
|
||||
)
|
||||
# We can also verify some base parameters if needed (headless, args) but our focus is proxy.
|
||||
return DummyBrowser()
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = DummyChromium()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
# Monkeypatch async_playwright to return our dummy async playwright context.
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
# Create a BrowserConfig with the dummy proxy.
|
||||
config = BrowserConfig(headless=False, disable_security=False, proxy=dummy_proxy)
|
||||
browser_obj = Browser(config=config)
|
||||
# Call get_playwright_browser and verify that the returned browser is as expected.
|
||||
result_browser = await browser_obj.get_playwright_browser()
|
||||
assert isinstance(result_browser, DummyBrowser), 'Expected DummyBrowser from _setup_builtin_browser with proxy provided'
|
||||
await browser_obj.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_browser_window_size(monkeypatch):
|
||||
"""
|
||||
Test that when window_width and window_height are provided in BrowserContextConfig,
|
||||
they're properly converted to a dictionary when passed to Playwright.
|
||||
"""
|
||||
|
||||
class DummyPage:
|
||||
def __init__(self):
|
||||
self.url = 'about:blank'
|
||||
|
||||
async def goto(self, url):
|
||||
pass
|
||||
|
||||
async def wait_for_load_state(self, state):
|
||||
pass
|
||||
|
||||
async def title(self):
|
||||
return 'Test Page'
|
||||
|
||||
async def bring_to_front(self):
|
||||
pass
|
||||
|
||||
async def evaluate(self, script):
|
||||
return True
|
||||
|
||||
def is_closed(self):
|
||||
return False
|
||||
|
||||
class DummyContext:
|
||||
def __init__(self):
|
||||
self.pages = [DummyPage()]
|
||||
self.tracing = self
|
||||
|
||||
async def new_page(self):
|
||||
return DummyPage()
|
||||
|
||||
async def add_init_script(self, script):
|
||||
pass
|
||||
|
||||
async def start(self):
|
||||
pass
|
||||
|
||||
async def stop(self, path=None):
|
||||
pass
|
||||
|
||||
def on(self, event, handler):
|
||||
pass
|
||||
|
||||
async def close(self):
|
||||
pass
|
||||
|
||||
async def grant_permissions(self, permissions, origin=None):
|
||||
pass
|
||||
|
||||
class DummyBrowser:
|
||||
def __init__(self):
|
||||
self.contexts = []
|
||||
|
||||
async def new_context(self, **kwargs):
|
||||
# Assert that record_video_size is a dictionary with expected values
|
||||
assert isinstance(kwargs['record_video_size'], dict), (
|
||||
f'Expected record_video_size to be a dictionary, got {type(kwargs["record_video_size"])}'
|
||||
)
|
||||
assert kwargs['record_video_size']['width'] == 1280, (
|
||||
f'Expected width to be 1280, got {kwargs["record_video_size"].get("width")}'
|
||||
)
|
||||
assert kwargs['record_video_size']['height'] == 1100, (
|
||||
f'Expected height to be 1100, got {kwargs["record_video_size"].get("height")}'
|
||||
)
|
||||
|
||||
context = DummyContext()
|
||||
self.contexts.append(context)
|
||||
return context
|
||||
|
||||
async def close(self):
|
||||
pass
|
||||
|
||||
class DummyPlaywright:
|
||||
def __init__(self):
|
||||
self.chromium = self
|
||||
|
||||
async def launch(self, **kwargs):
|
||||
return DummyBrowser()
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
class DummyAsyncPlaywrightContext:
|
||||
async def start(self):
|
||||
return DummyPlaywright()
|
||||
|
||||
# Monkeypatch async_playwright to return our dummy async playwright context
|
||||
monkeypatch.setattr('browser_use.browser.browser.async_playwright', lambda: DummyAsyncPlaywrightContext())
|
||||
|
||||
# Create browser with default config
|
||||
browser_obj = Browser()
|
||||
|
||||
# Get browser instance
|
||||
playwright_browser = await browser_obj.get_playwright_browser()
|
||||
|
||||
# Create context config with specific window size
|
||||
context_config = BrowserContextConfig(window_width=1280, window_height=1100)
|
||||
|
||||
# Create browser context - this will test if window dimensions are properly converted
|
||||
browser_context = BrowserContext(browser=browser_obj, config=context_config)
|
||||
await browser_context._initialize_session()
|
||||
|
||||
# Clean up
|
||||
await browser_context.close()
|
||||
await browser_obj.close()
|
||||
201
browser-use/tests/test_browser_config_models.py
Normal file
201
browser-use/tests/test_browser_config_models.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from browser_use.browser.browser import Browser, BrowserConfig, ProxySettings
|
||||
from browser_use.browser.context import BrowserContext, BrowserContextConfig
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_settings_pydantic_model():
|
||||
"""
|
||||
Test that ProxySettings as a Pydantic model is correctly converted to a dictionary when used.
|
||||
"""
|
||||
# Create ProxySettings with Pydantic model
|
||||
proxy_settings = ProxySettings(
|
||||
server='http://example.proxy:8080', bypass='localhost', username='testuser', password='testpass'
|
||||
)
|
||||
|
||||
# Verify the model has correct dict-like access
|
||||
assert proxy_settings['server'] == 'http://example.proxy:8080'
|
||||
assert proxy_settings.get('bypass') == 'localhost'
|
||||
assert proxy_settings.get('nonexistent', 'default') == 'default'
|
||||
|
||||
# Verify model_dump works correctly
|
||||
proxy_dict = proxy_settings.model_dump()
|
||||
assert isinstance(proxy_dict, dict)
|
||||
assert proxy_dict['server'] == 'http://example.proxy:8080'
|
||||
assert proxy_dict['bypass'] == 'localhost'
|
||||
assert proxy_dict['username'] == 'testuser'
|
||||
assert proxy_dict['password'] == 'testpass'
|
||||
|
||||
# We don't launch the actual browser - we just verify the model itself works as expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_window_size_config():
|
||||
"""
|
||||
Test that BrowserContextConfig correctly handles window_width and window_height properties.
|
||||
"""
|
||||
# Create config with specific window dimensions
|
||||
config = BrowserContextConfig(window_width=1280, window_height=1100)
|
||||
|
||||
# Verify the properties are set correctly
|
||||
assert config.window_width == 1280
|
||||
assert config.window_height == 1100
|
||||
|
||||
# Verify model_dump works correctly
|
||||
config_dict = config.model_dump()
|
||||
assert isinstance(config_dict, dict)
|
||||
assert config_dict['window_width'] == 1280
|
||||
assert config_dict['window_height'] == 1100
|
||||
|
||||
# Create with different values
|
||||
config2 = BrowserContextConfig(window_width=1920, window_height=1080)
|
||||
assert config2.window_width == 1920
|
||||
assert config2.window_height == 1080
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(os.environ.get('CI') == 'true', reason='Skip browser test in CI')
|
||||
async def test_window_size_with_real_browser():
|
||||
"""
|
||||
Integration test that verifies our window size Pydantic model is correctly
|
||||
passed to Playwright and the actual browser window is configured with these settings.
|
||||
This test is skipped in CI environments.
|
||||
"""
|
||||
# Create browser config with headless mode
|
||||
browser_config = BrowserConfig(
|
||||
headless=True, # Use headless for faster test
|
||||
)
|
||||
|
||||
# Create context config with specific dimensions we can check
|
||||
context_config = BrowserContextConfig(
|
||||
window_width=1024,
|
||||
window_height=768,
|
||||
maximum_wait_page_load_time=2.0, # Faster timeouts for test
|
||||
minimum_wait_page_load_time=0.2,
|
||||
no_viewport=True, # Use actual window size instead of viewport
|
||||
)
|
||||
|
||||
# Create browser and context
|
||||
browser = Browser(config=browser_config)
|
||||
try:
|
||||
# Initialize browser
|
||||
playwright_browser = await browser.get_playwright_browser()
|
||||
assert playwright_browser is not None, 'Browser initialization failed'
|
||||
|
||||
# Create context
|
||||
browser_context = BrowserContext(browser=browser, config=context_config)
|
||||
try:
|
||||
# Initialize session
|
||||
await browser_context._initialize_session()
|
||||
|
||||
# Get the current page
|
||||
page = await browser_context.get_current_page()
|
||||
assert page is not None, 'Failed to get current page'
|
||||
|
||||
# Get the context configuration used for browser window size
|
||||
video_size = await page.evaluate("""
|
||||
() => {
|
||||
// This returns information about the context recording settings
|
||||
// which should match our configured video size (browser_window_size)
|
||||
try {
|
||||
const settings = window.getPlaywrightContextSettings ?
|
||||
window.getPlaywrightContextSettings() : null;
|
||||
if (settings && settings.recordVideo) {
|
||||
return settings.recordVideo.size;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Fallback to window dimensions
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
}
|
||||
""")
|
||||
|
||||
# Let's also check the viewport size
|
||||
viewport_size = await page.evaluate("""
|
||||
() => {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
print(f'Window size config: width={context_config.window_width}, height={context_config.window_height}')
|
||||
print(f'Browser viewport size: {viewport_size}')
|
||||
|
||||
# This is a lightweight test to verify that the page has a size (details may vary by browser)
|
||||
assert viewport_size['width'] > 0, 'Expected viewport width to be positive'
|
||||
assert viewport_size['height'] > 0, 'Expected viewport height to be positive'
|
||||
|
||||
# For browser context creation in record_video_size, this is what truly matters
|
||||
# Verify that our window size was properly serialized to a dictionary
|
||||
print(f'Content of context session: {browser_context.session.context}')
|
||||
print('✅ Browser window size used in the test')
|
||||
finally:
|
||||
# Clean up context
|
||||
await browser_context.close()
|
||||
finally:
|
||||
# Clean up browser
|
||||
await browser.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_with_real_browser():
|
||||
"""
|
||||
Integration test that verifies our proxy Pydantic model is correctly
|
||||
passed to Playwright without requiring a working proxy server.
|
||||
|
||||
This test:
|
||||
1. Creates a ProxySettings Pydantic model
|
||||
2. Passes it to BrowserConfig
|
||||
3. Verifies browser initialization works (proving the model was correctly serialized)
|
||||
4. We don't actually verify proxy functionality (would require a working proxy)
|
||||
"""
|
||||
# Create proxy settings with a fake proxy server
|
||||
proxy_settings = ProxySettings(
|
||||
server='http://non.existent.proxy:9999', bypass='localhost', username='testuser', password='testpass'
|
||||
)
|
||||
|
||||
# Test model serialization
|
||||
proxy_dict = proxy_settings.model_dump()
|
||||
assert isinstance(proxy_dict, dict)
|
||||
assert proxy_dict['server'] == 'http://non.existent.proxy:9999'
|
||||
|
||||
# Create browser config with proxy
|
||||
browser_config = BrowserConfig(
|
||||
headless=True,
|
||||
proxy=proxy_settings,
|
||||
)
|
||||
|
||||
# Create browser
|
||||
browser = Browser(config=browser_config)
|
||||
try:
|
||||
# Initialize browser - this should succeed even with invalid proxy
|
||||
# because we're just checking configuration, not actual proxy functionality
|
||||
try:
|
||||
playwright_browser = await browser.get_playwright_browser()
|
||||
assert playwright_browser is not None, 'Browser initialization failed'
|
||||
|
||||
# Success - the browser was initialized with our proxy settings
|
||||
# We won't try to make requests (which would fail with non-existent proxy)
|
||||
print('✅ Browser initialized with proxy settings successfully')
|
||||
|
||||
# We can inspect browser settings here to verify proxy was passed
|
||||
# but the specific API to access these settings depends on the browser
|
||||
|
||||
except Exception as e:
|
||||
# Make sure any exception isn't related to the proxy configuration format
|
||||
# (Network errors due to non-existent proxy are acceptable, invalid type conversion isn't)
|
||||
error_text = str(e).lower()
|
||||
assert 'proxy' not in error_text or any(
|
||||
term in error_text for term in ['connect', 'connection', 'network', 'timeout', 'unreachable']
|
||||
), f'Proxy configuration error (not network error): {e}'
|
||||
finally:
|
||||
# Clean up browser
|
||||
await browser.close()
|
||||
107
browser-use/tests/test_browser_window_size_height.py
Normal file
107
browser-use/tests/test_browser_window_size_height.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""
|
||||
Example script demonstrating the browser_window_size feature.
|
||||
This script shows how to set a custom window size for the browser.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
from browser_use.browser.context import BrowserContextConfig
|
||||
|
||||
|
||||
async def main():
|
||||
"""Demonstrate setting a custom browser window size"""
|
||||
# Create a browser with a specific window size
|
||||
config = BrowserContextConfig(window_width=800, window_height=400) # Small size to clearly demonstrate the fix
|
||||
|
||||
browser = None
|
||||
browser_context = None
|
||||
|
||||
try:
|
||||
# Initialize the browser with error handling
|
||||
try:
|
||||
browser = Browser(
|
||||
config=BrowserConfig(
|
||||
headless=False, # Use non-headless mode to see the window
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'Failed to initialize browser: {e}')
|
||||
return 1
|
||||
|
||||
# Create a browser context
|
||||
try:
|
||||
browser_context = await browser.new_context(config=config)
|
||||
except Exception as e:
|
||||
print(f'Failed to create browser context: {e}')
|
||||
return 1
|
||||
|
||||
# Get the current page
|
||||
page = await browser_context.get_current_page()
|
||||
|
||||
# Navigate to a test page with error handling
|
||||
try:
|
||||
await page.goto('https://example.com')
|
||||
await page.wait_for_load_state('domcontentloaded')
|
||||
except Exception as e:
|
||||
print(f'Failed to navigate to example.com: {e}')
|
||||
print('Continuing with test anyway...')
|
||||
|
||||
# Wait a bit to see the window
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Get the actual viewport size using JavaScript
|
||||
viewport_size = await page.evaluate("""
|
||||
() => {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
print(f'Configured window size: width={config.window_width}, height={config.window_height}')
|
||||
print(f'Actual viewport size: {viewport_size}')
|
||||
|
||||
# Validate the window size
|
||||
validate_window_size({'width': config.window_width, 'height': config.window_height}, viewport_size)
|
||||
|
||||
# Wait a bit more to see the window
|
||||
await asyncio.sleep(3)
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f'Unexpected error: {e}')
|
||||
return 1
|
||||
|
||||
finally:
|
||||
# Close resources
|
||||
if browser_context:
|
||||
await browser_context.close()
|
||||
if browser:
|
||||
await browser.close()
|
||||
|
||||
|
||||
def validate_window_size(configured: dict[str, Any], actual: dict[str, Any]) -> None:
|
||||
"""Compare configured window size with actual size and report differences"""
|
||||
# Allow for small differences due to browser chrome, scrollbars, etc.
|
||||
width_diff = abs(configured['width'] - actual['width'])
|
||||
height_diff = abs(configured['height'] - actual['height'])
|
||||
|
||||
# Tolerance of 5% or 20px, whichever is greater
|
||||
width_tolerance = max(configured['width'] * 0.05, 20)
|
||||
height_tolerance = max(configured['height'] * 0.05, 20)
|
||||
|
||||
if width_diff > width_tolerance or height_diff > height_tolerance:
|
||||
print('WARNING: Significant difference between configured and actual window size!')
|
||||
print(f'Width difference: {width_diff}px, Height difference: {height_diff}px')
|
||||
else:
|
||||
print('Window size validation passed: actual size matches configured size within tolerance')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
result = asyncio.run(main())
|
||||
sys.exit(result)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import asyncio
|
||||
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
from browser_use.browser.context import BrowserContextConfig
|
||||
|
||||
|
||||
async def test():
|
||||
print('Testing browser window sizing with no_viewport=False...')
|
||||
browser = Browser(BrowserConfig(headless=False))
|
||||
context_config = BrowserContextConfig(window_width=1440, window_height=900, no_viewport=False)
|
||||
browser_context = await browser.new_context(config=context_config)
|
||||
page = await browser_context.get_current_page()
|
||||
await page.goto('https://example.com')
|
||||
await asyncio.sleep(2)
|
||||
viewport = await page.evaluate('() => ({width: window.innerWidth, height: window.innerHeight})')
|
||||
print('Configured size: width=1440, height=900')
|
||||
print(f'Actual viewport size: {viewport}')
|
||||
|
||||
# Get the actual window size
|
||||
window_size = await page.evaluate("""
|
||||
() => ({
|
||||
width: window.outerWidth,
|
||||
height: window.outerHeight
|
||||
})
|
||||
""")
|
||||
print(f'Actual window size: {window_size}')
|
||||
|
||||
await browser_context.close()
|
||||
await browser.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(test())
|
||||
363
browser-use/tests/test_context.py
Normal file
363
browser-use/tests/test_context.py
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
import base64
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from browser_use.browser.context import BrowserContext, BrowserContextConfig
|
||||
from browser_use.browser.views import BrowserState
|
||||
from browser_use.dom.views import DOMElementNode
|
||||
|
||||
|
||||
def test_is_url_allowed():
|
||||
"""
|
||||
Test the _is_url_allowed method to verify that it correctly checks URLs against
|
||||
the allowed domains configuration.
|
||||
Scenario 1: When allowed_domains is None, all URLs should be allowed.
|
||||
Scenario 2: When allowed_domains is a list, only URLs matching the allowed domain(s) are allowed.
|
||||
Scenario 3: When the URL is malformed, it should return False.
|
||||
Scenario 4: When allowed_domains contain glob patterns, see: test_url_allowlist_security.py
|
||||
"""
|
||||
# Create a dummy Browser mock. Only the 'config' attribute is needed for _is_url_allowed.
|
||||
dummy_browser = Mock()
|
||||
# Set an empty config for dummy_browser; it won't be used in _is_url_allowed.
|
||||
dummy_browser.config = Mock()
|
||||
# Scenario 1: allowed_domains is None, any URL should be allowed.
|
||||
config1 = BrowserContextConfig(allowed_domains=None)
|
||||
context1 = BrowserContext(browser=dummy_browser, config=config1)
|
||||
assert context1._is_url_allowed('http://anydomain.com') is True
|
||||
assert context1._is_url_allowed('https://anotherdomain.org/path') is True
|
||||
# Scenario 2: allowed_domains is provided.
|
||||
allowed = ['example.com', 'mysite.org']
|
||||
config2 = BrowserContextConfig(allowed_domains=allowed)
|
||||
context2 = BrowserContext(browser=dummy_browser, config=config2)
|
||||
# URL exactly matching
|
||||
assert context2._is_url_allowed('http://example.com') is True
|
||||
# URL with subdomain (should be allowed)
|
||||
assert context2._is_url_allowed('http://sub.example.com/path') is True
|
||||
# URL with different domain (should not be allowed)
|
||||
assert context2._is_url_allowed('http://notexample.com') is False
|
||||
# URL that matches second allowed domain
|
||||
assert context2._is_url_allowed('https://mysite.org/page') is True
|
||||
# URL with port number, still allowed (port is stripped)
|
||||
assert context2._is_url_allowed('http://example.com:8080') is True
|
||||
# Scenario 3: Malformed URL or empty domain
|
||||
# urlparse will return an empty netloc for some malformed URLs.
|
||||
assert context2._is_url_allowed('notaurl') is False
|
||||
|
||||
|
||||
def test_convert_simple_xpath_to_css_selector():
|
||||
"""
|
||||
Test the _convert_simple_xpath_to_css_selector method of BrowserContext.
|
||||
This verifies that simple XPath expressions (with and without indices) are correctly converted to CSS selectors.
|
||||
"""
|
||||
# Test empty xpath returns empty string
|
||||
assert BrowserContext._convert_simple_xpath_to_css_selector('') == ''
|
||||
# Test a simple xpath without indices
|
||||
xpath = '/html/body/div/span'
|
||||
expected = 'html > body > div > span'
|
||||
result = BrowserContext._convert_simple_xpath_to_css_selector(xpath)
|
||||
assert result == expected
|
||||
# Test xpath with an index on one element: [2] should translate to :nth-of-type(2)
|
||||
xpath = '/html/body/div[2]/span'
|
||||
expected = 'html > body > div:nth-of-type(2) > span'
|
||||
result = BrowserContext._convert_simple_xpath_to_css_selector(xpath)
|
||||
assert result == expected
|
||||
# Test xpath with indices on multiple elements:
|
||||
# For "li[3]" -> li:nth-of-type(3) and for "a[1]" -> a:nth-of-type(1)
|
||||
xpath = '/ul/li[3]/a[1]'
|
||||
expected = 'ul > li:nth-of-type(3) > a:nth-of-type(1)'
|
||||
result = BrowserContext._convert_simple_xpath_to_css_selector(xpath)
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_get_initial_state():
|
||||
"""
|
||||
Test the _get_initial_state method to verify it returns the correct initial BrowserState.
|
||||
The test checks that when a dummy page with a URL is provided,
|
||||
the returned state contains that URL and other default values.
|
||||
"""
|
||||
# Create a dummy browser since only its existence is needed.
|
||||
dummy_browser = Mock()
|
||||
dummy_browser.config = Mock()
|
||||
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
|
||||
|
||||
# Define a dummy page with a 'url' attribute.
|
||||
class DummyPage:
|
||||
url = 'http://dummy.com'
|
||||
|
||||
dummy_page = DummyPage()
|
||||
# Call _get_initial_state with a page: URL should be set from page.url.
|
||||
state_with_page = context._get_initial_state(page=dummy_page)
|
||||
assert state_with_page.url == dummy_page.url
|
||||
# Verify that the element_tree is initialized with tag 'root'
|
||||
assert state_with_page.element_tree.tag_name == 'root'
|
||||
# Call _get_initial_state without a page: URL should be empty.
|
||||
state_without_page = context._get_initial_state()
|
||||
assert state_without_page.url == ''
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_javascript():
|
||||
"""
|
||||
Test the execute_javascript method by mocking the current page's evaluate function.
|
||||
This ensures that when execute_javascript is called, it correctly returns the value
|
||||
from the page's evaluate method.
|
||||
"""
|
||||
|
||||
# Define a dummy page with an async evaluate method.
|
||||
class DummyPage:
|
||||
async def evaluate(self, script):
|
||||
return 'dummy_result'
|
||||
|
||||
# Create a dummy session object with a dummy current_page.
|
||||
dummy_session = type('DummySession', (), {})()
|
||||
dummy_session.current_page = DummyPage()
|
||||
# Create a dummy browser mock with a minimal config.
|
||||
dummy_browser = Mock()
|
||||
dummy_browser.config = Mock()
|
||||
# Initialize the BrowserContext with the dummy browser and config.
|
||||
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
|
||||
# Manually set the session to our dummy session.
|
||||
context.session = dummy_session
|
||||
# Call execute_javascript and verify it returns the expected result.
|
||||
result = await context.execute_javascript('return 1+1')
|
||||
assert result == 'dummy_result'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enhanced_css_selector_for_element():
|
||||
"""
|
||||
Test the _enhanced_css_selector_for_element method to verify that
|
||||
it returns the correct CSS selector string for a dummy DOMElementNode.
|
||||
The test checks that:
|
||||
- The provided xpath is correctly converted (handling indices),
|
||||
- Class attributes are appended as CSS classes,
|
||||
- Standard and dynamic attributes (including ones with special characters)
|
||||
are correctly added to the selector.
|
||||
"""
|
||||
# Create a dummy DOMElementNode instance with a complex set of attributes.
|
||||
dummy_element = DOMElementNode(
|
||||
tag_name='div',
|
||||
is_visible=True,
|
||||
parent=None,
|
||||
xpath='/html/body/div[2]',
|
||||
attributes={'class': 'foo bar', 'id': 'my-id', 'placeholder': 'some "quoted" text', 'data-testid': '123'},
|
||||
children=[],
|
||||
)
|
||||
# Call the method with include_dynamic_attributes=True.
|
||||
actual_selector = BrowserContext._enhanced_css_selector_for_element(dummy_element, include_dynamic_attributes=True)
|
||||
# Expected conversion:
|
||||
# 1. The xpath "/html/body/div[2]" converts to "html > body > div:nth-of-type(2)".
|
||||
# 2. The class attribute "foo bar" appends ".foo.bar".
|
||||
# 3. The "id" attribute is added as [id="my-id"].
|
||||
# 4. The "placeholder" attribute contains quotes; it is added as
|
||||
# [placeholder*="some \"quoted\" text"].
|
||||
# 5. The dynamic attribute "data-testid" is added as [data-testid="123"].
|
||||
expected_selector = (
|
||||
'html > body > div:nth-of-type(2).foo.bar[id="my-id"][placeholder*="some \\"quoted\\" text"][data-testid="123"]'
|
||||
)
|
||||
assert actual_selector == expected_selector, f'Expected {expected_selector}, but got {actual_selector}'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_scroll_info():
|
||||
"""
|
||||
Test the get_scroll_info method by mocking the page's evaluate method.
|
||||
This dummy page returns preset values for window.scrollY, window.innerHeight,
|
||||
and document.documentElement.scrollHeight. The test then verifies that the
|
||||
computed scroll information (pixels_above and pixels_below) match the expected values.
|
||||
"""
|
||||
|
||||
# Define a dummy page with an async evaluate method returning preset values.
|
||||
class DummyPage:
|
||||
async def evaluate(self, script):
|
||||
if 'window.scrollY' in script:
|
||||
return 100 # scrollY
|
||||
elif 'window.innerHeight' in script:
|
||||
return 500 # innerHeight
|
||||
elif 'document.documentElement.scrollHeight' in script:
|
||||
return 1200 # total scrollable height
|
||||
return None
|
||||
|
||||
# Create a dummy session with a dummy current_page.
|
||||
dummy_session = type('DummySession', (), {})()
|
||||
dummy_session.current_page = DummyPage()
|
||||
# We also need a dummy context attribute but it won't be used in this test.
|
||||
dummy_session.context = type('DummyContext', (), {})()
|
||||
# Create a dummy browser mock.
|
||||
dummy_browser = Mock()
|
||||
dummy_browser.config = Mock()
|
||||
# Initialize BrowserContext with the dummy browser and config.
|
||||
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
|
||||
# Manually set the session to our dummy session.
|
||||
context.session = dummy_session
|
||||
# Call get_scroll_info on the dummy page.
|
||||
pixels_above, pixels_below = await context.get_scroll_info(dummy_session.current_page)
|
||||
# Expected calculations:
|
||||
# pixels_above = scrollY = 100
|
||||
# pixels_below = total_height - (scrollY + innerHeight) = 1200 - (100 + 500) = 600
|
||||
assert pixels_above == 100, f'Expected 100 pixels above, got {pixels_above}'
|
||||
assert pixels_below == 600, f'Expected 600 pixels below, got {pixels_below}'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_context():
|
||||
"""
|
||||
Test the reset_context method to ensure it correctly closes all existing tabs,
|
||||
resets the cached state, and creates a new page.
|
||||
"""
|
||||
|
||||
# Dummy Page with close and wait_for_load_state methods.
|
||||
class DummyPage:
|
||||
def __init__(self, url='http://dummy.com'):
|
||||
self.url = url
|
||||
self.closed = False
|
||||
|
||||
async def close(self):
|
||||
self.closed = True
|
||||
|
||||
async def wait_for_load_state(self):
|
||||
pass
|
||||
|
||||
# Dummy Context that holds pages and can create a new page.
|
||||
class DummyContext:
|
||||
def __init__(self):
|
||||
self.pages = []
|
||||
|
||||
async def new_page(self):
|
||||
new_page = DummyPage(url='')
|
||||
self.pages.append(new_page)
|
||||
return new_page
|
||||
|
||||
# Create a dummy session with a context containing two pages.
|
||||
dummy_session = type('DummySession', (), {})()
|
||||
dummy_context = DummyContext()
|
||||
page1 = DummyPage(url='http://page1.com')
|
||||
page2 = DummyPage(url='http://page2.com')
|
||||
dummy_context.pages.extend([page1, page2])
|
||||
dummy_session.context = dummy_context
|
||||
dummy_session.current_page = page1
|
||||
dummy_session.cached_state = None
|
||||
# Create a dummy browser mock.
|
||||
dummy_browser = Mock()
|
||||
dummy_browser.config = Mock()
|
||||
# Initialize BrowserContext using our dummy_browser and config,
|
||||
# and manually set its session to our dummy session.
|
||||
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
|
||||
context.session = dummy_session
|
||||
# Confirm session has 2 pages before reset.
|
||||
assert len(dummy_session.context.pages) == 2
|
||||
# Call reset_context which should close existing pages,
|
||||
# reset the cached state, and create a new page as current_page.
|
||||
await context.reset_context()
|
||||
# Verify that initial pages were closed.
|
||||
assert page1.closed is True
|
||||
assert page2.closed is True
|
||||
# Check that a new page is created and set as current_page.
|
||||
assert dummy_session.current_page is not None
|
||||
new_page = dummy_session.current_page
|
||||
# New page URL should be empty as per _get_initial_state.
|
||||
assert new_page.url == ''
|
||||
# Verify that cached_state is reset to an initial BrowserState.
|
||||
state = dummy_session.cached_state
|
||||
assert isinstance(state, BrowserState)
|
||||
assert state.url == ''
|
||||
assert state.element_tree.tag_name == 'root'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_take_screenshot():
|
||||
"""
|
||||
Test the take_screenshot method to verify that it returns a base64 encoded screenshot string.
|
||||
A dummy page with a mocked screenshot method is used, returning a predefined byte string.
|
||||
"""
|
||||
|
||||
class DummyPage:
|
||||
async def screenshot(self, full_page, animations):
|
||||
# Verify that parameters are forwarded correctly.
|
||||
assert full_page is True, 'full_page parameter was not correctly passed'
|
||||
assert animations == 'disabled', 'animations parameter was not correctly passed'
|
||||
# Return a test byte string.
|
||||
return b'test'
|
||||
|
||||
# Create a dummy session with the DummyPage as the current_page.
|
||||
dummy_session = type('DummySession', (), {})()
|
||||
dummy_session.current_page = DummyPage()
|
||||
dummy_session.context = None # Not used in this test
|
||||
# Create a dummy browser mock.
|
||||
dummy_browser = Mock()
|
||||
dummy_browser.config = Mock()
|
||||
# Initialize the BrowserContext with the dummy browser and config.
|
||||
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
|
||||
# Manually set the session to our dummy session.
|
||||
context.session = dummy_session
|
||||
# Call take_screenshot and check that it returns the expected base64 encoded string.
|
||||
result = await context.take_screenshot(full_page=True)
|
||||
expected = base64.b64encode(b'test').decode('utf-8')
|
||||
assert result == expected, f'Expected {expected}, but got {result}'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_page_behavior():
|
||||
"""
|
||||
Test the refresh_page method of BrowserContext to verify that it correctly reloads the current page
|
||||
and waits for the page's load state. This is done by creating a dummy page that flags when its
|
||||
reload and wait_for_load_state methods are called.
|
||||
"""
|
||||
|
||||
class DummyPage:
|
||||
def __init__(self):
|
||||
self.reload_called = False
|
||||
self.wait_for_load_state_called = False
|
||||
|
||||
async def reload(self):
|
||||
self.reload_called = True
|
||||
|
||||
async def wait_for_load_state(self):
|
||||
self.wait_for_load_state_called = True
|
||||
|
||||
# Create a dummy session with the dummy page as the current_page.
|
||||
dummy_page = DummyPage()
|
||||
dummy_session = type('DummySession', (), {})()
|
||||
dummy_session.current_page = dummy_page
|
||||
dummy_session.context = None # Not required for this test
|
||||
# Create a dummy browser mock
|
||||
dummy_browser = Mock()
|
||||
dummy_browser.config = Mock()
|
||||
# Initialize BrowserContext with the dummy browser and config,
|
||||
# and manually set its session to our dummy session.
|
||||
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
|
||||
context.session = dummy_session
|
||||
# Call refresh_page and verify that reload and wait_for_load_state were called.
|
||||
await context.refresh_page()
|
||||
assert dummy_page.reload_called is True, 'Expected the page to call reload()'
|
||||
assert dummy_page.wait_for_load_state_called is True, 'Expected the page to call wait_for_load_state()'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_highlights_failure():
|
||||
"""
|
||||
Test the remove_highlights method to ensure that if the page.evaluate call fails,
|
||||
the exception is caught and does not propagate (i.e. the method handles errors gracefully).
|
||||
"""
|
||||
|
||||
# Dummy page that always raises an exception when evaluate is called.
|
||||
class DummyPage:
|
||||
async def evaluate(self, script):
|
||||
raise Exception('dummy error')
|
||||
|
||||
# Create a dummy session with the DummyPage as current_page.
|
||||
dummy_session = type('DummySession', (), {})()
|
||||
dummy_session.current_page = DummyPage()
|
||||
dummy_session.context = None # Not used in this test
|
||||
# Create a dummy browser mock.
|
||||
dummy_browser = Mock()
|
||||
dummy_browser.config = Mock()
|
||||
# Initialize BrowserContext with the dummy browser and configuration.
|
||||
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
|
||||
context.session = dummy_session
|
||||
# Call remove_highlights and verify that no exception is raised.
|
||||
try:
|
||||
await context.remove_highlights()
|
||||
except Exception as e:
|
||||
pytest.fail(f'remove_highlights raised an exception: {e}')
|
||||
1256
browser-use/tests/test_controller.py
Normal file
1256
browser-use/tests/test_controller.py
Normal file
File diff suppressed because it is too large
Load diff
202
browser-use/tests/test_core_functionality.py
Normal file
202
browser-use/tests/test_core_functionality.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import asyncio
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from langchain_openai import AzureChatOpenAI
|
||||
from pydantic import SecretStr
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for each test case."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
async def browser(event_loop):
|
||||
browser_instance = Browser(
|
||||
config=BrowserConfig(
|
||||
headless=True,
|
||||
)
|
||||
)
|
||||
yield browser_instance
|
||||
await browser_instance.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def context(browser):
|
||||
async with await browser.new_context() as context:
|
||||
yield context
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llm():
|
||||
"""Initialize language model for testing"""
|
||||
return AzureChatOpenAI(
|
||||
model='gpt-4o',
|
||||
api_version='2024-10-21',
|
||||
azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
|
||||
api_key=SecretStr(os.getenv('AZURE_OPENAI_KEY', '')),
|
||||
)
|
||||
|
||||
|
||||
# pytest -s -k test_search_google
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_google(llm, context):
|
||||
"""Test 'Search Google' action"""
|
||||
agent = Agent(
|
||||
task="Search Google for 'OpenAI'.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
history: AgentHistoryList = await agent.run(max_steps=2)
|
||||
action_names = history.action_names()
|
||||
assert 'search_google' in action_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_go_to_url(llm, context):
|
||||
"""Test 'Navigate to URL' action"""
|
||||
agent = Agent(
|
||||
task="Navigate to 'https://www.python.org'.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
history = await agent.run(max_steps=2)
|
||||
action_names = history.action_names()
|
||||
assert 'go_to_url' in action_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_go_back(llm, context):
|
||||
"""Test 'Go back' action"""
|
||||
agent = Agent(
|
||||
task="Go to 'https://www.example.com', then go back.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
history = await agent.run(max_steps=3)
|
||||
action_names = history.action_names()
|
||||
assert 'go_to_url' in action_names
|
||||
assert 'go_back' in action_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_click_element(llm, context):
|
||||
"""Test 'Click element' action"""
|
||||
agent = Agent(
|
||||
task="Go to 'https://www.python.org' and click on the first link.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
history = await agent.run(max_steps=4)
|
||||
action_names = history.action_names()
|
||||
assert 'go_to_url' in action_names or 'open_tab' in action_names
|
||||
assert 'click_element_by_index' in action_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_input_text(llm, context):
|
||||
"""Test 'Input text' action"""
|
||||
agent = Agent(
|
||||
task="Go to 'https://www.google.com' and input 'OpenAI' into the search box.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
history = await agent.run(max_steps=4)
|
||||
action_names = history.action_names()
|
||||
assert 'go_to_url' in action_names
|
||||
assert 'input_text' in action_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_tab(llm, context):
|
||||
"""Test 'Switch tab' action"""
|
||||
agent = Agent(
|
||||
task="Open new tabs with 'https://www.google.com' and 'https://www.wikipedia.org', then switch to the first tab.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
history = await agent.run(max_steps=6)
|
||||
action_names = history.action_names()
|
||||
open_tab_count = action_names.count('open_tab')
|
||||
assert open_tab_count >= 2
|
||||
assert 'switch_tab' in action_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_new_tab(llm, context):
|
||||
"""Test 'Open new tab' action"""
|
||||
agent = Agent(
|
||||
task="Open a new tab and go to 'https://www.example.com'.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
history = await agent.run(max_steps=3)
|
||||
action_names = history.action_names()
|
||||
assert 'open_tab' in action_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extract_page_content(llm, context):
|
||||
"""Test 'Extract page content' action"""
|
||||
agent = Agent(
|
||||
task="Go to 'https://www.example.com' and extract the page content.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
history = await agent.run(max_steps=3)
|
||||
action_names = history.action_names()
|
||||
assert 'go_to_url' in action_names
|
||||
assert 'extract_content' in action_names
|
||||
|
||||
|
||||
# pytest -k test_done_action
|
||||
@pytest.mark.asyncio
|
||||
async def test_done_action(llm, context):
|
||||
"""Test 'Complete task' action"""
|
||||
agent = Agent(
|
||||
task="Navigate to 'https://www.example.com' and signal that the task is done.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
|
||||
history = await agent.run(max_steps=3)
|
||||
action_names = history.action_names()
|
||||
assert 'go_to_url' in action_names
|
||||
assert 'done' in action_names
|
||||
|
||||
|
||||
# run with: pytest -k test_scroll_down
|
||||
@pytest.mark.asyncio
|
||||
async def test_scroll_down(llm, context):
|
||||
"""Test 'Scroll down' action and validate that the page actually scrolled"""
|
||||
agent = Agent(
|
||||
task="Go to 'https://en.wikipedia.org/wiki/Internet' and scroll down the page.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
)
|
||||
# Get the browser instance
|
||||
page = await context.get_current_page()
|
||||
|
||||
# Navigate to the page and get initial scroll position
|
||||
await agent.run(max_steps=1)
|
||||
initial_scroll_position = await page.evaluate('window.scrollY;')
|
||||
|
||||
# Perform the scroll down action
|
||||
await agent.run(max_steps=2)
|
||||
final_scroll_position = await page.evaluate('window.scrollY;')
|
||||
|
||||
# Validate that the scroll position has changed
|
||||
assert final_scroll_position > initial_scroll_position, 'Page did not scroll down'
|
||||
|
||||
# Validate that the 'scroll_down' action was executed
|
||||
history = agent.history
|
||||
action_names = history.action_names()
|
||||
assert 'scroll_down' in action_names
|
||||
40
browser-use/tests/test_dropdown.py
Normal file
40
browser-use/tests/test_dropdown.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""
|
||||
Test dropdown interaction functionality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dropdown(llm, browser_context):
|
||||
"""Test selecting an option from a dropdown menu."""
|
||||
agent = Agent(
|
||||
task=(
|
||||
'go to https://codepen.io/geheimschriftstift/pen/mPLvQz and first get all options for the dropdown and then select the 5th option'
|
||||
),
|
||||
llm=llm,
|
||||
browser_context=browser_context,
|
||||
)
|
||||
|
||||
try:
|
||||
history: AgentHistoryList = await agent.run(20)
|
||||
result = history.final_result()
|
||||
|
||||
# Verify dropdown interaction
|
||||
assert result is not None
|
||||
assert 'Duck' in result, "Expected 5th option 'Duck' to be selected"
|
||||
|
||||
# Verify dropdown state
|
||||
element = await browser_context.get_element_by_selector('select')
|
||||
assert element is not None, 'Dropdown element should exist'
|
||||
|
||||
value = await element.evaluate('el => el.value')
|
||||
assert value == '5', 'Dropdown should have 5th option selected'
|
||||
|
||||
except Exception as e:
|
||||
pytest.fail(f'Dropdown test failed: {str(e)}')
|
||||
finally:
|
||||
await browser_context.close()
|
||||
44
browser-use/tests/test_dropdown_complex.py
Normal file
44
browser-use/tests/test_dropdown_complex.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""
|
||||
Test complex dropdown interaction functionality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dropdown_complex(llm, browser_context):
|
||||
"""Test selecting an option from a complex dropdown menu."""
|
||||
agent = Agent(
|
||||
task=(
|
||||
'go to https://codepen.io/shyam-king/pen/pvzpByJ and first get all options for the dropdown and then select the json option'
|
||||
),
|
||||
llm=llm,
|
||||
browser_context=browser_context,
|
||||
)
|
||||
|
||||
try:
|
||||
history: AgentHistoryList = await agent.run(20)
|
||||
result = history.final_result()
|
||||
|
||||
# Verify dropdown interaction
|
||||
assert result is not None
|
||||
assert 'json' in result.lower(), "Expected 'json' option to be selected"
|
||||
|
||||
# Verify dropdown state
|
||||
element = await browser_context.get_element_by_selector('.select-selected')
|
||||
assert element is not None, 'Custom dropdown element should exist'
|
||||
|
||||
text = await element.text_content()
|
||||
assert 'json' in text.lower(), 'Dropdown should display json option'
|
||||
|
||||
# Verify the selected option's effect
|
||||
code_element = await browser_context.get_element_by_selector('pre code')
|
||||
assert code_element is not None, 'Code element should be visible when JSON is selected'
|
||||
|
||||
except Exception as e:
|
||||
pytest.fail(f'Complex dropdown test failed: {str(e)}')
|
||||
finally:
|
||||
await browser_context.close()
|
||||
40
browser-use/tests/test_dropdown_error.py
Normal file
40
browser-use/tests/test_dropdown_error.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""
|
||||
Simple try of the agent.
|
||||
|
||||
@dev You need to add OPENAI_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
from browser_use.browser.context import BrowserContext
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from browser_use import Agent, AgentHistoryList
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4o')
|
||||
# browser = Browser(config=BrowserConfig(headless=False))
|
||||
|
||||
agent = Agent(
|
||||
task=('go to https://codepen.io/shyam-king/pen/emOyjKm and select number "4" and return the output of "selected value"'),
|
||||
llm=llm,
|
||||
browser_context=BrowserContext(
|
||||
browser=Browser(config=BrowserConfig(headless=False, disable_security=True)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def test_dropdown():
|
||||
history: AgentHistoryList = await agent.run(20)
|
||||
# await controller.browser.close(force=True)
|
||||
|
||||
result = history.final_result()
|
||||
assert result is not None
|
||||
assert '4' in result
|
||||
print(result)
|
||||
|
||||
# await browser.close()
|
||||
98
browser-use/tests/test_excluded_actions.py
Normal file
98
browser-use/tests/test_excluded_actions.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import asyncio
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from langchain_openai import AzureChatOpenAI
|
||||
from pydantic import SecretStr
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
from browser_use.controller.service import Controller
|
||||
|
||||
# run with:
|
||||
# python -m pytest tests/test_excluded_actions.py -v -k "test_only_open_tab_allowed" --capture=no
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for each test case."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
async def browser(event_loop):
|
||||
browser_instance = Browser(
|
||||
config=BrowserConfig(
|
||||
headless=True,
|
||||
)
|
||||
)
|
||||
yield browser_instance
|
||||
await browser_instance.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def context(browser):
|
||||
async with await browser.new_context() as context:
|
||||
yield context
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llm():
|
||||
"""Initialize language model for testing"""
|
||||
return AzureChatOpenAI(
|
||||
model='gpt-4o',
|
||||
api_version='2024-10-21',
|
||||
azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
|
||||
api_key=SecretStr(os.getenv('AZURE_OPENAI_KEY', '')),
|
||||
)
|
||||
|
||||
|
||||
# pytest tests/test_excluded_actions.py -v -k "test_only_open_tab_allowed" --capture=no
|
||||
@pytest.mark.asyncio
|
||||
async def test_only_open_tab_allowed(llm, context):
|
||||
"""Test that only open_tab action is available while others are excluded"""
|
||||
|
||||
# Create list of all default actions except open_tab
|
||||
excluded_actions = [
|
||||
'search_google',
|
||||
'go_to_url',
|
||||
'go_back',
|
||||
'click_element',
|
||||
'input_text',
|
||||
'switch_tab',
|
||||
'extract_content',
|
||||
'done',
|
||||
'scroll_down',
|
||||
'scroll_up',
|
||||
'send_keys',
|
||||
'scroll_to_text',
|
||||
'get_dropdown_options',
|
||||
'select_dropdown_option',
|
||||
]
|
||||
|
||||
# Initialize controller with excluded actions
|
||||
controller = Controller(exclude_actions=excluded_actions)
|
||||
|
||||
# Create agent with a task that would normally use other actions
|
||||
agent = Agent(
|
||||
task="Go to google.com and search for 'python programming'",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
controller=controller,
|
||||
)
|
||||
|
||||
history: AgentHistoryList = await agent.run(max_steps=2)
|
||||
|
||||
# Verify that only open_tab was used
|
||||
action_names = history.action_names()
|
||||
|
||||
# Only open_tab should be in the actions
|
||||
assert all(action == 'open_tab' for action in action_names), (
|
||||
f'Found unexpected actions: {[a for a in action_names if a != "open_tab"]}'
|
||||
)
|
||||
|
||||
# open_tab should be used at least once
|
||||
assert 'open_tab' in action_names, 'open_tab action was not used'
|
||||
21
browser-use/tests/test_full_screen.py
Normal file
21
browser-use/tests/test_full_screen.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import asyncio
|
||||
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
|
||||
async def test_full_screen(start_fullscreen: bool, maximize: bool):
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(
|
||||
headless=False,
|
||||
args=['--start-maximized'],
|
||||
)
|
||||
context = await browser.new_context(no_viewport=True, viewport=None)
|
||||
page = await context.new_page()
|
||||
await page.goto('https://google.com')
|
||||
|
||||
await asyncio.sleep(10)
|
||||
await browser.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(test_full_screen(False, False))
|
||||
40
browser-use/tests/test_gif_path.py
Normal file
40
browser-use/tests/test_gif_path.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""
|
||||
Simple try of the agent.
|
||||
|
||||
@dev You need to add OPENAI_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
from browser_use.browser.context import BrowserContext
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from browser_use import Agent, AgentHistoryList
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4o')
|
||||
|
||||
agent = Agent(
|
||||
task=('go to google.com and search for text "hi there"'),
|
||||
llm=llm,
|
||||
browser_context=BrowserContext(
|
||||
browser=Browser(config=BrowserConfig(headless=False, disable_security=True)),
|
||||
),
|
||||
generate_gif='./google.gif',
|
||||
)
|
||||
|
||||
|
||||
async def test_gif_path():
|
||||
if os.path.exists('./google.gif'):
|
||||
os.unlink('./google.gif')
|
||||
|
||||
history: AgentHistoryList = await agent.run(20)
|
||||
|
||||
result = history.final_result()
|
||||
assert result is not None
|
||||
|
||||
assert os.path.exists('./google.gif'), 'google.gif was not created'
|
||||
137
browser-use/tests/test_mind2web.py
Normal file
137
browser-use/tests/test_mind2web.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
Test browser automation using Mind2Web dataset tasks with pytest framework.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_openai import AzureChatOpenAI
|
||||
from pydantic import SecretStr
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
from browser_use.utils import logger
|
||||
|
||||
# Constants
|
||||
MAX_STEPS = 50
|
||||
TEST_SUBSET_SIZE = 10
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def event_loop():
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
async def browser(event_loop):
|
||||
browser_instance = Browser(
|
||||
config=BrowserConfig(
|
||||
headless=True,
|
||||
)
|
||||
)
|
||||
yield browser_instance
|
||||
await browser_instance.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def context(browser):
|
||||
async with await browser.new_context() as new_context:
|
||||
yield new_context
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_cases() -> list[dict[str, Any]]:
|
||||
"""Load test cases from Mind2Web dataset"""
|
||||
file_path = os.path.join(os.path.dirname(__file__), 'mind2web_data/processed.json')
|
||||
logger.info(f'Loading test cases from {file_path}')
|
||||
|
||||
with open(file_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
subset = data[:TEST_SUBSET_SIZE]
|
||||
logger.info(f'Loaded {len(subset)}/{len(data)} test cases')
|
||||
return subset
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llm():
|
||||
"""Initialize language model for testing"""
|
||||
|
||||
# return ChatAnthropic(model_name='claude-3-5-sonnet-20240620', timeout=25, stop=None)
|
||||
return AzureChatOpenAI(
|
||||
model='gpt-4o',
|
||||
api_version='2024-10-21',
|
||||
azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
|
||||
api_key=SecretStr(os.getenv('AZURE_OPENAI_KEY', '')),
|
||||
)
|
||||
|
||||
|
||||
# run with: pytest -s -v tests/test_mind2web.py:test_random_samples
|
||||
@pytest.mark.asyncio
|
||||
async def test_random_samples(test_cases: list[dict[str, Any]], llm, context, validator):
|
||||
"""Test a random sampling of tasks across different websites"""
|
||||
import random
|
||||
|
||||
logger.info('=== Testing Random Samples ===')
|
||||
|
||||
# Take random samples
|
||||
samples = random.sample(test_cases, 1)
|
||||
|
||||
for i, case in enumerate(samples, 1):
|
||||
task = f'Go to {case["website"]}.com and {case["confirmed_task"]}'
|
||||
logger.info(f'--- Random Sample {i}/{len(samples)} ---')
|
||||
logger.info(f'Task: {task}\n')
|
||||
|
||||
agent = Agent(task, llm, browser_context=context)
|
||||
|
||||
await agent.run()
|
||||
|
||||
logger.info('Validating random sample task...')
|
||||
|
||||
# TODO: Validate the task
|
||||
|
||||
|
||||
def test_dataset_integrity(test_cases):
|
||||
"""Test the integrity of the test dataset"""
|
||||
logger.info('\n=== Testing Dataset Integrity ===')
|
||||
|
||||
required_fields = ['website', 'confirmed_task', 'action_reprs']
|
||||
missing_fields = []
|
||||
|
||||
logger.info(f'Checking {len(test_cases)} test cases for required fields')
|
||||
|
||||
for i, case in enumerate(test_cases, 1):
|
||||
logger.debug(f'Checking case {i}/{len(test_cases)}')
|
||||
|
||||
for field in required_fields:
|
||||
if field not in case:
|
||||
missing_fields.append(f'Case {i}: {field}')
|
||||
logger.warning(f"Missing field '{field}' in case {i}")
|
||||
|
||||
# Type checks
|
||||
if not isinstance(case.get('confirmed_task'), str):
|
||||
logger.error(f"Case {i}: 'confirmed_task' must be string")
|
||||
assert False, 'Task must be string'
|
||||
|
||||
if not isinstance(case.get('action_reprs'), list):
|
||||
logger.error(f"Case {i}: 'action_reprs' must be list")
|
||||
assert False, 'Actions must be list'
|
||||
|
||||
if len(case.get('action_reprs', [])) == 0:
|
||||
logger.error(f"Case {i}: 'action_reprs' must not be empty")
|
||||
assert False, 'Must have at least one action'
|
||||
|
||||
if missing_fields:
|
||||
logger.error('Dataset integrity check failed')
|
||||
assert False, f'Missing fields: {missing_fields}'
|
||||
else:
|
||||
logger.info('✅ Dataset integrity check passed')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
160
browser-use/tests/test_models.py
Normal file
160
browser-use/tests/test_models.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import asyncio
|
||||
import os
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_google_genai import ChatGoogleGenerativeAI
|
||||
from langchain_ollama import ChatOllama
|
||||
from langchain_openai import AzureChatOpenAI, ChatOpenAI
|
||||
from pydantic import SecretStr
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for each test case."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
async def browser(event_loop):
|
||||
browser_instance = Browser(
|
||||
config=BrowserConfig(
|
||||
headless=True,
|
||||
)
|
||||
)
|
||||
yield browser_instance
|
||||
await browser_instance.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def context(browser):
|
||||
async with await browser.new_context() as context:
|
||||
yield context
|
||||
|
||||
|
||||
api_key_gemini = SecretStr(os.getenv('GOOGLE_API_KEY') or '')
|
||||
api_key_deepseek = SecretStr(os.getenv('DEEPSEEK_API_KEY') or '')
|
||||
api_key_anthropic = SecretStr(os.getenv('ANTHROPIC_API_KEY') or '')
|
||||
|
||||
|
||||
# pytest -s -v tests/test_models.py
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
ChatOpenAI(model='gpt-4o'),
|
||||
ChatOpenAI(model='gpt-4o-mini'),
|
||||
AzureChatOpenAI(
|
||||
model='gpt-4o',
|
||||
api_version='2024-10-21',
|
||||
azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
|
||||
api_key=SecretStr(os.getenv('AZURE_OPENAI_KEY', '')),
|
||||
),
|
||||
# ChatOpenAI(
|
||||
# base_url='https://api.deepseek.com/v1',
|
||||
# model='deepseek-reasoner',
|
||||
# api_key=api_key_deepseek,
|
||||
# ),
|
||||
# run: ollama start
|
||||
ChatOllama(
|
||||
model='qwen2.5:latest',
|
||||
num_ctx=128000,
|
||||
),
|
||||
AzureChatOpenAI(
|
||||
model='gpt-4o-mini',
|
||||
api_version='2024-10-21',
|
||||
azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
|
||||
api_key=SecretStr(os.getenv('AZURE_OPENAI_KEY', '')),
|
||||
),
|
||||
ChatAnthropic(
|
||||
model_name='claude-3-5-sonnet-20240620',
|
||||
timeout=100,
|
||||
temperature=0.0,
|
||||
stop=None,
|
||||
api_key=api_key_anthropic,
|
||||
),
|
||||
ChatGoogleGenerativeAI(model='gemini-2.0-flash-exp', api_key=api_key_gemini),
|
||||
ChatGoogleGenerativeAI(model='gemini-1.5-pro', api_key=api_key_gemini),
|
||||
ChatGoogleGenerativeAI(model='gemini-1.5-flash-latest', api_key=api_key_gemini),
|
||||
ChatOpenAI(
|
||||
base_url='https://api.deepseek.com/v1',
|
||||
model='deepseek-chat',
|
||||
api_key=api_key_deepseek,
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
'azure-gpt-4o',
|
||||
#'deepseek-reasoner',
|
||||
'qwen2.5:latest',
|
||||
'azure-gpt-4o-mini',
|
||||
'claude-3-5-sonnet',
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-latest',
|
||||
'deepseek-chat',
|
||||
],
|
||||
)
|
||||
async def llm(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_search(llm, context):
|
||||
"""Test 'Search Google' action"""
|
||||
model_name = llm.model if hasattr(llm, 'model') else llm.model_name
|
||||
print(f'\nTesting model: {model_name}')
|
||||
|
||||
use_vision = True
|
||||
models_without_vision = ['deepseek-chat', 'deepseek-reasoner']
|
||||
if hasattr(llm, 'model') and llm.model in models_without_vision:
|
||||
use_vision = False
|
||||
elif hasattr(llm, 'model_name') and llm.model_name in models_without_vision:
|
||||
use_vision = False
|
||||
|
||||
# require ollama run
|
||||
local_models = ['qwen2.5:latest']
|
||||
if model_name in local_models:
|
||||
# check if ollama is running
|
||||
# ping ollama http://127.0.0.1
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get('http://127.0.0.1:11434/')
|
||||
if response.status_code != 200:
|
||||
raise Exception('Ollama is not running - start with `ollama start`')
|
||||
except Exception:
|
||||
raise Exception('Ollama is not running - start with `ollama start`')
|
||||
|
||||
agent = Agent(
|
||||
task="Search Google for 'elon musk' then click on the first result and scroll down.",
|
||||
llm=llm,
|
||||
browser_context=context,
|
||||
max_failures=2,
|
||||
use_vision=use_vision,
|
||||
)
|
||||
history: AgentHistoryList = await agent.run(max_steps=2)
|
||||
done = history.is_done()
|
||||
successful = history.is_successful()
|
||||
action_names = history.action_names()
|
||||
print(f'Actions performed: {action_names}')
|
||||
errors = [e for e in history.errors() if e is not None]
|
||||
errors = '\n'.join(errors)
|
||||
passed = False
|
||||
if 'search_google' in action_names:
|
||||
passed = True
|
||||
elif 'go_to_url' in action_names:
|
||||
passed = True
|
||||
elif 'open_tab' in action_names:
|
||||
passed = True
|
||||
|
||||
else:
|
||||
passed = False
|
||||
print(f'Model {model_name}: {"✅ PASSED - " if passed else "❌ FAILED - "} Done: {done} Successful: {successful}')
|
||||
|
||||
assert passed, f'Model {model_name} not working\nActions performed: {action_names}\nErrors: {errors}'
|
||||
66
browser-use/tests/test_qwen.py
Normal file
66
browser-use/tests/test_qwen.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import asyncio
|
||||
|
||||
import pytest
|
||||
from langchain_ollama import ChatOllama
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
from browser_use.browser.browser import Browser, BrowserConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llm():
|
||||
"""Initialize language model for testing"""
|
||||
|
||||
# return ChatAnthropic(model_name='claude-3-5-sonnet-20240620', timeout=25, stop=None)
|
||||
# NOTE: Make sure to run ollama server with `ollama start'
|
||||
return ChatOllama(
|
||||
model='qwen2.5:latest',
|
||||
num_ctx=128000,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for each test case."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
async def browser(event_loop):
|
||||
browser_instance = Browser(
|
||||
config=BrowserConfig(
|
||||
headless=True,
|
||||
)
|
||||
)
|
||||
yield browser_instance
|
||||
await browser_instance.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def context(browser):
|
||||
async with await browser.new_context() as context:
|
||||
yield context
|
||||
|
||||
|
||||
# pytest tests/test_qwen.py -v -k "test_qwen_url" --capture=no
|
||||
# @pytest.mark.asyncio
|
||||
async def test_qwen_url(llm, context):
|
||||
"""Test complex ecommerce interaction sequence"""
|
||||
agent = Agent(
|
||||
task='go_to_url amazon.com',
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
history: AgentHistoryList = await agent.run(max_steps=3)
|
||||
|
||||
# Verify sequence of actions
|
||||
action_sequence = []
|
||||
for action in history.model_actions():
|
||||
action_name = list(action.keys())[0]
|
||||
if action_name in ['go_to_url', 'open_tab']:
|
||||
action_sequence.append('navigate')
|
||||
|
||||
assert 'navigate' in action_sequence # Navigated to Amazon
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue