File size: 15,965 Bytes
db4810d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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.
    """
    # Create a dummy Browser mock. Only the 'config' attribute is needed for _is_url_allowed.
    dummy_browser = Mock()
    # Set an empty config for dummy_browser; it won't be used in _is_url_allowed.
    dummy_browser.config = Mock()
    # Scenario 1: allowed_domains is None, any URL should be allowed.
    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
    # Scenario 2: allowed_domains is provided.
    allowed = ["example.com", "mysite.org"]
    config2 = BrowserContextConfig(allowed_domains=allowed)
    context2 = BrowserContext(browser=dummy_browser, config=config2)
    # URL exactly matching
    assert context2._is_url_allowed("http://example.com") is True
    # URL with subdomain (should be allowed)
    assert context2._is_url_allowed("http://sub.example.com/path") is True
    # URL with different domain (should not be allowed)
    assert context2._is_url_allowed("http://notexample.com") is False
    # URL that matches second allowed domain
    assert context2._is_url_allowed("https://mysite.org/page") is True
    # URL with port number, still allowed (port is stripped)
    assert context2._is_url_allowed("http://example.com:8080") is True
    # Scenario 3: Malformed URL or empty domain
    # urlparse will return an empty netloc for some malformed URLs.
    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.
    """
    # Test empty xpath returns empty string
    assert BrowserContext._convert_simple_xpath_to_css_selector('') == ''
    # Test a simple xpath without indices
    xpath = "/html/body/div/span"
    expected = "html > body > div > span"
    result = BrowserContext._convert_simple_xpath_to_css_selector(xpath)
    assert result == expected
    # Test xpath with an index on one element: [2] should translate to :nth-of-type(2)
    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
    # Test xpath with indices on multiple elements:
    # For "li[3]" -> li:nth-of-type(3) and for "a[1]" -> a:nth-of-type(1)
    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.
    """
    # Create a dummy browser since only its existence is needed.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    # Define a dummy page with a 'url' attribute.
    class DummyPage:
        url = "http://dummy.com"
    dummy_page = DummyPage()
    # Call _get_initial_state with a page: URL should be set from page.url.
    state_with_page = context._get_initial_state(page=dummy_page)
    assert state_with_page.url == dummy_page.url
    # Verify that the element_tree is initialized with tag 'root'
    assert state_with_page.element_tree.tag_name == 'root'
    # Call _get_initial_state without a page: URL should be empty.
    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.
    """
    # Define a dummy page with an async evaluate method.
    class DummyPage:
        async def evaluate(self, script):
            return "dummy_result"
    # Create a dummy session object with a dummy current_page.
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = DummyPage()
    # Create a dummy browser mock with a minimal config.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize the BrowserContext with the dummy browser and config.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    # Manually set the session to our dummy session.
    context.session = dummy_session
    # Call execute_javascript and verify it returns the expected result.
    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.
    """
    # Create a dummy DOMElementNode instance with a complex set of attributes.
    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=[]
    )
    # Call the method with include_dynamic_attributes=True.
    actual_selector = BrowserContext._enhanced_css_selector_for_element(dummy_element, include_dynamic_attributes=True)
    # Expected conversion:
    # 1. The xpath "/html/body/div[2]" converts to "html > body > div:nth-of-type(2)".
    # 2. The class attribute "foo bar" appends ".foo.bar".
    # 3. The "id" attribute is added as [id="my-id"].
    # 4. The "placeholder" attribute contains quotes; it is added as
    #    [placeholder*="some \"quoted\" text"].
    # 5. The dynamic attribute "data-testid" is added as [data-testid="123"].
    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.
    """
    # Define a dummy page with an async evaluate method returning preset values.
    class DummyPage:
        async def evaluate(self, script):
            if "window.scrollY" in script:
                return 100  # scrollY
            elif "window.innerHeight" in script:
                return 500  # innerHeight
            elif "document.documentElement.scrollHeight" in script:
                return 1200  # total scrollable height
            return None
    # Create a dummy session with a dummy current_page.
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = DummyPage()
    # We also need a dummy context attribute but it won't be used in this test.
    dummy_session.context = type("DummyContext", (), {})()
    # Create a dummy browser mock.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize BrowserContext with the dummy browser and config.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    # Manually set the session to our dummy session.
    context.session = dummy_session
    # Call get_scroll_info on the dummy page.
    pixels_above, pixels_below = await context.get_scroll_info(dummy_session.current_page)
    # Expected calculations:
    # pixels_above = scrollY = 100
    # pixels_below = total_height - (scrollY + innerHeight) = 1200 - (100 + 500) = 600
    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.
    """
    # Dummy Page with close and wait_for_load_state methods.
    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
    # Dummy Context that holds pages and can create a new page.
    class DummyContext:
        def __init__(self):
            self.pages = []
        async def new_page(self):
            new_page = DummyPage(url="")
            self.pages.append(new_page)
            return new_page
    # Create a dummy session with a context containing two pages.
    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
    # Create a dummy browser mock.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize BrowserContext using our dummy_browser and config,
    # and manually set its session to our dummy session.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    context.session = dummy_session
    # Confirm session has 2 pages before reset.
    assert len(dummy_session.context.pages) == 2
    # Call reset_context which should close existing pages,
    # reset the cached state, and create a new page as current_page.
    await context.reset_context()
    # Verify that initial pages were closed.
    assert page1.closed is True
    assert page2.closed is True
    # Check that a new page is created and set as current_page.
    assert dummy_session.current_page is not None
    new_page = dummy_session.current_page
    # New page URL should be empty as per _get_initial_state.
    assert new_page.url == ""
    # Verify that cached_state is reset to an initial BrowserState.
    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):
            # Verify that parameters are forwarded correctly.
            assert full_page is True, "full_page parameter was not correctly passed"
            assert animations == 'disabled', "animations parameter was not correctly passed"
            # Return a test byte string.
            return b'test'
    # Create a dummy session with the DummyPage as the current_page.
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = DummyPage()
    dummy_session.context = None  # Not used in this test
    # Create a dummy browser mock.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize the BrowserContext with the dummy browser and config.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    # Manually set the session to our dummy session.
    context.session = dummy_session
    # Call take_screenshot and check that it returns the expected base64 encoded string.
    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
    # Create a dummy session with the dummy page as the current_page.
    dummy_page = DummyPage()
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = dummy_page
    dummy_session.context = None  # Not required for this test
    # Create a dummy browser mock
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize BrowserContext with the dummy browser and config,
    # and manually set its session to our dummy session.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    context.session = dummy_session
    # Call refresh_page and verify that reload and wait_for_load_state were called.
    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).
    """
    # Dummy page that always raises an exception when evaluate is called.
    class DummyPage:
        async def evaluate(self, script):
            raise Exception("dummy error")
    # Create a dummy session with the DummyPage as current_page.
    dummy_session = type("DummySession", (), {})()
    dummy_session.current_page = DummyPage()
    dummy_session.context = None  # Not used in this test
    # Create a dummy browser mock.
    dummy_browser = Mock()
    dummy_browser.config = Mock()
    # Initialize BrowserContext with the dummy browser and configuration.
    context = BrowserContext(browser=dummy_browser, config=BrowserContextConfig())
    context.session = dummy_session
    # Call remove_highlights and verify that no exception is raised.
    try:
        await context.remove_highlights()
    except Exception as e:
        pytest.fail(f"remove_highlights raised an exception: {e}")