Path: blob/trunk/py/selenium/webdriver/common/action_chains.py
1864 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_CHECKING, Union2122from selenium.webdriver.remote.webelement import WebElement2324from .actions.action_builder import ActionBuilder25from .actions.key_input import KeyInput26from .actions.pointer_input import PointerInput27from .actions.wheel_input import ScrollOrigin, WheelInput28from .utils import keys_to_typing2930if TYPE_CHECKING:31from selenium.webdriver.remote.webdriver import WebDriver3233AnyDevice = Union[PointerInput, KeyInput, WheelInput]343536class ActionChains:37"""ActionChains are a way to automate low level interactions such as mouse38movements, mouse button actions, key press, and context menu interactions.39This is useful for doing more complex actions like hover over and drag and40drop.4142Generate user actions.43When you call methods for actions on the ActionChains object,44the actions are stored in a queue in the ActionChains object.45When you call perform(), the events are fired in the order they46are queued up.4748ActionChains can be used in a chain pattern::4950menu = driver.find_element(By.CSS_SELECTOR, ".nav")51hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")5253ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()5455Or actions can be queued up one by one, then performed.::5657menu = driver.find_element(By.CSS_SELECTOR, ".nav")58hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")5960actions = ActionChains(driver)61actions.move_to_element(menu)62actions.click(hidden_submenu)63actions.perform()6465Either way, the actions are performed in the order they are called, one after66another.67"""6869def __init__(self, driver: WebDriver, duration: int = 250, devices: list[AnyDevice] | None = None) -> None:70"""Creates a new ActionChains.7172:Args:73- driver: The WebDriver instance which performs user actions.74- duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in PointerInput75"""76self._driver = driver77mouse = None78keyboard = None79wheel = None80if devices is not None and isinstance(devices, list):81for device in devices:82if isinstance(device, PointerInput):83mouse = device84if isinstance(device, KeyInput):85keyboard = device86if isinstance(device, WheelInput):87wheel = device88self.w3c_actions = ActionBuilder(driver, mouse=mouse, keyboard=keyboard, wheel=wheel, duration=duration)8990def perform(self) -> None:91"""Performs all stored actions."""92self.w3c_actions.perform()9394def reset_actions(self) -> None:95"""Clears actions that are already stored locally and on the remote96end."""97self.w3c_actions.clear_actions()98for device in self.w3c_actions.devices:99device.clear_actions()100101def click(self, on_element: WebElement | None = None) -> ActionChains:102"""Clicks an element.103104:Args:105- on_element: The element to click.106If None, clicks on current mouse position.107"""108if on_element:109self.move_to_element(on_element)110111self.w3c_actions.pointer_action.click()112self.w3c_actions.key_action.pause()113self.w3c_actions.key_action.pause()114115return self116117def click_and_hold(self, on_element: WebElement | None = None) -> ActionChains:118"""Holds down the left mouse button on an element.119120:Args:121- on_element: The element to mouse down.122If None, clicks on current mouse position.123"""124if on_element:125self.move_to_element(on_element)126127self.w3c_actions.pointer_action.click_and_hold()128self.w3c_actions.key_action.pause()129130return self131132def context_click(self, on_element: WebElement | None = None) -> ActionChains:133"""Performs a context-click (right click) on an element.134135:Args:136- on_element: The element to context-click.137If None, clicks on current mouse position.138"""139if on_element:140self.move_to_element(on_element)141142self.w3c_actions.pointer_action.context_click()143self.w3c_actions.key_action.pause()144self.w3c_actions.key_action.pause()145146return self147148def double_click(self, on_element: WebElement | None = None) -> ActionChains:149"""Double-clicks an element.150151:Args:152- on_element: The element to double-click.153If None, clicks on current mouse position.154"""155if on_element:156self.move_to_element(on_element)157158self.w3c_actions.pointer_action.double_click()159for _ in range(4):160self.w3c_actions.key_action.pause()161162return self163164def drag_and_drop(self, source: WebElement, target: WebElement) -> ActionChains:165"""Holds down the left mouse button on the source element, then moves166to the target element and releases the mouse button.167168:Args:169- source: The element to mouse down.170- target: The element to mouse up.171"""172self.click_and_hold(source)173self.release(target)174return self175176def drag_and_drop_by_offset(self, source: WebElement, xoffset: int, yoffset: int) -> ActionChains:177"""Holds down the left mouse button on the source element, then moves178to the target offset and releases the mouse button.179180:Args:181- source: The element to mouse down.182- xoffset: X offset to move to.183- yoffset: Y offset to move to.184"""185self.click_and_hold(source)186self.move_by_offset(xoffset, yoffset)187self.release()188return self189190def key_down(self, value: str, element: WebElement | None = None) -> ActionChains:191"""Sends a key press only, without releasing it. Should only be used192with modifier keys (Control, Alt and Shift).193194:Args:195- value: The modifier key to send. Values are defined in `Keys` class.196- element: The element to send keys.197If None, sends a key to current focused element.198199Example, pressing ctrl+c::200201ActionChains(driver).key_down(Keys.CONTROL).send_keys("c").key_up(Keys.CONTROL).perform()202"""203if element:204self.click(element)205206self.w3c_actions.key_action.key_down(value)207self.w3c_actions.pointer_action.pause()208209return self210211def key_up(self, value: str, element: WebElement | None = None) -> ActionChains:212"""Releases a modifier key.213214:Args:215- value: The modifier key to send. Values are defined in Keys class.216- element: The element to send keys.217If None, sends a key to current focused element.218219Example, pressing ctrl+c::220221ActionChains(driver).key_down(Keys.CONTROL).send_keys("c").key_up(Keys.CONTROL).perform()222"""223if element:224self.click(element)225226self.w3c_actions.key_action.key_up(value)227self.w3c_actions.pointer_action.pause()228229return self230231def move_by_offset(self, xoffset: int, yoffset: int) -> ActionChains:232"""Moving the mouse to an offset from current mouse position.233234:Args:235- xoffset: X offset to move to, as a positive or negative integer.236- yoffset: Y offset to move to, as a positive or negative integer.237"""238239self.w3c_actions.pointer_action.move_by(xoffset, yoffset)240self.w3c_actions.key_action.pause()241242return self243244def move_to_element(self, to_element: WebElement) -> ActionChains:245"""Moving the mouse to the middle of an element.246247:Args:248- to_element: The WebElement to move to.249"""250251self.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 by an offset of the specified element. Offsets are258relative to the in-view center point of the element.259260:Args:261- to_element: The WebElement to move to.262- xoffset: X offset to move to, as a positive or negative integer.263- yoffset: Y offset to move to, as a positive or negative integer.264"""265266self.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."""273274self.w3c_actions.pointer_action.pause(seconds)275self.w3c_actions.key_action.pause(int(seconds))276277return self278279def release(self, on_element: WebElement | None = None) -> ActionChains:280"""Releasing a held mouse button on an element.281282:Args:283- on_element: The element to mouse up.284If None, releases on current mouse position.285"""286if on_element:287self.move_to_element(on_element)288289self.w3c_actions.pointer_action.release()290self.w3c_actions.key_action.pause()291292return self293294def send_keys(self, *keys_to_send: str) -> ActionChains:295"""Sends keys to current focused element.296297:Args:298- keys_to_send: The keys to send. Modifier keys constants can be found in the299'Keys' class.300"""301typing = keys_to_typing(keys_to_send)302303for key in typing:304self.key_down(key)305self.key_up(key)306307return self308309def send_keys_to_element(self, element: WebElement, *keys_to_send: str) -> ActionChains:310"""Sends keys to an element.311312:Args:313- element: The element to send keys.314- keys_to_send: The keys to send. Modifier keys constants can be found in the315'Keys' class.316"""317self.click(element)318self.send_keys(*keys_to_send)319return self320321def scroll_to_element(self, element: WebElement) -> ActionChains:322"""If the element is outside the viewport, scrolls the bottom of the323element to the bottom of the viewport.324325:Args:326- element: Which element to scroll into the viewport.327"""328329self.w3c_actions.wheel_action.scroll(origin=element)330return self331332def scroll_by_amount(self, delta_x: int, delta_y: int) -> ActionChains:333"""Scrolls by provided amounts with the origin in the top left corner334of the viewport.335336:Args:337- delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.338- delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.339"""340341self.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"""Scrolls by provided amount based on a provided origin. The scroll346origin is either the center of an element or the upper left of the347viewport plus any offsets. If the origin is an element, and the element348is not in the viewport, the bottom of the element will first be349scrolled to the bottom of the viewport.350351:Args:352- origin: Where scroll originates (viewport or element center) plus provided offsets.353- delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.354- delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.355356:Raises: If the origin with offset is outside the viewport.357- MoveTargetOutOfBoundsException - If the origin with offset is outside the viewport.358"""359360if 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