Files
browser-use/tests/ci/test_browser_session_start.py
Nick Sweeting aed2fa27bb add tests for
2025-05-27 17:09:18 -07:00

329 lines
12 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 BrowserProfile
from browser_use.browser.session import BrowserSession
# Set up test logging
logger = logging.getLogger('browser_session_start_tests')
# logger.setLevel(logging.DEBUG)
class TestBrowserSessionStart:
"""Tests for BrowserSession.start() method initialization and concurrency."""
@pytest.fixture
async def browser_profile(self):
"""Create and provide a BrowserProfile with headless mode."""
profile = BrowserProfile(headless=True, user_data_dir=None)
yield profile
@pytest.fixture
async def browser_session(self, browser_profile):
"""Create a BrowserSession instance without starting it."""
session = BrowserSession(browser_profile=browser_profile)
yield session
# Cleanup: ensure session is stopped
try:
await session.stop()
except Exception:
pass
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
result1 = await browser_session.start()
assert browser_session.initialized is True
assert browser_session.browser_context is not None
assert result1 is browser_session
# Start the session again - should return immediately without re-initialization
result2 = await browser_session.start()
assert result2 is browser_session
assert browser_session.initialized is True
assert browser_session.browser_context is not None
# Both results should be the same instance
assert result1 is result2
async def test_concurrent_start_calls(self, browser_session):
"""Test simultaneously calling .start() from two parallel coroutines."""
# logger.info('Testing concurrent start calls')
# Track how many times the lock is actually acquired for initialization
original_start_lock = browser_session._start_lock
lock_acquire_count = 0
class CountingLock:
def __init__(self, original_lock):
self.original_lock = original_lock
async def __aenter__(self):
nonlocal lock_acquire_count
lock_acquire_count += 1
return await self.original_lock.__aenter__()
async def __aexit__(self, exc_type, exc_val, exc_tb):
return await self.original_lock.__aexit__(exc_type, exc_val, exc_tb)
browser_session._start_lock = CountingLock(original_start_lock)
# Start two concurrent calls to start()
results = await asyncio.gather(browser_session.start(), browser_session.start(), return_exceptions=True)
# Both should succeed and return the same session instance
assert all(result is browser_session for result in results)
assert browser_session.initialized is True
assert browser_session.browser_context is not None
# The lock should have been acquired twice (once per coroutine)
# but only one should have done the actual initialization
assert lock_acquire_count == 2
async def test_start_with_closed_browser_connection(self, browser_session):
"""Test calling .start() on a session that's started but has a closed browser connection."""
# logger.info('Testing start with closed browser connection')
# Start the session normally
await browser_session.start()
assert browser_session.initialized is True
assert browser_session.browser_context is not None
# Simulate a closed browser connection by closing the browser
if browser_session.browser:
await browser_session.browser.close()
# The session should detect the closed connection and reinitialize
result = await browser_session.start()
assert result is browser_session
assert browser_session.initialized is True
assert browser_session.browser_context is not None
async def test_start_with_missing_browser_context(self, browser_session):
"""Test calling .start() when browser_context is None but initialized is True."""
# logger.info('Testing start with missing browser context')
# Manually set initialized to True but leave browser_context as None
browser_session.initialized = True
browser_session.browser_context = None
# Start should detect this inconsistent state and reinitialize
result = await browser_session.start()
assert result is browser_session
assert browser_session.initialized is True
assert browser_session.browser_context is not None
async def test_start_initialization_failure(self, browser_session):
"""Test that initialization failure properly resets the initialized flag."""
# logger.info('Testing start initialization failure')
# Mock setup_playwright to raise an exception
original_setup_playwright = browser_session.setup_playwright
async def failing_setup_playwright():
raise RuntimeError('Simulated initialization failure')
browser_session.setup_playwright = failing_setup_playwright
# Start should fail and reset initialized flag
with pytest.raises(RuntimeError, match='Simulated initialization failure'):
await browser_session.start()
assert browser_session.initialized is False
# Restore the original method and try again - should work
browser_session.setup_playwright = original_setup_playwright
result = await browser_session.start()
assert result is browser_session
assert browser_session.initialized is True
async def test_close_unstarted_session(self, browser_session):
"""Test calling .close() on a session that hasn't been started yet."""
# logger.info('Testing close on unstarted session')
# Ensure session is not started
assert browser_session.initialized is False
assert browser_session.browser_context is None
# Close should not raise an exception
await browser_session.stop()
# State should remain unchanged
assert browser_session.initialized is False
assert browser_session.browser_context is None
async def test_close_alias_method(self, browser_session):
"""Test the deprecated .close() alias method."""
# logger.info('Testing deprecated close alias method')
# Start the session
await browser_session.start()
assert browser_session.initialized is True
# Use the deprecated close method
await browser_session.close()
# Session should be stopped
assert browser_session.initialized is False
async def test_context_manager_usage(self, browser_session):
"""Test using BrowserSession as an async context manager."""
# logger.info('Testing context manager usage')
# Use as context manager
async with browser_session as session:
assert session is browser_session
assert session.initialized is True
assert session.browser_context is not None
# Should be stopped after exiting context
assert browser_session.initialized is False
async def test_multiple_concurrent_operations_after_start(self, browser_session):
"""Test that multiple operations can run concurrently after start() completes."""
# logger.info('Testing multiple concurrent operations after start')
# Start the session
await browser_session.start()
# Run multiple operations concurrently that require initialization
async def get_tabs():
return await browser_session.get_tabs_info()
async def get_current_page():
return await browser_session.get_current_page()
async def take_screenshot():
return await browser_session.take_screenshot()
# All operations should succeed concurrently
results = await asyncio.gather(get_tabs(), get_current_page(), take_screenshot(), return_exceptions=True)
# Check that all operations completed successfully
assert len(results) == 3
assert all(not isinstance(r, Exception) for r in results)
async def test_start_with_keep_alive_profile(self):
"""Test start/stop behavior with keep_alive=True profile."""
# logger.info('Testing start with keep_alive profile')
profile = BrowserProfile(headless=True, user_data_dir=None, keep_alive=True)
session = BrowserSession(browser_profile=profile)
try:
await session.start()
assert session.initialized is True
# Stop should not actually close the browser with keep_alive=True
await session.stop()
# initialized flag should still be False after stop()
assert session.initialized is False
finally:
# Force cleanup for test
session.browser_profile.keep_alive = False
await session.stop()
async def test_require_initialization_decorator_already_started(self, browser_session):
"""Test @require_initialization decorator when session is already started."""
# logger.info('Testing @require_initialization decorator with already started session')
# Start the session first
await browser_session.start()
assert browser_session.initialized is True
assert browser_session.browser_context is not None
# Track if start() gets called again by monitoring the lock acquisition
original_start_lock = browser_session._start_lock
lock_acquire_count = 0
class CountingLock:
def __init__(self, original_lock):
self._original_lock = original_lock
async def __aenter__(self):
nonlocal lock_acquire_count
lock_acquire_count += 1
return await self._original_lock.__aenter__()
async def __aexit__(self, exc_type, exc_val, exc_tb):
return await self._original_lock.__aexit__(exc_type, exc_val, exc_tb)
browser_session._start_lock = CountingLock(original_start_lock)
# Call a method decorated with @require_initialization
# This should work without calling start() again
tabs_info = await browser_session.get_tabs_info()
# Verify the method worked and start() wasn't called again (lock not acquired)
assert isinstance(tabs_info, list)
assert lock_acquire_count == 0 # start() should not have been called
assert browser_session.initialized is True
async def test_require_initialization_decorator_not_started(self, browser_session):
"""Test @require_initialization decorator when session is not started."""
# logger.info('Testing @require_initialization decorator with unstarted session')
# Ensure session is not started
assert browser_session.initialized is False
assert browser_session.browser_context is None
# Track calls to start() method
original_start = browser_session.start
start_call_count = 0
async def counting_start():
nonlocal start_call_count
start_call_count += 1
return await original_start()
browser_session.start = counting_start
# Call a method that requires initialization
tabs_info = await browser_session.get_tabs_info()
# Verify the decorator called start() and the session is now initialized
assert start_call_count == 1 # start() should have been called once
assert browser_session.initialized is True
assert browser_session.browser_context is not None
assert isinstance(tabs_info, list) # Should return valid tabs info
async def test_require_initialization_decorator_with_closed_page(self, browser_session):
"""Test @require_initialization decorator handles closed pages correctly."""
# logger.info('Testing @require_initialization decorator with closed page')
# Start the session and get a page
await browser_session.start()
current_page = await browser_session.get_current_page()
assert current_page is not None
assert not current_page.is_closed()
# Close the current page
await current_page.close()
# Call a method decorated with @require_initialization
# This should create a new tab since the current page is closed
tabs_info = await browser_session.get_tabs_info()
# Verify a new page was created
assert isinstance(tabs_info, list)
new_current_page = await browser_session.get_current_page()
assert new_current_page is not None
assert not new_current_page.is_closed()
assert new_current_page != current_page # Should be a different page