0
# ActionChains for Complex Interactions
1
2
This document covers ActionChains in Python Selenium WebDriver, which enable complex user interactions like mouse movements, drag and drop, keyboard combinations, and scrolling actions.
3
4
## ActionChains Class
5
6
{ .api }
7
```python
8
from selenium.webdriver.common.action_chains import ActionChains
9
10
class ActionChains:
11
def __init__(
12
self,
13
driver: WebDriver,
14
duration: int = 250,
15
devices: Optional[List[AnyDevice]] = None
16
) -> None
17
```
18
19
**Description**: ActionChains are a way to automate low level interactions such as mouse movements, mouse button actions, key press, and context menu interactions. Actions are stored in a queue and executed when `perform()` is called.
20
21
**Parameters**:
22
- `driver`: The WebDriver instance which performs user actions
23
- `duration`: Override the default 250 msecs of DEFAULT_MOVE_DURATION in PointerInput
24
- `devices`: Optional list of input devices (PointerInput, KeyInput, WheelInput)
25
26
**Usage Patterns**:
27
28
1. **Method Chaining Pattern**:
29
```python
30
menu = driver.find_element(By.CSS_SELECTOR, ".nav")
31
hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")
32
33
ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()
34
```
35
36
2. **Sequential Actions Pattern**:
37
```python
38
actions = ActionChains(driver)
39
actions.move_to_element(menu)
40
actions.click(hidden_submenu)
41
actions.perform()
42
```
43
44
## Core ActionChains Methods
45
46
### Action Execution
47
48
{ .api }
49
```python
50
def perform(self) -> None
51
```
52
53
**Description**: Performs all stored actions in the order they were added.
54
55
{ .api }
56
```python
57
def reset_actions(self) -> None
58
```
59
60
**Description**: Clears actions that are already stored locally and on the remote end.
61
62
**Example**:
63
```python
64
actions = ActionChains(driver)
65
actions.click(element1)
66
actions.click(element2)
67
actions.perform() # Execute both clicks
68
69
actions.reset_actions() # Clear the action queue
70
```
71
72
## Mouse Actions
73
74
### Click Actions
75
76
{ .api }
77
```python
78
def click(self, on_element: Optional[WebElement] = None) -> ActionChains
79
```
80
81
**Description**: Clicks an element.
82
83
**Parameters**:
84
- `on_element`: The element to click. If None, clicks on current mouse position
85
86
**Example**:
87
```python
88
# Click on specific element
89
ActionChains(driver).click(button_element).perform()
90
91
# Click at current mouse position
92
ActionChains(driver).move_to_element(element).click().perform()
93
```
94
95
{ .api }
96
```python
97
def double_click(self, on_element: Optional[WebElement] = None) -> ActionChains
98
```
99
100
**Description**: Double-clicks an element.
101
102
**Parameters**:
103
- `on_element`: The element to double-click. If None, clicks on current mouse position
104
105
**Example**:
106
```python
107
# Double-click to select word or open item
108
text_element = driver.find_element(By.ID, "editable-text")
109
ActionChains(driver).double_click(text_element).perform()
110
```
111
112
{ .api }
113
```python
114
def context_click(self, on_element: Optional[WebElement] = None) -> ActionChains
115
```
116
117
**Description**: Performs a context-click (right click) on an element.
118
119
**Parameters**:
120
- `on_element`: The element to context-click. If None, clicks on current mouse position
121
122
**Example**:
123
```python
124
# Right-click to open context menu
125
image = driver.find_element(By.ID, "photo")
126
ActionChains(driver).context_click(image).perform()
127
128
# Handle context menu
129
context_menu = driver.find_element(By.CLASS_NAME, "context-menu")
130
save_option = context_menu.find_element(By.TEXT, "Save Image")
131
save_option.click()
132
```
133
134
### Click and Hold Operations
135
136
{ .api }
137
```python
138
def click_and_hold(self, on_element: Optional[WebElement] = None) -> ActionChains
139
```
140
141
**Description**: Holds down the left mouse button on an element.
142
143
**Parameters**:
144
- `on_element`: The element to mouse down. If None, holds at current mouse position
145
146
{ .api }
147
```python
148
def release(self, on_element: Optional[WebElement] = None) -> ActionChains
149
```
150
151
**Description**: Releases a held mouse button on an element.
152
153
**Parameters**:
154
- `on_element`: The element to mouse up. If None, releases at current mouse position
155
156
**Example**:
157
```python
158
# Manual drag operation
159
source = driver.find_element(By.ID, "draggable")
160
target = driver.find_element(By.ID, "droppable")
161
162
ActionChains(driver)\
163
.click_and_hold(source)\
164
.move_to_element(target)\
165
.release()\
166
.perform()
167
```
168
169
### Mouse Movement
170
171
{ .api }
172
```python
173
def move_to_element(self, to_element: WebElement) -> ActionChains
174
```
175
176
**Description**: Moves the mouse to the middle of an element.
177
178
**Parameters**:
179
- `to_element`: The WebElement to move to
180
181
**Example**:
182
```python
183
# Hover to reveal dropdown menu
184
menu_item = driver.find_element(By.CLASS_NAME, "dropdown-trigger")
185
ActionChains(driver).move_to_element(menu_item).perform()
186
187
# Wait for dropdown to appear
188
dropdown = WebDriverWait(driver, 5).until(
189
EC.visibility_of_element_located((By.CLASS_NAME, "dropdown-menu"))
190
)
191
```
192
193
{ .api }
194
```python
195
def move_to_element_with_offset(
196
self,
197
to_element: WebElement,
198
xoffset: int,
199
yoffset: int
200
) -> ActionChains
201
```
202
203
**Description**: Move the mouse by an offset of the specified element. Offsets are relative to the in-view center point of the element.
204
205
**Parameters**:
206
- `to_element`: The WebElement to move to
207
- `xoffset`: X offset to move to, as a positive or negative integer
208
- `yoffset`: Y offset to move to, as a positive or negative integer
209
210
**Example**:
211
```python
212
# Move to specific position within element
213
canvas = driver.find_element(By.ID, "drawing-canvas")
214
ActionChains(driver)\
215
.move_to_element_with_offset(canvas, 100, 50)\
216
.click()\
217
.perform()
218
```
219
220
{ .api }
221
```python
222
def move_by_offset(self, xoffset: int, yoffset: int) -> ActionChains
223
```
224
225
**Description**: Moves the mouse to an offset from current mouse position.
226
227
**Parameters**:
228
- `xoffset`: X offset to move to, as a positive or negative integer
229
- `yoffset`: Y offset to move to, as a positive or negative integer
230
231
**Example**:
232
```python
233
# Draw a simple shape by moving mouse
234
ActionChains(driver)\
235
.click_and_hold()\
236
.move_by_offset(100, 0)\
237
.move_by_offset(0, 100)\
238
.move_by_offset(-100, 0)\
239
.move_by_offset(0, -100)\
240
.release()\
241
.perform()
242
```
243
244
## Drag and Drop Actions
245
246
{ .api }
247
```python
248
def drag_and_drop(self, source: WebElement, target: WebElement) -> ActionChains
249
```
250
251
**Description**: Holds down the left mouse button on the source element, then moves to the target element and releases the mouse button.
252
253
**Parameters**:
254
- `source`: The element to drag from
255
- `target`: The element to drop to
256
257
**Example**:
258
```python
259
# Simple drag and drop
260
source = driver.find_element(By.ID, "item1")
261
target = driver.find_element(By.ID, "basket")
262
263
ActionChains(driver).drag_and_drop(source, target).perform()
264
```
265
266
{ .api }
267
```python
268
def drag_and_drop_by_offset(
269
self,
270
source: WebElement,
271
xoffset: int,
272
yoffset: int
273
) -> ActionChains
274
```
275
276
**Description**: Holds down the left mouse button on the source element, then moves to the target offset and releases the mouse button.
277
278
**Parameters**:
279
- `source`: The element to drag from
280
- `xoffset`: X offset to move to
281
- `yoffset`: Y offset to move to
282
283
**Example**:
284
```python
285
# Drag element to specific position
286
slider = driver.find_element(By.CLASS_NAME, "slider-handle")
287
288
# Move slider 200 pixels to the right
289
ActionChains(driver)\
290
.drag_and_drop_by_offset(slider, 200, 0)\
291
.perform()
292
```
293
294
## Keyboard Actions
295
296
### Key Press and Release
297
298
{ .api }
299
```python
300
def key_down(self, value: str, element: Optional[WebElement] = None) -> ActionChains
301
```
302
303
**Description**: Sends a key press only, without releasing it. Should only be used with modifier keys (Control, Alt and Shift).
304
305
**Parameters**:
306
- `value`: The modifier key to send. Values are defined in Keys class
307
- `element`: The element to send keys to. If None, sends to currently focused element
308
309
{ .api }
310
```python
311
def key_up(self, value: str, element: Optional[WebElement] = None) -> ActionChains
312
```
313
314
**Description**: Releases a modifier key.
315
316
**Parameters**:
317
- `value`: The modifier key to release. Values are defined in Keys class
318
- `element`: The element to send keys to. If None, sends to currently focused element
319
320
**Example**:
321
```python
322
from selenium.webdriver.common.keys import Keys
323
324
# Ctrl+C (Copy)
325
ActionChains(driver)\
326
.key_down(Keys.CONTROL)\
327
.send_keys('c')\
328
.key_up(Keys.CONTROL)\
329
.perform()
330
331
# Ctrl+A (Select All) then type new text
332
text_area = driver.find_element(By.ID, "editor")
333
ActionChains(driver)\
334
.click(text_area)\
335
.key_down(Keys.CONTROL)\
336
.send_keys('a')\
337
.key_up(Keys.CONTROL)\
338
.send_keys("New content")\
339
.perform()
340
```
341
342
### Text Input
343
344
{ .api }
345
```python
346
def send_keys(self, *keys_to_send: str) -> ActionChains
347
```
348
349
**Description**: Sends keys to current focused element.
350
351
**Parameters**:
352
- `*keys_to_send`: The keys to send. Modifier keys constants can be found in the Keys class
353
354
**Example**:
355
```python
356
# Type text to focused element
357
ActionChains(driver)\
358
.send_keys("Hello World")\
359
.send_keys(Keys.ENTER)\
360
.perform()
361
```
362
363
{ .api }
364
```python
365
def send_keys_to_element(self, element: WebElement, *keys_to_send: str) -> ActionChains
366
```
367
368
**Description**: Sends keys to a specific element.
369
370
**Parameters**:
371
- `element`: The element to send keys to
372
- `*keys_to_send`: The keys to send. Modifier keys constants can be found in the Keys class
373
374
**Example**:
375
```python
376
username_field = driver.find_element(By.NAME, "username")
377
password_field = driver.find_element(By.NAME, "password")
378
379
ActionChains(driver)\
380
.send_keys_to_element(username_field, "testuser")\
381
.send_keys_to_element(password_field, "password123")\
382
.send_keys_to_element(password_field, Keys.ENTER)\
383
.perform()
384
```
385
386
## Keys Class Constants
387
388
{ .api }
389
```python
390
from selenium.webdriver.common.keys import Keys
391
392
class Keys:
393
# Modifier Keys
394
CONTROL = "\ue009"
395
ALT = "\ue00a"
396
SHIFT = "\ue008"
397
META = "\ue03d"
398
COMMAND = "\ue03d" # Same as META
399
400
# Navigation Keys
401
ENTER = "\ue007"
402
RETURN = "\ue006"
403
TAB = "\ue004"
404
SPACE = "\ue00d"
405
BACKSPACE = "\ue003"
406
DELETE = "\ue017"
407
ESCAPE = "\ue00c"
408
409
# Arrow Keys
410
LEFT = "\ue012"
411
UP = "\ue013"
412
RIGHT = "\ue014"
413
DOWN = "\ue015"
414
ARROW_LEFT = LEFT
415
ARROW_UP = UP
416
ARROW_RIGHT = RIGHT
417
ARROW_DOWN = DOWN
418
419
# Page Navigation
420
PAGE_UP = "\ue00e"
421
PAGE_DOWN = "\ue00f"
422
HOME = "\ue011"
423
END = "\ue010"
424
425
# Function Keys
426
F1 = "\ue031"
427
F2 = "\ue032"
428
# ... F3 through F12
429
F12 = "\ue03c"
430
431
# Numpad Keys
432
NUMPAD0 = "\ue01a"
433
NUMPAD1 = "\ue01b"
434
# ... NUMPAD2 through NUMPAD9
435
NUMPAD9 = "\ue023"
436
ADD = "\ue025"
437
SUBTRACT = "\ue027"
438
MULTIPLY = "\ue024"
439
DIVIDE = "\ue029"
440
```
441
442
**Description**: Set of special keys codes for use with ActionChains and send_keys methods.
443
444
**Common Key Combinations**:
445
```python
446
from selenium.webdriver.common.keys import Keys
447
448
# Copy (Ctrl+C)
449
ActionChains(driver)\
450
.key_down(Keys.CONTROL)\
451
.send_keys('c')\
452
.key_up(Keys.CONTROL)\
453
.perform()
454
455
# Select All (Ctrl+A)
456
ActionChains(driver)\
457
.key_down(Keys.CONTROL)\
458
.send_keys('a')\
459
.key_up(Keys.CONTROL)\
460
.perform()
461
462
# Undo (Ctrl+Z)
463
ActionChains(driver)\
464
.key_down(Keys.CONTROL)\
465
.send_keys('z')\
466
.key_up(Keys.CONTROL)\
467
.perform()
468
469
# New Tab (Ctrl+T)
470
ActionChains(driver)\
471
.key_down(Keys.CONTROL)\
472
.send_keys('t')\
473
.key_up(Keys.CONTROL)\
474
.perform()
475
```
476
477
## Scrolling Actions
478
479
### Element-based Scrolling
480
481
{ .api }
482
```python
483
def scroll_to_element(self, element: WebElement) -> ActionChains
484
```
485
486
**Description**: If the element is outside the viewport, scrolls the bottom of the element to the bottom of the viewport.
487
488
**Parameters**:
489
- `element`: Which element to scroll into the viewport
490
491
**Example**:
492
```python
493
footer = driver.find_element(By.ID, "footer")
494
ActionChains(driver).scroll_to_element(footer).perform()
495
```
496
497
### Amount-based Scrolling
498
499
{ .api }
500
```python
501
def scroll_by_amount(self, delta_x: int, delta_y: int) -> ActionChains
502
```
503
504
**Description**: Scrolls by provided amounts with the origin in the top left corner of the viewport.
505
506
**Parameters**:
507
- `delta_x`: Distance along X axis to scroll. A negative value scrolls left
508
- `delta_y`: Distance along Y axis to scroll. A negative value scrolls up
509
510
**Example**:
511
```python
512
# Scroll down 500 pixels
513
ActionChains(driver).scroll_by_amount(0, 500).perform()
514
515
# Scroll left 200 pixels and up 300 pixels
516
ActionChains(driver).scroll_by_amount(-200, -300).perform()
517
```
518
519
### Origin-based Scrolling
520
521
{ .api }
522
```python
523
def scroll_from_origin(
524
self,
525
scroll_origin: ScrollOrigin,
526
delta_x: int,
527
delta_y: int
528
) -> ActionChains
529
```
530
531
**Description**: Scrolls by provided amount based on a provided origin. The scroll origin is either the center of an element or the upper left of the viewport plus any offsets.
532
533
**Parameters**:
534
- `scroll_origin`: Where scroll originates (viewport or element center) plus provided offsets
535
- `delta_x`: Distance along X axis to scroll. A negative value scrolls left
536
- `delta_y`: Distance along Y axis to scroll. A negative value scrolls up
537
538
## ScrollOrigin Class
539
540
{ .api }
541
```python
542
from selenium.webdriver.common.actions.wheel_input import ScrollOrigin
543
544
class ScrollOrigin:
545
def __init__(self, origin: Union[str, WebElement], x_offset: int, y_offset: int) -> None
546
547
@classmethod
548
def from_element(cls, element: WebElement, x_offset: int = 0, y_offset: int = 0)
549
550
@classmethod
551
def from_viewport(cls, x_offset: int = 0, y_offset: int = 0)
552
```
553
554
**Description**: Represents a scroll origin point for scrolling operations.
555
556
**Factory Methods**:
557
- `from_element()`: Create origin from element center with optional offset
558
- `from_viewport()`: Create origin from viewport corner with optional offset
559
560
**Example**:
561
```python
562
from selenium.webdriver.common.actions.wheel_input import ScrollOrigin
563
564
# Scroll from element center
565
element = driver.find_element(By.ID, "content")
566
scroll_origin = ScrollOrigin.from_element(element)
567
ActionChains(driver)\
568
.scroll_from_origin(scroll_origin, 0, 200)\
569
.perform()
570
571
# Scroll from element with offset
572
scroll_origin = ScrollOrigin.from_element(element, 100, 50)
573
ActionChains(driver)\
574
.scroll_from_origin(scroll_origin, 0, -100)\
575
.perform()
576
577
# Scroll from viewport
578
scroll_origin = ScrollOrigin.from_viewport(200, 100)
579
ActionChains(driver)\
580
.scroll_from_origin(scroll_origin, 0, 300)\
581
.perform()
582
```
583
584
## Timing Control
585
586
{ .api }
587
```python
588
def pause(self, seconds: Union[float, int]) -> ActionChains
589
```
590
591
**Description**: Pause all inputs for the specified duration in seconds.
592
593
**Parameters**:
594
- `seconds`: Duration to pause in seconds (can be fractional)
595
596
**Example**:
597
```python
598
# Add pauses between actions for better visual feedback
599
button1 = driver.find_element(By.ID, "btn1")
600
button2 = driver.find_element(By.ID, "btn2")
601
602
ActionChains(driver)\
603
.click(button1)\
604
.pause(1.5)\
605
.click(button2)\
606
.pause(0.5)\
607
.perform()
608
```
609
610
## Context Manager Support
611
612
ActionChains can be used as a context manager:
613
614
{ .api }
615
```python
616
def __enter__(self) -> ActionChains
617
def __exit__(self, _type, _value, _traceback) -> None
618
```
619
620
**Example**:
621
```python
622
with ActionChains(driver) as actions:
623
actions.move_to_element(menu_item)
624
actions.click(submenu_item)
625
# perform() is automatically called at the end of the with block
626
```
627
628
## Complex Interaction Examples
629
630
### Multi-Step Drag and Drop with Hover Effects
631
632
```python
633
def complex_drag_drop():
634
source = driver.find_element(By.ID, "draggable-item")
635
intermediate = driver.find_element(By.ID, "hover-zone")
636
target = driver.find_element(By.ID, "drop-zone")
637
638
ActionChains(driver)\
639
.click_and_hold(source)\
640
.pause(0.5)\
641
.move_to_element(intermediate)\
642
.pause(1.0)\
643
.move_to_element(target)\
644
.pause(0.5)\
645
.release()\
646
.perform()
647
```
648
649
### Advanced Text Editing
650
651
```python
652
def advanced_text_editing():
653
text_area = driver.find_element(By.ID, "editor")
654
655
ActionChains(driver)\
656
.click(text_area)\
657
.key_down(Keys.CONTROL)\
658
.send_keys('a')\
659
.key_up(Keys.CONTROL)\
660
.send_keys("New content here")\
661
.key_down(Keys.SHIFT)\
662
.send_keys(Keys.LEFT, Keys.LEFT, Keys.LEFT, Keys.LEFT)\
663
.key_up(Keys.SHIFT)\
664
.key_down(Keys.CONTROL)\
665
.send_keys('b')\
666
.key_up(Keys.CONTROL)\
667
.perform()
668
```
669
670
### Interactive Drawing
671
672
```python
673
def draw_signature():
674
canvas = driver.find_element(By.ID, "signature-pad")
675
676
# Draw a simple signature
677
ActionChains(driver)\
678
.move_to_element_with_offset(canvas, -100, 0)\
679
.click_and_hold()\
680
.move_by_offset(50, -20)\
681
.move_by_offset(30, 40)\
682
.move_by_offset(40, -30)\
683
.move_by_offset(30, 20)\
684
.release()\
685
.perform()
686
```
687
688
### Keyboard Navigation
689
690
```python
691
def navigate_table_with_keyboard():
692
table = driver.find_element(By.ID, "data-table")
693
first_cell = table.find_element(By.CSS_SELECTOR, "tr:first-child td:first-child")
694
695
# Navigate and edit table cells
696
ActionChains(driver)\
697
.click(first_cell)\
698
.send_keys("Cell 1 Value")\
699
.send_keys(Keys.TAB)\
700
.send_keys("Cell 2 Value")\
701
.send_keys(Keys.ENTER)\
702
.send_keys("Next Row Value")\
703
.perform()
704
```
705
706
## Best Practices
707
708
### 1. Always Call perform()
709
710
```python
711
# ✅ Correct - actions are executed
712
ActionChains(driver).click(element).perform()
713
714
# ❌ Incorrect - actions are queued but never executed
715
ActionChains(driver).click(element)
716
```
717
718
### 2. Use Method Chaining for Related Actions
719
720
```python
721
# ✅ Good - related actions chained together
722
ActionChains(driver)\
723
.move_to_element(menu)\
724
.click(submenu)\
725
.perform()
726
727
# ✅ Also good - sequential building for complex logic
728
actions = ActionChains(driver)
729
actions.move_to_element(menu)
730
if condition:
731
actions.pause(1.0)
732
actions.click(submenu)
733
actions.perform()
734
```
735
736
### 3. Reset Actions for Reusable ActionChains
737
738
```python
739
actions = ActionChains(driver)
740
741
# First set of actions
742
actions.click(button1).perform()
743
actions.reset_actions() # Clear previous actions
744
745
# Second set of actions
746
actions.click(button2).perform()
747
```
748
749
### 4. Handle Element State Before Actions
750
751
```python
752
from selenium.webdriver.support.ui import WebDriverWait
753
from selenium.webdriver.support import expected_conditions as EC
754
755
# Wait for element to be clickable before action
756
element = WebDriverWait(driver, 10).until(
757
EC.element_to_be_clickable((By.ID, "interactive-button"))
758
)
759
760
ActionChains(driver).click(element).perform()
761
```
762
763
### 5. Use Pauses for User Experience
764
765
```python
766
# Add natural pauses to mimic human interaction
767
ActionChains(driver)\
768
.move_to_element(dropdown_trigger)\
769
.pause(0.5)\
770
.click()\
771
.pause(1.0)\
772
.click(dropdown_item)\
773
.perform()
774
```