From 01da12bafbc6b621884339cff41bd77f19f051af Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 10 Jul 2025 11:51:29 +0530 Subject: [PATCH 1/7] implement bidi module INPUT --- py/selenium/webdriver/common/bidi/input.py | 441 +++++++++++++++++++++ py/selenium/webdriver/remote/webdriver.py | 25 ++ 2 files changed, 466 insertions(+) create mode 100644 py/selenium/webdriver/common/bidi/input.py diff --git a/py/selenium/webdriver/common/bidi/input.py b/py/selenium/webdriver/common/bidi/input.py new file mode 100644 index 0000000000000..607ae45577ac9 --- /dev/null +++ b/py/selenium/webdriver/common/bidi/input.py @@ -0,0 +1,441 @@ +# 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. + +from dataclasses import dataclass +from typing import List, Optional, Union + +from selenium.webdriver.common.bidi.common import command_builder +from selenium.webdriver.common.bidi.session import Session + + +class PointerType: + """Represents the possible pointer types.""" + + MOUSE = "mouse" + PEN = "pen" + TOUCH = "touch" + + VALID_TYPES = {MOUSE, PEN, TOUCH} + + +class Origin: + """Represents the possible origin types.""" + + VIEWPORT = "viewport" + POINTER = "pointer" + + +@dataclass +class ElementOrigin: + """Represents an element origin for input actions.""" + + type: str + element: dict + + def __init__(self, element_reference: dict): + self.type = "element" + self.element = element_reference + + def to_dict(self) -> dict: + """Convert the ElementOrigin to a dictionary.""" + return {"type": self.type, "element": self.element} + + +@dataclass +class PointerParameters: + """Represents pointer parameters for pointer actions.""" + + pointer_type: str = PointerType.MOUSE + + def __post_init__(self): + if self.pointer_type not in PointerType.VALID_TYPES: + raise ValueError(f"Invalid pointer type: {self.pointer_type}. Must be one of {PointerType.VALID_TYPES}") + + def to_dict(self) -> dict: + """Convert the PointerParameters to a dictionary.""" + return {"pointerType": self.pointer_type} + + +@dataclass +class PointerCommonProperties: + """Common properties for pointer actions.""" + + width: int = 1 + height: int = 1 + pressure: float = 0.0 + tangential_pressure: float = 0.0 + twist: int = 0 + altitude_angle: float = 0.0 + azimuth_angle: float = 0.0 + + def __post_init__(self): + if self.width < 1: + raise ValueError("width must be at least 1") + if self.height < 1: + raise ValueError("height must be at least 1") + if not (0.0 <= self.pressure <= 1.0): + raise ValueError("pressure must be between 0.0 and 1.0") + if not (0.0 <= self.tangential_pressure <= 1.0): + raise ValueError("tangential_pressure must be between 0.0 and 1.0") + if not (0 <= self.twist <= 359): + raise ValueError("twist must be between 0 and 359") + if not (0.0 <= self.altitude_angle <= 1.5707963267948966): + raise ValueError("altitude_angle must be between 0.0 and π/2") + if not (0.0 <= self.azimuth_angle <= 6.283185307179586): + raise ValueError("azimuth_angle must be between 0.0 and 2π") + + def to_dict(self) -> dict: + """Convert the PointerCommonProperties to a dictionary.""" + result = {} + if self.width != 1: + result["width"] = self.width + if self.height != 1: + result["height"] = self.height + if self.pressure != 0.0: + result["pressure"] = self.pressure + if self.tangential_pressure != 0.0: + result["tangentialPressure"] = self.tangential_pressure + if self.twist != 0: + result["twist"] = self.twist + if self.altitude_angle != 0.0: + result["altitudeAngle"] = self.altitude_angle + if self.azimuth_angle != 0.0: + result["azimuthAngle"] = self.azimuth_angle + return result + + +# Action classes +@dataclass +class PauseAction: + """Represents a pause action.""" + + type: str = "pause" + duration: Optional[int] = None + + def to_dict(self) -> dict: + """Convert the PauseAction to a dictionary.""" + result = {"type": self.type} + if self.duration is not None: + result["duration"] = self.duration + return result + + +@dataclass +class KeyDownAction: + """Represents a key down action.""" + + type: str = "keyDown" + value: str = "" + + def to_dict(self) -> dict: + """Convert the KeyDownAction to a dictionary.""" + return {"type": self.type, "value": self.value} + + +@dataclass +class KeyUpAction: + """Represents a key up action.""" + + type: str = "keyUp" + value: str = "" + + def to_dict(self) -> dict: + """Convert the KeyUpAction to a dictionary.""" + return {"type": self.type, "value": self.value} + + +@dataclass +class PointerDownAction: + """Represents a pointer down action.""" + + type: str = "pointerDown" + button: int = 0 + properties: Optional[PointerCommonProperties] = None + + def to_dict(self) -> dict: + """Convert the PointerDownAction to a dictionary.""" + result = {"type": self.type, "button": self.button} + if self.properties: + result.update(self.properties.to_dict()) + return result + + +@dataclass +class PointerUpAction: + """Represents a pointer up action.""" + + type: str = "pointerUp" + button: int = 0 + + def to_dict(self) -> dict: + """Convert the PointerUpAction to a dictionary.""" + return {"type": self.type, "button": self.button} + + +@dataclass +class PointerMoveAction: + """Represents a pointer move action.""" + + type: str = "pointerMove" + x: float = 0 + y: float = 0 + duration: Optional[int] = None + origin: Optional[Union[str, ElementOrigin]] = None + properties: Optional[PointerCommonProperties] = None + + def to_dict(self) -> dict: + """Convert the PointerMoveAction to a dictionary.""" + result = {"type": self.type, "x": self.x, "y": self.y} + if self.duration is not None: + result["duration"] = self.duration + if self.origin is not None: + if isinstance(self.origin, ElementOrigin): + result["origin"] = self.origin.to_dict() + else: + result["origin"] = self.origin + if self.properties: + result.update(self.properties.to_dict()) + return result + + +@dataclass +class WheelScrollAction: + """Represents a wheel scroll action.""" + + type: str = "scroll" + x: int = 0 + y: int = 0 + delta_x: int = 0 + delta_y: int = 0 + duration: Optional[int] = None + origin: Optional[Union[str, ElementOrigin]] = Origin.VIEWPORT + + def to_dict(self) -> dict: + """Convert the WheelScrollAction to a dictionary.""" + result = {"type": self.type, "x": self.x, "y": self.y, "deltaX": self.delta_x, "deltaY": self.delta_y} + if self.duration is not None: + result["duration"] = self.duration + if self.origin is not None: + if isinstance(self.origin, ElementOrigin): + result["origin"] = self.origin.to_dict() + else: + result["origin"] = self.origin + return result + + +# Source Actions +@dataclass +class NoneSourceActions: + """Represents a sequence of none actions.""" + + type: str = "none" + id: str = "" + actions: List[PauseAction] = None + + def __post_init__(self): + if self.actions is None: + self.actions = [] + + def to_dict(self) -> dict: + """Convert the NoneSourceActions to a dictionary.""" + return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} + + +@dataclass +class KeySourceActions: + """Represents a sequence of key actions.""" + + type: str = "key" + id: str = "" + actions: List[Union[PauseAction, KeyDownAction, KeyUpAction]] = None + + def __post_init__(self): + if self.actions is None: + self.actions = [] + + def to_dict(self) -> dict: + """Convert the KeySourceActions to a dictionary.""" + return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} + + +@dataclass +class PointerSourceActions: + """Represents a sequence of pointer actions.""" + + type: str = "pointer" + id: str = "" + parameters: Optional[PointerParameters] = None + actions: List[Union[PauseAction, PointerDownAction, PointerUpAction, PointerMoveAction]] = None + + def __post_init__(self): + if self.actions is None: + self.actions = [] + if self.parameters is None: + self.parameters = PointerParameters() + + def to_dict(self) -> dict: + """Convert the PointerSourceActions to a dictionary.""" + result = {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} + if self.parameters: + result["parameters"] = self.parameters.to_dict() + return result + + +@dataclass +class WheelSourceActions: + """Represents a sequence of wheel actions.""" + + type: str = "wheel" + id: str = "" + actions: List[Union[PauseAction, WheelScrollAction]] = None + + def __post_init__(self): + if self.actions is None: + self.actions = [] + + def to_dict(self) -> dict: + """Convert the WheelSourceActions to a dictionary.""" + return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} + + +@dataclass +class FileDialogInfo: + """Represents file dialog information from input.fileDialogOpened event.""" + + context: str + multiple: bool + element: Optional[dict] = None + + @classmethod + def from_dict(cls, data: dict) -> "FileDialogInfo": + """Creates a FileDialogInfo instance from a dictionary. + + Parameters: + ----------- + data: A dictionary containing the file dialog information. + + Returns: + ------- + FileDialogInfo: A new instance of FileDialogInfo. + """ + return cls(context=data["context"], multiple=data["multiple"], element=data.get("element")) + + +class FileDialogOpened: + """Event class for input.fileDialogOpened event.""" + + event_class = "input.fileDialogOpened" + + @classmethod + def from_json(cls, json): + """Create FileDialogInfo from JSON data.""" + return FileDialogInfo.from_dict(json) + + +class Input: + """ + BiDi implementation of the input module. + """ + + def __init__(self, conn): + self.conn = conn + self.subscriptions = {} + self.callbacks = {} + + def perform_actions( + self, + context: str, + actions: List[Union[NoneSourceActions, KeySourceActions, PointerSourceActions, WheelSourceActions]], + ) -> None: + """Performs a sequence of user input actions. + + Parameters: + ----------- + context: The browsing context ID where actions should be performed. + actions: A list of source actions to perform. + """ + params = {"context": context, "actions": [action.to_dict() for action in actions]} + self.conn.execute(command_builder("input.performActions", params)) + + def release_actions(self, context: str) -> None: + """Releases all input state for the given context. + + Parameters: + ----------- + context: The browsing context ID to release actions for. + """ + params = {"context": context} + self.conn.execute(command_builder("input.releaseActions", params)) + + def set_files(self, context: str, element: dict, files: List[str]) -> None: + """Sets files for a file input element. + + Parameters: + ----------- + context: The browsing context ID. + element: The element reference (script.SharedReference). + files: A list of file paths to set. + """ + params = {"context": context, "element": element, "files": files} + self.conn.execute(command_builder("input.setFiles", params)) + + def add_file_dialog_handler(self, handler): + """Add a handler for file dialog opened events. + + Parameters: + ----------- + handler: Callback function that takes a FileDialogInfo object. + + Returns: + -------- + int: Callback ID for removing the handler later. + """ + # Subscribe to the event if not already subscribed + if FileDialogOpened.event_class not in self.subscriptions: + session = Session(self.conn) + self.conn.execute(session.subscribe(FileDialogOpened.event_class)) + self.subscriptions[FileDialogOpened.event_class] = [] + + # Add callback - the callback receives the parsed FileDialogInfo directly + callback_id = self.conn.add_callback(FileDialogOpened, handler) + + self.subscriptions[FileDialogOpened.event_class].append(callback_id) + self.callbacks[callback_id] = handler + + return callback_id + + def remove_file_dialog_handler(self, callback_id: int) -> None: + """Remove a file dialog handler. + + Parameters: + ----------- + callback_id: The callback ID returned by add_file_dialog_handler. + """ + if callback_id in self.callbacks: + del self.callbacks[callback_id] + + if FileDialogOpened.event_class in self.subscriptions: + if callback_id in self.subscriptions[FileDialogOpened.event_class]: + self.subscriptions[FileDialogOpened.event_class].remove(callback_id) + + # If no more callbacks for this event, unsubscribe + if not self.subscriptions[FileDialogOpened.event_class]: + session = Session(self.conn) + self.conn.execute(session.unsubscribe(FileDialogOpened.event_class)) + del self.subscriptions[FileDialogOpened.event_class] + + self.conn.remove_callback(FileDialogOpened, callback_id) diff --git a/py/selenium/webdriver/remote/webdriver.py b/py/selenium/webdriver/remote/webdriver.py index 0ab678103b844..e6fc9f6572c29 100644 --- a/py/selenium/webdriver/remote/webdriver.py +++ b/py/selenium/webdriver/remote/webdriver.py @@ -40,6 +40,7 @@ ) from selenium.webdriver.common.bidi.browser import Browser from selenium.webdriver.common.bidi.browsing_context import BrowsingContext +from selenium.webdriver.common.bidi.input import Input from selenium.webdriver.common.bidi.network import Network from selenium.webdriver.common.bidi.permissions import Permissions from selenium.webdriver.common.bidi.script import Script @@ -270,6 +271,7 @@ def __init__( self._storage = None self._webextension = None self._permissions = None + self._input = None self._devtools = None def __repr__(self): @@ -1390,6 +1392,29 @@ def webextension(self): return self._webextension + @property + def input(self): + """Returns an input module object for BiDi input commands. + + Returns: + -------- + Input: an object containing access to BiDi input commands. + + Examples: + --------- + >>> from selenium.webdriver.common.bidi.input import KeySourceActions, KeyDownAction, KeyUpAction + >>> key_actions = KeySourceActions(id="keyboard", actions=[KeyDownAction(value="a"), KeyUpAction(value="a")]) + >>> driver.input.perform_actions(driver.current_window_handle, [key_actions]) + >>> driver.input.release_actions(driver.current_window_handle) + """ + if not self._websocket_connection: + self._start_bidi() + + if self._input is None: + self._input = Input(self._websocket_connection) + + return self._input + def _get_cdp_details(self): import json From 5ed81028b97950372273d7689bad2783657329bd Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 10 Jul 2025 11:51:47 +0530 Subject: [PATCH 2/7] add tests --- .../webdriver/common/bidi_input_tests.py | 519 ++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 py/test/selenium/webdriver/common/bidi_input_tests.py diff --git a/py/test/selenium/webdriver/common/bidi_input_tests.py b/py/test/selenium/webdriver/common/bidi_input_tests.py new file mode 100644 index 0000000000000..127d57813d551 --- /dev/null +++ b/py/test/selenium/webdriver/common/bidi_input_tests.py @@ -0,0 +1,519 @@ +# 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 os +import tempfile +import time + +import pytest + +from selenium.webdriver.common.bidi.input import ( + ElementOrigin, + FileDialogInfo, + KeyDownAction, + KeySourceActions, + KeyUpAction, + Origin, + PauseAction, + PointerCommonProperties, + PointerDownAction, + PointerMoveAction, + PointerParameters, + PointerSourceActions, + PointerType, + PointerUpAction, + WheelScrollAction, + WheelSourceActions, +) +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + + +def test_input_initialized(driver): + """Test that the input module is initialized properly.""" + assert driver.input is not None + + +def test_basic_key_input(driver, pages): + """Test basic keyboard input using BiDi.""" + pages.load("single_text_input.html") + + input_element = driver.find_element(By.ID, "textInput") + + # Create keyboard actions to type "hello" + key_actions = KeySourceActions( + id="keyboard", + actions=[ + KeyDownAction(value="h"), + KeyUpAction(value="h"), + KeyDownAction(value="e"), + KeyUpAction(value="e"), + KeyDownAction(value="l"), + KeyUpAction(value="l"), + KeyDownAction(value="l"), + KeyUpAction(value="l"), + KeyDownAction(value="o"), + KeyUpAction(value="o"), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [key_actions]) + + WebDriverWait(driver, 5).until(lambda d: input_element.get_attribute("value") == "hello") + assert input_element.get_attribute("value") == "hello" + + +def test_key_input_with_pause(driver, pages): + """Test keyboard input with pause actions.""" + pages.load("single_text_input.html") + + input_element = driver.find_element(By.ID, "textInput") + + # Create keyboard actions with pauses + key_actions = KeySourceActions( + id="keyboard", + actions=[ + KeyDownAction(value="a"), + KeyUpAction(value="a"), + PauseAction(duration=100), + KeyDownAction(value="b"), + KeyUpAction(value="b"), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [key_actions]) + + WebDriverWait(driver, 5).until(lambda d: input_element.get_attribute("value") == "ab") + assert input_element.get_attribute("value") == "ab" + + +def test_pointer_click(driver, pages): + """Test basic pointer click using BiDi.""" + pages.load("javascriptPage.html") + + button = driver.find_element(By.ID, "clickField") + + # Get button location + location = button.location + size = button.size + x = location["x"] + size["width"] // 2 + y = location["y"] + size["height"] // 2 + + # Create pointer actions for a click + pointer_actions = 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_actions]) + + WebDriverWait(driver, 5).until(lambda d: button.get_attribute("value") == "Clicked") + assert button.get_attribute("value") == "Clicked" + + +def test_pointer_move_with_element_origin(driver, pages): + """Test pointer move with element origin.""" + pages.load("javascriptPage.html") + + button = driver.find_element(By.ID, "clickField") + + # Get element reference for BiDi + element_id = button._id # This gets the internal element ID + element_ref = {"sharedId": element_id} + element_origin = ElementOrigin(element_ref) + + # Create pointer actions with element origin + pointer_actions = PointerSourceActions( + id="mouse", + parameters=PointerParameters(pointer_type=PointerType.MOUSE), + actions=[ + PointerMoveAction(x=0, y=0, origin=element_origin), + 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_pointer_with_common_properties(driver, pages): + """Test pointer actions with common properties.""" + 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 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 + ) + + 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_wheel_scroll(driver, pages): + """Test wheel scroll actions.""" + # page that can be scrolled + pages.load("scroll3.html") + + # Scroll down + wheel_actions = WheelSourceActions( + 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]) + + # Verify the page scrolled by checking scroll position + scroll_y = driver.execute_script("return window.pageYOffset;") + assert scroll_y == 100 + + +def test_combined_input_actions(driver, pages): + """Test combining multiple input sources.""" + pages.load("single_text_input.html") + + input_element = driver.find_element(By.ID, "textInput") + + # First click on the input field, then type + location = input_element.location + size = input_element.size + x = location["x"] + size["width"] // 2 + y = location["y"] + size["height"] // 2 + + # Pointer actions to click + pointer_actions = PointerSourceActions( + id="mouse", + parameters=PointerParameters(pointer_type=PointerType.MOUSE), + actions=[ + PauseAction(duration=0), # Sync with keyboard + PointerMoveAction(x=x, y=y), + PointerDownAction(button=0), + PointerUpAction(button=0), + ], + ) + + # Keyboard actions to type + key_actions = KeySourceActions( + id="keyboard", + actions=[ + PauseAction(duration=0), # Sync with pointer + # write "test" + KeyDownAction(value="t"), + KeyUpAction(value="t"), + KeyDownAction(value="e"), + KeyUpAction(value="e"), + KeyDownAction(value="s"), + KeyUpAction(value="s"), + KeyDownAction(value="t"), + KeyUpAction(value="t"), + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [pointer_actions, key_actions]) + + WebDriverWait(driver, 5).until(lambda d: input_element.get_attribute("value") == "test") + assert input_element.get_attribute("value") == "test" + + +def test_set_files(driver, pages): + """Test setting files on file input element.""" + pages.load("formPage.html") + + upload_element = driver.find_element(By.ID, "upload") + assert upload_element.get_attribute("value") == "" + + # Create a temporary file + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as temp_file: + temp_file.write("test content") + temp_file_path = temp_file.name + + try: + # Get element reference for BiDi + element_id = upload_element._id + element_ref = {"sharedId": element_id} + + # Set files using BiDi + driver.input.set_files(driver.current_window_handle, element_ref, [temp_file_path]) + + # Verify file was set + value = upload_element.get_attribute("value") + assert os.path.basename(temp_file_path) in value + + finally: + # Clean up temp file + if os.path.exists(temp_file_path): + os.unlink(temp_file_path) + + +def test_set_multiple_files(driver, pages): + """Test setting multiple files on file input element.""" + pages.load("formPage.html") + + # Use the same upload element but try to set multiple files + # Note: The HTML file only has one file input, so this test demonstrates + # the API even though the element may not support multiple files + upload_element = driver.find_element(By.ID, "upload") + + # Create temporary files + temp_files = [] + for i in range(2): + temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) + temp_file.write(f"test content {i}") + temp_files.append(temp_file.name) + temp_file.close() + + try: + # Get element reference for BiDi + element_id = upload_element._id + element_ref = {"sharedId": element_id} + + # Set multiple files using BiDi (this might fail if element doesn't support multiple) + # The test mainly demonstrates the API + driver.input.set_files(driver.current_window_handle, element_ref, temp_files) + + # Verify files were set (exact verification depends on browser implementation) + value = upload_element.get_attribute("value") + assert value != "" # Should have some value now + + except Exception: + # This might fail if the element doesn't support multiple files + # which is expected for the current HTML + pass + + finally: + # Clean up temp files + for temp_file_path in temp_files: + if os.path.exists(temp_file_path): + os.unlink(temp_file_path) + + +def test_release_actions(driver, pages): + """Test releasing input actions.""" + pages.load("single_text_input.html") + + input_element = driver.find_element(By.ID, "textInput") + + # Perform some actions first + key_actions = KeySourceActions( + id="keyboard", + actions=[ + KeyDownAction(value="a"), + # Note: not releasing the key + ], + ) + + driver.input.perform_actions(driver.current_window_handle, [key_actions]) + + # Now release all actions + driver.input.release_actions(driver.current_window_handle) + + # The key should be released now, so typing more should work normally + key_actions2 = KeySourceActions( + id="keyboard", + actions=[ + KeyDownAction(value="b"), + KeyUpAction(value="b"), + ], + ) + + 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")) + + +def test_pause_action_without_duration(driver): + """Test pause action without explicit duration.""" + pause = PauseAction() + assert pause.type == "pause" + assert pause.duration is None + + pause_dict = pause.to_dict() + assert pause_dict["type"] == "pause" + assert "duration" not in pause_dict + + +def test_pause_action_with_duration(driver): + """Test pause action with explicit duration.""" + pause = PauseAction(duration=1000) + assert pause.duration == 1000 + + pause_dict = pause.to_dict() + assert pause_dict["type"] == "pause" + assert pause_dict["duration"] == 1000 + + +def test_pointer_parameters_validation(): + """Test pointer parameters validation.""" + # Valid pointer type + params = PointerParameters(pointer_type=PointerType.MOUSE) + assert params.pointer_type == PointerType.MOUSE + + # Invalid pointer type should raise ValueError + with pytest.raises(ValueError, match="Invalid pointer type"): + PointerParameters(pointer_type="invalid") + + +def test_pointer_common_properties_validation(): + """Test pointer common properties validation.""" + # Valid properties + props = PointerCommonProperties( + width=2, height=2, pressure=0.5, tangential_pressure=0.3, twist=180, altitude_angle=0.5, azimuth_angle=3.14 + ) + assert props.width == 2 + assert props.pressure == 0.5 + + # Invalid width + with pytest.raises(ValueError, match="width must be at least 1"): + PointerCommonProperties(width=0) + + # Invalid pressure + with pytest.raises(ValueError, match="pressure must be between 0.0 and 1.0"): + PointerCommonProperties(pressure=1.5) + + # Invalid twist + with pytest.raises(ValueError, match="twist must be between 0 and 359"): + PointerCommonProperties(twist=360) + + +def test_element_origin_creation(): + """Test ElementOrigin creation and serialization.""" + element_ref = {"sharedId": "test-element-id"} + origin = ElementOrigin(element_ref) + + assert origin.type == "element" + assert origin.element == element_ref + + origin_dict = origin.to_dict() + assert origin_dict["type"] == "element" + assert origin_dict["element"] == element_ref + + +def test_source_actions_serialization(): + """Test that source actions serialize correctly to dictionaries.""" + # Test KeySourceActions + key_actions = KeySourceActions( + id="test-keyboard", actions=[KeyDownAction(value="a"), PauseAction(duration=100), KeyUpAction(value="a")] + ) + + key_dict = key_actions.to_dict() + assert key_dict["type"] == "key" + assert key_dict["id"] == "test-keyboard" + assert len(key_dict["actions"]) == 3 + + # Test PointerSourceActions + pointer_actions = PointerSourceActions( + id="test-mouse", + parameters=PointerParameters(pointer_type=PointerType.MOUSE), + actions=[PointerMoveAction(x=100, y=200), PointerDownAction(button=0), PointerUpAction(button=0)], + ) + + pointer_dict = pointer_actions.to_dict() + assert pointer_dict["type"] == "pointer" + assert pointer_dict["id"] == "test-mouse" + assert "parameters" in pointer_dict + assert len(pointer_dict["actions"]) == 3 + + +@pytest.mark.parametrize("multiple", [True, False]) +@pytest.mark.xfail_firefox(reason="File dialog handling not implemented in Firefox yet") +def test_file_dialog_event_handler_multiple(driver, multiple): + """Test file dialog event handler with multiple as true and false.""" + file_dialog_events = [] + + def file_dialog_handler(file_dialog_info): + file_dialog_events.append(file_dialog_info) + + # Test event handler registration + handler_id = driver.input.add_file_dialog_handler(file_dialog_handler) + assert handler_id is not None + + driver.get(f"data:text/html,") + + # Use script.evaluate to trigger the file dialog with user activation + driver.script._evaluate( + expression="document.getElementById('upload').click()", + target={"context": driver.current_window_handle}, + await_promise=False, + user_activation=True, + ) + + # Wait for the file dialog event to be triggered + WebDriverWait(driver, 5).until(lambda d: len(file_dialog_events) > 0) + + assert len(file_dialog_events) > 0 + file_dialog_info = file_dialog_events[0] + assert isinstance(file_dialog_info, FileDialogInfo) + assert file_dialog_info.context == driver.current_window_handle + # Check if multiple attribute is set correctly (True, False) + assert file_dialog_info.multiple is multiple + + driver.input.remove_file_dialog_handler(handler_id) + + +@pytest.mark.xfail_firefox(reason="File dialog handling not implemented in Firefox yet") +def test_file_dialog_event_handler_unsubscribe(driver): + """Test file dialog event handler unsubscribe.""" + file_dialog_events = [] + + def file_dialog_handler(file_dialog_info): + file_dialog_events.append(file_dialog_info) + + # Register the handler + handler_id = driver.input.add_file_dialog_handler(file_dialog_handler) + assert handler_id is not None + + # Unsubscribe the handler + driver.input.remove_file_dialog_handler(handler_id) + + driver.get("data:text/html,") + + # Trigger the file dialog + driver.script._evaluate( + expression="document.getElementById('upload').click()", + target={"context": driver.current_window_handle}, + await_promise=False, + user_activation=True, + ) + + # Wait to ensure no events are captured + time.sleep(1) + assert len(file_dialog_events) == 0 From f0b7a1937ca8c4dcd131b042bbc3fb7b56c40286 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Mon, 14 Jul 2025 13:27:02 +0530 Subject: [PATCH 3/7] correct `test_set_multiple_files` --- .../webdriver/common/bidi_input_tests.py | 112 +----------------- 1 file changed, 4 insertions(+), 108 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_input_tests.py b/py/test/selenium/webdriver/common/bidi_input_tests.py index 127d57813d551..87c5fbb27ad17 100644 --- a/py/test/selenium/webdriver/common/bidi_input_tests.py +++ b/py/test/selenium/webdriver/common/bidi_input_tests.py @@ -283,13 +283,10 @@ def test_set_files(driver, pages): os.unlink(temp_file_path) -def test_set_multiple_files(driver, pages): - """Test setting multiple files on file input element.""" - pages.load("formPage.html") +def test_set_multiple_files(driver): + """Test setting multiple files on a file input element with 'multiple' attribute using BiDi.""" + driver.get("data:text/html,") - # Use the same upload element but try to set multiple files - # Note: The HTML file only has one file input, so this test demonstrates - # the API even though the element may not support multiple files upload_element = driver.find_element(By.ID, "upload") # Create temporary files @@ -305,18 +302,10 @@ def test_set_multiple_files(driver, pages): element_id = upload_element._id element_ref = {"sharedId": element_id} - # Set multiple files using BiDi (this might fail if element doesn't support multiple) - # The test mainly demonstrates the API driver.input.set_files(driver.current_window_handle, element_ref, temp_files) - # Verify files were set (exact verification depends on browser implementation) value = upload_element.get_attribute("value") - assert value != "" # Should have some value now - - except Exception: - # This might fail if the element doesn't support multiple files - # which is expected for the current HTML - pass + assert value != "" finally: # Clean up temp files @@ -360,99 +349,6 @@ def test_release_actions(driver, pages): WebDriverWait(driver, 5).until(lambda d: "b" in input_element.get_attribute("value")) -def test_pause_action_without_duration(driver): - """Test pause action without explicit duration.""" - pause = PauseAction() - assert pause.type == "pause" - assert pause.duration is None - - pause_dict = pause.to_dict() - assert pause_dict["type"] == "pause" - assert "duration" not in pause_dict - - -def test_pause_action_with_duration(driver): - """Test pause action with explicit duration.""" - pause = PauseAction(duration=1000) - assert pause.duration == 1000 - - pause_dict = pause.to_dict() - assert pause_dict["type"] == "pause" - assert pause_dict["duration"] == 1000 - - -def test_pointer_parameters_validation(): - """Test pointer parameters validation.""" - # Valid pointer type - params = PointerParameters(pointer_type=PointerType.MOUSE) - assert params.pointer_type == PointerType.MOUSE - - # Invalid pointer type should raise ValueError - with pytest.raises(ValueError, match="Invalid pointer type"): - PointerParameters(pointer_type="invalid") - - -def test_pointer_common_properties_validation(): - """Test pointer common properties validation.""" - # Valid properties - props = PointerCommonProperties( - width=2, height=2, pressure=0.5, tangential_pressure=0.3, twist=180, altitude_angle=0.5, azimuth_angle=3.14 - ) - assert props.width == 2 - assert props.pressure == 0.5 - - # Invalid width - with pytest.raises(ValueError, match="width must be at least 1"): - PointerCommonProperties(width=0) - - # Invalid pressure - with pytest.raises(ValueError, match="pressure must be between 0.0 and 1.0"): - PointerCommonProperties(pressure=1.5) - - # Invalid twist - with pytest.raises(ValueError, match="twist must be between 0 and 359"): - PointerCommonProperties(twist=360) - - -def test_element_origin_creation(): - """Test ElementOrigin creation and serialization.""" - element_ref = {"sharedId": "test-element-id"} - origin = ElementOrigin(element_ref) - - assert origin.type == "element" - assert origin.element == element_ref - - origin_dict = origin.to_dict() - assert origin_dict["type"] == "element" - assert origin_dict["element"] == element_ref - - -def test_source_actions_serialization(): - """Test that source actions serialize correctly to dictionaries.""" - # Test KeySourceActions - key_actions = KeySourceActions( - id="test-keyboard", actions=[KeyDownAction(value="a"), PauseAction(duration=100), KeyUpAction(value="a")] - ) - - key_dict = key_actions.to_dict() - assert key_dict["type"] == "key" - assert key_dict["id"] == "test-keyboard" - assert len(key_dict["actions"]) == 3 - - # Test PointerSourceActions - pointer_actions = PointerSourceActions( - id="test-mouse", - parameters=PointerParameters(pointer_type=PointerType.MOUSE), - actions=[PointerMoveAction(x=100, y=200), PointerDownAction(button=0), PointerUpAction(button=0)], - ) - - pointer_dict = pointer_actions.to_dict() - assert pointer_dict["type"] == "pointer" - assert pointer_dict["id"] == "test-mouse" - assert "parameters" in pointer_dict - assert len(pointer_dict["actions"]) == 3 - - @pytest.mark.parametrize("multiple", [True, False]) @pytest.mark.xfail_firefox(reason="File dialog handling not implemented in Firefox yet") def test_file_dialog_event_handler_multiple(driver, multiple): From 7e8e7f5538e48c868aaad7909da41bb0909e2836 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 25 Jul 2025 16:42:11 +0530 Subject: [PATCH 4/7] make type immutable via property --- py/selenium/webdriver/common/bidi/input.py | 61 +++++++++++++++---- .../webdriver/common/bidi_input_tests.py | 6 +- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/py/selenium/webdriver/common/bidi/input.py b/py/selenium/webdriver/common/bidi/input.py index 607ae45577ac9..910faf4683ee4 100644 --- a/py/selenium/webdriver/common/bidi/input.py +++ b/py/selenium/webdriver/common/bidi/input.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. +import math from dataclasses import dataclass from typing import List, Optional, Union @@ -93,9 +94,9 @@ def __post_init__(self): raise ValueError("tangential_pressure must be between 0.0 and 1.0") if not (0 <= self.twist <= 359): raise ValueError("twist must be between 0 and 359") - if not (0.0 <= self.altitude_angle <= 1.5707963267948966): + if not (0.0 <= self.altitude_angle <= math.pi / 2): raise ValueError("altitude_angle must be between 0.0 and π/2") - if not (0.0 <= self.azimuth_angle <= 6.283185307179586): + if not (0.0 <= self.azimuth_angle <= 2 * math.pi): raise ValueError("azimuth_angle must be between 0.0 and 2π") def to_dict(self) -> dict: @@ -123,9 +124,12 @@ def to_dict(self) -> dict: class PauseAction: """Represents a pause action.""" - type: str = "pause" duration: Optional[int] = None + @property + def type(self) -> str: + return "pause" + def to_dict(self) -> dict: """Convert the PauseAction to a dictionary.""" result = {"type": self.type} @@ -138,9 +142,12 @@ def to_dict(self) -> dict: class KeyDownAction: """Represents a key down action.""" - type: str = "keyDown" value: str = "" + @property + def type(self) -> str: + return "keyDown" + def to_dict(self) -> dict: """Convert the KeyDownAction to a dictionary.""" return {"type": self.type, "value": self.value} @@ -150,9 +157,12 @@ def to_dict(self) -> dict: class KeyUpAction: """Represents a key up action.""" - type: str = "keyUp" value: str = "" + @property + def type(self) -> str: + return "keyUp" + def to_dict(self) -> dict: """Convert the KeyUpAction to a dictionary.""" return {"type": self.type, "value": self.value} @@ -162,10 +172,13 @@ def to_dict(self) -> dict: class PointerDownAction: """Represents a pointer down action.""" - type: str = "pointerDown" button: int = 0 properties: Optional[PointerCommonProperties] = None + @property + def type(self) -> str: + return "pointerDown" + def to_dict(self) -> dict: """Convert the PointerDownAction to a dictionary.""" result = {"type": self.type, "button": self.button} @@ -178,9 +191,12 @@ def to_dict(self) -> dict: class PointerUpAction: """Represents a pointer up action.""" - type: str = "pointerUp" button: int = 0 + @property + def type(self) -> str: + return "pointerUp" + def to_dict(self) -> dict: """Convert the PointerUpAction to a dictionary.""" return {"type": self.type, "button": self.button} @@ -190,13 +206,16 @@ def to_dict(self) -> dict: class PointerMoveAction: """Represents a pointer move action.""" - type: str = "pointerMove" x: float = 0 y: float = 0 duration: Optional[int] = None origin: Optional[Union[str, ElementOrigin]] = None properties: Optional[PointerCommonProperties] = None + @property + def type(self) -> str: + return "pointerMove" + def to_dict(self) -> dict: """Convert the PointerMoveAction to a dictionary.""" result = {"type": self.type, "x": self.x, "y": self.y} @@ -216,7 +235,6 @@ def to_dict(self) -> dict: class WheelScrollAction: """Represents a wheel scroll action.""" - type: str = "scroll" x: int = 0 y: int = 0 delta_x: int = 0 @@ -224,6 +242,10 @@ class WheelScrollAction: duration: Optional[int] = None origin: Optional[Union[str, ElementOrigin]] = Origin.VIEWPORT + @property + def type(self) -> str: + return "scroll" + def to_dict(self) -> dict: """Convert the WheelScrollAction to a dictionary.""" result = {"type": self.type, "x": self.x, "y": self.y, "deltaX": self.delta_x, "deltaY": self.delta_y} @@ -242,7 +264,6 @@ def to_dict(self) -> dict: class NoneSourceActions: """Represents a sequence of none actions.""" - type: str = "none" id: str = "" actions: List[PauseAction] = None @@ -250,6 +271,10 @@ def __post_init__(self): if self.actions is None: self.actions = [] + @property + def type(self) -> str: + return "none" + def to_dict(self) -> dict: """Convert the NoneSourceActions to a dictionary.""" return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} @@ -259,7 +284,6 @@ def to_dict(self) -> dict: class KeySourceActions: """Represents a sequence of key actions.""" - type: str = "key" id: str = "" actions: List[Union[PauseAction, KeyDownAction, KeyUpAction]] = None @@ -267,6 +291,10 @@ def __post_init__(self): if self.actions is None: self.actions = [] + @property + def type(self) -> str: + return "key" + def to_dict(self) -> dict: """Convert the KeySourceActions to a dictionary.""" return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} @@ -276,7 +304,6 @@ def to_dict(self) -> dict: class PointerSourceActions: """Represents a sequence of pointer actions.""" - type: str = "pointer" id: str = "" parameters: Optional[PointerParameters] = None actions: List[Union[PauseAction, PointerDownAction, PointerUpAction, PointerMoveAction]] = None @@ -287,6 +314,10 @@ def __post_init__(self): if self.parameters is None: self.parameters = PointerParameters() + @property + def type(self) -> str: + return "pointer" + def to_dict(self) -> dict: """Convert the PointerSourceActions to a dictionary.""" result = {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} @@ -299,7 +330,6 @@ def to_dict(self) -> dict: class WheelSourceActions: """Represents a sequence of wheel actions.""" - type: str = "wheel" id: str = "" actions: List[Union[PauseAction, WheelScrollAction]] = None @@ -307,6 +337,10 @@ def __post_init__(self): if self.actions is None: self.actions = [] + @property + def type(self) -> str: + return "wheel" + def to_dict(self) -> dict: """Convert the WheelSourceActions to a dictionary.""" return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} @@ -335,6 +369,7 @@ def from_dict(cls, data: dict) -> "FileDialogInfo": return cls(context=data["context"], multiple=data["multiple"], element=data.get("element")) +# Event Class class FileDialogOpened: """Event class for input.fileDialogOpened event.""" diff --git a/py/test/selenium/webdriver/common/bidi_input_tests.py b/py/test/selenium/webdriver/common/bidi_input_tests.py index 87c5fbb27ad17..ecbe0bddd4f73 100644 --- a/py/test/selenium/webdriver/common/bidi_input_tests.py +++ b/py/test/selenium/webdriver/common/bidi_input_tests.py @@ -137,7 +137,7 @@ def test_pointer_move_with_element_origin(driver, pages): button = driver.find_element(By.ID, "clickField") # Get element reference for BiDi - element_id = button._id # This gets the internal element ID + element_id = button.id element_ref = {"sharedId": element_id} element_origin = ElementOrigin(element_ref) @@ -267,7 +267,7 @@ def test_set_files(driver, pages): try: # Get element reference for BiDi - element_id = upload_element._id + element_id = upload_element.id element_ref = {"sharedId": element_id} # Set files using BiDi @@ -299,7 +299,7 @@ def test_set_multiple_files(driver): try: # Get element reference for BiDi - element_id = upload_element._id + element_id = upload_element.id element_ref = {"sharedId": element_id} driver.input.set_files(driver.current_window_handle, element_ref, temp_files) From 826f8ef16475115b87f6bf216b72c3beab4fab09 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 25 Jul 2025 17:01:35 +0530 Subject: [PATCH 5/7] resolve mypy errors --- py/selenium/webdriver/common/bidi/input.py | 50 +++++++++++----------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/py/selenium/webdriver/common/bidi/input.py b/py/selenium/webdriver/common/bidi/input.py index 910faf4683ee4..111a4878c5f2a 100644 --- a/py/selenium/webdriver/common/bidi/input.py +++ b/py/selenium/webdriver/common/bidi/input.py @@ -16,8 +16,8 @@ # under the License. import math -from dataclasses import dataclass -from typing import List, Optional, Union +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Union from selenium.webdriver.common.bidi.common import command_builder from selenium.webdriver.common.bidi.session import Session @@ -101,7 +101,7 @@ def __post_init__(self): def to_dict(self) -> dict: """Convert the PointerCommonProperties to a dictionary.""" - result = {} + result: Dict[str, Any] = {} if self.width != 1: result["width"] = self.width if self.height != 1: @@ -132,7 +132,7 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the PauseAction to a dictionary.""" - result = {"type": self.type} + result: Dict[str, Any] = {"type": self.type} if self.duration is not None: result["duration"] = self.duration return result @@ -181,7 +181,7 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the PointerDownAction to a dictionary.""" - result = {"type": self.type, "button": self.button} + result: Dict[str, Any] = {"type": self.type, "button": self.button} if self.properties: result.update(self.properties.to_dict()) return result @@ -218,7 +218,7 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the PointerMoveAction to a dictionary.""" - result = {"type": self.type, "x": self.x, "y": self.y} + result: Dict[str, Any] = {"type": self.type, "x": self.x, "y": self.y} if self.duration is not None: result["duration"] = self.duration if self.origin is not None: @@ -248,7 +248,13 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the WheelScrollAction to a dictionary.""" - result = {"type": self.type, "x": self.x, "y": self.y, "deltaX": self.delta_x, "deltaY": self.delta_y} + result: Dict[str, Any] = { + "type": self.type, + "x": self.x, + "y": self.y, + "deltaX": self.delta_x, + "deltaY": self.delta_y, + } if self.duration is not None: result["duration"] = self.duration if self.origin is not None: @@ -265,11 +271,7 @@ class NoneSourceActions: """Represents a sequence of none actions.""" id: str = "" - actions: List[PauseAction] = None - - def __post_init__(self): - if self.actions is None: - self.actions = [] + actions: List[PauseAction] = field(default_factory=list) @property def type(self) -> str: @@ -285,11 +287,7 @@ class KeySourceActions: """Represents a sequence of key actions.""" id: str = "" - actions: List[Union[PauseAction, KeyDownAction, KeyUpAction]] = None - - def __post_init__(self): - if self.actions is None: - self.actions = [] + actions: List[Union[PauseAction, KeyDownAction, KeyUpAction]] = field(default_factory=list) @property def type(self) -> str: @@ -306,11 +304,11 @@ class PointerSourceActions: id: str = "" parameters: Optional[PointerParameters] = None - actions: List[Union[PauseAction, PointerDownAction, PointerUpAction, PointerMoveAction]] = None + actions: List[Union[PauseAction, PointerDownAction, PointerUpAction, PointerMoveAction]] = field( + default_factory=list + ) def __post_init__(self): - if self.actions is None: - self.actions = [] if self.parameters is None: self.parameters = PointerParameters() @@ -320,7 +318,11 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the PointerSourceActions to a dictionary.""" - result = {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]} + result: Dict[str, Any] = { + "type": self.type, + "id": self.id, + "actions": [action.to_dict() for action in self.actions], + } if self.parameters: result["parameters"] = self.parameters.to_dict() return result @@ -331,11 +333,7 @@ class WheelSourceActions: """Represents a sequence of wheel actions.""" id: str = "" - actions: List[Union[PauseAction, WheelScrollAction]] = None - - def __post_init__(self): - if self.actions is None: - self.actions = [] + actions: List[Union[PauseAction, WheelScrollAction]] = field(default_factory=list) @property def type(self) -> str: From 8d4f3bff8227525b7200aeb4896328aafd5ff6da Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 25 Jul 2025 17:03:59 +0530 Subject: [PATCH 6/7] resolve mypy errors in `script.py` --- py/selenium/webdriver/common/bidi/script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/selenium/webdriver/common/bidi/script.py b/py/selenium/webdriver/common/bidi/script.py index 74b8a3568ac3a..50e93e18288a7 100644 --- a/py/selenium/webdriver/common/bidi/script.py +++ b/py/selenium/webdriver/common/bidi/script.py @@ -319,7 +319,7 @@ def execute(self, script: str, *args) -> dict: ) if result.type == "success": - return result.result + return result.result if result.result is not None else {} else: error_message = "Error while executing script" if result.exception_details: From 126efb9127f4b0820fc08f21c4a260b810cd1410 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Tue, 5 Aug 2025 22:49:56 +0530 Subject: [PATCH 7/7] use list, dict instead of typing --- py/selenium/webdriver/common/bidi/input.py | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/py/selenium/webdriver/common/bidi/input.py b/py/selenium/webdriver/common/bidi/input.py index 111a4878c5f2a..8b2f40e532210 100644 --- a/py/selenium/webdriver/common/bidi/input.py +++ b/py/selenium/webdriver/common/bidi/input.py @@ -17,7 +17,7 @@ import math from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union from selenium.webdriver.common.bidi.common import command_builder from selenium.webdriver.common.bidi.session import Session @@ -101,7 +101,7 @@ def __post_init__(self): def to_dict(self) -> dict: """Convert the PointerCommonProperties to a dictionary.""" - result: Dict[str, Any] = {} + result: dict[str, Any] = {} if self.width != 1: result["width"] = self.width if self.height != 1: @@ -132,7 +132,7 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the PauseAction to a dictionary.""" - result: Dict[str, Any] = {"type": self.type} + result: dict[str, Any] = {"type": self.type} if self.duration is not None: result["duration"] = self.duration return result @@ -181,7 +181,7 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the PointerDownAction to a dictionary.""" - result: Dict[str, Any] = {"type": self.type, "button": self.button} + result: dict[str, Any] = {"type": self.type, "button": self.button} if self.properties: result.update(self.properties.to_dict()) return result @@ -218,7 +218,7 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the PointerMoveAction to a dictionary.""" - result: Dict[str, Any] = {"type": self.type, "x": self.x, "y": self.y} + result: dict[str, Any] = {"type": self.type, "x": self.x, "y": self.y} if self.duration is not None: result["duration"] = self.duration if self.origin is not None: @@ -248,7 +248,7 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the WheelScrollAction to a dictionary.""" - result: Dict[str, Any] = { + result: dict[str, Any] = { "type": self.type, "x": self.x, "y": self.y, @@ -271,7 +271,7 @@ class NoneSourceActions: """Represents a sequence of none actions.""" id: str = "" - actions: List[PauseAction] = field(default_factory=list) + actions: list[PauseAction] = field(default_factory=list) @property def type(self) -> str: @@ -287,7 +287,7 @@ class KeySourceActions: """Represents a sequence of key actions.""" id: str = "" - actions: List[Union[PauseAction, KeyDownAction, KeyUpAction]] = field(default_factory=list) + actions: list[Union[PauseAction, KeyDownAction, KeyUpAction]] = field(default_factory=list) @property def type(self) -> str: @@ -304,7 +304,7 @@ class PointerSourceActions: id: str = "" parameters: Optional[PointerParameters] = None - actions: List[Union[PauseAction, PointerDownAction, PointerUpAction, PointerMoveAction]] = field( + actions: list[Union[PauseAction, PointerDownAction, PointerUpAction, PointerMoveAction]] = field( default_factory=list ) @@ -318,7 +318,7 @@ def type(self) -> str: def to_dict(self) -> dict: """Convert the PointerSourceActions to a dictionary.""" - result: Dict[str, Any] = { + result: dict[str, Any] = { "type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions], @@ -333,7 +333,7 @@ class WheelSourceActions: """Represents a sequence of wheel actions.""" id: str = "" - actions: List[Union[PauseAction, WheelScrollAction]] = field(default_factory=list) + actions: list[Union[PauseAction, WheelScrollAction]] = field(default_factory=list) @property def type(self) -> str: @@ -392,7 +392,7 @@ def __init__(self, conn): def perform_actions( self, context: str, - actions: List[Union[NoneSourceActions, KeySourceActions, PointerSourceActions, WheelSourceActions]], + actions: list[Union[NoneSourceActions, KeySourceActions, PointerSourceActions, WheelSourceActions]], ) -> None: """Performs a sequence of user input actions. @@ -414,7 +414,7 @@ def release_actions(self, context: str) -> None: params = {"context": context} self.conn.execute(command_builder("input.releaseActions", params)) - def set_files(self, context: str, element: dict, files: List[str]) -> None: + def set_files(self, context: str, element: dict, files: list[str]) -> None: """Sets files for a file input element. Parameters: