mirror of
https://github.com/browser-use/browser-use
synced 2026-05-13 17:56:35 +02:00
449 lines
16 KiB
Python
449 lines
16 KiB
Python
"""
|
|
Test browser session recent events tracking functionality.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import time
|
|
|
|
import pytest
|
|
from pytest_httpserver import HTTPServer
|
|
from werkzeug.wrappers import Response
|
|
|
|
from browser_use.browser import BrowserProfile, BrowserSession
|
|
|
|
|
|
class TestBrowserRecentEvents:
|
|
"""Test recent events tracking functionality"""
|
|
|
|
async def test_recent_events_on_network_timeout(self, httpserver: HTTPServer):
|
|
"""Test that recent events captures network timeout information"""
|
|
# Create a page with multiple resources that never finish loading
|
|
html_content = """
|
|
<html>
|
|
<head>
|
|
<title>Slow Loading Test Page</title>
|
|
<script src="/slow-script.js"></script>
|
|
<link rel="stylesheet" href="/slow-style.css">
|
|
</head>
|
|
<body>
|
|
<h1>Testing Loading Status</h1>
|
|
<p>This page has resources that never finish loading.</p>
|
|
<img src="/slow-image.jpg" alt="Slow loading image">
|
|
<iframe src="/slow-iframe.html" width="400" height="300"></iframe>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Main page loads immediately
|
|
httpserver.expect_request('/').respond_with_data(html_content, content_type='text/html')
|
|
|
|
# Handler that sleeps longer than the timeout
|
|
def slow_handler(request):
|
|
# Sleep for 5 seconds - longer than the 1s maximum_wait_page_load_time
|
|
time.sleep(5)
|
|
return Response('/* Never loads in time */', content_type='text/plain')
|
|
|
|
# Set up all the slow endpoints
|
|
httpserver.expect_request('/slow-script.js').respond_with_handler(slow_handler)
|
|
httpserver.expect_request('/slow-style.css').respond_with_handler(slow_handler)
|
|
httpserver.expect_request('/slow-image.jpg').respond_with_handler(slow_handler)
|
|
httpserver.expect_request('/slow-iframe.html').respond_with_handler(slow_handler)
|
|
|
|
# Create browser session with very short timeout
|
|
browser_session = BrowserSession(
|
|
browser_profile=BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=None,
|
|
keep_alive=False,
|
|
maximum_wait_page_load_time=1.0, # 1 second max wait
|
|
wait_for_network_idle_page_load_time=0.1, # 100ms idle time
|
|
minimum_wait_page_load_time=0.1, # Don't wait extra
|
|
)
|
|
)
|
|
|
|
try:
|
|
await browser_session.start()
|
|
|
|
# Navigate to the page with the slow iframe
|
|
# Don't await to allow navigation to start in background
|
|
nav_task = asyncio.create_task(browser_session.navigate(httpserver.url_for('/')))
|
|
|
|
# Give navigation a moment to start loading resources
|
|
await asyncio.sleep(0.5)
|
|
|
|
# Get state while resources are still loading
|
|
# _wait_for_stable_network should detect pending requests and timeout
|
|
state = await browser_session.get_browser_state_with_recovery()
|
|
|
|
# Wait for navigation to complete
|
|
await nav_task
|
|
|
|
# Verify recent events contains event information
|
|
assert state.recent_events is not None, 'Recent events should be set'
|
|
|
|
# Parse JSON events
|
|
events = json.loads(state.recent_events)
|
|
assert len(events) > 0, 'Should have at least one event'
|
|
|
|
# Check event types present
|
|
event_types = [e.get('event_type') for e in events]
|
|
print(f'Event types in recent_events: {event_types}')
|
|
|
|
# Should have navigation-related events
|
|
assert 'NavigateToUrlEvent' in event_types or 'NavigationCompleteEvent' in event_types, (
|
|
'Should have navigation events in recent events'
|
|
)
|
|
|
|
# Check for any error or timeout information in events
|
|
error_indicators = []
|
|
for event in events:
|
|
# NavigationCompleteEvent with error_message
|
|
if event.get('event_type') == 'NavigationCompleteEvent' and event.get('error_message'):
|
|
error_indicators.append(event)
|
|
# BrowserErrorEvent indicating timeout
|
|
elif event.get('event_type') == 'BrowserErrorEvent' and 'timeout' in str(event.get('message', '')).lower():
|
|
error_indicators.append(event)
|
|
|
|
# Since we have slow-loading resources, we should see either:
|
|
# 1. Navigation completed normally (resources loaded in background)
|
|
# 2. Navigation reported timeout/error due to slow resources
|
|
nav_events = [e for e in events if e.get('event_type') == 'NavigationCompleteEvent']
|
|
if nav_events:
|
|
print(f'Found {len(nav_events)} NavigationCompleteEvent(s)')
|
|
# Navigation occurred, that's what matters for LLM context
|
|
assert True
|
|
else:
|
|
# No navigation complete event yet, but we should see NavigateToUrlEvent
|
|
assert 'NavigateToUrlEvent' in event_types, 'Should see navigation attempt in events'
|
|
|
|
finally:
|
|
await browser_session.kill()
|
|
|
|
async def test_recent_events_on_successful_load(self, httpserver: HTTPServer):
|
|
"""Test that recent events shows successful navigation when page loads successfully"""
|
|
# Set up a simple page that loads quickly
|
|
httpserver.expect_request('/fast').respond_with_data(
|
|
'<html><head><title>Fast Page</title></head><body><h1>Quick loading page</h1></body></html>',
|
|
content_type='text/html',
|
|
)
|
|
|
|
browser_session = BrowserSession(
|
|
browser_profile=BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=None,
|
|
keep_alive=False,
|
|
maximum_wait_page_load_time=5.0, # Generous timeout
|
|
)
|
|
)
|
|
|
|
try:
|
|
await browser_session.start()
|
|
|
|
# Navigate to the fast-loading page
|
|
await browser_session.navigate(httpserver.url_for('/fast'))
|
|
|
|
# Get browser state
|
|
state = await browser_session.get_browser_state_with_recovery()
|
|
|
|
# Recent events should show successful navigation
|
|
assert state.recent_events is not None
|
|
|
|
# Parse JSON and verify events
|
|
events = json.loads(state.recent_events)
|
|
event_types = [e.get('event_type') for e in events]
|
|
|
|
# Should have navigation events
|
|
assert 'NavigationCompleteEvent' in event_types, 'Should have NavigationCompleteEvent'
|
|
|
|
# Check the navigation was successful (no errors)
|
|
nav_events = [e for e in events if e.get('event_type') == 'NavigationCompleteEvent']
|
|
last_nav = nav_events[-1]
|
|
assert last_nav.get('error_message') is None, 'Should not have error message'
|
|
assert last_nav.get('status') == 200, 'Should have successful status'
|
|
|
|
finally:
|
|
await browser_session.kill()
|
|
|
|
async def test_recent_events_tracks_multiple_navigations(self, httpserver: HTTPServer):
|
|
"""Test that recent events properly tracks multiple navigations"""
|
|
# Set up pages
|
|
slow_html = """
|
|
<html>
|
|
<head>
|
|
<title>Slow Page</title>
|
|
<script src="/slow.js"></script>
|
|
</head>
|
|
<body><h1>Slow page</h1></body>
|
|
</html>
|
|
"""
|
|
|
|
httpserver.expect_request('/slow').respond_with_data(slow_html, content_type='text/html')
|
|
|
|
def slow_handler(req):
|
|
import time
|
|
|
|
time.sleep(5)
|
|
return Response('slow')
|
|
|
|
httpserver.expect_request('/slow.js').respond_with_handler(slow_handler)
|
|
|
|
httpserver.expect_request('/fast').respond_with_data(
|
|
'<html><head><title>Fast Page</title></head><body><h1>Fast page</h1></body></html>',
|
|
content_type='text/html',
|
|
)
|
|
|
|
browser_session = BrowserSession(
|
|
browser_profile=BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=None,
|
|
keep_alive=False,
|
|
maximum_wait_page_load_time=0.5, # Short timeout for first page
|
|
)
|
|
)
|
|
|
|
try:
|
|
await browser_session.start()
|
|
|
|
# Navigate to slow page
|
|
await browser_session.navigate(httpserver.url_for('/slow'))
|
|
state1 = await browser_session.get_browser_state_with_recovery()
|
|
assert state1.recent_events is not None
|
|
events1 = json.loads(state1.recent_events)
|
|
event_types1 = [e.get('event_type') for e in events1]
|
|
assert 'NavigationCompleteEvent' in event_types1 or 'NavigateToUrlEvent' in event_types1
|
|
|
|
# Navigate to fast page
|
|
await browser_session.navigate(httpserver.url_for('/fast'))
|
|
state2 = await browser_session.get_browser_state_with_recovery()
|
|
|
|
# Recent events should show both navigations
|
|
assert state2.recent_events is not None
|
|
events2 = json.loads(state2.recent_events)
|
|
|
|
# Count navigation events (last 10 events should include both)
|
|
nav_complete_count = sum(1 for e in events2 if e.get('event_type') == 'NavigationCompleteEvent')
|
|
nav_url_count = sum(1 for e in events2 if e.get('event_type') == 'NavigateToUrlEvent')
|
|
|
|
# Should have events from both navigations
|
|
assert nav_complete_count >= 1 or nav_url_count >= 2, 'Recent events should show multiple navigation attempts'
|
|
|
|
finally:
|
|
await browser_session.kill()
|
|
|
|
async def test_recent_events_preserved_in_minimal_state(self, httpserver: HTTPServer):
|
|
"""Test that recent events is preserved even when falling back to minimal state"""
|
|
# Create a page that causes DOM processing to fail
|
|
malformed_html = """
|
|
<html>
|
|
<head>
|
|
<title>Malformed Page</title>
|
|
<script src="/slow.js"></script>
|
|
<script>
|
|
// This might cause DOM processing issues
|
|
Object.defineProperty(document, 'querySelectorAll', {
|
|
get() { throw new Error('DOM processing blocked'); }
|
|
});
|
|
</script>
|
|
</head>
|
|
<body><h1>Page with DOM issues</h1></body>
|
|
</html>
|
|
"""
|
|
|
|
httpserver.expect_request('/malformed').respond_with_data(malformed_html, content_type='text/html')
|
|
|
|
def slow_handler(req):
|
|
import time
|
|
|
|
time.sleep(5)
|
|
return Response('slow')
|
|
|
|
httpserver.expect_request('/slow.js').respond_with_handler(slow_handler)
|
|
|
|
browser_session = BrowserSession(
|
|
browser_profile=BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=None,
|
|
keep_alive=False,
|
|
maximum_wait_page_load_time=0.5, # Short timeout
|
|
)
|
|
)
|
|
|
|
try:
|
|
await browser_session.start()
|
|
|
|
# Navigate to the malformed page
|
|
await browser_session.navigate(httpserver.url_for('/malformed'))
|
|
|
|
# Get browser state - this might fall back to minimal state
|
|
state = await browser_session.get_browser_state_with_recovery()
|
|
|
|
# Even if we get minimal state, recent events should be preserved
|
|
assert state.recent_events is not None
|
|
events = json.loads(state.recent_events)
|
|
assert len(events) > 0, 'Should have events even in minimal state'
|
|
|
|
# Should have navigation attempt
|
|
event_types = [e.get('event_type') for e in events]
|
|
assert 'NavigateToUrlEvent' in event_types or 'NavigationCompleteEvent' in event_types, (
|
|
'Should have navigation events even in minimal state'
|
|
)
|
|
|
|
finally:
|
|
await browser_session.kill()
|
|
|
|
@pytest.mark.parametrize('timeout_seconds', [0.5, 1.0, 2.0])
|
|
async def test_recent_events_with_different_timeouts(self, httpserver: HTTPServer, timeout_seconds: float):
|
|
"""Test that recent events captures navigation with different timeout configurations"""
|
|
# Set up a slow page
|
|
httpserver.expect_request(f'/timeout_{timeout_seconds}').respond_with_data(
|
|
f'<html><head><title>Timeout Test {timeout_seconds}s</title>'
|
|
f'<script src="/slow_{timeout_seconds}.js"></script></head>'
|
|
f'<body><h1>Testing {timeout_seconds}s timeout</h1></body></html>',
|
|
content_type='text/html',
|
|
)
|
|
|
|
def very_slow_handler(req):
|
|
import time
|
|
|
|
time.sleep(10)
|
|
return Response('slow')
|
|
|
|
httpserver.expect_request(f'/slow_{timeout_seconds}.js').respond_with_handler(very_slow_handler)
|
|
|
|
browser_session = BrowserSession(
|
|
browser_profile=BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=None,
|
|
keep_alive=False,
|
|
maximum_wait_page_load_time=timeout_seconds,
|
|
wait_for_network_idle_page_load_time=0.1,
|
|
minimum_wait_page_load_time=0.1,
|
|
)
|
|
)
|
|
|
|
try:
|
|
await browser_session.start()
|
|
|
|
# Navigate to the page
|
|
await browser_session.navigate(httpserver.url_for(f'/timeout_{timeout_seconds}'))
|
|
|
|
# Get browser state
|
|
state = await browser_session.get_browser_state_with_recovery()
|
|
|
|
# Verify recent events captured the navigation
|
|
assert state.recent_events is not None
|
|
events = json.loads(state.recent_events)
|
|
event_types = [e.get('event_type') for e in events]
|
|
|
|
# Should have navigation events regardless of timeout
|
|
assert 'NavigateToUrlEvent' in event_types or 'NavigationCompleteEvent' in event_types, (
|
|
f'Should have navigation events for {timeout_seconds}s timeout'
|
|
)
|
|
|
|
# If navigation completed with error, it might mention timeout
|
|
nav_complete_events = [e for e in events if e.get('event_type') == 'NavigationCompleteEvent']
|
|
if nav_complete_events and nav_complete_events[-1].get('error_message'):
|
|
print(f'Navigation error for {timeout_seconds}s timeout: {nav_complete_events[-1].get("error_message")}')
|
|
|
|
finally:
|
|
await browser_session.kill()
|
|
|
|
|
|
class TestEventHistoryInfrastructure:
|
|
"""Tests for NEW event history tracking infrastructure only."""
|
|
|
|
async def test_event_bus_history_tracking(self, httpserver: HTTPServer):
|
|
"""Test that event bus properly tracks event history."""
|
|
browser_session = BrowserSession(browser_profile=BrowserProfile(headless=True, user_data_dir=None, keep_alive=False))
|
|
|
|
try:
|
|
await browser_session.start()
|
|
initial_history_count = len(browser_session.event_bus.event_history)
|
|
|
|
# Set up test page
|
|
httpserver.expect_request('/history-test').respond_with_data(
|
|
'<html><body><h1>Event History Test</h1></body></html>',
|
|
content_type='text/html',
|
|
)
|
|
|
|
# Perform actions that generate events
|
|
await browser_session.navigate(httpserver.url_for('/history-test'))
|
|
await browser_session.take_screenshot()
|
|
|
|
# Verify event history has grown
|
|
final_history_count = len(browser_session.event_bus.event_history)
|
|
assert final_history_count > initial_history_count, 'Event history should track new events'
|
|
|
|
# Verify events are stored properly
|
|
for event_id, event in browser_session.event_bus.event_history.items():
|
|
assert event_id is not None
|
|
assert event is not None
|
|
assert hasattr(event, 'event_type') or hasattr(event, '__class__')
|
|
|
|
finally:
|
|
await browser_session.kill()
|
|
|
|
async def test_generate_recent_events_summary_format(self, httpserver: HTTPServer):
|
|
"""Test that _generate_recent_events_summary produces valid JSON."""
|
|
browser_session = BrowserSession(browser_profile=BrowserProfile(headless=True, user_data_dir=None, keep_alive=False))
|
|
|
|
try:
|
|
await browser_session.start()
|
|
|
|
# Generate some events
|
|
httpserver.expect_request('/json-test').respond_with_data(
|
|
'<html><body><h1>JSON Test</h1></body></html>',
|
|
content_type='text/html',
|
|
)
|
|
await browser_session.navigate(httpserver.url_for('/json-test'))
|
|
|
|
# Test the NEW method _generate_recent_events_summary
|
|
recent_events_json = browser_session._generate_recent_events_summary(max_events=5)
|
|
|
|
# Should return valid JSON
|
|
assert recent_events_json != '[]', 'Should have events'
|
|
events = json.loads(recent_events_json) # Should not raise JSON decode error
|
|
assert isinstance(events, list), 'Should return a list of events'
|
|
|
|
# Events should exclude problematic fields like 'state'
|
|
for event in events:
|
|
assert isinstance(event, dict), 'Each event should be a dict'
|
|
assert 'state' not in event, "Event should not contain 'state' field (circular reference)"
|
|
|
|
finally:
|
|
await browser_session.kill()
|
|
|
|
async def test_event_history_limits(self, httpserver: HTTPServer):
|
|
"""Test that event history summary respects max_events parameter."""
|
|
browser_session = BrowserSession(browser_profile=BrowserProfile(headless=True, user_data_dir=None, keep_alive=False))
|
|
|
|
try:
|
|
await browser_session.start()
|
|
|
|
# Generate multiple events
|
|
httpserver.expect_request('/limit-test').respond_with_data(
|
|
'<html><body><h1>Limit Test</h1></body></html>',
|
|
content_type='text/html',
|
|
)
|
|
|
|
# Perform multiple actions to generate events
|
|
await browser_session.navigate(httpserver.url_for('/limit-test'))
|
|
await browser_session.take_screenshot()
|
|
await browser_session.get_tabs_info()
|
|
|
|
# Test different limits
|
|
summary_3 = browser_session._generate_recent_events_summary(max_events=3)
|
|
summary_1 = browser_session._generate_recent_events_summary(max_events=1)
|
|
|
|
events_3 = json.loads(summary_3)
|
|
events_1 = json.loads(summary_1)
|
|
|
|
# Should respect the limits
|
|
assert len(events_3) <= 3, 'Should limit to 3 events'
|
|
assert len(events_1) <= 1, 'Should limit to 1 event'
|
|
assert len(events_1) <= len(events_3), 'Smaller limit should have fewer events'
|
|
|
|
finally:
|
|
await browser_session.kill()
|