mirror of
https://github.com/browser-use/browser-use
synced 2026-05-06 17:52:15 +02:00
518 lines
19 KiB
Python
518 lines
19 KiB
Python
"""Test GetDropdownOptionsEvent and SelectDropdownOptionEvent functionality.
|
|
|
|
This file consolidates all tests related to dropdown functionality including:
|
|
- Native <select> dropdowns
|
|
- ARIA role="menu" dropdowns
|
|
- Custom dropdown implementations
|
|
"""
|
|
|
|
import pytest
|
|
from pytest_httpserver import HTTPServer
|
|
|
|
from browser_use.agent.views import ActionResult
|
|
from browser_use.browser import BrowserSession
|
|
from browser_use.browser.events import GetDropdownOptionsEvent, NavigationCompleteEvent, SelectDropdownOptionEvent
|
|
from browser_use.browser.profile import BrowserProfile
|
|
from browser_use.tools.service import Tools
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def http_server():
|
|
"""Create and provide a test HTTP server that serves static content."""
|
|
server = HTTPServer()
|
|
server.start()
|
|
|
|
# Add route for native dropdown test page
|
|
server.expect_request('/native-dropdown').respond_with_data(
|
|
"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Native Dropdown Test</title>
|
|
</head>
|
|
<body>
|
|
<h1>Native Dropdown Test</h1>
|
|
<select id="test-dropdown" name="test-dropdown">
|
|
<option value="">Please select</option>
|
|
<option value="option1">First Option</option>
|
|
<option value="option2">Second Option</option>
|
|
<option value="option3">Third Option</option>
|
|
</select>
|
|
<div id="result">No selection made</div>
|
|
<script>
|
|
document.getElementById('test-dropdown').addEventListener('change', function(e) {
|
|
document.getElementById('result').textContent = 'Selected: ' + e.target.options[e.target.selectedIndex].text;
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
""",
|
|
content_type='text/html',
|
|
)
|
|
|
|
# Add route for ARIA menu test page
|
|
server.expect_request('/aria-menu').respond_with_data(
|
|
"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>ARIA Menu Test</title>
|
|
<style>
|
|
.menu {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
border: 1px solid #ccc;
|
|
background: white;
|
|
width: 200px;
|
|
}
|
|
.menu-item {
|
|
padding: 10px 20px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
.menu-item:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
.menu-item-anchor {
|
|
text-decoration: none;
|
|
color: #333;
|
|
display: block;
|
|
}
|
|
#result {
|
|
margin-top: 20px;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
min-height: 20px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>ARIA Menu Test</h1>
|
|
<p>This menu uses ARIA roles instead of native select elements</p>
|
|
|
|
<ul class="menu menu-format-standard menu-regular" role="menu" id="pyNavigation1752753375773" style="display: block;">
|
|
<li class="menu-item menu-item-enabled" role="presentation">
|
|
<a href="#" onclick="pd(event);" class="menu-item-anchor" tabindex="0" role="menuitem">
|
|
<span class="menu-item-title-wrap"><span class="menu-item-title">Filter</span></span>
|
|
</a>
|
|
</li>
|
|
<li class="menu-item menu-item-enabled" role="presentation" id="menu-item-$PpyNavigation1752753375773$ppyElements$l2">
|
|
<a href="#" onclick="pd(event);" class="menu-item-anchor menu-item-expand" tabindex="0" role="menuitem" aria-haspopup="true">
|
|
<span class="menu-item-title-wrap"><span class="menu-item-title">Sort</span></span>
|
|
</a>
|
|
<div class="menu-panel-wrapper">
|
|
<ul class="menu menu-format-standard menu-regular" role="menu" id="$PpyNavigation1752753375773$ppyElements$l2">
|
|
<li class="menu-item menu-item-enabled" role="presentation">
|
|
<a href="#" onclick="pd(event);" class="menu-item-anchor" tabindex="0" role="menuitem">
|
|
<span class="menu-item-title-wrap"><span class="menu-item-title">Lowest to highest</span></span>
|
|
</a>
|
|
</li>
|
|
<li class="menu-item menu-item-enabled" role="presentation">
|
|
<a href="#" onclick="pd(event);" class="menu-item-anchor" tabindex="0" role="menuitem">
|
|
<span class="menu-item-title-wrap"><span class="menu-item-title">Highest to lowest</span></span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</li>
|
|
<li class="menu-item menu-item-enabled" role="presentation">
|
|
<a href="#" onclick="pd(event);" class="menu-item-anchor" tabindex="0" role="menuitem">
|
|
<span class="menu-item-title-wrap"><span class="menu-item-title">Appearance</span></span>
|
|
</a>
|
|
</li>
|
|
<li class="menu-item menu-item-enabled" role="presentation">
|
|
<a href="#" onclick="pd(event);" class="menu-item-anchor" tabindex="0" role="menuitem">
|
|
<span class="menu-item-title-wrap"><span class="menu-item-title">Summarize</span></span>
|
|
</a>
|
|
</li>
|
|
<li class="menu-item menu-item-enabled" role="presentation">
|
|
<a href="#" onclick="pd(event);" class="menu-item-anchor" tabindex="0" role="menuitem">
|
|
<span class="menu-item-title-wrap"><span class="menu-item-title">Delete</span></span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div id="result">Click an option to see the result</div>
|
|
|
|
<script>
|
|
// Mock the pd function that prevents default
|
|
function pd(event) {
|
|
event.preventDefault();
|
|
const text = event.target.closest('[role="menuitem"]').textContent.trim();
|
|
document.getElementById('result').textContent = 'Clicked: ' + text;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
""",
|
|
content_type='text/html',
|
|
)
|
|
|
|
# Add route for custom dropdown test page
|
|
server.expect_request('/custom-dropdown').respond_with_data(
|
|
"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Custom Dropdown Test</title>
|
|
<style>
|
|
.dropdown {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 200px;
|
|
}
|
|
.dropdown-button {
|
|
padding: 10px;
|
|
border: 1px solid #ccc;
|
|
background: white;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
}
|
|
.dropdown-menu {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
border: 1px solid #ccc;
|
|
background: white;
|
|
display: block;
|
|
z-index: 1000;
|
|
}
|
|
.dropdown-menu.hidden {
|
|
display: none;
|
|
}
|
|
.dropdown .item {
|
|
padding: 10px;
|
|
cursor: pointer;
|
|
}
|
|
.dropdown .item:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
.dropdown .item.selected {
|
|
background: #e0e0e0;
|
|
}
|
|
#result {
|
|
margin-top: 20px;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Custom Dropdown Test</h1>
|
|
<p>This is a custom dropdown implementation (like Semantic UI)</p>
|
|
|
|
<div class="dropdown ui" id="custom-dropdown">
|
|
<div class="dropdown-button" onclick="toggleDropdown()">
|
|
<span id="selected-text">Choose an option</span>
|
|
</div>
|
|
<div class="dropdown-menu" id="dropdown-menu">
|
|
<div class="item" data-value="red" onclick="selectOption('Red', 'red')">Red</div>
|
|
<div class="item" data-value="green" onclick="selectOption('Green', 'green')">Green</div>
|
|
<div class="item" data-value="blue" onclick="selectOption('Blue', 'blue')">Blue</div>
|
|
<div class="item" data-value="yellow" onclick="selectOption('Yellow', 'yellow')">Yellow</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="result">No selection made</div>
|
|
|
|
<script>
|
|
function toggleDropdown() {
|
|
const menu = document.getElementById('dropdown-menu');
|
|
menu.classList.toggle('hidden');
|
|
}
|
|
|
|
function selectOption(text, value) {
|
|
document.getElementById('selected-text').textContent = text;
|
|
document.getElementById('result').textContent = 'Selected: ' + text + ' (value: ' + value + ')';
|
|
// Mark as selected
|
|
document.querySelectorAll('.item').forEach(item => item.classList.remove('selected'));
|
|
event.target.classList.add('selected');
|
|
// Close dropdown
|
|
document.getElementById('dropdown-menu').classList.add('hidden');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
""",
|
|
content_type='text/html',
|
|
)
|
|
|
|
yield server
|
|
server.stop()
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def base_url(http_server):
|
|
"""Return the base URL for the test HTTP server."""
|
|
return f'http://{http_server.host}:{http_server.port}'
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
async def browser_session():
|
|
"""Create and provide a Browser instance with security disabled."""
|
|
browser_session = BrowserSession(
|
|
browser_profile=BrowserProfile(
|
|
headless=True,
|
|
user_data_dir=None,
|
|
keep_alive=True,
|
|
chromium_sandbox=False, # Disable sandbox for CI environment
|
|
)
|
|
)
|
|
await browser_session.start()
|
|
yield browser_session
|
|
await browser_session.kill()
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def tools():
|
|
"""Create and provide a Tools instance."""
|
|
return Tools()
|
|
|
|
|
|
class TestGetDropdownOptionsEvent:
|
|
"""Test GetDropdownOptionsEvent functionality for various dropdown types."""
|
|
|
|
@pytest.mark.skip(reason='Dropdown text assertion issue - test expects specific text format')
|
|
async def test_native_select_dropdown(self, tools, browser_session: BrowserSession, base_url):
|
|
"""Test get_dropdown_options with native HTML select element."""
|
|
# Navigate to the native dropdown test page
|
|
await tools.navigate(url=f'{base_url}/native-dropdown', new_tab=False, browser_session=browser_session)
|
|
|
|
# Initialize the DOM state to populate the selector map
|
|
await browser_session.get_browser_state_summary()
|
|
|
|
# Find the select element by ID
|
|
dropdown_index = await browser_session.get_index_by_id('test-dropdown')
|
|
|
|
assert dropdown_index is not None, 'Could not find select element'
|
|
|
|
# Test via tools action
|
|
result = await tools.dropdown_options(index=dropdown_index, browser_session=browser_session)
|
|
|
|
# Verify the result
|
|
assert isinstance(result, ActionResult)
|
|
assert result.extracted_content is not None
|
|
|
|
# Verify all expected options are present
|
|
expected_options = ['Please select', 'First Option', 'Second Option', 'Third Option']
|
|
for option in expected_options:
|
|
assert option in result.extracted_content, f"Option '{option}' not found in result content"
|
|
|
|
# Verify instruction is included
|
|
assert 'Use the exact text string' in result.extracted_content and 'select_dropdown' in result.extracted_content
|
|
|
|
# Also test direct event dispatch
|
|
node = await browser_session.get_element_by_index(dropdown_index)
|
|
assert node is not None
|
|
event = browser_session.event_bus.dispatch(GetDropdownOptionsEvent(node=node))
|
|
dropdown_data = await event.event_result(timeout=3.0)
|
|
|
|
assert dropdown_data is not None
|
|
assert 'options' in dropdown_data
|
|
assert 'type' in dropdown_data
|
|
assert dropdown_data['type'] == 'select'
|
|
|
|
@pytest.mark.skip(reason='ARIA menu detection issue - element not found in selector map')
|
|
async def test_aria_menu_dropdown(self, tools, browser_session: BrowserSession, base_url):
|
|
"""Test get_dropdown_options with ARIA role='menu' element."""
|
|
# Navigate to the ARIA menu test page
|
|
await tools.navigate(url=f'{base_url}/aria-menu', new_tab=False, browser_session=browser_session)
|
|
|
|
# Initialize the DOM state
|
|
await browser_session.get_browser_state_summary()
|
|
|
|
# Find the ARIA menu by ID
|
|
menu_index = await browser_session.get_index_by_id('pyNavigation1752753375773')
|
|
|
|
assert menu_index is not None, 'Could not find ARIA menu element'
|
|
|
|
# Test via tools action
|
|
result = await tools.dropdown_options(index=menu_index, browser_session=browser_session)
|
|
|
|
# Verify the result
|
|
assert isinstance(result, ActionResult)
|
|
assert result.extracted_content is not None
|
|
|
|
# Verify expected ARIA menu options are present
|
|
expected_options = ['Filter', 'Sort', 'Appearance', 'Summarize', 'Delete']
|
|
for option in expected_options:
|
|
assert option in result.extracted_content, f"Option '{option}' not found in result content"
|
|
|
|
# Also test direct event dispatch
|
|
node = await browser_session.get_element_by_index(menu_index)
|
|
assert node is not None
|
|
event = browser_session.event_bus.dispatch(GetDropdownOptionsEvent(node=node))
|
|
dropdown_data = await event.event_result(timeout=3.0)
|
|
|
|
assert dropdown_data is not None
|
|
assert 'options' in dropdown_data
|
|
assert 'type' in dropdown_data
|
|
assert dropdown_data['type'] == 'aria'
|
|
|
|
@pytest.mark.skip(reason='Custom dropdown detection issue - element not found in selector map')
|
|
async def test_custom_dropdown(self, tools, browser_session: BrowserSession, base_url):
|
|
"""Test get_dropdown_options with custom dropdown implementation."""
|
|
# Navigate to the custom dropdown test page
|
|
await tools.navigate(url=f'{base_url}/custom-dropdown', new_tab=False, browser_session=browser_session)
|
|
|
|
# Initialize the DOM state
|
|
await browser_session.get_browser_state_summary()
|
|
|
|
# Find the custom dropdown by ID
|
|
dropdown_index = await browser_session.get_index_by_id('custom-dropdown')
|
|
|
|
assert dropdown_index is not None, 'Could not find custom dropdown element'
|
|
|
|
# Test via tools action
|
|
result = await tools.dropdown_options(index=dropdown_index, browser_session=browser_session)
|
|
|
|
# Verify the result
|
|
assert isinstance(result, ActionResult)
|
|
assert result.extracted_content is not None
|
|
|
|
# Verify expected custom dropdown options are present
|
|
expected_options = ['Red', 'Green', 'Blue', 'Yellow']
|
|
for option in expected_options:
|
|
assert option in result.extracted_content, f"Option '{option}' not found in result content"
|
|
|
|
# Also test direct event dispatch
|
|
node = await browser_session.get_element_by_index(dropdown_index)
|
|
assert node is not None
|
|
event = browser_session.event_bus.dispatch(GetDropdownOptionsEvent(node=node))
|
|
dropdown_data = await event.event_result(timeout=3.0)
|
|
|
|
assert dropdown_data is not None
|
|
assert 'options' in dropdown_data
|
|
assert 'type' in dropdown_data
|
|
assert dropdown_data['type'] == 'custom'
|
|
|
|
|
|
class TestSelectDropdownOptionEvent:
|
|
"""Test SelectDropdownOptionEvent functionality for various dropdown types."""
|
|
|
|
@pytest.mark.skip(reason='Timeout issue - test takes too long to complete')
|
|
async def test_select_native_dropdown_option(self, tools, browser_session: BrowserSession, base_url):
|
|
"""Test select_dropdown_option with native HTML select element."""
|
|
# Navigate to the native dropdown test page
|
|
await tools.navigate(url=f'{base_url}/native-dropdown', new_tab=False, browser_session=browser_session)
|
|
await browser_session.event_bus.expect(NavigationCompleteEvent, timeout=10.0)
|
|
|
|
# Initialize the DOM state
|
|
await browser_session.get_browser_state_summary()
|
|
|
|
# Find the select element by ID
|
|
dropdown_index = await browser_session.get_index_by_id('test-dropdown')
|
|
|
|
assert dropdown_index is not None
|
|
|
|
# Test via tools action
|
|
result = await tools.select_dropdown(index=dropdown_index, text='Second Option', browser_session=browser_session)
|
|
|
|
# Verify the result
|
|
assert isinstance(result, ActionResult)
|
|
assert result.extracted_content is not None
|
|
assert 'Second Option' in result.extracted_content
|
|
|
|
# Verify the selection actually worked using CDP
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
result = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('test-dropdown').selectedIndex", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
selected_index = result.get('result', {}).get('value', -1)
|
|
assert selected_index == 2, f'Expected selected index 2, got {selected_index}'
|
|
|
|
@pytest.mark.skip(reason='Timeout issue - test takes too long to complete')
|
|
async def test_select_aria_menu_option(self, tools, browser_session: BrowserSession, base_url):
|
|
"""Test select_dropdown_option with ARIA menu."""
|
|
# Navigate to the ARIA menu test page
|
|
await tools.navigate(url=f'{base_url}/aria-menu', new_tab=False, browser_session=browser_session)
|
|
await browser_session.event_bus.expect(NavigationCompleteEvent, timeout=10.0)
|
|
|
|
# Initialize the DOM state
|
|
await browser_session.get_browser_state_summary()
|
|
|
|
# Find the ARIA menu by ID
|
|
menu_index = await browser_session.get_index_by_id('pyNavigation1752753375773')
|
|
|
|
assert menu_index is not None
|
|
|
|
# Test via tools action
|
|
result = await tools.select_dropdown(index=menu_index, text='Filter', browser_session=browser_session)
|
|
|
|
# Verify the result
|
|
assert isinstance(result, ActionResult)
|
|
assert result.extracted_content is not None
|
|
assert 'Filter' in result.extracted_content
|
|
|
|
# Verify the click had an effect using CDP
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
result = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('result').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
result_text = result.get('result', {}).get('value', '')
|
|
assert 'Filter' in result_text, f"Expected 'Filter' in result text, got '{result_text}'"
|
|
|
|
@pytest.mark.skip(reason='Timeout issue - test takes too long to complete')
|
|
async def test_select_custom_dropdown_option(self, tools, browser_session: BrowserSession, base_url):
|
|
"""Test select_dropdown_option with custom dropdown."""
|
|
# Navigate to the custom dropdown test page
|
|
await tools.navigate(url=f'{base_url}/custom-dropdown', new_tab=False, browser_session=browser_session)
|
|
await browser_session.event_bus.expect(NavigationCompleteEvent, timeout=10.0)
|
|
|
|
# Initialize the DOM state
|
|
await browser_session.get_browser_state_summary()
|
|
|
|
# Find the custom dropdown by ID
|
|
dropdown_index = await browser_session.get_index_by_id('custom-dropdown')
|
|
|
|
assert dropdown_index is not None
|
|
|
|
# Test via tools action
|
|
result = await tools.select_dropdown(index=dropdown_index, text='Blue', browser_session=browser_session)
|
|
|
|
# Verify the result
|
|
assert isinstance(result, ActionResult)
|
|
assert result.extracted_content is not None
|
|
assert 'Blue' in result.extracted_content
|
|
|
|
# Verify the selection worked using CDP
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
result = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('result').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
result_text = result.get('result', {}).get('value', '')
|
|
assert 'Blue' in result_text, f"Expected 'Blue' in result text, got '{result_text}'"
|
|
|
|
@pytest.mark.skip(reason='Timeout issue - test takes too long to complete')
|
|
async def test_select_invalid_option_error(self, tools, browser_session: BrowserSession, base_url):
|
|
"""Test select_dropdown_option with non-existent option text."""
|
|
# Navigate to the native dropdown test page
|
|
await tools.navigate(url=f'{base_url}/native-dropdown', new_tab=False, browser_session=browser_session)
|
|
await browser_session.event_bus.expect(NavigationCompleteEvent, timeout=10.0)
|
|
|
|
# Initialize the DOM state
|
|
await browser_session.get_browser_state_summary()
|
|
|
|
# Find the select element by ID
|
|
dropdown_index = await browser_session.get_index_by_id('test-dropdown')
|
|
|
|
assert dropdown_index is not None
|
|
|
|
# Try to select non-existent option via direct event
|
|
node = await browser_session.get_element_by_index(dropdown_index)
|
|
assert node is not None
|
|
event = browser_session.event_bus.dispatch(SelectDropdownOptionEvent(node=node, text='Non-existent Option'))
|
|
|
|
try:
|
|
selection_data = await event.event_result(timeout=3.0)
|
|
# Should have an error in the result
|
|
assert selection_data is not None
|
|
assert 'error' in selection_data or 'not found' in str(selection_data).lower()
|
|
except Exception as e:
|
|
# Or raise an exception
|
|
assert 'not found' in str(e).lower() or 'no option' in str(e).lower()
|