mirror of
https://github.com/browser-use/browser-use.git
synced 2026-09-14 19:59:47 +08:00
721 lines
26 KiB
Python
721 lines
26 KiB
Python
import asyncio
|
|
import inspect
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from browser_use.browser.services import BrowserServiceBundle, ClickService, DropdownService, TypeService
|
|
from browser_use.filesystem.file_system import FileSystem
|
|
from browser_use.tools.service import Tools
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_browser_service_bundle_navigates_and_clicks(browser_session, httpserver) -> None:
|
|
httpserver.expect_request('/services').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<button id="reveal" onclick="document.getElementById('answer').textContent = 'services-ok'">Reveal</button>
|
|
<p id="answer">hidden</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/services'))
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
button_index = next(
|
|
idx
|
|
for idx, element in state.dom_state.selector_map.items()
|
|
if getattr(element, 'tag_name', None) == 'button' and getattr(element, 'attributes', {}).get('id') == 'reveal'
|
|
)
|
|
|
|
await services.actions.click.click_index(button_index)
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('answer').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
assert readback.get('result', {}).get('value') == 'services-ok'
|
|
|
|
|
|
def test_browser_service_bundle_exposes_lightweight_state(browser_session) -> None:
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
|
|
assert services.downloads.list_downloads() == browser_session.downloaded_files
|
|
assert services.dialogs.closed_messages() == []
|
|
assert services.actions.navigation is not services.navigation
|
|
|
|
|
|
def test_direct_action_services_do_not_define_event_bus_fallbacks() -> None:
|
|
for service in (
|
|
ClickService.click_index,
|
|
ClickService.click_coordinates,
|
|
TypeService.type_index,
|
|
DropdownService.get_options,
|
|
DropdownService.select_option,
|
|
):
|
|
assert 'event_bus.dispatch' not in inspect.getsource(service)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_browser_services_can_navigate_and_click_coordinates_without_event_dispatch(
|
|
browser_session, httpserver, monkeypatch
|
|
) -> None:
|
|
httpserver.expect_request('/direct-services').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<button id="reveal" style="margin: 40px; width: 160px; height: 60px;"
|
|
onclick="document.getElementById('answer').textContent = 'direct-services-ok'">
|
|
Reveal
|
|
</button>
|
|
<p id="answer">hidden</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/direct-services'))
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
rect_result = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={
|
|
'expression': """
|
|
const rect = document.getElementById('reveal').getBoundingClientRect();
|
|
JSON.stringify({ x: Math.floor(rect.left + rect.width / 2), y: Math.floor(rect.top + rect.height / 2) });
|
|
""",
|
|
'returnByValue': True,
|
|
},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
|
|
coords = json.loads(rect_result['result']['value'])
|
|
|
|
def fail_dispatch(*args, **kwargs):
|
|
raise AssertionError('Direct click services should not dispatch through the event bus')
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_dispatch)
|
|
|
|
await services.actions.click.click_coordinates(coords['x'], coords['y'])
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('answer').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
|
|
assert readback.get('result', {}).get('value') == 'direct-services-ok'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_browser_services_can_click_and_type_index_without_event_dispatch(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/direct-index-services').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<input id="name" value="">
|
|
<button id="reveal" onclick="document.getElementById('answer').textContent =
|
|
document.getElementById('name').value + '-done'">
|
|
Reveal
|
|
</button>
|
|
<p id="answer">hidden</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/direct-index-services'))
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
input_index = next(
|
|
idx
|
|
for idx, element in state.dom_state.selector_map.items()
|
|
if getattr(element, 'tag_name', None) == 'input' and getattr(element, 'attributes', {}).get('id') == 'name'
|
|
)
|
|
button_index = next(
|
|
idx
|
|
for idx, element in state.dom_state.selector_map.items()
|
|
if getattr(element, 'tag_name', None) == 'button' and getattr(element, 'attributes', {}).get('id') == 'reveal'
|
|
)
|
|
|
|
def fail_dispatch(*args, **kwargs):
|
|
raise AssertionError('Direct services should not dispatch through the event bus')
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_dispatch)
|
|
|
|
await services.actions.type.type_index(input_index, 'browser-use')
|
|
await services.actions.click.click_index(button_index)
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('answer').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
|
|
assert readback.get('result', {}).get('value') == 'browser-use-done'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_tools_click_and_type_use_direct_services(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/direct-tool-actions').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<input id="name" value="">
|
|
<button id="reveal" onclick="document.getElementById('answer').textContent =
|
|
document.getElementById('name').value + '-tools-done'">
|
|
Reveal
|
|
</button>
|
|
<p id="answer">hidden</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
tools = Tools()
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/direct-tool-actions'))
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
input_index = next(
|
|
idx
|
|
for idx, element in state.dom_state.selector_map.items()
|
|
if getattr(element, 'tag_name', None) == 'input' and getattr(element, 'attributes', {}).get('id') == 'name'
|
|
)
|
|
button_index = next(
|
|
idx
|
|
for idx, element in state.dom_state.selector_map.items()
|
|
if getattr(element, 'tag_name', None) == 'button' and getattr(element, 'attributes', {}).get('id') == 'reveal'
|
|
)
|
|
|
|
def fail_dispatch(*args, **kwargs):
|
|
raise AssertionError('Public click/input tools should use direct services, not event bus dispatch')
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_dispatch)
|
|
|
|
type_result = await tools.registry.execute_action(
|
|
action_name='input',
|
|
params={'index': input_index, 'text': 'browser-use'},
|
|
browser_session=browser_session,
|
|
)
|
|
click_result = await tools.registry.execute_action(
|
|
action_name='click',
|
|
params={'index': button_index},
|
|
browser_session=browser_session,
|
|
)
|
|
|
|
assert type_result.error is None
|
|
assert click_result.error is None
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('answer').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
|
|
assert readback.get('result', {}).get('value') == 'browser-use-tools-done'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_navigation_and_keyboard_tools_use_direct_services(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/direct-public-one').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<input id="keys" value="">
|
|
<p id="page">one</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
httpserver.expect_request('/direct-public-two').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<p id="page">two</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
tools = Tools()
|
|
original_dispatch = browser_session.event_bus.dispatch
|
|
forbidden_control_events = {'NavigateToUrlEvent', 'GoBackEvent', 'SendKeysEvent'}
|
|
|
|
def fail_action_dispatch(event, *args, **kwargs):
|
|
if event.__class__.__name__ in forbidden_control_events:
|
|
raise AssertionError('Public navigation/keyboard tools should use direct services, not old control-flow events')
|
|
return original_dispatch(event, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_action_dispatch)
|
|
|
|
first_url = httpserver.url_for('/direct-public-one')
|
|
second_url = httpserver.url_for('/direct-public-two')
|
|
first_result = await tools.registry.execute_action(
|
|
action_name='navigate',
|
|
params={'url': first_url},
|
|
browser_session=browser_session,
|
|
)
|
|
second_result = await tools.registry.execute_action(
|
|
action_name='navigate',
|
|
params={'url': second_url},
|
|
browser_session=browser_session,
|
|
)
|
|
back_result = await tools.registry.execute_action(
|
|
action_name='go_back',
|
|
params={},
|
|
browser_session=browser_session,
|
|
)
|
|
|
|
assert first_result.error is None
|
|
assert second_result.error is None
|
|
assert back_result.error is None
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
path_result = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': 'location.pathname', 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
assert path_result.get('result', {}).get('value') == '/direct-public-one'
|
|
|
|
await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('keys').focus()", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
keys_result = await tools.registry.execute_action(
|
|
action_name='send_keys',
|
|
params={'keys': 'abc'},
|
|
browser_session=browser_session,
|
|
)
|
|
assert keys_result.error is None
|
|
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('keys').value", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
assert readback.get('result', {}).get('value') == 'abc'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_navigation_service_clears_selector_cache_between_pages(browser_session, httpserver) -> None:
|
|
httpserver.expect_request('/cache-one').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<button id="old-action">Old Action</button>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
httpserver.expect_request('/cache-two').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<button id="new-action">New Action</button>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/cache-one'))
|
|
first_state = await services.state.get_state(include_screenshot=False)
|
|
assert any(node.attributes.get('id') == 'old-action' for node in first_state.dom_state.selector_map.values())
|
|
assert browser_session._cached_selector_map
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/cache-two'), verify_not_empty=False)
|
|
|
|
assert browser_session._cached_selector_map == {}
|
|
second_state = await services.state.get_state(include_screenshot=False)
|
|
assert not any(node.attributes.get('id') == 'old-action' for node in second_state.dom_state.selector_map.values())
|
|
assert any(node.attributes.get('id') == 'new-action' for node in second_state.dom_state.selector_map.values())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tab_service_switch_and_close_do_not_require_event_dispatch(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/tab-one').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body><p id="page">one</p></body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
httpserver.expect_request('/tab-two').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body><p id="page">two</p></body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/tab-one'))
|
|
first_target = browser_session.agent_focus_target_id
|
|
assert first_target is not None
|
|
second_target = await browser_session._cdp_create_new_page(httpserver.url_for('/tab-two'))
|
|
await browser_session.get_or_create_cdp_session(target_id=second_target, focus=False)
|
|
|
|
def fail_dispatch(*args, **kwargs):
|
|
raise AssertionError('Tab service should not require event dispatch to switch or close tabs')
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_dispatch)
|
|
|
|
switched = await services.tabs.switch(second_target)
|
|
assert switched == second_target
|
|
assert browser_session.agent_focus_target_id == second_target
|
|
|
|
await services.tabs.close(second_target)
|
|
assert browser_session.agent_focus_target_id == first_target
|
|
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
assert state.url.endswith('/tab-one')
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_page_scroll_tool_uses_direct_service(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/direct-public-scroll').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body style="margin:0">
|
|
<div style="height: 2600px; padding-top: 20px">top</div>
|
|
<p id="bottom">bottom</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
tools = Tools()
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
await services.navigation.navigate(httpserver.url_for('/direct-public-scroll'))
|
|
|
|
original_dispatch = browser_session.event_bus.dispatch
|
|
|
|
def fail_scroll_dispatch(event, *args, **kwargs):
|
|
if event.__class__.__name__ == 'ScrollEvent':
|
|
raise AssertionError('Public page scroll should use direct services, not ScrollEvent')
|
|
return original_dispatch(event, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_scroll_dispatch)
|
|
|
|
scroll_result = await tools.registry.execute_action(
|
|
action_name='scroll',
|
|
params={'down': True, 'pages': 1.0},
|
|
browser_session=browser_session,
|
|
)
|
|
assert scroll_result.error is None
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': 'window.scrollY', 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
assert readback.get('result', {}).get('value') > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_element_scroll_tool_uses_direct_service(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/direct-element-scroll').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<div id="scroller" tabindex="0" role="region"
|
|
style="height: 140px; width: 300px; overflow-y: auto; border: 1px solid black">
|
|
<div style="height: 1200px">scrollable content</div>
|
|
</div>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
tools = Tools()
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
await services.navigation.navigate(httpserver.url_for('/direct-element-scroll'))
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
scroll_index = next(
|
|
idx for idx, element in state.dom_state.selector_map.items() if getattr(element, 'attributes', {}).get('id') == 'scroller'
|
|
)
|
|
|
|
original_dispatch = browser_session.event_bus.dispatch
|
|
|
|
def fail_scroll_dispatch(event, *args, **kwargs):
|
|
if event.__class__.__name__ == 'ScrollEvent':
|
|
raise AssertionError('Public element scroll should use direct services, not ScrollEvent')
|
|
return original_dispatch(event, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_scroll_dispatch)
|
|
|
|
scroll_result = await tools.registry.execute_action(
|
|
action_name='scroll',
|
|
params={'down': True, 'pages': 1.0, 'index': scroll_index},
|
|
browser_session=browser_session,
|
|
)
|
|
assert scroll_result.error is None
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('scroller').scrollTop", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
assert readback.get('result', {}).get('value') > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_upload_tool_uses_direct_service(browser_session, httpserver, tmp_path, monkeypatch) -> None:
|
|
upload_path = tmp_path / 'codexify-upload.txt'
|
|
upload_path.write_text('upload-ok', encoding='utf-8')
|
|
httpserver.expect_request('/direct-upload').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<input id="file" type="file" onchange="document.getElementById('answer').textContent = this.files[0].name">
|
|
<p id="answer">hidden</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
tools = Tools()
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
await services.navigation.navigate(httpserver.url_for('/direct-upload'))
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
file_index = next(
|
|
idx for idx, element in state.dom_state.selector_map.items() if getattr(element, 'attributes', {}).get('id') == 'file'
|
|
)
|
|
|
|
original_dispatch = browser_session.event_bus.dispatch
|
|
|
|
def fail_upload_dispatch(event, *args, **kwargs):
|
|
if event.__class__.__name__ == 'UploadFileEvent':
|
|
raise AssertionError('Public upload should use direct services, not UploadFileEvent')
|
|
return original_dispatch(event, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_upload_dispatch)
|
|
|
|
upload_result = await tools.registry.execute_action(
|
|
action_name='upload_file',
|
|
params={'index': file_index, 'path': str(upload_path)},
|
|
browser_session=browser_session,
|
|
available_file_paths=[str(upload_path)],
|
|
file_system=FileSystem(tmp_path / 'files', create_default_files=False),
|
|
)
|
|
assert upload_result.error is None
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('answer').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
assert readback.get('result', {}).get('value') == upload_path.name
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_find_text_tool_uses_direct_service(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/direct-find-text').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body style="margin:0">
|
|
<div style="height: 2200px">top</div>
|
|
<p id="target">Codexify target text</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
tools = Tools()
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
await services.navigation.navigate(httpserver.url_for('/direct-find-text'))
|
|
|
|
original_dispatch = browser_session.event_bus.dispatch
|
|
|
|
def fail_scroll_to_text_dispatch(event, *args, **kwargs):
|
|
if event.__class__.__name__ == 'ScrollToTextEvent':
|
|
raise AssertionError('Public find_text should use direct services, not ScrollToTextEvent')
|
|
return original_dispatch(event, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_scroll_to_text_dispatch)
|
|
|
|
find_result = await tools.registry.execute_action(
|
|
action_name='find_text',
|
|
params={'text': 'Codexify target text'},
|
|
browser_session=browser_session,
|
|
)
|
|
assert find_result.error is None
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': 'window.scrollY', 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
assert readback.get('result', {}).get('value') > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_dropdown_tools_use_direct_service(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/direct-dropdown').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<select id="choice" onchange="document.getElementById('answer').textContent = this.value">
|
|
<option value="alpha">Alpha</option>
|
|
<option value="beta">Beta</option>
|
|
</select>
|
|
<p id="answer">hidden</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
tools = Tools()
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
await services.navigation.navigate(httpserver.url_for('/direct-dropdown'))
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
select_index = next(
|
|
idx for idx, element in state.dom_state.selector_map.items() if getattr(element, 'attributes', {}).get('id') == 'choice'
|
|
)
|
|
|
|
def fail_dropdown_dispatch(event, *args, **kwargs):
|
|
if event.__class__.__name__ in {'GetDropdownOptionsEvent', 'SelectDropdownOptionEvent'}:
|
|
raise AssertionError('Public dropdown tools should use direct services, not dropdown events')
|
|
return original_dispatch(event, *args, **kwargs)
|
|
|
|
original_dispatch = browser_session.event_bus.dispatch
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_dropdown_dispatch)
|
|
|
|
options_result = await tools.registry.execute_action(
|
|
action_name='dropdown_options',
|
|
params={'index': select_index},
|
|
browser_session=browser_session,
|
|
)
|
|
select_result = await tools.registry.execute_action(
|
|
action_name='select_dropdown',
|
|
params={'index': select_index, 'text': 'Beta'},
|
|
browser_session=browser_session,
|
|
)
|
|
assert options_result.error is None
|
|
assert 'Beta' in (options_result.extracted_content or '')
|
|
assert select_result.error is None
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('answer').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
assert readback.get('result', {}).get('value') == 'beta'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_browser_download_service_downloads_and_tracks_without_event_dispatch(
|
|
browser_session, httpserver, monkeypatch
|
|
) -> None:
|
|
httpserver.expect_request('/download-page').respond_with_data(
|
|
'<!doctype html><html><body>download page</body></html>',
|
|
content_type='text/html',
|
|
)
|
|
httpserver.expect_request('/files/report.csv').respond_with_data(
|
|
'name,value\ncodexify,1\n',
|
|
content_type='text/csv',
|
|
)
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/download-page'))
|
|
|
|
def fail_dispatch(*args, **kwargs):
|
|
raise AssertionError('Direct download services should not dispatch through the event bus')
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_dispatch)
|
|
|
|
result = await services.downloads.download_url(
|
|
httpserver.url_for('/files/report.csv'),
|
|
suggested_filename='../../report.csv',
|
|
content_type='text/csv',
|
|
)
|
|
|
|
assert result is not None
|
|
path = Path(result['path'])
|
|
downloads_dir = Path(browser_session.browser_profile.downloads_path).expanduser().resolve()
|
|
assert path.name == 'report.csv'
|
|
assert downloads_dir in path.resolve().parents
|
|
assert path.read_text(encoding='utf-8') == 'name,value\ncodexify,1\n'
|
|
assert str(path) in services.downloads.list_downloads()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_print_button_click_tracks_pdf_without_download_event_dispatch(browser_session, httpserver, monkeypatch) -> None:
|
|
httpserver.expect_request('/print-button').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<head><title>Codexify Print Smoke</title></head>
|
|
<body>
|
|
<button id="print" onclick="window.print()">Print</button>
|
|
<p>content for generated pdf</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/print-button'))
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
button_index = next(
|
|
idx for idx, element in state.dom_state.selector_map.items() if getattr(element, 'attributes', {}).get('id') == 'print'
|
|
)
|
|
|
|
original_dispatch = browser_session.event_bus.dispatch
|
|
|
|
def fail_download_dispatch(event, *args, **kwargs):
|
|
if event.__class__.__name__ == 'FileDownloadedEvent':
|
|
raise AssertionError('Print-button PDF tracking should call the direct download handler, not FileDownloadedEvent')
|
|
return original_dispatch(event, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_download_dispatch)
|
|
|
|
result = await services.actions.click.click_index(button_index)
|
|
|
|
assert result is not None
|
|
assert result.get('pdf_generated') is True
|
|
path = Path(result['path'])
|
|
assert path.exists()
|
|
assert path.suffix == '.pdf'
|
|
assert str(path) in services.downloads.list_downloads()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_browser_dialog_service_records_auto_closed_dialogs_without_click_dispatch(
|
|
browser_session, httpserver, monkeypatch
|
|
) -> None:
|
|
httpserver.expect_request('/dialog-services').respond_with_data(
|
|
"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<button id="alert" onclick="alert('dialog-service-ok'); document.getElementById('answer').textContent = 'after-alert'">
|
|
Alert
|
|
</button>
|
|
<p id="answer">hidden</p>
|
|
</body>
|
|
</html>""",
|
|
content_type='text/html',
|
|
)
|
|
services = BrowserServiceBundle.from_session(browser_session)
|
|
services.dialogs.clear_closed_messages()
|
|
|
|
await services.navigation.navigate(httpserver.url_for('/dialog-services'))
|
|
state = await services.state.get_state(include_screenshot=False)
|
|
button_index = next(
|
|
idx
|
|
for idx, element in state.dom_state.selector_map.items()
|
|
if getattr(element, 'tag_name', None) == 'button' and getattr(element, 'attributes', {}).get('id') == 'alert'
|
|
)
|
|
|
|
def fail_dispatch(*args, **kwargs):
|
|
raise AssertionError('Direct click services should not dispatch through the event bus')
|
|
|
|
monkeypatch.setattr(browser_session.event_bus, 'dispatch', fail_dispatch)
|
|
|
|
await services.actions.click.click_index(button_index)
|
|
await asyncio.sleep(0.1)
|
|
|
|
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
readback = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
params={'expression': "document.getElementById('answer').textContent", 'returnByValue': True},
|
|
session_id=cdp_session.session_id,
|
|
)
|
|
|
|
assert readback.get('result', {}).get('value') == 'after-alert'
|
|
assert '[alert] dialog-service-ok' in services.dialogs.closed_messages()
|