File size: 19,193 Bytes
d7b3d84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
"""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()