Path: blob/trunk/py/selenium/webdriver/common/action_chains.py
4004 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.16"""The ActionChains implementation."""1718from __future__ import annotations1920from typing import TYPE_CHECKING2122from selenium.webdriver.common.actions.action_builder import ActionBuilder23from selenium.webdriver.common.actions.key_input import KeyInput24from selenium.webdriver.common.actions.pointer_input import PointerInput25from selenium.webdriver.common.actions.wheel_input import ScrollOrigin, WheelInput26from selenium.webdriver.common.utils import keys_to_typing27from selenium.webdriver.remote.webelement import WebElement2829if TYPE_CHECKING:30from selenium.webdriver.remote.webdriver import WebDriver313233class ActionChains:34"""Automate low-level interactions like mouse movements, button actions, key presses, and context menus.3536ActionChains are a way to automate low level interactions such as mouse37movements, mouse button actions, key press, and context menu interactions.38This is useful for doing more complex actions like hover over and drag and39drop.4041Generate user actions.42When you call methods for actions on the ActionChains object,43the actions are stored in a queue in the ActionChains object.44When you call perform(), the events are fired in the order they45are queued up.4647ActionChains can be used in a chain pattern::4849menu = driver.find_element(By.CSS_SELECTOR, ".nav")50hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")5152ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()5354Or actions can be queued up one by one, then performed.::5556menu = driver.find_element(By.CSS_SELECTOR, ".nav")57hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")5859actions = ActionChains(driver)60actions.move_to_element(menu)61actions.click(hidden_submenu)62actions.perform()6364Either way, the actions are performed in the order they are called, one after65another.66"""6768def __init__(69self,70driver: WebDriver,71duration: int = 250,72devices: list[PointerInput | KeyInput | WheelInput] | None = None,73) -> None:74"""Creates a new ActionChains.7576Args:77driver: The WebDriver instance which performs user actions.78duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in PointerInput79devices: Optional list of input devices (PointerInput, KeyInput, WheelInput) to use.80If not provided, default devices will be created.81"""82self._driver = driver83mouse = None84keyboard = None85wheel = None86if devices is not None and isinstance(devices, list):87for device in devices:88if isinstance(device, PointerInput):89mouse = device90if isinstance(device, KeyInput):91keyboard = device92if isinstance(device, WheelInput):93wheel = device94self.w3c_actions = ActionBuilder(driver, mouse=mouse, keyboard=keyboard, wheel=wheel, duration=duration)9596def perform(self) -> None:97"""Performs all stored actions."""98self.w3c_actions.perform()99100def reset_actions(self) -> None:101"""Clear actions stored locally and on the remote end."""102self.w3c_actions.clear_actions()103for device in self.w3c_actions.devices:104device.clear_actions()105106def click(self, on_element: WebElement | None = None) -> ActionChains:107"""Clicks an element.108109Args:110on_element: The element to click.111If None, clicks on current mouse position.112"""113if on_element:114self.move_to_element(on_element)115116self.w3c_actions.pointer_action.click()117self.w3c_actions.key_action.pause()118self.w3c_actions.key_action.pause()119120return self121122def click_and_hold(self, on_element: WebElement | None = None) -> ActionChains:123"""Holds down the left mouse button on an element.124125Args:126on_element: The element to mouse down.127If None, clicks on current mouse position.128"""129if on_element:130self.move_to_element(on_element)131132self.w3c_actions.pointer_action.click_and_hold()133self.w3c_actions.key_action.pause()134135return self136137def context_click(self, on_element: WebElement | None = None) -> ActionChains:138"""Performs a context-click (right click) on an element.139140Args:141on_element: The element to context-click.142If None, clicks on current mouse position.143"""144if on_element:145self.move_to_element(on_element)146147self.w3c_actions.pointer_action.context_click()148self.w3c_actions.key_action.pause()149self.w3c_actions.key_action.pause()150151return self152153def double_click(self, on_element: WebElement | None = None) -> ActionChains:154"""Double-clicks an element.155156Args:157on_element: The element to double-click.158If None, clicks on current mouse position.159"""160if on_element:161self.move_to_element(on_element)162163self.w3c_actions.pointer_action.double_click()164for _ in range(4):165self.w3c_actions.key_action.pause()166167return self168169def drag_and_drop(self, source: WebElement, target: WebElement) -> ActionChains:170"""Hold down the left mouse button on an element, then move to target and release.171172Args:173source: The element to mouse down.174target: The element to mouse up.175"""176self.click_and_hold(source)177self.release(target)178return self179180def drag_and_drop_by_offset(self, source: WebElement, xoffset: int, yoffset: int) -> ActionChains:181"""Hold down the left mouse button on an element, then move by offset and release.182183Args:184source: The element to mouse down.185xoffset: X offset to move to.186yoffset: Y offset to move to.187"""188self.click_and_hold(source)189self.move_by_offset(xoffset, yoffset)190self.release()191return self192193def key_down(self, value: str, element: WebElement | None = None) -> ActionChains:194"""Send a key press only without releasing it (modifier keys only).195196Args:197value: The modifier key to send. Values are defined in `Keys` class.198element: The element to send keys.199If None, sends a key to current focused element.200201Example, pressing ctrl+c::202203ActionChains(driver).key_down(Keys.CONTROL).send_keys("c").key_up(Keys.CONTROL).perform()204"""205if element:206self.click(element)207208self.w3c_actions.key_action.key_down(value)209self.w3c_actions.pointer_action.pause()210211return self212213def key_up(self, value: str, element: WebElement | None = None) -> ActionChains:214"""Releases a modifier key.215216Args:217value: The modifier key to send. Values are defined in Keys class.218element: The element to send keys.219If None, sends a key to current focused element.220221Example, pressing ctrl+c::222223ActionChains(driver).key_down(Keys.CONTROL).send_keys("c").key_up(Keys.CONTROL).perform()224"""225if element:226self.click(element)227228self.w3c_actions.key_action.key_up(value)229self.w3c_actions.pointer_action.pause()230231return self232233def move_by_offset(self, xoffset: int, yoffset: int) -> ActionChains:234"""Moving the mouse to an offset from current mouse position.235236Args:237xoffset: X offset to move to, as a positive or negative integer.238yoffset: Y offset to move to, as a positive or negative integer.239"""240self.w3c_actions.pointer_action.move_by(xoffset, yoffset)241self.w3c_actions.key_action.pause()242243return self244245def move_to_element(self, to_element: WebElement) -> ActionChains:246"""Moving the mouse to the middle of an element.247248Args:249to_element: The WebElement to move to.250"""251self.w3c_actions.pointer_action.move_to(to_element)252self.w3c_actions.key_action.pause()253254return self255256def move_to_element_with_offset(self, to_element: WebElement, xoffset: int, yoffset: int) -> ActionChains:257"""Move the mouse to an element with the specified offsets.258259Offsets are relative to the in-view center point of the element.260261Args:262to_element: The WebElement to move to.263xoffset: X offset to move to, as a positive or negative integer.264yoffset: Y offset to move to, as a positive or negative integer.265"""266self.w3c_actions.pointer_action.move_to(to_element, int(xoffset), int(yoffset))267self.w3c_actions.key_action.pause()268269return self270271def pause(self, seconds: float | int) -> ActionChains:272"""Pause all inputs for the specified duration in seconds."""273self.w3c_actions.pointer_action.pause(seconds)274self.w3c_actions.key_action.pause(int(seconds))275276return self277278def release(self, on_element: WebElement | None = None) -> ActionChains:279"""Releasing a held mouse button on an element.280281Args:282on_element: The element to mouse up.283If None, releases on current mouse position.284"""285if on_element:286self.move_to_element(on_element)287288self.w3c_actions.pointer_action.release()289self.w3c_actions.key_action.pause()290291return self292293def send_keys(self, *keys_to_send: str) -> ActionChains:294"""Sends keys to current focused element.295296Args:297keys_to_send: The keys to send. Modifier keys constants can be found in the298'Keys' class.299"""300typing = keys_to_typing(keys_to_send)301302for key in typing:303self.key_down(key)304self.key_up(key)305306return self307308def send_keys_to_element(self, element: WebElement, *keys_to_send: str) -> ActionChains:309"""Sends keys to an element.310311Args:312element: The element to send keys.313keys_to_send: The keys to send. Modifier keys constants can be found in the314'Keys' class.315"""316self.click(element)317self.send_keys(*keys_to_send)318return self319320def scroll_to_element(self, element: WebElement) -> ActionChains:321"""Scroll the element into the viewport if it's outside it.322323Scrolls the bottom of the element to the bottom of the viewport.324325Args:326element: Which element to scroll into the viewport.327"""328self.w3c_actions.wheel_action.scroll(origin=element)329return self330331def scroll_by_amount(self, delta_x: int, delta_y: int) -> ActionChains:332"""Scroll by a provided amount with the origin in the top left corner.333334Scrolls by provided amounts with the origin in the top left corner335of the viewport.336337Args:338delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.339delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.340"""341self.w3c_actions.wheel_action.scroll(delta_x=delta_x, delta_y=delta_y)342return self343344def scroll_from_origin(self, scroll_origin: ScrollOrigin, delta_x: int, delta_y: int) -> ActionChains:345"""Scroll by a provided amount based on a scroll origin (element or viewport).346347The scroll origin is either the center of an element or the upper left of the348viewport plus any offsets. If the origin is an element, and the element349is not in the viewport, the bottom of the element will first be350scrolled to the bottom of the viewport.351352Args:353scroll_origin: Where scroll originates (viewport or element center) plus provided offsets.354delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.355delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.356357Raises:358MoveTargetOutOfBoundsException: If the origin with offset is outside the viewport.359"""360if not isinstance(scroll_origin, ScrollOrigin):361raise TypeError(f"Expected object of type ScrollOrigin, got: {type(scroll_origin)}")362363self.w3c_actions.wheel_action.scroll(364origin=scroll_origin.origin,365x=scroll_origin.x_offset,366y=scroll_origin.y_offset,367delta_x=delta_x,368delta_y=delta_y,369)370return self371372# Context manager so ActionChains can be used in a 'with .. as' statements.373374def __enter__(self) -> ActionChains:375return self # Return created instance of self.376377def __exit__(self, _type, _value, _traceback) -> None:378pass # Do nothing, does not require additional cleanup.379380381