mirror of
https://github.com/browser-use/browser-use
synced 2026-05-06 17:52:15 +02:00
326 lines
11 KiB
Python
326 lines
11 KiB
Python
"""
|
|
Tests for browser session file upload functionality using real HTML.
|
|
|
|
Tests cover common real-world file upload patterns:
|
|
- Standard form file uploads
|
|
- Hidden file inputs triggered by buttons
|
|
- Multiple file upload fields
|
|
- Complex nested structures
|
|
"""
|
|
|
|
import pytest
|
|
from pytest_httpserver import HTTPServer
|
|
|
|
from browser_use.browser.profile import BrowserProfile
|
|
from browser_use.browser.session import BrowserSession
|
|
|
|
|
|
class TestBrowserSessionFileUploads:
|
|
"""Tests for file upload element finding functionality with real HTML."""
|
|
|
|
@pytest.fixture
|
|
async def browser_session(self):
|
|
"""Create a BrowserSession instance for testing."""
|
|
session = BrowserSession(browser_profile=BrowserProfile(headless=True, user_data_dir=None, keep_alive=True))
|
|
yield session
|
|
await session.kill()
|
|
|
|
@pytest.fixture
|
|
def test_server(self, httpserver: HTTPServer):
|
|
"""Set up test HTTP server with various file upload scenarios."""
|
|
return httpserver
|
|
|
|
async def test_standard_file_upload_patterns(self, browser_session: BrowserSession, test_server: HTTPServer):
|
|
"""Test finding file inputs in standard form layouts and modern UI patterns."""
|
|
html = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body>
|
|
<h1>File Upload Test Page</h1>
|
|
|
|
<!-- Pattern 1: Simple visible file input in form -->
|
|
<form id="simple-form">
|
|
<label for="file1">Choose file:</label>
|
|
<input type="file" id="file1" name="document">
|
|
<button type="submit">Upload</button>
|
|
</form>
|
|
|
|
<!-- Pattern 2: Hidden file input with custom button (Material/Bootstrap style) -->
|
|
<div class="upload-section">
|
|
<button class="btn btn-primary" onclick="document.getElementById('hidden-file').click()">
|
|
Select File
|
|
</button>
|
|
<input type="file" id="hidden-file" style="display: none;" accept=".pdf,.doc,.docx">
|
|
<span class="filename">No file selected</span>
|
|
</div>
|
|
|
|
<!-- Pattern 3: Nested file input in card/modal structure -->
|
|
<div class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>Upload Document</h3>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label>Select your file:</label>
|
|
<div class="input-wrapper">
|
|
<input type="file" id="modal-file" class="form-control">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pattern 4: Multiple file inputs -->
|
|
<form id="multi-upload">
|
|
<div class="field-group">
|
|
<label>Profile Photo</label>
|
|
<input type="file" name="photo" accept="image/*">
|
|
</div>
|
|
<div class="field-group">
|
|
<label>Resume</label>
|
|
<input type="file" name="resume" accept=".pdf">
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Pattern 5: Dropzone-style upload area -->
|
|
<div class="dropzone" onclick="this.querySelector('input').click()">
|
|
<div class="dz-message">
|
|
<i class="icon-upload"></i>
|
|
<p>Drop files here or click to upload</p>
|
|
</div>
|
|
<input type="file" multiple style="display: none;">
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
test_server.expect_request('/upload').respond_with_data(html, content_type='text/html')
|
|
await browser_session.start()
|
|
page = await browser_session.get_current_page()
|
|
await page.goto(test_server.url_for('/upload'))
|
|
|
|
# Wait for page to load
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
# Get browser state to populate selector map
|
|
await browser_session.get_state_summary(cache_clickable_elements_hashes=False)
|
|
|
|
# Get the selector map after page load
|
|
selector_map = await browser_session.get_selector_map()
|
|
|
|
# The selector map contains clickable elements, so let's test by looking for elements directly
|
|
# Test 1: Find file input when it's the clicked element itself
|
|
all_file_inputs = []
|
|
for idx, elem in selector_map.items():
|
|
if elem.tag_name == 'input' and elem.attributes.get('type') == 'file':
|
|
all_file_inputs.append((idx, elem))
|
|
|
|
# Should find at least one file input in the map
|
|
assert len(all_file_inputs) > 0, f'No file inputs found in selector map. Map has {len(selector_map)} elements'
|
|
|
|
# Test finding the first file input
|
|
first_file_idx, first_file_elem = all_file_inputs[0]
|
|
file_input = await browser_session.find_file_upload_element_by_index(first_file_idx)
|
|
assert file_input is not None
|
|
assert file_input == first_file_elem
|
|
|
|
# Test 2: Find hidden file input from button - look for any button
|
|
button_indices = []
|
|
for idx, elem in selector_map.items():
|
|
if elem.tag_name == 'button':
|
|
button_indices.append(idx)
|
|
|
|
# Try finding file input from each button
|
|
found_from_button = False
|
|
for button_idx in button_indices:
|
|
file_input = await browser_session.find_file_upload_element_by_index(button_idx)
|
|
if file_input is not None:
|
|
found_from_button = True
|
|
break
|
|
|
|
assert found_from_button, 'Could not find any file input from any button'
|
|
|
|
# Test 3: Find file input from parent containers
|
|
div_indices = []
|
|
for idx, elem in selector_map.items():
|
|
if elem.tag_name == 'div':
|
|
div_indices.append(idx)
|
|
|
|
# Try finding file input from divs
|
|
found_from_div = False
|
|
for div_idx in div_indices[:10]: # Test first 10 divs
|
|
file_input = await browser_session.find_file_upload_element_by_index(div_idx)
|
|
if file_input is not None:
|
|
found_from_div = True
|
|
break
|
|
|
|
assert found_from_div, 'Could not find any file input from any div container'
|
|
|
|
async def test_dom_traversal_functionality(self, browser_session: BrowserSession, test_server: HTTPServer):
|
|
"""Test that the file upload finder correctly traverses the DOM."""
|
|
html = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body>
|
|
<!-- Test case 1: Deep nesting -->
|
|
<div class="container">
|
|
<button class="deep-button">Upload from Deep</button>
|
|
<div>
|
|
<div>
|
|
<div>
|
|
<input type="file" id="deep-file">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test case 2: Sibling traversal -->
|
|
<div class="upload-group">
|
|
<div class="left-section">
|
|
<button class="sibling-button">Choose File</button>
|
|
</div>
|
|
<div class="right-section">
|
|
<input type="file" id="sibling-file">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test case 3: Parent traversal -->
|
|
<div class="outer-container">
|
|
<input type="file" id="parent-file" style="display: none;">
|
|
<div class="inner-container">
|
|
<button class="parent-button">Select</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test case 4: Mixed case HTML -->
|
|
<FORM>
|
|
<INPUT TYPE="FILE" ID="mixed-case-file">
|
|
<BUTTON TYPE="button" CLASS="mixed-button">Upload</BUTTON>
|
|
</FORM>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
test_server.expect_request('/traversal').respond_with_data(html, content_type='text/html')
|
|
await browser_session.start()
|
|
page = await browser_session.get_current_page()
|
|
await page.goto(test_server.url_for('/traversal'))
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
# Get browser state to populate selector map
|
|
await browser_session.get_state_summary(cache_clickable_elements_hashes=False)
|
|
|
|
selector_map = await browser_session.get_selector_map()
|
|
|
|
# Test that buttons can find nearby file inputs
|
|
button_count = 0
|
|
buttons_that_found_file = 0
|
|
|
|
for idx, elem in selector_map.items():
|
|
if elem.tag_name == 'button':
|
|
button_count += 1
|
|
file_input = await browser_session.find_file_upload_element_by_index(idx)
|
|
if file_input is not None:
|
|
buttons_that_found_file += 1
|
|
# Verify it's actually a file input
|
|
assert file_input.tag_name == 'input'
|
|
assert file_input.attributes.get('type', '').lower() == 'file'
|
|
|
|
# Most buttons should be able to find a file input through DOM traversal
|
|
assert button_count > 0, 'No buttons found in selector map'
|
|
assert buttons_that_found_file > 0, 'No buttons could find file inputs'
|
|
assert buttons_that_found_file >= button_count - 1, 'Most buttons should find file inputs'
|
|
|
|
async def test_traversal_limits(self, browser_session: BrowserSession, test_server: HTTPServer):
|
|
"""Test that traversal limits (max_height and max_descendant_depth) work correctly."""
|
|
html = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body>
|
|
<!-- Deep nesting to test limits -->
|
|
<div class="wrapper">
|
|
<button class="test-button">Test Button</button>
|
|
<div>
|
|
<div>
|
|
<div>
|
|
<div>
|
|
<div>
|
|
<input type="file" id="deeply-nested">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Direct sibling -->
|
|
<div class="direct-wrapper">
|
|
<button class="direct-button">Direct Button</button>
|
|
<input type="file" id="direct-file">
|
|
</div>
|
|
|
|
<!-- Test with element that is itself a file input -->
|
|
<input type="file" id="self-file" class="clickable">
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
test_server.expect_request('/limits').respond_with_data(html, content_type='text/html')
|
|
await browser_session.start()
|
|
page = await browser_session.get_current_page()
|
|
await page.goto(test_server.url_for('/limits'))
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
# Get browser state to populate selector map
|
|
await browser_session.get_state_summary(cache_clickable_elements_hashes=False)
|
|
|
|
selector_map = await browser_session.get_selector_map()
|
|
|
|
# Test 1: Button with deep nesting
|
|
test_button_idx = None
|
|
for idx, elem in selector_map.items():
|
|
if elem.tag_name == 'button' and elem.attributes.get('class') == 'test-button':
|
|
test_button_idx = idx
|
|
break
|
|
|
|
if test_button_idx is not None:
|
|
# With default limits, should find the deeply nested file
|
|
file_input = await browser_session.find_file_upload_element_by_index(test_button_idx)
|
|
assert file_input is not None
|
|
|
|
# With very limited traversal, might not find it
|
|
file_input_limited = await browser_session.find_file_upload_element_by_index(
|
|
test_button_idx, max_height=0, max_descendant_depth=1
|
|
)
|
|
# Could be None if file is too deep
|
|
|
|
# Test 2: Direct sibling should be found easily
|
|
direct_button_idx = None
|
|
for idx, elem in selector_map.items():
|
|
if elem.tag_name == 'button' and elem.attributes.get('class') == 'direct-button':
|
|
direct_button_idx = idx
|
|
break
|
|
|
|
if direct_button_idx is not None:
|
|
file_input = await browser_session.find_file_upload_element_by_index(direct_button_idx, max_height=1)
|
|
assert file_input is not None
|
|
assert file_input.attributes.get('id') == 'direct-file'
|
|
|
|
# Test 3: Element that is itself a file input
|
|
self_file_idx = None
|
|
for idx, elem in selector_map.items():
|
|
if elem.tag_name == 'input' and elem.attributes.get('id') == 'self-file':
|
|
self_file_idx = idx
|
|
break
|
|
|
|
if self_file_idx is not None:
|
|
file_input = await browser_session.find_file_upload_element_by_index(self_file_idx)
|
|
assert file_input is not None
|
|
assert file_input.attributes.get('id') == 'self-file'
|
|
|
|
# Test 4: Invalid index returns None
|
|
invalid_index = max(selector_map.keys()) + 100 if selector_map else 999
|
|
file_input = await browser_session.find_file_upload_element_by_index(invalid_index)
|
|
assert file_input is None
|