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
+ )