Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/common/action_chains.py
1864 views
1
# Licensed to the Software Freedom Conservancy (SFC) under one
2
# or more contributor license agreements. See the NOTICE file
3
# distributed with this work for additional information
4
# regarding copyright ownership. The SFC licenses this file
5
# to you under the Apache License, Version 2.0 (the
6
# "License"); you may not use this file except in compliance
7
# with the License. You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing,
12
# software distributed under the License is distributed on an
13
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
# KIND, either express or implied. See the License for the
15
# specific language governing permissions and limitations
16
# under the License.
17
"""The ActionChains implementation."""
18
19
from __future__ import annotations
20
21
from typing import TYPE_CHECKING, Union
22
23
from selenium.webdriver.remote.webelement import WebElement
24
25
from .actions.action_builder import ActionBuilder
26
from .actions.key_input import KeyInput
27
from .actions.pointer_input import PointerInput
28
from .actions.wheel_input import ScrollOrigin, WheelInput
29
from .utils import keys_to_typing
30
31
if TYPE_CHECKING:
32
from selenium.webdriver.remote.webdriver import WebDriver
33
34
AnyDevice = Union[PointerInput, KeyInput, WheelInput]
35
36
37
class ActionChains:
38
"""ActionChains are a way to automate low level interactions such as mouse
39
movements, mouse button actions, key press, and context menu interactions.
40
This is useful for doing more complex actions like hover over and drag and
41
drop.
42
43
Generate user actions.
44
When you call methods for actions on the ActionChains object,
45
the actions are stored in a queue in the ActionChains object.
46
When you call perform(), the events are fired in the order they
47
are queued up.
48
49
ActionChains can be used in a chain pattern::
50
51
menu = driver.find_element(By.CSS_SELECTOR, ".nav")
52
hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")
53
54
ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()
55
56
Or actions can be queued up one by one, then performed.::
57
58
menu = driver.find_element(By.CSS_SELECTOR, ".nav")
59
hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")
60
61
actions = ActionChains(driver)
62
actions.move_to_element(menu)
63
actions.click(hidden_submenu)
64
actions.perform()
65
66
Either way, the actions are performed in the order they are called, one after
67
another.
68
"""
69
70
def __init__(self, driver: WebDriver, duration: int = 250, devices: list[AnyDevice] | None = None) -> None:
71
"""Creates a new ActionChains.
72
73
:Args:
74
- driver: The WebDriver instance which performs user actions.
75
- duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in PointerInput
76
"""
77
self._driver = driver
78
mouse = None
79
keyboard = None
80
wheel = None
81
if devices is not None and isinstance(devices, list):
82
for device in devices:
83
if isinstance(device, PointerInput):
84
mouse = device
85
if isinstance(device, KeyInput):
86
keyboard = device
87
if isinstance(device, WheelInput):
88
wheel = device
89
self.w3c_actions = ActionBuilder(driver, mouse=mouse, keyboard=keyboard, wheel=wheel, duration=duration)
90
91
def perform(self) -> None:
92
"""Performs all stored actions."""
93
self.w3c_actions.perform()
94
95
def reset_actions(self) -> None:
96
"""Clears actions that are already stored locally and on the remote
97
end."""
98
self.w3c_actions.clear_actions()
99
for device in self.w3c_actions.devices:
100
device.clear_actions()
101
102
def click(self, on_element: WebElement | None = None) -> ActionChains:
103
"""Clicks an element.
104
105
:Args:
106
- on_element: The element to click.
107
If None, clicks on current mouse position.
108
"""
109
if on_element:
110
self.move_to_element(on_element)
111
112
self.w3c_actions.pointer_action.click()
113
self.w3c_actions.key_action.pause()
114
self.w3c_actions.key_action.pause()
115
116
return self
117
118
def click_and_hold(self, on_element: WebElement | None = None) -> ActionChains:
119
"""Holds down the left mouse button on an element.
120
121
:Args:
122
- on_element: The element to mouse down.
123
If None, clicks on current mouse position.
124
"""
125
if on_element:
126
self.move_to_element(on_element)
127
128
self.w3c_actions.pointer_action.click_and_hold()
129
self.w3c_actions.key_action.pause()
130
131
return self
132
133
def context_click(self, on_element: WebElement | None = None) -> ActionChains:
134
"""Performs a context-click (right click) on an element.
135
136
:Args:
137
- on_element: The element to context-click.
138
If None, clicks on current mouse position.
139
"""
140
if on_element:
141
self.move_to_element(on_element)
142
143
self.w3c_actions.pointer_action.context_click()
144
self.w3c_actions.key_action.pause()
145
self.w3c_actions.key_action.pause()
146
147
return self
148
149
def double_click(self, on_element: WebElement | None = None) -> ActionChains:
150
"""Double-clicks an element.
151
152
:Args:
153
- on_element: The element to double-click.
154
If None, clicks on current mouse position.
155
"""
156
if on_element:
157
self.move_to_element(on_element)
158
159
self.w3c_actions.pointer_action.double_click()
160
for _ in range(4):
161
self.w3c_actions.key_action.pause()
162
163
return self
164
165
def drag_and_drop(self, source: WebElement, target: WebElement) -> ActionChains:
166
"""Holds down the left mouse button on the source element, then moves
167
to the target element and releases the mouse button.
168
169
:Args:
170
- source: The element to mouse down.
171
- target: The element to mouse up.
172
"""
173
self.click_and_hold(source)
174
self.release(target)
175
return self
176
177
def drag_and_drop_by_offset(self, source: WebElement, xoffset: int, yoffset: int) -> ActionChains:
178
"""Holds down the left mouse button on the source element, then moves
179
to the target offset and releases the mouse button.
180
181
:Args:
182
- source: The element to mouse down.
183
- xoffset: X offset to move to.
184
- yoffset: Y offset to move to.
185
"""
186
self.click_and_hold(source)
187
self.move_by_offset(xoffset, yoffset)
188
self.release()
189
return self
190
191
def key_down(self, value: str, element: WebElement | None = None) -> ActionChains:
192
"""Sends a key press only, without releasing it. Should only be used
193
with modifier keys (Control, Alt and Shift).
194
195
:Args:
196
- value: The modifier key to send. Values are defined in `Keys` class.
197
- element: The element to send keys.
198
If None, sends a key to current focused element.
199
200
Example, pressing ctrl+c::
201
202
ActionChains(driver).key_down(Keys.CONTROL).send_keys("c").key_up(Keys.CONTROL).perform()
203
"""
204
if element:
205
self.click(element)
206
207
self.w3c_actions.key_action.key_down(value)
208
self.w3c_actions.pointer_action.pause()
209
210
return self
211
212
def key_up(self, value: str, element: WebElement | None = None) -> ActionChains:
213
"""Releases a modifier key.
214
215
:Args:
216
- value: The modifier key to send. Values are defined in Keys class.
217
- element: The element to send keys.
218
If None, sends a key to current focused element.
219
220
Example, pressing ctrl+c::
221
222
ActionChains(driver).key_down(Keys.CONTROL).send_keys("c").key_up(Keys.CONTROL).perform()
223
"""
224
if element:
225
self.click(element)
226
227
self.w3c_actions.key_action.key_up(value)
228
self.w3c_actions.pointer_action.pause()
229
230
return self
231
232
def move_by_offset(self, xoffset: int, yoffset: int) -> ActionChains:
233
"""Moving the mouse to an offset from current mouse position.
234
235
:Args:
236
- xoffset: X offset to move to, as a positive or negative integer.
237
- yoffset: Y offset to move to, as a positive or negative integer.
238
"""
239
240
self.w3c_actions.pointer_action.move_by(xoffset, yoffset)
241
self.w3c_actions.key_action.pause()
242
243
return self
244
245
def move_to_element(self, to_element: WebElement) -> ActionChains:
246
"""Moving the mouse to the middle of an element.
247
248
:Args:
249
- to_element: The WebElement to move to.
250
"""
251
252
self.w3c_actions.pointer_action.move_to(to_element)
253
self.w3c_actions.key_action.pause()
254
255
return self
256
257
def move_to_element_with_offset(self, to_element: WebElement, xoffset: int, yoffset: int) -> ActionChains:
258
"""Move the mouse by an offset of the specified element. Offsets are
259
relative to the in-view center point of the element.
260
261
:Args:
262
- to_element: The WebElement to move to.
263
- xoffset: X offset to move to, as a positive or negative integer.
264
- yoffset: Y offset to move to, as a positive or negative integer.
265
"""
266
267
self.w3c_actions.pointer_action.move_to(to_element, int(xoffset), int(yoffset))
268
self.w3c_actions.key_action.pause()
269
270
return self
271
272
def pause(self, seconds: float | int) -> ActionChains:
273
"""Pause all inputs for the specified duration in seconds."""
274
275
self.w3c_actions.pointer_action.pause(seconds)
276
self.w3c_actions.key_action.pause(int(seconds))
277
278
return self
279
280
def release(self, on_element: WebElement | None = None) -> ActionChains:
281
"""Releasing a held mouse button on an element.
282
283
:Args:
284
- on_element: The element to mouse up.
285
If None, releases on current mouse position.
286
"""
287
if on_element:
288
self.move_to_element(on_element)
289
290
self.w3c_actions.pointer_action.release()
291
self.w3c_actions.key_action.pause()
292
293
return self
294
295
def send_keys(self, *keys_to_send: str) -> ActionChains:
296
"""Sends keys to current focused element.
297
298
:Args:
299
- keys_to_send: The keys to send. Modifier keys constants can be found in the
300
'Keys' class.
301
"""
302
typing = keys_to_typing(keys_to_send)
303
304
for key in typing:
305
self.key_down(key)
306
self.key_up(key)
307
308
return self
309
310
def send_keys_to_element(self, element: WebElement, *keys_to_send: str) -> ActionChains:
311
"""Sends keys to an element.
312
313
:Args:
314
- element: The element to send keys.
315
- keys_to_send: The keys to send. Modifier keys constants can be found in the
316
'Keys' class.
317
"""
318
self.click(element)
319
self.send_keys(*keys_to_send)
320
return self
321
322
def scroll_to_element(self, element: WebElement) -> ActionChains:
323
"""If the element is outside the viewport, scrolls the bottom of the
324
element to the bottom of the viewport.
325
326
:Args:
327
- element: Which element to scroll into the viewport.
328
"""
329
330
self.w3c_actions.wheel_action.scroll(origin=element)
331
return self
332
333
def scroll_by_amount(self, delta_x: int, delta_y: int) -> ActionChains:
334
"""Scrolls by provided amounts with the origin in the top left corner
335
of the viewport.
336
337
:Args:
338
- delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.
339
- delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.
340
"""
341
342
self.w3c_actions.wheel_action.scroll(delta_x=delta_x, delta_y=delta_y)
343
return self
344
345
def scroll_from_origin(self, scroll_origin: ScrollOrigin, delta_x: int, delta_y: int) -> ActionChains:
346
"""Scrolls by provided amount based on a provided origin. The scroll
347
origin is either the center of an element or the upper left of the
348
viewport plus any offsets. If the origin is an element, and the element
349
is not in the viewport, the bottom of the element will first be
350
scrolled to the bottom of the viewport.
351
352
:Args:
353
- origin: Where scroll originates (viewport or element center) plus provided offsets.
354
- delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.
355
- delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.
356
357
:Raises: If the origin with offset is outside the viewport.
358
- MoveTargetOutOfBoundsException - If the origin with offset is outside the viewport.
359
"""
360
361
if not isinstance(scroll_origin, ScrollOrigin):
362
raise TypeError(f"Expected object of type ScrollOrigin, got: {type(scroll_origin)}")
363
364
self.w3c_actions.wheel_action.scroll(
365
origin=scroll_origin.origin,
366
x=scroll_origin.x_offset,
367
y=scroll_origin.y_offset,
368
delta_x=delta_x,
369
delta_y=delta_y,
370
)
371
return self
372
373
# Context manager so ActionChains can be used in a 'with .. as' statements.
374
375
def __enter__(self) -> ActionChains:
376
return self # Return created instance of self.
377
378
def __exit__(self, _type, _value, _traceback) -> None:
379
pass # Do nothing, does not require additional cleanup.
380
381