whs-authz-authn/automatic-oauth-vulnerability-detection
Watch
1
0
Fork
You've already forked automatic-oauth-vulnerability-detection
0

[Add] browser-use and main.py

This commit is contained in:
tv0924@icloud.com 2025-05-18 21:57:54 +09:00
commit 96914d44ac
30 changed files with 30952 additions and 1 deletions

View 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()

View 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

View 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

View 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))

View 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()

View 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()

View 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)

View file

@ -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())

View 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}')

File diff suppressed because it is too large Load diff

View 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

View 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()

View 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()

View 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()

View 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'

View 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))

View 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'

View 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'])

View 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}'

View 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