|
|
import asyncio |
|
|
import base64 |
|
|
import os |
|
|
import pytest |
|
|
from browser_use.browser.context import BrowserContext, BrowserContextConfig |
|
|
from browser_use.browser.views import BrowserState |
|
|
from browser_use.dom.views import DOMElementNode |
|
|
from unittest.mock import Mock |
|
|
|
|
|
def test_is_url_allowed(): |
|
|
""" |
|
|
Test the _is_url_allowed method to verify that it correctly checks URLs against |
|
|
the allowed domains configuration. |
|
|
Scenario 1: When allowed_domains is None, all URLs should be allowed. |
|
|
Scenario 2: When allowed_domains is a list, only URLs matching the allowed domain(s) are allowed. |
|
|
Scenario 3: When the URL is malformed, it should return False. |
|
|
""" |
|
|
|
|
|
dummy_browser = Mock() |
|
|
|
|
|
dummy_browser.config = Mock() |
|
|
|
|
|
config1 = BrowserContextConfig(allowed_domains=None) |
|
|
context1 = BrowserContext(browser=dummy_browser, config=config1) |
|
|
assert context1._is_url_allowed("http://anydomain.com") is True |
|
|
assert context1._is_url_allowed("https://anotherdomain.org/path") is True |
|
|
|
|
|
allowed = ["example.com", "mysite.org"] |
|
|
config2 = BrowserContextConfig(allowed_domains=allowed) |
|
|
context2 = BrowserContext(browser=dummy_browser, config=config2) |
|
|
|
|
|
assert context2._is_url_allowed("http://example.com") is True |
|
|
|
|
|
assert context2._is_url_allowed("http://sub.example.com/path") is True |
|
|
|
|
|
assert context2._is_url_allowed("http://notexample.com") is False |
|
|
|
|
|
assert context2._is_url_allowed("https://mysite.org/page") is True |
|
|
|
|
|
assert context2._is_url_allowed("http://example.com:8080") is True |
|
|
|
|
|
|
|
|
assert context2._is_url_allowed("notaurl") is False |
|
|
def test_convert_simple_xpath_to_css_selector(): |
|
|
""" |
|
|
Test the _convert_simple_xpath_to_css_selector method of BrowserContext. |
|
|
This verifies that simple XPath expressions (with and without indices) are correctly converted to CSS selectors. |
|
|
""" |
|
|
|
|
|
assert BrowserContext._convert_simple_xpath_to_css_selector('') == '' |
|
|
|
|
|
xpath = "/html/body/div/span" |
|
|
expected = "html > body > div > span" |
|
|
result = BrowserContext._convert_simple_xpath_to_css_selector(xpath) |
|
|
assert result == expected |
|
|
|
|
|
xpath = "/html/body/div[2]/span" |
|
|
expected = "html > body > div:nth-of-type(2) > span" |
|
|
result = BrowserContext._convert_simple_xpath_to_css_selector(xpath) |
|
|
assert result == expected |
|
|
|
|
|
|
|
|
xpath = "/ul/li[3]/a[1]" |
|
|
expected = "ul > li:nth-of-type(3) > a:nth-of-type(1)" |
|
|
result = BrowserContext._convert_simple_xpath_to_css_selector(xpath) |
|
|
assert result == expected |
|
|
def test_get_initial_state(): |
|
|
""" |
|
|
Test the _get_initial_state method to verify it returns the correct initial BrowserState. |
|
|
The test checks that when a dummy page with a URL is provided, |
|
|
the returned state contains that URL and other default values. |
|
|
""" |
|
|
|
|
|
dummy_browser = Mock() |
|
|
dummy_browser.config = Mock() |
|
|
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig()) |
|
|
|
|
|
class DummyPage: |
|
|
url = "http://dummy.com" |
|
|
dummy_page = DummyPage() |
|
|
|
|
|
state_with_page = context._get_initial_state(page=dummy_page) |
|
|
assert state_with_page.url == dummy_page.url |
|
|
|
|
|
assert state_with_page.element_tree.tag_name == 'root' |
|
|
|
|
|
state_without_page = context._get_initial_state() |
|
|
assert state_without_page.url == "" |
|
|
@pytest.mark.asyncio |
|
|
async def test_execute_javascript(): |
|
|
""" |
|
|
Test the execute_javascript method by mocking the current page's evaluate function. |
|
|
This ensures that when execute_javascript is called, it correctly returns the value |
|
|
from the page's evaluate method. |
|
|
""" |
|
|
|
|
|
class DummyPage: |
|
|
async def evaluate(self, script): |
|
|
return "dummy_result" |
|
|
|
|
|
dummy_session = type("DummySession", (), {})() |
|
|
dummy_session.current_page = DummyPage() |
|
|
|
|
|
dummy_browser = Mock() |
|
|
dummy_browser.config = Mock() |
|
|
|
|
|
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig()) |
|
|
|
|
|
context.session = dummy_session |
|
|
|
|
|
result = await context.execute_javascript("return 1+1") |
|
|
assert result == "dummy_result" |
|
|
@pytest.mark.asyncio |
|
|
async def test_enhanced_css_selector_for_element(): |
|
|
""" |
|
|
Test the _enhanced_css_selector_for_element method to verify that |
|
|
it returns the correct CSS selector string for a dummy DOMElementNode. |
|
|
The test checks that: |
|
|
- The provided xpath is correctly converted (handling indices), |
|
|
- Class attributes are appended as CSS classes, |
|
|
- Standard and dynamic attributes (including ones with special characters) |
|
|
are correctly added to the selector. |
|
|
""" |
|
|
|
|
|
dummy_element = DOMElementNode( |
|
|
tag_name="div", |
|
|
is_visible=True, |
|
|
parent=None, |
|
|
xpath="/html/body/div[2]", |
|
|
attributes={ |
|
|
"class": "foo bar", |
|
|
"id": "my-id", |
|
|
"placeholder": 'some "quoted" text', |
|
|
"data-testid": "123" |
|
|
}, |
|
|
children=[] |
|
|
) |
|
|
|
|
|
actual_selector = BrowserContext._enhanced_css_selector_for_element(dummy_element, include_dynamic_attributes=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
expected_selector = 'html > body > div:nth-of-type(2).foo.bar[id="my-id"][placeholder*="some \\"quoted\\" text"][data-testid="123"]' |
|
|
assert actual_selector == expected_selector, f"Expected {expected_selector}, but got {actual_selector}" |
|
|
@pytest.mark.asyncio |
|
|
async def test_get_scroll_info(): |
|
|
""" |
|
|
Test the get_scroll_info method by mocking the page's evaluate method. |
|
|
This dummy page returns preset values for window.scrollY, window.innerHeight, |
|
|
and document.documentElement.scrollHeight. The test then verifies that the |
|
|
computed scroll information (pixels_above and pixels_below) match the expected values. |
|
|
""" |
|
|
|
|
|
class DummyPage: |
|
|
async def evaluate(self, script): |
|
|
if "window.scrollY" in script: |
|
|
return 100 |
|
|
elif "window.innerHeight" in script: |
|
|
return 500 |
|
|
elif "document.documentElement.scrollHeight" in script: |
|
|
return 1200 |
|
|
return None |
|
|
|
|
|
dummy_session = type("DummySession", (), {})() |
|
|
dummy_session.current_page = DummyPage() |
|
|
|
|
|
dummy_session.context = type("DummyContext", (), {})() |
|
|
|
|
|
dummy_browser = Mock() |
|
|
dummy_browser.config = Mock() |
|
|
|
|
|
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig()) |
|
|
|
|
|
context.session = dummy_session |
|
|
|
|
|
pixels_above, pixels_below = await context.get_scroll_info(dummy_session.current_page) |
|
|
|
|
|
|
|
|
|
|
|
assert pixels_above == 100, f"Expected 100 pixels above, got {pixels_above}" |
|
|
assert pixels_below == 600, f"Expected 600 pixels below, got {pixels_below}" |
|
|
@pytest.mark.asyncio |
|
|
async def test_reset_context(): |
|
|
""" |
|
|
Test the reset_context method to ensure it correctly closes all existing tabs, |
|
|
resets the cached state, and creates a new page. |
|
|
""" |
|
|
|
|
|
class DummyPage: |
|
|
def __init__(self, url="http://dummy.com"): |
|
|
self.url = url |
|
|
self.closed = False |
|
|
async def close(self): |
|
|
self.closed = True |
|
|
async def wait_for_load_state(self): |
|
|
pass |
|
|
|
|
|
class DummyContext: |
|
|
def __init__(self): |
|
|
self.pages = [] |
|
|
async def new_page(self): |
|
|
new_page = DummyPage(url="") |
|
|
self.pages.append(new_page) |
|
|
return new_page |
|
|
|
|
|
dummy_session = type("DummySession", (), {})() |
|
|
dummy_context = DummyContext() |
|
|
page1 = DummyPage(url="http://page1.com") |
|
|
page2 = DummyPage(url="http://page2.com") |
|
|
dummy_context.pages.extend([page1, page2]) |
|
|
dummy_session.context = dummy_context |
|
|
dummy_session.current_page = page1 |
|
|
dummy_session.cached_state = None |
|
|
|
|
|
dummy_browser = Mock() |
|
|
dummy_browser.config = Mock() |
|
|
|
|
|
|
|
|
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig()) |
|
|
context.session = dummy_session |
|
|
|
|
|
assert len(dummy_session.context.pages) == 2 |
|
|
|
|
|
|
|
|
await context.reset_context() |
|
|
|
|
|
assert page1.closed is True |
|
|
assert page2.closed is True |
|
|
|
|
|
assert dummy_session.current_page is not None |
|
|
new_page = dummy_session.current_page |
|
|
|
|
|
assert new_page.url == "" |
|
|
|
|
|
state = dummy_session.cached_state |
|
|
assert isinstance(state, BrowserState) |
|
|
assert state.url == "" |
|
|
assert state.element_tree.tag_name == 'root' |
|
|
@pytest.mark.asyncio |
|
|
async def test_take_screenshot(): |
|
|
""" |
|
|
Test the take_screenshot method to verify that it returns a base64 encoded screenshot string. |
|
|
A dummy page with a mocked screenshot method is used, returning a predefined byte string. |
|
|
""" |
|
|
class DummyPage: |
|
|
async def screenshot(self, full_page, animations): |
|
|
|
|
|
assert full_page is True, "full_page parameter was not correctly passed" |
|
|
assert animations == 'disabled', "animations parameter was not correctly passed" |
|
|
|
|
|
return b'test' |
|
|
|
|
|
dummy_session = type("DummySession", (), {})() |
|
|
dummy_session.current_page = DummyPage() |
|
|
dummy_session.context = None |
|
|
|
|
|
dummy_browser = Mock() |
|
|
dummy_browser.config = Mock() |
|
|
|
|
|
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig()) |
|
|
|
|
|
context.session = dummy_session |
|
|
|
|
|
result = await context.take_screenshot(full_page=True) |
|
|
expected = base64.b64encode(b'test').decode('utf-8') |
|
|
assert result == expected, f"Expected {expected}, but got {result}" |
|
|
@pytest.mark.asyncio |
|
|
async def test_refresh_page_behavior(): |
|
|
""" |
|
|
Test the refresh_page method of BrowserContext to verify that it correctly reloads the current page |
|
|
and waits for the page's load state. This is done by creating a dummy page that flags when its |
|
|
reload and wait_for_load_state methods are called. |
|
|
""" |
|
|
class DummyPage: |
|
|
def __init__(self): |
|
|
self.reload_called = False |
|
|
self.wait_for_load_state_called = False |
|
|
async def reload(self): |
|
|
self.reload_called = True |
|
|
async def wait_for_load_state(self): |
|
|
self.wait_for_load_state_called = True |
|
|
|
|
|
dummy_page = DummyPage() |
|
|
dummy_session = type("DummySession", (), {})() |
|
|
dummy_session.current_page = dummy_page |
|
|
dummy_session.context = None |
|
|
|
|
|
dummy_browser = Mock() |
|
|
dummy_browser.config = Mock() |
|
|
|
|
|
|
|
|
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig()) |
|
|
context.session = dummy_session |
|
|
|
|
|
await context.refresh_page() |
|
|
assert dummy_page.reload_called is True, "Expected the page to call reload()" |
|
|
assert dummy_page.wait_for_load_state_called is True, "Expected the page to call wait_for_load_state()" |
|
|
@pytest.mark.asyncio |
|
|
async def test_remove_highlights_failure(): |
|
|
""" |
|
|
Test the remove_highlights method to ensure that if the page.evaluate call fails, |
|
|
the exception is caught and does not propagate (i.e. the method handles errors gracefully). |
|
|
""" |
|
|
|
|
|
class DummyPage: |
|
|
async def evaluate(self, script): |
|
|
raise Exception("dummy error") |
|
|
|
|
|
dummy_session = type("DummySession", (), {})() |
|
|
dummy_session.current_page = DummyPage() |
|
|
dummy_session.context = None |
|
|
|
|
|
dummy_browser = Mock() |
|
|
dummy_browser.config = Mock() |
|
|
|
|
|
context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig()) |
|
|
context.session = dummy_session |
|
|
|
|
|
try: |
|
|
await context.remove_highlights() |
|
|
except Exception as e: |
|
|
pytest.fail(f"remove_highlights raised an exception: {e}") |