diff --git a/py/test/selenium/webdriver/common/bidi_errors_tests.py b/py/test/selenium/webdriver/common/bidi_errors_tests.py new file mode 100644 index 0000000000000..2d826b280ca0b --- /dev/null +++ b/py/test/selenium/webdriver/common/bidi_errors_tests.py @@ -0,0 +1,149 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.by import By + + +def test_invalid_browsing_context_id(driver): + """Test that invalid browsing context ID raises an error.""" + with pytest.raises(WebDriverException): + driver.browsing_context.close("invalid-context-id") + + +def test_invalid_navigation_url(driver): + """Test that navigation with invalid context should fail.""" + with pytest.raises(WebDriverException): + # Invalid context ID should fail + driver.browsing_context.navigate("invalid-context-id", "about:blank") + + +def test_invalid_geolocation_coordinates(driver): + """Test that invalid geolocation coordinates raise an error.""" + from selenium.webdriver.common.bidi.emulation import GeolocationCoordinates + + with pytest.raises((WebDriverException, ValueError, TypeError)): + # Invalid latitude (> 90) + coords = GeolocationCoordinates(latitude=999, longitude=180, accuracy=10) + driver.emulation.set_geolocation_override(coordinates=coords) + + +def test_invalid_timezone(driver): + """Test that invalid timezone string raises an error.""" + with pytest.raises((WebDriverException, ValueError)): + driver.emulation.set_timezone_override("Invalid/Timezone") + + +def test_invalid_set_cookie(driver, pages): + """Test that setting cookie with None raises an error.""" + pages.load("blank.html") + + with pytest.raises((WebDriverException, TypeError, AttributeError)): + driver.storage.set_cookie(None) + + +def test_remove_nonexistent_context(driver): + """Test that removing non-existent context raises an error.""" + with pytest.raises(WebDriverException): + driver.browser.remove_user_context("non-existent-context-id") + + +def test_invalid_perform_actions_missing_context(driver, pages): + """Test that perform_actions without context raises an error.""" + pages.load("blank.html") + + with pytest.raises(TypeError): + # Missing required 'context' parameter + driver.input.perform_actions(actions=[]) + + +def test_error_recovery_after_invalid_navigation(driver): + """Test that driver can recover after failed navigation.""" + # Try an invalid navigation with bad context + with pytest.raises(WebDriverException): + driver.browsing_context.navigate("invalid-context", "about:blank") + + # Driver should still be functional + driver.get("about:blank") + assert driver.find_element(By.TAG_NAME, "body") is not None + + +def test_multiple_error_conditions(driver, pages): + """Test handling multiple error conditions in sequence.""" + pages.load("blank.html") + + # First error + with pytest.raises(WebDriverException): + driver.browser.remove_user_context("invalid") + + # Driver should still work + assert driver.find_element(By.TAG_NAME, "body") is not None + + # Second error + with pytest.raises((WebDriverException, ValueError)): + driver.emulation.set_timezone_override("Invalid") + + # Driver still functional + driver.get("about:blank") + + +class TestBidiErrorHandling: + """Test class for error handling in BiDi operations.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test in this class.""" + pages.load("blank.html") + + def test_error_on_invalid_context_operations(self, driver): + """Test error handling with invalid context operations.""" + # Try to close non-existent context + with pytest.raises(WebDriverException): + driver.browsing_context.close("nonexistent") + + def test_error_recovery_sequence(self, driver): + """Test that driver recovers properly from errors.""" + # First operation fails + with pytest.raises(WebDriverException): + driver.browser.remove_user_context("bad-id") + + # Recovery test + element = driver.find_element(By.TAG_NAME, "body") + assert element is not None + + def test_consecutive_errors(self, driver): + """Test handling consecutive errors.""" + errors_caught = 0 + + # First error + try: + driver.browser.remove_user_context("id1") + except WebDriverException: + errors_caught += 1 + + # Second error + try: + driver.browser.remove_user_context("id2") + except WebDriverException: + errors_caught += 1 + + assert errors_caught == 2 + + # Driver should still work + driver.get("about:blank") diff --git a/py/test/selenium/webdriver/common/bidi_input_tests.py b/py/test/selenium/webdriver/common/bidi_input_tests.py index ecbe0bddd4f73..9929a01117924 100644 --- a/py/test/selenium/webdriver/common/bidi_input_tests.py +++ b/py/test/selenium/webdriver/common/bidi_input_tests.py @@ -27,6 +27,7 @@ KeyDownAction, KeySourceActions, KeyUpAction, + NoneSourceActions, Origin, PauseAction, PointerCommonProperties, @@ -73,7 +74,9 @@ def test_basic_key_input(driver, pages): driver.input.perform_actions(driver.current_window_handle, [key_actions]) - WebDriverWait(driver, 5).until(lambda d: input_element.get_attribute("value") == "hello") + WebDriverWait(driver, 5).until( + lambda d: input_element.get_attribute("value") == "hello" + ) assert input_element.get_attribute("value") == "hello" @@ -97,7 +100,9 @@ def test_key_input_with_pause(driver, pages): driver.input.perform_actions(driver.current_window_handle, [key_actions]) - WebDriverWait(driver, 5).until(lambda d: input_element.get_attribute("value") == "ab") + WebDriverWait(driver, 5).until( + lambda d: input_element.get_attribute("value") == "ab" + ) assert input_element.get_attribute("value") == "ab" @@ -170,7 +175,13 @@ def test_pointer_with_common_properties(driver, pages): # Create pointer properties properties = PointerCommonProperties( - width=2, height=2, pressure=0.5, tangential_pressure=0.0, twist=45, altitude_angle=0.5, azimuth_angle=1.0 + width=2, + height=2, + pressure=0.5, + tangential_pressure=0.0, + twist=45, + altitude_angle=0.5, + azimuth_angle=1.0, ) pointer_actions = PointerSourceActions( @@ -196,7 +207,12 @@ def test_wheel_scroll(driver, pages): # Scroll down wheel_actions = WheelSourceActions( - id="wheel", actions=[WheelScrollAction(x=100, y=100, delta_x=0, delta_y=100, origin=Origin.VIEWPORT)] + id="wheel", + actions=[ + WheelScrollAction( + x=100, y=100, delta_x=0, delta_y=100, origin=Origin.VIEWPORT + ) + ], ) driver.input.perform_actions(driver.current_window_handle, [wheel_actions]) @@ -247,9 +263,13 @@ def test_combined_input_actions(driver, pages): ], ) - driver.input.perform_actions(driver.current_window_handle, [pointer_actions, key_actions]) + driver.input.perform_actions( + driver.current_window_handle, [pointer_actions, key_actions] + ) - WebDriverWait(driver, 5).until(lambda d: input_element.get_attribute("value") == "test") + WebDriverWait(driver, 5).until( + lambda d: input_element.get_attribute("value") == "test" + ) assert input_element.get_attribute("value") == "test" @@ -261,7 +281,9 @@ def test_set_files(driver, pages): assert upload_element.get_attribute("value") == "" # Create a temporary file - with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as temp_file: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".txt", delete=False + ) as temp_file: temp_file.write("test content") temp_file_path = temp_file.name @@ -271,7 +293,9 @@ def test_set_files(driver, pages): element_ref = {"sharedId": element_id} # Set files using BiDi - driver.input.set_files(driver.current_window_handle, element_ref, [temp_file_path]) + driver.input.set_files( + driver.current_window_handle, element_ref, [temp_file_path] + ) # Verify file was set value = upload_element.get_attribute("value") @@ -346,7 +370,9 @@ def test_release_actions(driver, pages): driver.input.perform_actions(driver.current_window_handle, [key_actions2]) # Should be able to type normally - WebDriverWait(driver, 5).until(lambda d: "b" in input_element.get_attribute("value")) + WebDriverWait(driver, 5).until( + lambda d: "b" in input_element.get_attribute("value") + ) @pytest.mark.parametrize("multiple", [True, False]) @@ -362,7 +388,9 @@ def file_dialog_handler(file_dialog_info): handler_id = driver.input.add_file_dialog_handler(file_dialog_handler) assert handler_id is not None - driver.get(f"data:text/html,") + driver.get( + f"data:text/html," + ) # Use script.evaluate to trigger the file dialog with user activation driver.script._evaluate( @@ -413,3 +441,509 @@ def file_dialog_handler(file_dialog_info): # Wait to ensure no events are captured time.sleep(1) assert len(file_dialog_events) == 0 + + +# Edge Cases and Additional Tests + + +def test_perform_actions_with_none_source(driver, pages): + """Test performing NoneSourceActions (pause only).""" + pages.load("single_text_input.html") + + # Create none actions (pause only - no actual input) + none_actions = NoneSourceActions( + id="none_id", + actions=[ + PauseAction(duration=100), + PauseAction(duration=50), + ], + ) + + # Should execute without error + driver.input.perform_actions(driver.current_window_handle, [none_actions]) + + # Verify input field is still empty + input_element = driver.find_element(By.ID, "textInput") + assert input_element.get_attribute("value") == "" + + +def test_perform_actions_rapid_key_sequence(driver, pages): + """Test rapid key input sequence without pause between keys.""" + pages.load("single_text_input.html") + + input_element = driver.find_element(By.ID, "textInput") + + # Create rapid key sequence + key_actions = KeySourceActions( + id="keyboard", + actions=[ + KeyDownAction(value="a"), + KeyUpAction(value="a"), + KeyDownAction(value="b"), + KeyUpAction(value="b"), + KeyDownAction(value="c"), + KeyUpAction(value="c"), + KeyDownAction(value="d"), + KeyUpAction(value="d"), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [key_actions]) + + WebDriverWait(driver, 5).until( + lambda d: input_element.get_attribute("value") == "abcd" + ) + assert input_element.get_attribute("value") == "abcd" + + +def test_perform_actions_multiple_pointer_buttons(driver, pages): + """Test pointer actions with different button values.""" + pages.load("javascriptPage.html") + + button = driver.find_element(By.ID, "clickField") + location = button.location + size = button.size + x = location["x"] + size["width"] // 2 + y = location["y"] + size["height"] // 2 + + # Test with button 0 (left click) + pointer_actions_left = PointerSourceActions( + id="mouse_left", + parameters=PointerParameters(pointer_type=PointerType.MOUSE), + actions=[ + PointerMoveAction(x=x, y=y), + PointerDownAction(button=0), + PointerUpAction(button=0), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [pointer_actions_left]) + + WebDriverWait(driver, 5).until(lambda d: button.get_attribute("value") == "Clicked") + assert button.get_attribute("value") == "Clicked" + + +def test_perform_actions_pointer_touch_type(driver, pages): + """Test pointer actions with touch pointer type.""" + pages.load("javascriptPage.html") + + button = driver.find_element(By.ID, "clickField") + location = button.location + size = button.size + x = location["x"] + size["width"] // 2 + y = location["y"] + size["height"] // 2 + + # Create touch actions + touch_actions = PointerSourceActions( + id="touch", + parameters=PointerParameters(pointer_type=PointerType.TOUCH), + actions=[ + PointerMoveAction(x=x, y=y), + PointerDownAction(button=0), + PointerUpAction(button=0), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [touch_actions]) + + # Touch should work similar to mouse click + WebDriverWait(driver, 5).until(lambda d: button.get_attribute("value") == "Clicked") + assert button.get_attribute("value") == "Clicked" + + +def test_perform_actions_pointer_pen_type(driver, pages): + """Test pointer actions with pen pointer type.""" + pages.load("javascriptPage.html") + + button = driver.find_element(By.ID, "clickField") + location = button.location + size = button.size + x = location["x"] + size["width"] // 2 + y = location["y"] + size["height"] // 2 + + # Create pen actions + pen_actions = PointerSourceActions( + id="pen", + parameters=PointerParameters(pointer_type=PointerType.PEN), + actions=[ + PointerMoveAction(x=x, y=y), + PointerDownAction(button=0), + PointerUpAction(button=0), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [pen_actions]) + + WebDriverWait(driver, 5).until(lambda d: button.get_attribute("value") == "Clicked") + assert button.get_attribute("value") == "Clicked" + + +def test_perform_actions_pointer_move_with_duration(driver, pages): + """Test pointer move action with duration parameter.""" + pages.load("javascriptPage.html") + + button = driver.find_element(By.ID, "clickField") + location = button.location + size = button.size + x = location["x"] + size["width"] // 2 + y = location["y"] + size["height"] // 2 + + # Start point (off the button) + start_x = x - 100 + start_y = y - 100 + + # Create pointer actions with duration on move + pointer_actions = PointerSourceActions( + id="mouse", + parameters=PointerParameters(pointer_type=PointerType.MOUSE), + actions=[ + PointerMoveAction(x=start_x, y=start_y), + PointerMoveAction(x=x, y=y, duration=500), # Slow move + PointerDownAction(button=0), + PointerUpAction(button=0), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [pointer_actions]) + + WebDriverWait(driver, 5).until(lambda d: button.get_attribute("value") == "Clicked") + assert button.get_attribute("value") == "Clicked" + + +def test_wheel_scroll_negative_delta(driver, pages): + """Test wheel scroll with negative delta values (up/left).""" + pages.load("scroll3.html") + + # First scroll down + wheel_actions_down = WheelSourceActions( + id="wheel_down", + actions=[ + WheelScrollAction( + x=100, y=100, delta_x=0, delta_y=100, origin=Origin.VIEWPORT + ) + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [wheel_actions_down]) + + scroll_y_down = driver.execute_script("return window.pageYOffset;") + assert scroll_y_down > 0 + + # Then scroll back up (negative delta) + wheel_actions_up = WheelSourceActions( + id="wheel_up", + actions=[ + WheelScrollAction( + x=100, y=100, delta_x=0, delta_y=-50, origin=Origin.VIEWPORT + ) + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [wheel_actions_up]) + + scroll_y_up = driver.execute_script("return window.pageYOffset;") + assert scroll_y_up < scroll_y_down + + +def test_wheel_scroll_with_duration(driver, pages): + """Test wheel scroll action with duration parameter.""" + pages.load("scroll3.html") + + wheel_actions = WheelSourceActions( + id="wheel", + actions=[ + WheelScrollAction( + x=100, + y=100, + delta_x=0, + delta_y=100, + duration=500, + origin=Origin.VIEWPORT, + ) + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [wheel_actions]) + + scroll_y = driver.execute_script("return window.pageYOffset;") + assert scroll_y == 100 + + +def test_wheel_scroll_horizontal(driver, pages): + """Test wheel scroll with horizontal movement.""" + pages.load("scroll3.html") + + # Scroll horizontally + wheel_actions = WheelSourceActions( + id="wheel", + actions=[ + WheelScrollAction( + x=100, y=100, delta_x=50, delta_y=0, origin=Origin.VIEWPORT + ) + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [wheel_actions]) + + # Check horizontal scroll occurred + scroll_x = driver.execute_script("return window.pageXOffset;") + assert scroll_x >= 0 + + +def test_key_input_special_characters(driver, pages): + """Test keyboard input with special characters.""" + pages.load("single_text_input.html") + + input_element = driver.find_element(By.ID, "textInput") + + # Create keyboard actions for special characters + key_actions = KeySourceActions( + id="keyboard", + actions=[ + KeyDownAction(value="!"), + KeyUpAction(value="!"), + KeyDownAction(value="@"), + KeyUpAction(value="@"), + KeyDownAction(value="#"), + KeyUpAction(value="#"), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [key_actions]) + + WebDriverWait(driver, 5).until( + lambda d: "!" in input_element.get_attribute("value") + ) + + +def test_set_files_empty_file_list(driver, pages): + """Test setting an empty file list on a file input element.""" + pages.load("formPage.html") + + upload_element = driver.find_element(By.ID, "upload") + + # Get element reference for BiDi + element_id = upload_element.id + element_ref = {"sharedId": element_id} + + # Set empty file list + driver.input.set_files(driver.current_window_handle, element_ref, []) + + # Value should be empty + value = upload_element.get_attribute("value") + assert value == "" + + +def test_set_files_with_absolute_path(driver): + """Test setting a file using absolute file path.""" + driver.get("data:text/html,") + + upload_element = driver.find_element(By.ID, "upload") + + # Create a temporary file + with tempfile.NamedTemporaryFile( + mode="w", suffix=".txt", delete=False + ) as temp_file: + temp_file.write("test file content") + temp_file_path = temp_file.name + + try: + # Get element reference + element_id = upload_element.id + element_ref = {"sharedId": element_id} + + # Set file using absolute path + driver.input.set_files( + driver.current_window_handle, element_ref, [temp_file_path] + ) + + value = upload_element.get_attribute("value") + assert os.path.basename(temp_file_path) in value + + finally: + if os.path.exists(temp_file_path): + os.unlink(temp_file_path) + + +def test_release_actions_clears_pointer_state(driver, pages): + """Test that release_actions properly clears pointer state.""" + pages.load("javascriptPage.html") + + button = driver.find_element(By.ID, "clickField") + location = button.location + size = button.size + x = location["x"] + size["width"] // 2 + y = location["y"] + size["height"] // 2 + + # Press pointer button but don't release + pointer_actions = PointerSourceActions( + id="mouse", + parameters=PointerParameters(pointer_type=PointerType.MOUSE), + actions=[ + PointerMoveAction(x=x, y=y), + PointerDownAction(button=0), + # Not releasing button + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [pointer_actions]) + + # Release all actions + driver.input.release_actions(driver.current_window_handle) + + # Now move and try clicking again - should work normally + pointer_actions2 = PointerSourceActions( + id="mouse", + parameters=PointerParameters(pointer_type=PointerType.MOUSE), + actions=[ + PointerMoveAction(x=x, y=y), + PointerDownAction(button=0), + PointerUpAction(button=0), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [pointer_actions2]) + + WebDriverWait(driver, 5).until(lambda d: button.get_attribute("value") == "Clicked") + assert button.get_attribute("value") == "Clicked" + + +def test_multiple_file_dialog_handlers(driver): + """Test registering multiple file dialog handlers.""" + handlers_triggered = [] + + def handler_1(file_dialog_info): + handlers_triggered.append(1) + + def handler_2(file_dialog_info): + handlers_triggered.append(2) + + # Register two handlers + handler_id_1 = driver.input.add_file_dialog_handler(handler_1) + handler_id_2 = driver.input.add_file_dialog_handler(handler_2) + + assert handler_id_1 is not None + assert handler_id_2 is not None + assert handler_id_1 != handler_id_2 + + # Clean up + driver.input.remove_file_dialog_handler(handler_id_1) + driver.input.remove_file_dialog_handler(handler_id_2) + + +def test_pointer_common_properties_pressure_values(driver, pages): + """Test pointer actions with various pressure values.""" + pages.load("javascriptPage.html") + + button = driver.find_element(By.ID, "clickField") + location = button.location + size = button.size + x = location["x"] + size["width"] // 2 + y = location["y"] + size["height"] // 2 + + # Test with different pressure values + properties = PointerCommonProperties( + width=2, + height=2, + pressure=0.75, # High pressure + tangential_pressure=0.25, + twist=90, + altitude_angle=0.7, + azimuth_angle=1.5, + ) + + pointer_actions = PointerSourceActions( + id="mouse", + parameters=PointerParameters(pointer_type=PointerType.MOUSE), + actions=[ + PointerMoveAction(x=x, y=y, properties=properties), + PointerDownAction(button=0, properties=properties), + PointerUpAction(button=0), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [pointer_actions]) + + WebDriverWait(driver, 5).until(lambda d: button.get_attribute("value") == "Clicked") + assert button.get_attribute("value") == "Clicked" + + +def test_combined_keyboard_and_wheel_actions(driver, pages): + """Test combining keyboard and wheel scroll actions.""" + pages.load("scroll3.html") + + # Combine keyboard and wheel actions + key_actions = KeySourceActions( + id="keyboard", + actions=[PauseAction(duration=0)], # Sync with wheel + ) + + wheel_actions = WheelSourceActions( + id="wheel", + actions=[ + PauseAction(duration=0), # Sync with keyboard + WheelScrollAction( + x=100, y=100, delta_x=0, delta_y=100, origin=Origin.VIEWPORT + ), + ], + ) + + driver.input.perform_actions( + driver.current_window_handle, [key_actions, wheel_actions] + ) + + scroll_y = driver.execute_script("return window.pageYOffset;") + assert scroll_y == 100 + + +def test_key_input_with_value_attribute(driver, pages): + """Test KeyDownAction and KeyUpAction use value attribute correctly.""" + pages.load("single_text_input.html") + + input_element = driver.find_element(By.ID, "textInput") + + # Use explicit value attribute in actions + key_actions = KeySourceActions( + id="keyboard", + actions=[ + KeyDownAction(value="x"), + KeyUpAction(value="x"), + KeyDownAction(value="y"), + KeyUpAction(value="y"), + KeyDownAction(value="z"), + KeyUpAction(value="z"), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [key_actions]) + + WebDriverWait(driver, 5).until( + lambda d: input_element.get_attribute("value") == "xyz" + ) + assert input_element.get_attribute("value") == "xyz" + + +def test_wheel_scroll_with_element_origin(driver, pages): + """Test wheel scroll with element origin instead of viewport.""" + pages.load("scroll3.html") + + # Get a reference to a scrollable element (body) + body_element = driver.find_element(By.TAG_NAME, "body") + element_id = body_element.id + element_ref = {"sharedId": element_id} + element_origin = ElementOrigin(element_ref) + + # Scroll with element origin + wheel_actions = WheelSourceActions( + id="wheel", + actions=[ + WheelScrollAction( + x=100, y=100, delta_x=0, delta_y=100, origin=element_origin + ) + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [wheel_actions]) + + scroll_y = driver.execute_script("return window.pageYOffset;") + assert scroll_y >= 0 diff --git a/py/test/selenium/webdriver/common/bidi_integration_tests.py b/py/test/selenium/webdriver/common/bidi_integration_tests.py new file mode 100644 index 0000000000000..85323f49b3ccf --- /dev/null +++ b/py/test/selenium/webdriver/common/bidi_integration_tests.py @@ -0,0 +1,266 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.by import By +from selenium.webdriver.common.window import WindowTypes +from selenium.webdriver.support.ui import WebDriverWait + + +class TestBidiNetworkWithCookies: + """Test integration of network and storage modules.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test in this class.""" + pages.load("blank.html") + yield + # Cleanup: delete all cookies to prevent bleed-through + driver.delete_all_cookies() + + def test_cookies_interaction(self, driver, pages): + """Test that cookies work with network operations.""" + pages.load("blank.html") + + # Set a cookie + driver.add_cookie({"name": "test_cookie", "value": "test_value"}) + + # Verify cookie is set + cookies = driver.get_cookies() + assert len(cookies) > 0 + assert any(c.get("name") == "test_cookie" for c in cookies) + + def test_cookie_modification(self, driver, pages): + """Test that modifying cookies works properly.""" + pages.load("blank.html") + + # Add first cookie + driver.add_cookie({"name": "cookie1", "value": "value1"}) + + cookies_before = driver.get_cookies() + initial_count = len(cookies_before) + + # Add second cookie + driver.add_cookie({"name": "cookie2", "value": "value2"}) + + cookies_after = driver.get_cookies() + assert len(cookies_after) > initial_count + + +class TestBidiScriptWithNavigation: + """Test integration of script execution and navigation.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test in this class.""" + driver.delete_all_cookies() + pages.load("blank.html") + yield + # Cleanup: delete all cookies to prevent bleed-through + driver.delete_all_cookies() + + def test_script_execution_after_navigation(self, driver, pages): + """Test script execution after page navigation.""" + # First page + pages.load("blank.html") + driver.execute_script("window.page1_loaded = true;") + + # Navigate to different page + pages.load("blank.html") + + # Previous page variable should not exist + result = driver.execute_script("return window.page1_loaded;") + assert result is None + + # New variable should work + driver.execute_script("window.page2_loaded = true;") + result = driver.execute_script("return window.page2_loaded;") + assert result is True + + def test_global_variable_lifecycle(self, driver, pages): + """Test global variable lifecycle across operations.""" + pages.load("blank.html") + + # Set a global variable + driver.execute_script("window.test_var = {data: 'value'};") + + # Verify it exists + result = driver.execute_script("return window.test_var.data;") + assert result == "value" + + # Navigate away + driver.get("about:blank") + + # Variable should not exist anymore + result = driver.execute_script("return typeof window.test_var;") + assert result == "undefined" + + +class TestBidiEmulationWithNavigation: + """Test integration of emulation and navigation.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test in this class.""" + pages.load("blank.html") + yield + # Cleanup: delete all cookies to prevent bleed-through + driver.delete_all_cookies() + + def test_basic_navigation(self, driver, pages): + """Test basic navigation.""" + pages.load("blank.html") + assert driver.find_element(By.TAG_NAME, "body") is not None + + +class TestBidiContextManagement: + """Test integration of context creation and management.""" + + def test_create_and_close_context(self, driver): + """Test creating and closing a user context.""" + new_context = driver.browser.create_user_context() + + try: + assert new_context is not None + finally: + driver.browser.remove_user_context(new_context) + + def test_multiple_contexts_creation(self, driver): + """Test creating multiple contexts.""" + context1 = driver.browser.create_user_context() + context2 = driver.browser.create_user_context() + + try: + assert context1 is not None + assert context2 is not None + assert context1 != context2 + finally: + driver.browser.remove_user_context(context1) + driver.browser.remove_user_context(context2) + + +class TestBidiEventHandlers: + """Test integration of event handlers.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test in this class.""" + pages.load("blank.html") + yield + # Cleanup: delete all cookies to prevent bleed-through + driver.delete_all_cookies() + + def test_multiple_console_handlers(self, driver): + """Test multiple console message handlers.""" + messages1 = [] + messages2 = [] + + handler1 = driver.script.add_console_message_handler(messages1.append) + handler2 = driver.script.add_console_message_handler(messages2.append) + + try: + driver.execute_script("console.log('test message');") + WebDriverWait(driver, 5).until( + lambda _: len(messages1) > 0 and len(messages2) > 0 + ) + + assert len(messages1) > 0 + assert len(messages2) > 0 + finally: + driver.script.remove_console_message_handler(handler1) + driver.script.remove_console_message_handler(handler2) + + +class TestBidiStorageOperations: + """Test storage operations.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test in this class.""" + driver.delete_all_cookies() + pages.load("blank.html") + yield + # Cleanup: delete all cookies to prevent bleed-through + driver.delete_all_cookies() + + def test_cookie_operations(self, driver, pages): + """Test basic cookie operations.""" + pages.load("blank.html") + + # Set cookie + driver.add_cookie({"name": "test", "value": "data"}) + + # Get cookies + cookies = driver.get_cookies() + assert any(c.get("name") == "test" for c in cookies) + + # Delete cookie + driver.delete_cookie("test") + + # Verify deletion + cookies_after = driver.get_cookies() + assert not any(c.get("name") == "test" for c in cookies_after) + + def test_cookie_attributes(self, driver, pages): + """Test cookie with various attributes.""" + pages.load("blank.html") + + driver.add_cookie( + {"name": "attr_cookie", "value": "test_value", "path": "/", "secure": False} + ) + + cookies = driver.get_cookies() + cookie = next((c for c in cookies if c.get("name") == "attr_cookie"), None) + + assert cookie is not None + assert cookie.get("value") == "test_value" + + +class TestBidiBrowsingContexts: + """Test browsing context operations.""" + + @pytest.fixture(autouse=True) + def setup(self, driver): + """Setup for each test in this class.""" + driver.delete_all_cookies() + yield + # Cleanup: delete all cookies to prevent bleed-through + driver.delete_all_cookies() + + def test_create_new_window(self, driver): + """Test creating a new window context.""" + # Create new tab + new_context = driver.browsing_context.create(type=WindowTypes.TAB) + + try: + assert new_context is not None + finally: + driver.browsing_context.close(new_context) + + def test_navigation_in_context(self, driver, pages): + """Test navigation in a specific context.""" + pages.load("blank.html") + + # Navigate using the BiDi API with the current context + driver.browsing_context.navigate( + context=driver.current_window_handle, url=pages.url("blank.html") + ) + + # Verify page loaded + element = driver.find_element(By.TAG_NAME, "body") + assert element is not None diff --git a/py/test/selenium/webdriver/common/bidi_log_tests.py b/py/test/selenium/webdriver/common/bidi_log_tests.py new file mode 100644 index 0000000000000..fbbd3a8166b2d --- /dev/null +++ b/py/test/selenium/webdriver/common/bidi_log_tests.py @@ -0,0 +1,161 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +import pytest + +from selenium.webdriver.support.ui import WebDriverWait + + +def test_log_module_initialized(driver): + """Test that the log module is initialized properly.""" + assert driver.script is not None + + +class TestBidiLogging: + """Test class for BiDi logging functionality.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test in this class.""" + pages.load("blank.html") + + def test_console_log_message(self, driver): + """Test capturing console.log messages.""" + log_entries = [] + + def callback(log_entry): + log_entries.append(log_entry) + + handler_id = driver.script.add_console_message_handler(callback) + + try: + driver.execute_script("console.log('test message');") + WebDriverWait(driver, 5).until(lambda _: log_entries) + + assert len(log_entries) > 0 + finally: + driver.script.remove_console_message_handler(handler_id) + + def test_console_multiple_messages(self, driver): + """Test capturing multiple console messages.""" + log_entries = [] + + handler_id = driver.script.add_console_message_handler(log_entries.append) + + try: + driver.execute_script( + """ + console.log('message 1'); + console.log('message 2'); + console.log('message 3'); + """ + ) + + WebDriverWait(driver, 5).until(lambda _: len(log_entries) >= 3) + + assert len(log_entries) >= 3 + finally: + driver.script.remove_console_message_handler(handler_id) + + def test_add_and_remove_handler(self, driver): + """Test adding and removing log handlers.""" + log_entries1 = [] + log_entries2 = [] + + handler_id1 = driver.script.add_console_message_handler(log_entries1.append) + handler_id2 = driver.script.add_console_message_handler(log_entries2.append) + + try: + driver.execute_script("console.log('first message');") + WebDriverWait(driver, 5).until( + lambda _: len(log_entries1) > 0 and len(log_entries2) > 0 + ) + + assert len(log_entries1) > 0 + assert len(log_entries2) > 0 + + # Remove first handler + driver.script.remove_console_message_handler(handler_id1) + + initial_count1 = len(log_entries1) + initial_count2 = len(log_entries2) + + # Trigger another message + driver.execute_script("console.log('second message');") + WebDriverWait(driver, 5).until(lambda _: len(log_entries2) > initial_count2) + + # First handler should not receive new messages + assert len(log_entries1) == initial_count1 + assert len(log_entries2) > initial_count2 + finally: + driver.script.remove_console_message_handler(handler_id2) + + def test_handler_receives_all_levels(self, driver): + """Test that a single handler can receive all log levels.""" + log_levels = [] + + def callback(entry): + log_levels.append(entry) + + handler_id = driver.script.add_console_message_handler(callback) + + try: + driver.execute_script( + """ + console.log('log'); + console.warn('warn'); + console.error('error'); + console.debug('debug'); + console.info('info'); + """ + ) + + WebDriverWait(driver, 5).until(lambda _: len(log_levels) >= 5) + + assert len(log_levels) >= 5 + finally: + driver.script.remove_console_message_handler(handler_id) + + def test_log_with_multiple_arguments(self, driver): + """Test console.log with multiple arguments.""" + log_entries = [] + + handler_id = driver.script.add_console_message_handler(log_entries.append) + + try: + driver.execute_script("console.log('arg1', 'arg2', 'arg3');") + WebDriverWait(driver, 5).until(lambda _: log_entries) + + assert len(log_entries) > 0 + finally: + driver.script.remove_console_message_handler(handler_id) + + def test_log_entry_attributes(self, driver): + """Test log entry has expected attributes.""" + log_entries = [] + + handler_id = driver.script.add_console_message_handler(log_entries.append) + + try: + driver.execute_script("console.log('test');") + WebDriverWait(driver, 5).until(lambda _: log_entries) + + assert len(log_entries) > 0 + assert hasattr(log_entries[0], "text") or hasattr(log_entries[0], "args") + finally: + driver.script.remove_console_message_handler(handler_id) diff --git a/py/test/selenium/webdriver/common/bidi_script_tests.py b/py/test/selenium/webdriver/common/bidi_script_tests.py index 35f8e455573be..5d5fc6ef9b780 100644 --- a/py/test/selenium/webdriver/common/bidi_script_tests.py +++ b/py/test/selenium/webdriver/common/bidi_script_tests.py @@ -17,6 +17,7 @@ import pytest +from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.bidi.log import LogLevel from selenium.webdriver.common.bidi.script import RealmType, ResultOwnership from selenium.webdriver.common.by import By @@ -41,18 +42,21 @@ def test_logs_console_messages(driver, pages): pages.load("bidi/logEntryAdded.html") log_entries = [] - driver.script.add_console_message_handler(log_entries.append) + handler_id = driver.script.add_console_message_handler(log_entries.append) - driver.find_element(By.ID, "jsException").click() - driver.find_element(By.ID, "consoleLog").click() + try: + driver.find_element(By.ID, "jsException").click() + driver.find_element(By.ID, "consoleLog").click() - WebDriverWait(driver, 5).until(lambda _: log_entries) + WebDriverWait(driver, 5).until(lambda _: log_entries) - log_entry = log_entries[0] - assert log_entry.level == LogLevel.INFO - assert log_entry.method == "log" - assert log_entry.text == "Hello, world!" - assert log_entry.type_ == "console" + log_entry = log_entries[0] + assert log_entry.level == LogLevel.INFO + assert log_entry.method == "log" + assert log_entry.text == "Hello, world!" + assert log_entry.type_ == "console" + finally: + driver.script.remove_console_message_handler(handler_id) def test_logs_console_errors(driver, pages): @@ -63,34 +67,41 @@ def log_error(entry): if entry.level == LogLevel.ERROR: log_entries.append(entry) - driver.script.add_console_message_handler(log_error) + handler_id = driver.script.add_console_message_handler(log_error) - driver.find_element(By.ID, "consoleLog").click() - driver.find_element(By.ID, "consoleError").click() + try: + driver.find_element(By.ID, "consoleLog").click() + driver.find_element(By.ID, "consoleError").click() - WebDriverWait(driver, 5).until(lambda _: log_entries) + WebDriverWait(driver, 5).until(lambda _: log_entries) - assert len(log_entries) == 1 + assert len(log_entries) == 1 - log_entry = log_entries[0] - assert log_entry.level == LogLevel.ERROR - assert log_entry.method == "error" - assert log_entry.text == "I am console error" - assert log_entry.type_ == "console" + log_entry = log_entries[0] + assert log_entry.level == LogLevel.ERROR + assert log_entry.method == "error" + assert log_entry.text == "I am console error" + assert log_entry.type_ == "console" + finally: + driver.script.remove_console_message_handler(handler_id) def test_logs_multiple_console_messages(driver, pages): pages.load("bidi/logEntryAdded.html") log_entries = [] - driver.script.add_console_message_handler(log_entries.append) - driver.script.add_console_message_handler(log_entries.append) + handler_id1 = driver.script.add_console_message_handler(log_entries.append) + handler_id2 = driver.script.add_console_message_handler(log_entries.append) - driver.find_element(By.ID, "jsException").click() - driver.find_element(By.ID, "consoleLog").click() + try: + driver.find_element(By.ID, "jsException").click() + driver.find_element(By.ID, "consoleLog").click() - WebDriverWait(driver, 5).until(lambda _: len(log_entries) > 1) - assert len(log_entries) == 2 + WebDriverWait(driver, 5).until(lambda _: len(log_entries) > 1) + assert len(log_entries) == 2 + finally: + driver.script.remove_console_message_handler(handler_id1) + driver.script.remove_console_message_handler(handler_id2) def test_removes_console_message_handler(driver, pages): @@ -99,32 +110,41 @@ def test_removes_console_message_handler(driver, pages): log_entries1 = [] log_entries2 = [] - id = driver.script.add_console_message_handler(log_entries1.append) - driver.script.add_console_message_handler(log_entries2.append) + id1 = driver.script.add_console_message_handler(log_entries1.append) + id2 = driver.script.add_console_message_handler(log_entries2.append) - driver.find_element(By.ID, "consoleLog").click() - WebDriverWait(driver, 5).until(lambda _: len(log_entries1) and len(log_entries2)) + try: + driver.find_element(By.ID, "consoleLog").click() + WebDriverWait(driver, 5).until( + lambda _: len(log_entries1) and len(log_entries2) + ) - driver.script.remove_console_message_handler(id) - driver.find_element(By.ID, "consoleLog").click() + driver.script.remove_console_message_handler(id1) + driver.find_element(By.ID, "consoleLog").click() - WebDriverWait(driver, 5).until(lambda _: len(log_entries2) == 2) - assert len(log_entries1) == 1 + WebDriverWait(driver, 5).until(lambda _: len(log_entries2) == 2) + assert len(log_entries1) == 1 + finally: + driver.script.remove_console_message_handler(id1) + driver.script.remove_console_message_handler(id2) def test_javascript_error_messages(driver, pages): pages.load("bidi/logEntryAdded.html") log_entries = [] - driver.script.add_javascript_error_handler(log_entries.append) + handler_id = driver.script.add_javascript_error_handler(log_entries.append) - driver.find_element(By.ID, "jsException").click() - WebDriverWait(driver, 5).until(lambda _: log_entries) + try: + driver.find_element(By.ID, "jsException").click() + WebDriverWait(driver, 5).until(lambda _: log_entries) - log_entry = log_entries[0] - assert log_entry.text == "Error: Not working" - assert log_entry.level == LogLevel.ERROR - assert log_entry.type_ == "javascript" + log_entry = log_entries[0] + assert log_entry.text == "Error: Not working" + assert log_entry.level == LogLevel.ERROR + assert log_entry.type_ == "javascript" + finally: + driver.script.remove_javascript_error_handler(handler_id) def test_removes_javascript_message_handler(driver, pages): @@ -133,17 +153,23 @@ def test_removes_javascript_message_handler(driver, pages): log_entries1 = [] log_entries2 = [] - id = driver.script.add_javascript_error_handler(log_entries1.append) - driver.script.add_javascript_error_handler(log_entries2.append) + id1 = driver.script.add_javascript_error_handler(log_entries1.append) + id2 = driver.script.add_javascript_error_handler(log_entries2.append) - driver.find_element(By.ID, "jsException").click() - WebDriverWait(driver, 5).until(lambda _: len(log_entries1) and len(log_entries2)) + try: + driver.find_element(By.ID, "jsException").click() + WebDriverWait(driver, 5).until( + lambda _: len(log_entries1) and len(log_entries2) + ) - driver.script.remove_javascript_error_handler(id) - driver.find_element(By.ID, "jsException").click() + driver.script.remove_javascript_error_handler(id1) + driver.find_element(By.ID, "jsException").click() - WebDriverWait(driver, 5).until(lambda _: len(log_entries2) == 2) - assert len(log_entries1) == 1 + WebDriverWait(driver, 5).until(lambda _: len(log_entries2) == 2) + assert len(log_entries1) == 1 + finally: + driver.script.remove_javascript_error_handler(id1) + driver.script.remove_javascript_error_handler(id2) def test_add_preload_script(driver, pages): @@ -159,7 +185,9 @@ def test_add_preload_script(driver, pages): # Check if the preload script was executed result = driver.script._evaluate( - "window.preloadExecuted", {"context": driver.current_window_handle}, await_promise=False + "window.preloadExecuted", + {"context": driver.current_window_handle}, + await_promise=False, ) assert result.result["value"] is True @@ -168,15 +196,21 @@ def test_add_preload_script_with_arguments(driver, pages): """Test adding a preload script with channel arguments.""" function_declaration = "(channelFunc) => { channelFunc('test_value'); window.preloadValue = 'received'; }" - arguments = [{"type": "channel", "value": {"channel": "test-channel", "ownership": "root"}}] + arguments = [ + {"type": "channel", "value": {"channel": "test-channel", "ownership": "root"}} + ] - script_id = driver.script._add_preload_script(function_declaration, arguments=arguments) + script_id = driver.script._add_preload_script( + function_declaration, arguments=arguments + ) assert script_id is not None pages.load("blank.html") result = driver.script._evaluate( - "window.preloadValue", {"context": driver.current_window_handle}, await_promise=False + "window.preloadValue", + {"context": driver.current_window_handle}, + await_promise=False, ) assert result.result["value"] == "received" @@ -186,13 +220,17 @@ def test_add_preload_script_with_contexts(driver, pages): function_declaration = "() => { window.contextSpecific = true; }" contexts = [driver.current_window_handle] - script_id = driver.script._add_preload_script(function_declaration, contexts=contexts) + script_id = driver.script._add_preload_script( + function_declaration, contexts=contexts + ) assert script_id is not None pages.load("blank.html") result = driver.script._evaluate( - "window.contextSpecific", {"context": driver.current_window_handle}, await_promise=False + "window.contextSpecific", + {"context": driver.current_window_handle}, + await_promise=False, ) assert result.result["value"] is True @@ -200,36 +238,50 @@ def test_add_preload_script_with_contexts(driver, pages): def test_add_preload_script_with_user_contexts(driver, pages): """Test adding a preload script with user contexts.""" function_declaration = "() => { window.contextSpecific = true; }" + original_handle = driver.current_window_handle user_context = driver.browser.create_user_context() context1 = driver.browsing_context.create(type="window", user_context=user_context) driver.switch_to.window(context1) - user_contexts = [user_context] + try: + user_contexts = [user_context] - script_id = driver.script._add_preload_script(function_declaration, user_contexts=user_contexts) - assert script_id is not None + script_id = driver.script._add_preload_script( + function_declaration, user_contexts=user_contexts + ) + assert script_id is not None - pages.load("blank.html") + pages.load("blank.html") - result = driver.script._evaluate( - "window.contextSpecific", {"context": driver.current_window_handle}, await_promise=False - ) - assert result.result["value"] is True + result = driver.script._evaluate( + "window.contextSpecific", + {"context": driver.current_window_handle}, + await_promise=False, + ) + assert result.result["value"] is True + finally: + driver.switch_to.window(original_handle) + driver.browsing_context.close(context1) + driver.browser.remove_user_context(user_context) def test_add_preload_script_with_sandbox(driver, pages): """Test adding a preload script with sandbox.""" function_declaration = "() => { window.sandboxScript = true; }" - script_id = driver.script._add_preload_script(function_declaration, sandbox="test-sandbox") + script_id = driver.script._add_preload_script( + function_declaration, sandbox="test-sandbox" + ) assert script_id is not None pages.load("blank.html") # calling evaluate without sandbox should return undefined result = driver.script._evaluate( - "window.sandboxScript", {"context": driver.current_window_handle}, await_promise=False + "window.sandboxScript", + {"context": driver.current_window_handle}, + await_promise=False, ) assert result.result["type"] == "undefined" @@ -246,8 +298,12 @@ def test_add_preload_script_invalid_arguments(driver): """Test that providing both contexts and user_contexts raises an error.""" function_declaration = "() => {}" - with pytest.raises(ValueError, match="Cannot specify both contexts and user_contexts"): - driver.script._add_preload_script(function_declaration, contexts=["context1"], user_contexts=["user1"]) + with pytest.raises( + ValueError, match="Cannot specify both contexts and user_contexts" + ): + driver.script._add_preload_script( + function_declaration, contexts=["context1"], user_contexts=["user1"] + ) def test_remove_preload_script(driver, pages): @@ -262,7 +318,9 @@ def test_remove_preload_script(driver, pages): # The script should not have executed result = driver.script._evaluate( - "typeof window.removableScript", {"context": driver.current_window_handle}, await_promise=False + "typeof window.removableScript", + {"context": driver.current_window_handle}, + await_promise=False, ) assert result.result["value"] == "undefined" @@ -271,7 +329,9 @@ def test_evaluate_expression(driver, pages): """Test evaluating a simple expression.""" pages.load("blank.html") - result = driver.script._evaluate("1 + 2", {"context": driver.current_window_handle}, await_promise=False) + result = driver.script._evaluate( + "1 + 2", {"context": driver.current_window_handle}, await_promise=False + ) assert result.realm is not None assert result.result["type"] == "number" @@ -284,7 +344,9 @@ def test_evaluate_with_await_promise(driver, pages): pages.load("blank.html") result = driver.script._evaluate( - "Promise.resolve(42)", {"context": driver.current_window_handle}, await_promise=True + "Promise.resolve(42)", + {"context": driver.current_window_handle}, + await_promise=True, ) assert result.result["type"] == "number" @@ -296,7 +358,9 @@ def test_evaluate_with_exception(driver, pages): pages.load("blank.html") result = driver.script._evaluate( - "throw new Error('Test error')", {"context": driver.current_window_handle}, await_promise=False + "throw new Error('Test error')", + {"context": driver.current_window_handle}, + await_promise=False, ) assert result.exception_details is not None @@ -334,7 +398,11 @@ def test_evaluate_with_serialization_options(driver, pages): """Test evaluating with serialization options.""" pages.load("shadowRootPage.html") - serialization_options = {"maxDomDepth": 2, "maxObjectDepth": 2, "includeShadowTree": "all"} + serialization_options = { + "maxDomDepth": 2, + "maxObjectDepth": 2, + "includeShadowTree": "all", + } result = driver.script._evaluate( "document.body", @@ -386,7 +454,9 @@ def test_call_function_with_this(driver, pages): # First set up an object driver.script._evaluate( - "window.testObj = { value: 10 }", {"context": driver.current_window_handle}, await_promise=False + "window.testObj = { value: 10 }", + {"context": driver.current_window_handle}, + await_promise=False, ) result = driver.script._call_function( @@ -419,7 +489,11 @@ def test_call_function_with_serialization_options(driver, pages): """Test calling a function with serialization options.""" pages.load("shadowRootPage.html") - serialization_options = {"maxDomDepth": 2, "maxObjectDepth": 2, "includeShadowTree": "all"} + serialization_options = { + "maxDomDepth": 2, + "maxObjectDepth": 2, + "includeShadowTree": "all", + } result = driver.script._call_function( "() => document.body", @@ -455,7 +529,9 @@ def test_call_function_with_await_promise(driver, pages): pages.load("blank.html") result = driver.script._call_function( - "() => Promise.resolve('async result')", await_promise=True, target={"context": driver.current_window_handle} + "() => Promise.resolve('async result')", + await_promise=True, + target={"context": driver.current_window_handle}, ) assert result.result["type"] == "string" @@ -534,7 +610,10 @@ def test_disown_handles(driver, pages): # Create an object with root ownership (this will return a handle) result = driver.script._evaluate( - "({foo: 'bar'})", target={"context": driver.current_window_handle}, await_promise=False, result_ownership="root" + "({foo: 'bar'})", + target={"context": driver.current_window_handle}, + await_promise=False, + result_ownership="root", ) handle = result.result["handle"] @@ -551,7 +630,9 @@ def test_disown_handles(driver, pages): assert result_before.result["value"] == "bar" # Disown the handle - driver.script._disown(handles=[handle], target={"context": driver.current_window_handle}) + driver.script._disown( + handles=[handle], target={"context": driver.current_window_handle} + ) # Try using the disowned handle (this should fail) with pytest.raises(Exception): @@ -814,8 +895,6 @@ def test_execute_script_with_exception(driver, pages): """Test executing script that throws an exception.""" pages.load("blank.html") - from selenium.common.exceptions import WebDriverException - with pytest.raises(WebDriverException) as exc_info: driver.script.execute( """() => { @@ -870,3 +949,438 @@ def test_execute_script_with_nested_objects(driver, pages): assert value_dict["userName"] == "John" assert value_dict["userAge"] == 30 assert value_dict["hobbyCount"] == 2 + + +class TestBidiScriptExecution: + """Test script execution via execute_script.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test.""" + pages.load("blank.html") + + def test_execute_script_returns_string(self, driver): + """Test executing script that returns string.""" + result = driver.execute_script("return 'hello';") + assert result == "hello" + + def test_execute_script_returns_number(self, driver): + """Test executing script that returns number.""" + result = driver.execute_script("return 42;") + assert result == 42 + + def test_execute_script_returns_boolean(self, driver): + """Test executing script that returns boolean.""" + result = driver.execute_script("return true;") + assert result is True + + def test_execute_script_returns_null(self, driver): + """Test executing script that returns null.""" + result = driver.execute_script("return null;") + assert result is None + + def test_execute_script_returns_object(self, driver): + """Test executing script that returns object.""" + result = driver.execute_script("return {x: 1, y: 2};") + assert isinstance(result, dict) + assert result["x"] == 1 + + def test_execute_script_returns_array(self, driver): + """Test executing script that returns array.""" + result = driver.execute_script("return [1, 2, 3, 4, 5];") + assert isinstance(result, list) + assert len(result) == 5 + + def test_execute_script_dom_query(self, driver, pages): + """Test executing script that queries DOM.""" + pages.load("formPage.html") + result = driver.execute_script( + "return document.querySelectorAll('input').length;" + ) + assert result > 0 + + def test_execute_script_with_arguments(self, driver): + """Test executing script with arguments.""" + result = driver.execute_script("return arguments[0] * arguments[1];", 3, 5) + assert result == 15 + + +class TestBidiScriptGlobalState: + """Test script execution with global state management.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test.""" + pages.load("blank.html") + + def test_global_state_persistence(self, driver): + """Test that global state persists across script calls.""" + driver.execute_script("window.testVar = 42;") + result = driver.execute_script("return window.testVar;") + assert result == 42 + + def test_multiple_global_variables(self, driver): + """Test managing multiple global variables.""" + driver.execute_script( + """ + window.var1 = 'first'; + window.var2 = 'second'; + window.var3 = 'third'; + """ + ) + + result = driver.execute_script( + """ + return { + v1: window.var1, + v2: window.var2, + v3: window.var3 + }; + """ + ) + + assert result["v1"] == "first" + assert result["v2"] == "second" + assert result["v3"] == "third" + + def test_function_definition_in_global_scope(self, driver): + """Test defining functions in global scope.""" + driver.execute_script( + """ + window.multiply = function(a, b) { + return a * b; + }; + """ + ) + + result = driver.execute_script("return window.multiply(3, 7);") + assert result == 21 + + def test_complex_object_in_global_scope(self, driver): + """Test storing complex objects globally.""" + driver.execute_script( + """ + window.data = { + users: [ + {name: 'Alice', age: 30}, + {name: 'Bob', age: 25} + ], + metadata: { + version: '1.0', + timestamp: Date.now() + } + }; + """ + ) + + result = driver.execute_script("return window.data.users.length;") + assert result == 2 + + +class TestBidiScriptPreloadScripts: + """Test preload script lifecycle and edge cases.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test.""" + pages.load("blank.html") + + def test_multiple_preload_scripts(self, driver, pages): + """Test adding multiple preload scripts.""" + id1 = driver.script._add_preload_script("() => { window.test1 = 'loaded'; }") + id2 = driver.script._add_preload_script("() => { window.test2 = 'loaded'; }") + + try: + pages.load("blank.html") + + result1 = driver.script._evaluate( + "window.test1", + {"context": driver.current_window_handle}, + await_promise=False, + ) + result2 = driver.script._evaluate( + "window.test2", + {"context": driver.current_window_handle}, + await_promise=False, + ) + + assert result1.result["value"] == "loaded" + assert result2.result["value"] == "loaded" + finally: + driver.script._remove_preload_script(script_id=id1) + driver.script._remove_preload_script(script_id=id2) + + def test_preload_script_with_function(self, driver, pages): + """Test preload script defining functions.""" + script_id = driver.script._add_preload_script( + "() => { window.customFunc = (x) => x * 2; }" + ) + + try: + pages.load("blank.html") + result = driver.script._evaluate( + "window.customFunc(5)", + {"context": driver.current_window_handle}, + await_promise=False, + ) + assert result.result["value"] == 10 + finally: + driver.script._remove_preload_script(script_id=script_id) + + def test_preload_script_removal_prevents_execution(self, driver, pages): + """Test that removing preload script prevents its execution.""" + script_id = driver.script._add_preload_script( + "() => { window.shouldNotExist = true; }" + ) + driver.script._remove_preload_script(script_id=script_id) + + pages.load("blank.html") + result = driver.script._evaluate( + "typeof window.shouldNotExist", + {"context": driver.current_window_handle}, + await_promise=False, + ) + assert result.result["value"] == "undefined" + + def test_preload_script_with_dom_manipulation(self, driver, pages): + """Test preload script that manipulates DOM.""" + script_id = driver.script._add_preload_script( + """ + () => { + document.addEventListener('DOMContentLoaded', function() { + var div = document.createElement('div'); + div.id = 'injected-element'; + div.textContent = 'injected'; + document.body.appendChild(div); + }); + } + """ + ) + + try: + pages.load("blank.html") + element = driver.find_element(By.ID, "injected-element") + assert element is not None + assert element.text == "injected" + finally: + driver.script._remove_preload_script(script_id=script_id) + + +class TestBidiScriptContextManagement: + """Test script execution across browsing contexts.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test.""" + pages.load("blank.html") + + def test_script_executes_in_current_context(self, driver): + """Test that scripts execute in the current browsing context.""" + # Set variable in current context + driver.execute_script("window.contextVar = 'main';") + + # Verify it's accessible + result = driver.execute_script("return window.contextVar;") + assert result == "main" + + def test_multiple_navigations_maintain_context(self, driver, pages): + """Test script context changes with navigation.""" + # Load first page + pages.load("blank.html") + driver.execute_script("window.page = 'blank';") + + # Load second page - context should reset + pages.load("formPage.html") + result = driver.execute_script("return window.page;") + assert result is None + + # Set new value + driver.execute_script("window.page = 'form';") + result = driver.execute_script("return window.page;") + assert result == "form" + + def test_script_can_access_dom_elements(self, driver, pages): + """Test that scripts can access and manipulate DOM.""" + pages.load("formPage.html") + + # Find element count + result = driver.execute_script( + """ + return document.querySelectorAll('input[type="text"]').length; + """ + ) + assert result > 0 + + def test_script_context_with_console_handler(self, driver, pages): + """Test script execution with console message handler active.""" + log_entries = [] + handler_id = driver.script.add_console_message_handler(log_entries.append) + + try: + pages.load("bidi/logEntryAdded.html") + driver.execute_script("console.log('test message');") + + # Give some time for handler to capture + WebDriverWait(driver, 3).until(lambda _: log_entries) + assert len(log_entries) > 0 + finally: + driver.script.remove_console_message_handler(handler_id) + + def test_script_error_handler_active(self, driver, pages): + """Test script execution with error handler active.""" + errors = [] + handler_id = driver.script.add_javascript_error_handler(errors.append) + + try: + pages.load("bidi/logEntryAdded.html") + # Click element that triggers JS error + driver.find_element(By.ID, "jsException").click() + + # Give time for error handler to capture + WebDriverWait(driver, 5).until(lambda _: errors) + assert len(errors) > 0 + finally: + driver.script.remove_javascript_error_handler(handler_id) + + +class TestBidiScriptComplexOperations: + """Test complex script operations and edge cases.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test.""" + pages.load("blank.html") + + def test_execute_script_with_timeout(self, driver): + """Test script execution within time constraints.""" + # Execute script that completes quickly + result = driver.execute_script( + """ + return new Promise((resolve) => { + setTimeout(() => resolve('completed'), 10); + }); + """ + ) + # Note: synchronous execute_script may not wait for promises + # This just tests that the method handles the call + assert result is not None + + def test_execute_script_with_dom_creation(self, driver): + """Test script that creates and manipulates DOM.""" + driver.execute_script( + """ + const div = document.createElement('div'); + div.id = 'created-element'; + div.textContent = 'Created by script'; + document.body.appendChild(div); + """ + ) + + # Verify element was created + result = driver.execute_script( + """ + const elem = document.getElementById('created-element'); + return elem ? elem.textContent : null; + """ + ) + assert result == "Created by script" + + def test_execute_script_with_nested_objects(self, driver): + """Test script that returns deeply nested objects.""" + result = driver.execute_script( + """ + return { + level1: { + level2: { + level3: { + value: 'deep' + } + } + } + }; + """ + ) + + assert result["level1"]["level2"]["level3"]["value"] == "deep" + + def test_execute_script_with_exception_handling(self, driver): + """Test script that handles exceptions internally.""" + result = driver.execute_script( + """ + try { + throw new Error('test error'); + } catch (e) { + return 'error caught: ' + e.message; + } + """ + ) + assert "error caught" in result + + +class TestBidiScriptErrorHandling: + """Test script error and logging scenarios.""" + + @pytest.fixture(autouse=True) + def setup(self, driver, pages): + """Setup for each test.""" + pages.load("blank.html") + + def test_script_error_handler_captures_errors(self, driver, pages): + """Test that error handler can capture script errors.""" + errors = [] + + def error_handler(entry): + errors.append(entry) + + handler_id = driver.script.add_javascript_error_handler(error_handler) + + try: + pages.load("bidi/logEntryAdded.html") + driver.find_element(By.ID, "jsException").click() + + WebDriverWait(driver, 5).until(lambda _: errors) + assert len(errors) > 0 + finally: + driver.script.remove_javascript_error_handler(handler_id) + + def test_multiple_error_handlers(self, driver, pages): + """Test multiple error handlers can be registered.""" + errors1 = [] + errors2 = [] + + handler_id1 = driver.script.add_javascript_error_handler(errors1.append) + handler_id2 = driver.script.add_javascript_error_handler(errors2.append) + + try: + pages.load("bidi/logEntryAdded.html") + driver.find_element(By.ID, "jsException").click() + + # Both handlers should receive events when error occurs + WebDriverWait(driver, 5).until( + lambda _: len(errors1) > 0 and len(errors2) > 0 + ) + assert len(errors1) > 0 + assert len(errors2) > 0 + finally: + driver.script.remove_javascript_error_handler(handler_id1) + driver.script.remove_javascript_error_handler(handler_id2) + + def test_console_message_with_logging(self, driver, pages): + """Test console message handler with actual logging.""" + log_entries = [] + handler_id = driver.script.add_console_message_handler(log_entries.append) + + try: + pages.load("bidi/logEntryAdded.html") + driver.find_element(By.ID, "consoleLog").click() + + WebDriverWait(driver, 5).until(lambda _: log_entries) + assert len(log_entries) > 0 + finally: + driver.script.remove_console_message_handler(handler_id) + + def test_execute_script_syntax_error(self, driver): + """Test executing script with syntax errors.""" + # This should raise an exception + with pytest.raises(Exception): + driver.execute_script("{{invalid syntax}}") diff --git a/py/test/selenium/webdriver/common/bidi_storage_tests.py b/py/test/selenium/webdriver/common/bidi_storage_tests.py index 01fb375f7c39a..157df78dd3cd9 100644 --- a/py/test/selenium/webdriver/common/bidi_storage_tests.py +++ b/py/test/selenium/webdriver/common/bidi_storage_tests.py @@ -98,7 +98,9 @@ def test_get_cookie_by_name(self, driver, pages, webserver): driver.add_cookie({"name": key, "value": value}) # Test - cookie_filter = CookieFilter(name=key, value=BytesValue(BytesValue.TYPE_STRING, "set")) + cookie_filter = CookieFilter( + name=key, value=BytesValue(BytesValue.TYPE_STRING, "set") + ) result = driver.storage.get_cookies(filter=cookie_filter) @@ -120,14 +122,18 @@ def test_get_cookie_in_default_user_context(self, driver, pages, webserver): driver.add_cookie({"name": key, "value": value}) # Test - cookie_filter = CookieFilter(name=key, value=BytesValue(BytesValue.TYPE_STRING, "set")) + cookie_filter = CookieFilter( + name=key, value=BytesValue(BytesValue.TYPE_STRING, "set") + ) driver.switch_to.new_window(WindowTypes.WINDOW) descriptor = BrowsingContextPartitionDescriptor(driver.current_window_handle) params = cookie_filter - result_after_switching_context = driver.storage.get_cookies(filter=params, partition=descriptor) + result_after_switching_context = driver.storage.get_cookies( + filter=params, partition=descriptor + ) assert len(result_after_switching_context.cookies) > 0 assert result_after_switching_context.cookies[0].value.value == value @@ -158,15 +164,21 @@ def test_get_cookie_in_a_user_context(self, driver, pages, webserver): descriptor = StorageKeyPartitionDescriptor(user_context=user_context) - parameters = PartialCookie(key, BytesValue(BytesValue.TYPE_STRING, value), webserver.host) + parameters = PartialCookie( + key, BytesValue(BytesValue.TYPE_STRING, value), webserver.host + ) driver.storage.set_cookie(cookie=parameters, partition=descriptor) # Test - cookie_filter = CookieFilter(name=key, value=BytesValue(BytesValue.TYPE_STRING, "set")) + cookie_filter = CookieFilter( + name=key, value=BytesValue(BytesValue.TYPE_STRING, "set") + ) # Create a new window with the user context - new_window = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context) + new_window = driver.browsing_context.create( + type=WindowTypes.TAB, user_context=user_context + ) driver.switch_to.window(new_window) @@ -181,9 +193,13 @@ def test_get_cookie_in_a_user_context(self, driver, pages, webserver): driver.switch_to.window(window_handle) - browsing_context_partition_descriptor = BrowsingContextPartitionDescriptor(window_handle) + browsing_context_partition_descriptor = BrowsingContextPartitionDescriptor( + window_handle + ) - result1 = driver.storage.get_cookies(filter=cookie_filter, partition=browsing_context_partition_descriptor) + result1 = driver.storage.get_cookies( + filter=cookie_filter, partition=browsing_context_partition_descriptor + ) assert len(result1.cookies) == 0 @@ -198,7 +214,9 @@ def test_add_cookie(self, driver, pages, webserver): key = generate_unique_key() value = "foo" - parameters = PartialCookie(key, BytesValue(BytesValue.TYPE_STRING, value), webserver.host) + parameters = PartialCookie( + key, BytesValue(BytesValue.TYPE_STRING, value), webserver.host + ) assert_cookie_is_not_present_with_name(driver, key) # Test @@ -223,7 +241,14 @@ def test_add_and_get_cookie(self, driver, pages, webserver): path = "/simpleTest.html" cookie = PartialCookie( - "fish", value, domain, path=path, http_only=True, secure=False, same_site=SameSite.LAX, expiry=expiry + "fish", + value, + domain, + path=path, + http_only=True, + secure=False, + same_site=SameSite.LAX, + expiry=expiry, ) # Test @@ -336,10 +361,18 @@ def test_add_cookies_with_different_paths(self, driver, pages, webserver): assert_no_cookies_are_present(driver) cookie1 = PartialCookie( - "fish", BytesValue(BytesValue.TYPE_STRING, "cod"), webserver.host, path="/simpleTest.html" + "fish", + BytesValue(BytesValue.TYPE_STRING, "cod"), + webserver.host, + path="/simpleTest.html", ) - cookie2 = PartialCookie("planet", BytesValue(BytesValue.TYPE_STRING, "earth"), webserver.host, path="/") + cookie2 = PartialCookie( + "planet", + BytesValue(BytesValue.TYPE_STRING, "earth"), + webserver.host, + path="/", + ) # Test driver.storage.set_cookie(cookie=cookie1) @@ -353,3 +386,377 @@ def test_add_cookies_with_different_paths(self, driver, pages, webserver): driver.get(pages.url("formPage.html")) assert_cookie_is_not_present_with_name(driver, "fish") + + def test_delete_cookies_by_name_filter(self, driver, pages, webserver): + """Test deleting cookies with specific name filter.""" + assert_no_cookies_are_present(driver) + + key1 = generate_unique_key() + key2 = generate_unique_key() + key3 = generate_unique_key() + + driver.add_cookie({"name": key1, "value": "value1"}) + driver.add_cookie({"name": key2, "value": "value2"}) + driver.add_cookie({"name": key3, "value": "value3"}) + + # Delete only key1 + driver.storage.delete_cookies(filter=CookieFilter(name=key1)) + + # Verify + assert_cookie_is_not_present_with_name(driver, key1) + assert_cookie_is_present_with_name(driver, key2) + assert_cookie_is_present_with_name(driver, key3) + + def test_delete_cookies_multiple_filters(self, driver, pages, webserver): + """Test deleting cookies with multiple filter criteria.""" + assert_no_cookies_are_present(driver) + + key = "multi_filter_delete_test" + value = BytesValue(BytesValue.TYPE_STRING, "test_value") + + # Create two cookies with same name but different http_only attributes + # This ensures the http_only filter actually affects which cookies are deleted + cookie1 = PartialCookie(key, value, webserver.host, http_only=True) + cookie2 = PartialCookie(key, value, webserver.host, http_only=False) + + driver.storage.set_cookie(cookie=cookie1) + driver.storage.set_cookie(cookie=cookie2) + + # Delete only http_only cookies - the http_only filter should actually matter here + driver.storage.delete_cookies(filter=CookieFilter(name=key, http_only=True)) + + # Verify - only the http_only=True cookie should be deleted + result = driver.storage.get_cookies(filter=CookieFilter(name=key)) + + # Should have one cookie remaining (the http_only=False one) + assert len(result.cookies) == 1 + assert result.cookies[0].http_only is False + + def test_delete_cookies_empty_filter(self, driver, pages, webserver): + """Test deleting with empty filter deletes all cookies.""" + assert_no_cookies_are_present(driver) + + # Add multiple cookies + for i in range(3): + driver.add_cookie({"name": f"cookie_{i}", "value": f"value_{i}"}) + + assert_some_cookies_are_present(driver) + + # Delete with empty filter + driver.storage.delete_cookies(filter=CookieFilter()) + + # Verify all deleted + assert_no_cookies_are_present(driver) + + def test_set_cookie_with_http_only_attribute(self, driver, pages, webserver): + """Test setting a cookie with http_only attribute.""" + assert_no_cookies_are_present(driver) + + key = "http_only_cookie" + value = BytesValue(BytesValue.TYPE_STRING, "protected") + + cookie = PartialCookie(key, value, webserver.host, http_only=True) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify + cookie_filter = CookieFilter(name=key, http_only=True) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].http_only is True + + def test_set_cookie_with_secure_attribute(self, driver, pages, webserver): + """Test setting a cookie with secure attribute.""" + assert_no_cookies_are_present(driver) + + key = "secure_cookie" + value = BytesValue(BytesValue.TYPE_STRING, "encrypted") + + cookie = PartialCookie(key, value, webserver.host, secure=True) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify + cookie_filter = CookieFilter(name=key, secure=True) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].secure is True + + def test_set_cookie_with_same_site_strict(self, driver, pages, webserver): + """Test setting a cookie with SameSite=Strict.""" + assert_no_cookies_are_present(driver) + + key = "samesite_strict" + value = BytesValue(BytesValue.TYPE_STRING, "strict") + + cookie = PartialCookie(key, value, webserver.host, same_site=SameSite.STRICT) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify + cookie_filter = CookieFilter(name=key, same_site=SameSite.STRICT) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].same_site == SameSite.STRICT + + def test_set_cookie_with_same_site_lax(self, driver, pages, webserver): + """Test setting a cookie with SameSite=Lax.""" + assert_no_cookies_are_present(driver) + + key = "samesite_lax" + value = BytesValue(BytesValue.TYPE_STRING, "lax") + + cookie = PartialCookie(key, value, webserver.host, same_site=SameSite.LAX) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify + cookie_filter = CookieFilter(name=key, same_site=SameSite.LAX) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].same_site == SameSite.LAX + + def test_set_cookie_with_same_site_none(self, driver, pages, webserver): + """Test setting a cookie with SameSite=None (requires Secure).""" + assert_no_cookies_are_present(driver) + + key = "samesite_none" + value = BytesValue(BytesValue.TYPE_STRING, "none") + + # SameSite=None typically requires secure=True + cookie = PartialCookie( + key, value, webserver.host, same_site=SameSite.NONE, secure=True + ) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify + cookie_filter = CookieFilter(name=key, same_site=SameSite.NONE) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].same_site == SameSite.NONE + + def test_set_cookie_with_path_and_domain(self, driver, pages, webserver): + """Test setting a cookie with specific path and domain.""" + assert_no_cookies_are_present(driver) + + key = "path_domain_cookie" + value = BytesValue(BytesValue.TYPE_STRING, "scoped") + path = "/simpleTest.html" + + cookie = PartialCookie(key, value, webserver.host, path=path) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify + cookie_filter = CookieFilter(name=key, path=path) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].path == path + assert result.cookies[0].domain == webserver.host + + def test_set_cookie_with_future_expiry(self, driver, pages, webserver): + """Test setting a cookie with a future expiry date.""" + assert_no_cookies_are_present(driver) + + key = "future_expiry_cookie" + value = BytesValue(BytesValue.TYPE_STRING, "future") + + # Set expiry to 1 hour from now + future_expiry = int(time.time() + 3600) + + cookie = PartialCookie(key, value, webserver.host, expiry=future_expiry) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify + cookie_filter = CookieFilter(name=key) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].expiry == future_expiry + + def test_set_cookie_with_string_value(self, driver, pages, webserver): + """Test setting a cookie with string value (standard format).""" + assert_no_cookies_are_present(driver) + + key = "string_value_cookie" + value = BytesValue(BytesValue.TYPE_STRING, "hello") + + cookie = PartialCookie(key, value, webserver.host) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify + cookie_filter = CookieFilter(name=key) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].value.value == "hello" + + def test_get_cookies_filter_by_domain(self, driver, pages, webserver): + """Test getting cookies filtered by domain.""" + assert_no_cookies_are_present(driver) + + key = generate_unique_key() + value = BytesValue(BytesValue.TYPE_STRING, "domain_test") + + cookie = PartialCookie(key, value, webserver.host) + driver.storage.set_cookie(cookie=cookie) + + # Filter by domain + cookie_filter = CookieFilter(domain=webserver.host) + result = driver.storage.get_cookies(filter=cookie_filter) + + # Should find the cookie + cookie_names = [c.name for c in result.cookies] + assert key in cookie_names + + def test_get_cookies_filter_by_path(self, driver, pages, webserver): + """Test getting cookies filtered by path.""" + assert_no_cookies_are_present(driver) + + key1 = generate_unique_key() + key2 = generate_unique_key() + value = BytesValue(BytesValue.TYPE_STRING, "path_test") + + # Cookie with specific path + cookie1 = PartialCookie(key1, value, webserver.host, path="/simpleTest.html") + # Cookie with root path + cookie2 = PartialCookie(key2, value, webserver.host, path="/") + + driver.storage.set_cookie(cookie=cookie1) + driver.storage.set_cookie(cookie=cookie2) + + # Filter by specific path + cookie_filter = CookieFilter(path="/simpleTest.html") + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert all(c.path == "/simpleTest.html" for c in result.cookies) + + def test_multiple_cookies_same_name_different_paths(self, driver, pages, webserver): + """Test setting multiple cookies with same name but different paths.""" + assert_no_cookies_are_present(driver) + + key = "multi_path_cookie" + value = BytesValue(BytesValue.TYPE_STRING, "test") + + # Create cookies with same name but different paths + cookie1 = PartialCookie(key, value, webserver.host, path="/") + cookie2 = PartialCookie(key, value, webserver.host, path="/simpleTest.html") + + driver.storage.set_cookie(cookie=cookie1) + driver.storage.set_cookie(cookie=cookie2) + + # Both should exist + cookie_filter = CookieFilter(name=key) + result = driver.storage.get_cookies(filter=cookie_filter) + + # Should find at least 2 cookies with this name (different paths) + assert len(result.cookies) >= 2 + + def test_delete_cookie_by_path(self, driver, pages, webserver): + """Test deleting cookies filtered by path.""" + assert_no_cookies_are_present(driver) + + key1 = generate_unique_key() + key2 = generate_unique_key() + value = BytesValue(BytesValue.TYPE_STRING, "delete_test") + + cookie1 = PartialCookie(key1, value, webserver.host, path="/simpleTest.html") + cookie2 = PartialCookie(key2, value, webserver.host, path="/") + + driver.storage.set_cookie(cookie=cookie1) + driver.storage.set_cookie(cookie=cookie2) + + # Delete only cookies with specific path + driver.storage.delete_cookies(filter=CookieFilter(path="/simpleTest.html")) + + # Verify path-specific cookie is deleted, root path cookie remains + result = driver.storage.get_cookies(filter=CookieFilter()) + cookie_names = [c.name for c in result.cookies] + + assert key1 not in cookie_names or all( + c.path != "/simpleTest.html" for c in result.cookies if c.name == key1 + ) + + def test_cookie_expiry_timestamp(self, driver, pages, webserver): + """Test that cookie expiry is stored correctly as timestamp.""" + assert_no_cookies_are_present(driver) + + key = "expiry_test" + value = BytesValue(BytesValue.TYPE_STRING, "expires") + + # Set expiry to specific time + expiry_time = int(time.time() + 7200) # 2 hours from now + + cookie = PartialCookie(key, value, webserver.host, expiry=expiry_time) + + driver.storage.set_cookie(cookie=cookie) + + # Get and verify + cookie_filter = CookieFilter(name=key) + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + assert result.cookies[0].expiry == expiry_time + + def test_cookie_combined_attributes(self, driver, pages, webserver): + """Test setting and getting cookie with multiple attributes combined.""" + assert_no_cookies_are_present(driver) + + key = "combined_attrs" + value = BytesValue(BytesValue.TYPE_STRING, "all_features") + path = "/simpleTest.html" + expiry = int(time.time() + 3600) + + cookie = PartialCookie( + key, + value, + webserver.host, + path=path, + http_only=True, + secure=True, + same_site=SameSite.LAX, + expiry=expiry, + ) + + # Test + driver.storage.set_cookie(cookie=cookie) + + # Verify with matching filter + cookie_filter = CookieFilter( + name=key, + path=path, + http_only=True, + secure=True, + same_site=SameSite.LAX, + expiry=expiry, + ) + + result = driver.storage.get_cookies(filter=cookie_filter) + + assert len(result.cookies) > 0 + cookie_result = result.cookies[0] + assert cookie_result.name == key + assert cookie_result.value.value == value.value + assert cookie_result.path == path + assert cookie_result.http_only is True + assert cookie_result.secure is True + assert cookie_result.same_site == SameSite.LAX + assert cookie_result.expiry == expiry diff --git a/py/test/selenium/webdriver/common/bidi_webextension_tests.py b/py/test/selenium/webdriver/common/bidi_webextension_tests.py index 7bea9f71e7e16..93c6c5a1d5528 100644 --- a/py/test/selenium/webdriver/common/bidi_webextension_tests.py +++ b/py/test/selenium/webdriver/common/bidi_webextension_tests.py @@ -179,4 +179,309 @@ def test_install_with_extension_id_uninstall(self, chromium_driver): ext_info = chromium_driver.webextension.install(path=path) extension_id = ext_info.get("extension") # Uninstall using the extension ID - uninstall_extension_and_verify_extension_uninstalled(chromium_driver, extension_id) + uninstall_extension_and_verify_extension_uninstalled( + chromium_driver, extension_id + ) + + +# Additional edge case tests for better WPT coverage + + +class TestFirefoxWebExtensionEdgeCases: + """Firefox WebExtension edge case tests.""" + + @pytest.mark.xfail_chrome + @pytest.mark.xfail_edge + def test_uninstall_extension_by_id_string(self, driver, pages): + """Test uninstalling extension using extension ID as string.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = install_extension(driver, path=path) + extension_id_string = ext_info.get("extension") + + # Uninstall using ID string directly + driver.webextension.uninstall(extension_id_string) + + # Verify uninstall was successful + driver.browsing_context.reload(driver.current_window_handle) + assert len(driver.find_elements(By.ID, "webextensions-selenium-example")) == 0 + + @pytest.mark.xfail_chrome + @pytest.mark.xfail_edge + def test_uninstall_extension_by_result_dict(self, driver, pages): + """Test uninstalling extension using result dictionary from install.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = install_extension(driver, path=path) + + # Uninstall using result dict + driver.webextension.uninstall(ext_info) + + # Verify uninstall was successful + driver.browsing_context.reload(driver.current_window_handle) + assert len(driver.find_elements(By.ID, "webextensions-selenium-example")) == 0 + + @pytest.mark.xfail_chrome + @pytest.mark.xfail_edge + def test_install_returns_extension_id(self, driver, pages): + """Test that install returns proper extension ID in result.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = install_extension(driver, path=path) + + # Verify result structure + assert "extension" in ext_info + assert isinstance(ext_info.get("extension"), str) + assert len(ext_info.get("extension", "")) > 0 + assert ext_info.get("extension") == EXTENSION_ID + + # Cleanup + driver.webextension.uninstall(ext_info) + + @pytest.mark.xfail_chrome + @pytest.mark.xfail_edge + def test_extension_content_script_injection(self, driver, pages): + """Test that extension content scripts are properly injected.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = install_extension(driver, path=path) + + # Load page and verify content script injection + pages.load("blank.html") + + # Element should be injected by extension + injected_element = WebDriverWait(driver, timeout=5).until( + lambda dr: dr.find_element(By.ID, "webextensions-selenium-example") + ) + + assert injected_element is not None + assert ( + "Content injected by webextensions-selenium-example" + in injected_element.text + ) + + # Cleanup + driver.webextension.uninstall(ext_info) + + @pytest.mark.xfail_chrome + @pytest.mark.xfail_edge + def test_uninstall_removes_content_scripts(self, driver, pages): + """Test that uninstalling extension removes content scripts.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = install_extension(driver, path=path) + + # Verify injection works + pages.load("blank.html") + WebDriverWait(driver, timeout=5).until( + lambda dr: dr.find_element(By.ID, "webextensions-selenium-example") + ) + + # Uninstall + driver.webextension.uninstall(ext_info) + + # Reload page and verify injection is gone + driver.browsing_context.reload(driver.current_window_handle) + assert len(driver.find_elements(By.ID, "webextensions-selenium-example")) == 0 + + @pytest.mark.xfail_chrome + @pytest.mark.xfail_edge + def test_install_from_archive_returns_extension_id(self, driver, pages): + """Test that archive install returns proper extension ID.""" + archive_path = os.path.join(EXTENSIONS, EXTENSION_ARCHIVE_PATH) + ext_info = install_extension(driver, archive_path=archive_path) + + # Verify result structure + assert "extension" in ext_info + assert isinstance(ext_info.get("extension"), str) + assert len(ext_info.get("extension", "")) > 0 + + # Cleanup + driver.webextension.uninstall(ext_info) + + @pytest.mark.xfail_chrome + @pytest.mark.xfail_edge + def test_multiple_installations_and_uninstalls(self, driver, pages): + """Test installing and uninstalling extension multiple times.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + + # Install/uninstall cycle 1 + ext_info_1 = install_extension(driver, path=path) + verify_extension_injection(driver, pages) + driver.webextension.uninstall(ext_info_1) + driver.browsing_context.reload(driver.current_window_handle) + assert len(driver.find_elements(By.ID, "webextensions-selenium-example")) == 0 + + # Install/uninstall cycle 2 + ext_info_2 = install_extension(driver, path=path) + verify_extension_injection(driver, pages) + driver.webextension.uninstall(ext_info_2) + driver.browsing_context.reload(driver.current_window_handle) + assert len(driver.find_elements(By.ID, "webextensions-selenium-example")) == 0 + + +class TestChromiumWebExtensionEdgeCases: + """Chrome/Edge WebExtension edge case tests.""" + + @pytest.mark.xfail_firefox + @pytest.fixture + def pages_chromium(self, webserver, chromium_driver): + class Pages: + def load(self, name): + chromium_driver.get(webserver.where_is(name, localhost=False)) + + return Pages() + + @pytest.mark.xfail_firefox + @pytest.fixture + def chromium_driver(self, chromium_options, request): + """Create a Chrome/Edge driver with webextension support enabled.""" + driver_option = request.config.option.drivers[0].lower() + + if driver_option == "chrome": + browser_class = webdriver.Chrome + browser_service = webdriver.ChromeService + elif driver_option == "edge": + browser_class = webdriver.Edge + browser_service = webdriver.EdgeService + + temp_dir = tempfile.mkdtemp(prefix="chromium-profile-") + + chromium_options.enable_bidi = True + chromium_options.enable_webextensions = True + chromium_options.add_argument(f"--user-data-dir={temp_dir}") + chromium_options.add_argument("--no-sandbox") + chromium_options.add_argument("--disable-dev-shm-usage") + + binary = request.config.option.binary + if binary: + chromium_options.binary_location = binary + + executable = request.config.option.executable + if executable: + service = browser_service(executable_path=executable) + else: + service = browser_service() + + chromium_driver = browser_class(options=chromium_options, service=service) + + yield chromium_driver + chromium_driver.quit() + + # delete the temp directory + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + + @pytest.mark.xfail_firefox + def test_uninstall_extension_by_id_string(self, chromium_driver, pages_chromium): + """Test uninstalling extension using extension ID as string.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = chromium_driver.webextension.install(path=path) + extension_id_string = ext_info.get("extension") + + # Uninstall using ID string directly + chromium_driver.webextension.uninstall(extension_id_string) + + # Verify uninstall was successful + chromium_driver.browsing_context.reload(chromium_driver.current_window_handle) + assert ( + len(chromium_driver.find_elements(By.ID, "webextensions-selenium-example")) + == 0 + ) + + @pytest.mark.xfail_firefox + def test_uninstall_extension_by_result_dict(self, chromium_driver, pages_chromium): + """Test uninstalling extension using result dictionary from install.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = chromium_driver.webextension.install(path=path) + + # Uninstall using result dict + chromium_driver.webextension.uninstall(ext_info) + + # Verify uninstall was successful + chromium_driver.browsing_context.reload(chromium_driver.current_window_handle) + assert ( + len(chromium_driver.find_elements(By.ID, "webextensions-selenium-example")) + == 0 + ) + + @pytest.mark.xfail_firefox + def test_install_returns_extension_id(self, chromium_driver, pages_chromium): + """Test that install returns proper extension ID in result.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = chromium_driver.webextension.install(path=path) + + # Verify result structure + assert "extension" in ext_info + assert isinstance(ext_info.get("extension"), str) + assert len(ext_info.get("extension", "")) > 0 + + # Cleanup + chromium_driver.webextension.uninstall(ext_info) + + @pytest.mark.xfail_firefox + def test_extension_content_script_injection(self, chromium_driver, pages_chromium): + """Test that extension content scripts are properly injected.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = chromium_driver.webextension.install(path=path) + + # Load page and verify content script injection + pages_chromium.load("blank.html") + + # Element should be injected by extension + injected_element = WebDriverWait(chromium_driver, timeout=5).until( + lambda dr: dr.find_element(By.ID, "webextensions-selenium-example") + ) + + assert injected_element is not None + assert ( + "Content injected by webextensions-selenium-example" + in injected_element.text + ) + + # Cleanup + chromium_driver.webextension.uninstall(ext_info) + + @pytest.mark.xfail_firefox + def test_uninstall_removes_content_scripts(self, chromium_driver, pages_chromium): + """Test that uninstalling extension removes content scripts.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + ext_info = chromium_driver.webextension.install(path=path) + + # Verify injection works + pages_chromium.load("blank.html") + WebDriverWait(chromium_driver, timeout=5).until( + lambda dr: dr.find_element(By.ID, "webextensions-selenium-example") + ) + + # Uninstall + chromium_driver.webextension.uninstall(ext_info) + + # Reload page and verify injection is gone + chromium_driver.browsing_context.reload(chromium_driver.current_window_handle) + assert ( + len(chromium_driver.find_elements(By.ID, "webextensions-selenium-example")) + == 0 + ) + + @pytest.mark.xfail_firefox + def test_multiple_installations_and_uninstalls( + self, chromium_driver, pages_chromium + ): + """Test installing and uninstalling extension multiple times.""" + path = os.path.join(EXTENSIONS, EXTENSION_PATH) + + # Install/uninstall cycle 1 + ext_info_1 = chromium_driver.webextension.install(path=path) + verify_extension_injection(chromium_driver, pages_chromium) + chromium_driver.webextension.uninstall(ext_info_1) + chromium_driver.browsing_context.reload(chromium_driver.current_window_handle) + assert ( + len(chromium_driver.find_elements(By.ID, "webextensions-selenium-example")) + == 0 + ) + + # Install/uninstall cycle 2 + ext_info_2 = chromium_driver.webextension.install(path=path) + verify_extension_injection(chromium_driver, pages_chromium) + chromium_driver.webextension.uninstall(ext_info_2) + chromium_driver.browsing_context.reload(chromium_driver.current_window_handle) + assert ( + len(chromium_driver.find_elements(By.ID, "webextensions-selenium-example")) + == 0 + )