mirror of
https://github.com/browser-use/browser-use
synced 2026-05-06 17:52:15 +02:00
435 lines
15 KiB
Python
435 lines
15 KiB
Python
"""
|
|
Test script for BrowserSession.start() method to ensure proper initialization,
|
|
concurrency handling, and error handling.
|
|
|
|
Tests cover:
|
|
- Calling .start() on a session that's already started
|
|
- Simultaneously calling .start() from two parallel coroutines
|
|
- Calling .start() on a session that's started but has a closed browser connection
|
|
- Calling .close() on a session that hasn't been started yet
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
from browser_use.browser.profile import (
|
|
BROWSERUSE_DEFAULT_CHANNEL,
|
|
BrowserChannel,
|
|
BrowserProfile,
|
|
)
|
|
from browser_use.browser.session import BrowserSession
|
|
from browser_use.config import CONFIG
|
|
|
|
# Set up test logging
|
|
logger = logging.getLogger('browser_session_start_tests')
|
|
# logger.setLevel(logging.DEBUG)
|
|
|
|
|
|
# run with pytest -k test_user_data_dir_not_allowed_to_corrupt_default_profile
|
|
|
|
|
|
class TestBrowserSessionStart:
|
|
"""Tests for BrowserSession.start() method initialization and concurrency."""
|
|
|
|
@pytest.fixture(scope='module')
|
|
async def browser_profile(self):
|
|
"""Create and provide a BrowserProfile with headless mode."""
|
|
profile = BrowserProfile(headless=True, user_data_dir=None, keep_alive=False)
|
|
yield profile
|
|
|
|
@pytest.fixture(scope='function')
|
|
async def browser_session(self, browser_profile):
|
|
"""Create a BrowserSession instance without starting it."""
|
|
session = BrowserSession(browser_profile=browser_profile)
|
|
yield session
|
|
await session.kill()
|
|
|
|
async def test_start_already_started_session(self, browser_session):
|
|
"""Test calling .start() on a session that's already started."""
|
|
# logger.info('Testing start on already started session')
|
|
|
|
# Start the session for the first time
|
|
await browser_session.start()
|
|
assert browser_session._cdp_client_root is not None
|
|
|
|
# Start the session again - should return immediately without re-initialization
|
|
await browser_session.start()
|
|
assert browser_session._cdp_client_root is not None
|
|
|
|
# @pytest.mark.skip(reason="Race condition - DOMWatchdog tries to inject scripts into tab that's being closed")
|
|
# async def test_page_lifecycle_management(self, browser_session: BrowserSession):
|
|
# """Test session handles page lifecycle correctly."""
|
|
# # logger.info('Testing page lifecycle management')
|
|
|
|
# # Start the session and get initial state
|
|
# await browser_session.start()
|
|
# initial_tabs = await browser_session.get_tabs()
|
|
# initial_count = len(initial_tabs)
|
|
|
|
# # Get current tab info
|
|
# current_url = await browser_session.get_current_page_url()
|
|
# assert current_url is not None
|
|
|
|
# # Get current tab ID
|
|
# current_tab_id = browser_session.agent_focus.target_id if browser_session.agent_focus else None
|
|
# assert current_tab_id is not None
|
|
|
|
# # Close the current tab using the event system
|
|
# from browser_use.browser.events import CloseTabEvent
|
|
|
|
# close_event = browser_session.event_bus.dispatch(CloseTabEvent(target_id=current_tab_id))
|
|
# await close_event
|
|
|
|
# # Operations should still work - may create new page or use existing
|
|
# tabs_after_close = await browser_session.get_tabs()
|
|
# assert isinstance(tabs_after_close, list)
|
|
|
|
# # Create a new tab explicitly
|
|
# event = browser_session.event_bus.dispatch(NavigateToUrlEvent(url='about:blank', new_tab=True))
|
|
# await event
|
|
# await event.event_result(raise_if_any=True, raise_if_none=False)
|
|
|
|
# # Should have at least one tab now
|
|
# final_tabs = await browser_session.get_tabs()
|
|
# assert len(final_tabs) >= 1
|
|
|
|
async def test_user_data_dir_not_allowed_to_corrupt_default_profile(self):
|
|
"""Test user_data_dir handling for different browser channels and version mismatches."""
|
|
# Test 1: Chromium with default user_data_dir and default channel should work fine
|
|
session = BrowserSession(
|
|
browser_profile=BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR,
|
|
channel=BROWSERUSE_DEFAULT_CHANNEL, # chromium
|
|
keep_alive=False,
|
|
),
|
|
)
|
|
|
|
try:
|
|
await session.start()
|
|
assert session._cdp_client_root is not None
|
|
# Verify the user_data_dir wasn't changed
|
|
assert session.browser_profile.user_data_dir == CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR
|
|
finally:
|
|
await session.kill()
|
|
|
|
# Test 2: Chrome with default user_data_dir should automatically change dir
|
|
profile2 = BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR,
|
|
channel=BrowserChannel.CHROME,
|
|
keep_alive=False,
|
|
)
|
|
|
|
# The validator should have changed the user_data_dir to avoid corruption
|
|
assert profile2.user_data_dir != CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR
|
|
assert profile2.user_data_dir == CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR.parent / 'default-chrome'
|
|
|
|
# Test 3: Edge with default user_data_dir should also change
|
|
profile3 = BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR,
|
|
channel=BrowserChannel.MSEDGE,
|
|
keep_alive=False,
|
|
)
|
|
|
|
assert profile3.user_data_dir != CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR
|
|
assert profile3.user_data_dir == CONFIG.BROWSER_USE_DEFAULT_USER_DATA_DIR.parent / 'default-msedge'
|
|
|
|
|
|
class TestBrowserSessionReusePatterns:
|
|
"""Tests for all browser re-use patterns documented in docs/customize/real-browser.mdx"""
|
|
|
|
async def test_sequential_agents_same_profile_different_browser(self, mock_llm):
|
|
"""Test Sequential Agents, Same Profile, Different Browser pattern"""
|
|
from browser_use import Agent
|
|
from browser_use.browser.profile import BrowserProfile
|
|
|
|
# Create a reusable profile
|
|
reused_profile = BrowserProfile(
|
|
user_data_dir=None, # Use temp dir for testing
|
|
headless=True,
|
|
)
|
|
|
|
# First agent
|
|
agent1 = Agent(
|
|
task='The first task...',
|
|
llm=mock_llm,
|
|
browser_profile=reused_profile,
|
|
)
|
|
await agent1.run()
|
|
|
|
# Verify first agent's session is closed
|
|
assert agent1.browser_session is not None
|
|
assert not agent1.browser_session._cdp_client_root is not None
|
|
|
|
# Second agent with same profile
|
|
agent2 = Agent(
|
|
task='The second task...',
|
|
llm=mock_llm,
|
|
browser_profile=reused_profile,
|
|
# Disable memory for tests
|
|
)
|
|
await agent2.run()
|
|
|
|
# Verify second agent created a new session
|
|
assert agent2.browser_session is not None
|
|
assert agent1.browser_session is not agent2.browser_session
|
|
assert not agent2.browser_session._cdp_client_root is not None
|
|
|
|
async def test_sequential_agents_same_profile_same_browser(self, mock_llm):
|
|
"""Test Sequential Agents, Same Profile, Same Browser pattern"""
|
|
from browser_use import Agent, BrowserSession
|
|
|
|
# Create a reusable session with keep_alive
|
|
reused_session = BrowserSession(
|
|
browser_profile=BrowserProfile(
|
|
user_data_dir=None, # Use temp dir for testing
|
|
headless=True,
|
|
keep_alive=True, # Don't close browser after agent.run()
|
|
),
|
|
)
|
|
|
|
try:
|
|
# Start the session manually (agents will reuse this initialized session)
|
|
await reused_session.start()
|
|
|
|
# First agent
|
|
agent1 = Agent(
|
|
task='The first task...',
|
|
llm=mock_llm,
|
|
browser_session=reused_session,
|
|
# Disable memory for tests
|
|
)
|
|
await agent1.run()
|
|
|
|
# Verify session is still alive
|
|
assert reused_session._cdp_client_root is not None
|
|
|
|
# Second agent reusing the same session
|
|
agent2 = Agent(
|
|
task='The second task...',
|
|
llm=mock_llm,
|
|
browser_session=reused_session,
|
|
# Disable memory for tests
|
|
)
|
|
await agent2.run()
|
|
|
|
# Verify same browser was used (using __eq__ to check browser_pid, cdp_url)
|
|
assert agent1.browser_session == agent2.browser_session
|
|
assert agent1.browser_session == reused_session
|
|
assert reused_session._cdp_client_root is not None
|
|
|
|
finally:
|
|
await reused_session.kill()
|
|
|
|
|
|
class TestBrowserSessionEventSystem:
|
|
"""Tests for the new event system integration in BrowserSession."""
|
|
|
|
@pytest.fixture(scope='function')
|
|
async def browser_session(self):
|
|
"""Create a BrowserSession instance for event system testing."""
|
|
profile = BrowserProfile(headless=True, user_data_dir=None, keep_alive=False)
|
|
session = BrowserSession(browser_profile=profile)
|
|
yield session
|
|
await session.kill()
|
|
|
|
async def test_event_bus_initialization(self, browser_session):
|
|
"""Test that event bus is properly initialized with unique name."""
|
|
# Event bus should be created during __init__
|
|
assert browser_session.event_bus is not None
|
|
assert browser_session.event_bus.name.startswith('EventBus_')
|
|
# Event bus name format may vary, just check it exists
|
|
|
|
async def test_event_handlers_registration(self, browser_session: BrowserSession):
|
|
"""Test that event handlers are properly registered."""
|
|
# Attach all watchdogs to register their handlers
|
|
await browser_session.attach_all_watchdogs()
|
|
|
|
# Check that handlers are registered in the event bus
|
|
from browser_use.browser.events import (
|
|
BrowserStartEvent,
|
|
BrowserStateRequestEvent,
|
|
BrowserStopEvent,
|
|
ClickElementEvent,
|
|
CloseTabEvent,
|
|
ScreenshotEvent,
|
|
ScrollEvent,
|
|
TypeTextEvent,
|
|
)
|
|
|
|
# These event types should have handlers registered
|
|
event_types_with_handlers = [
|
|
BrowserStartEvent,
|
|
BrowserStopEvent,
|
|
ClickElementEvent,
|
|
TypeTextEvent,
|
|
ScrollEvent,
|
|
CloseTabEvent,
|
|
BrowserStateRequestEvent,
|
|
ScreenshotEvent,
|
|
]
|
|
|
|
for event_type in event_types_with_handlers:
|
|
handlers = browser_session.event_bus.handlers.get(event_type.__name__, [])
|
|
assert len(handlers) > 0, f'No handlers registered for {event_type.__name__}'
|
|
|
|
async def test_direct_event_dispatching(self, browser_session):
|
|
"""Test direct event dispatching without using the public API."""
|
|
from browser_use.browser.events import BrowserConnectedEvent, BrowserStartEvent
|
|
|
|
# Dispatch BrowserStartEvent directly
|
|
start_event = browser_session.event_bus.dispatch(BrowserStartEvent())
|
|
|
|
# Wait for event to complete
|
|
await start_event
|
|
|
|
# Check if BrowserConnectedEvent was dispatched
|
|
assert browser_session._cdp_client_root is not None
|
|
|
|
# Check event history
|
|
event_history = list(browser_session.event_bus.event_history.values())
|
|
assert len(event_history) >= 2 # BrowserStartEvent + BrowserConnectedEvent + others
|
|
|
|
# Find the BrowserConnectedEvent in history
|
|
started_events = [e for e in event_history if isinstance(e, BrowserConnectedEvent)]
|
|
assert len(started_events) >= 1
|
|
assert started_events[0].cdp_url is not None
|
|
|
|
async def test_event_system_error_handling(self, browser_session):
|
|
"""Test error handling in event system."""
|
|
from browser_use.browser.events import BrowserStartEvent
|
|
|
|
# Create session with invalid CDP URL to trigger error
|
|
error_session = BrowserSession(
|
|
browser_profile=BrowserProfile(headless=True),
|
|
cdp_url='http://localhost:99999', # Invalid port
|
|
)
|
|
|
|
try:
|
|
# Dispatch start event directly - should trigger error handling
|
|
start_event = error_session.event_bus.dispatch(BrowserStartEvent())
|
|
|
|
# The event bus catches and logs the error, but the event awaits successfully
|
|
await start_event
|
|
|
|
# The session should not be initialized due to the error
|
|
assert error_session._cdp_client_root is None, 'Session should not be initialized after connection error'
|
|
|
|
# Verify the error was logged in the event history (good enough for error handling test)
|
|
assert len(error_session.event_bus.event_history) > 0, 'Event should be tracked even with errors'
|
|
|
|
finally:
|
|
await error_session.kill()
|
|
|
|
async def test_concurrent_event_dispatching(self, browser_session: BrowserSession):
|
|
"""Test that concurrent events are handled properly."""
|
|
from browser_use.browser.events import ScreenshotEvent
|
|
|
|
# Start browser first
|
|
await browser_session.start()
|
|
|
|
# Dispatch multiple events concurrently
|
|
screenshot_event1 = browser_session.event_bus.dispatch(ScreenshotEvent())
|
|
screenshot_event2 = browser_session.event_bus.dispatch(ScreenshotEvent())
|
|
|
|
# Both should complete successfully
|
|
results = await asyncio.gather(screenshot_event1, screenshot_event2, return_exceptions=True)
|
|
|
|
# Check that no exceptions were raised
|
|
for result in results:
|
|
assert not isinstance(result, Exception), f'Event failed with: {result}'
|
|
|
|
# async def test_many_parallel_browser_sessions(self):
|
|
# """Test spawning 12 parallel browser_sessions with different settings and ensure they all work"""
|
|
# from browser_use import BrowserSession
|
|
|
|
# browser_sessions = []
|
|
|
|
# for i in range(3):
|
|
# browser_sessions.append(
|
|
# BrowserSession(
|
|
# browser_profile=BrowserProfile(
|
|
# user_data_dir=None,
|
|
# headless=True,
|
|
# keep_alive=True,
|
|
# ),
|
|
# )
|
|
# )
|
|
# for i in range(3):
|
|
# browser_sessions.append(
|
|
# BrowserSession(
|
|
# browser_profile=BrowserProfile(
|
|
# user_data_dir=Path(tempfile.mkdtemp(prefix=f'browseruse-tmp-{i}')),
|
|
# headless=True,
|
|
# keep_alive=True,
|
|
# ),
|
|
# )
|
|
# )
|
|
# for i in range(3):
|
|
# browser_sessions.append(
|
|
# BrowserSession(
|
|
# browser_profile=BrowserProfile(
|
|
# user_data_dir=None,
|
|
# headless=True,
|
|
# keep_alive=False,
|
|
# ),
|
|
# )
|
|
# )
|
|
# for i in range(3):
|
|
# browser_sessions.append(
|
|
# BrowserSession(
|
|
# browser_profile=BrowserProfile(
|
|
# user_data_dir=Path(tempfile.mkdtemp(prefix=f'browseruse-tmp-{i}')),
|
|
# headless=True,
|
|
# keep_alive=False,
|
|
# ),
|
|
# )
|
|
# )
|
|
|
|
# print('Starting many parallel browser sessions...')
|
|
# await asyncio.gather(*[browser_session.start() for browser_session in browser_sessions])
|
|
|
|
# print('Ensuring all parallel browser sessions are connected and usable...')
|
|
# new_tab_tasks = []
|
|
# for browser_session in browser_sessions:
|
|
# assert browser_session._cdp_client_root is not None
|
|
# assert browser_session._cdp_client_root is not None
|
|
# new_tab_tasks.append(browser_session.create_new_tab('chrome://version'))
|
|
# await asyncio.gather(*new_tab_tasks)
|
|
|
|
# print('killing every 3rd browser_session to test parallel shutdown')
|
|
# kill_tasks = []
|
|
# for i in range(0, len(browser_sessions), 3):
|
|
# kill_tasks.append(browser_sessions[i].kill())
|
|
# browser_sessions[i] = None
|
|
# results = await asyncio.gather(*kill_tasks, return_exceptions=True)
|
|
# # Check that no exceptions were raised during cleanup
|
|
# for i, result in enumerate(results):
|
|
# if isinstance(result, Exception):
|
|
# print(f'Warning: Browser session kill raised exception: {type(result).__name__}: {result}')
|
|
|
|
# print('ensuring the remaining browser_sessions are still connected and usable')
|
|
# new_tab_tasks = []
|
|
# screenshot_tasks = []
|
|
# for browser_session in filter(bool, browser_sessions):
|
|
# assert browser_session._cdp_client_root is not None
|
|
# assert browser_session._cdp_client_root is not None
|
|
# new_tab_tasks.append(browser_session.create_new_tab('chrome://version'))
|
|
# screenshot_tasks.append(browser_session.take_screenshot())
|
|
# await asyncio.gather(*new_tab_tasks)
|
|
# await asyncio.gather(*screenshot_tasks)
|
|
|
|
# kill_tasks = []
|
|
# print('killing the remaining browser_sessions')
|
|
# for browser_session in filter(bool, browser_sessions):
|
|
# kill_tasks.append(browser_session.kill())
|
|
# results = await asyncio.gather(*kill_tasks, return_exceptions=True)
|
|
# # Check that no exceptions were raised during cleanup
|
|
# for i, result in enumerate(results):
|
|
# if isinstance(result, Exception):
|
|
# print(f'Warning: Browser session kill raised exception: {type(result).__name__}: {result}')
|