Skip to content

Commit b33a3c9

Browse files
Initial implementation of Pointer Actions
This finishes off the work that was done for the Python key actions and adds Pointer actions. It also completes the work to get ActionChains to work with ActionBuilder so that old test suites can start working straight away and give people the opportunity to move over to the new API.
1 parent 28b3a4e commit b33a3c9

File tree

7 files changed

+296
-71
lines changed

7 files changed

+296
-71
lines changed

py/selenium/webdriver/common/action_chains.py

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ def reset_actions(self):
8383
"""
8484
Clears actions that are already stored on the remote end.
8585
"""
86-
self._driver.execute(Command.W3C_CLEAR_ACTIONS)
86+
if self._driver.w3c:
87+
self._driver.execute(Command.W3C_CLEAR_ACTIONS)
88+
else:
89+
self._actions = []
8790

8891
def click(self, on_element=None):
8992
"""
@@ -93,10 +96,15 @@ def click(self, on_element=None):
9396
- on_element: The element to click.
9497
If None, clicks on current mouse position.
9598
"""
96-
if on_element:
97-
self.move_to_element(on_element)
98-
self._actions.append(lambda: self._driver.execute(
99-
Command.CLICK, {'button': 0}))
99+
if self._driver.w3c:
100+
self.w3c_actions.pointer_action.click(on_element)
101+
self.w3c_actions.key_action.pause()
102+
self.w3c_actions.key_action.pause()
103+
else:
104+
if on_element:
105+
self.move_to_element(on_element)
106+
self._actions.append(lambda: self._driver.execute(
107+
Command.CLICK, {'button': 0}))
100108
return self
101109

102110
def click_and_hold(self, on_element=None):
@@ -107,10 +115,16 @@ def click_and_hold(self, on_element=None):
107115
- on_element: The element to mouse down.
108116
If None, clicks on current mouse position.
109117
"""
110-
if on_element:
111-
self.move_to_element(on_element)
112-
self._actions.append(lambda: self._driver.execute(
113-
Command.MOUSE_DOWN, {}))
118+
if self._driver.w3c:
119+
self.w3c_actions.pointer_action.click_and_hold(on_element)
120+
self.w3c_actions.key_action.pause()
121+
if on_element:
122+
self.w3c_actions.key_action.pause()
123+
else:
124+
if on_element:
125+
self.move_to_element(on_element)
126+
self._actions.append(lambda: self._driver.execute(
127+
Command.MOUSE_DOWN, {}))
114128
return self
115129

116130
def context_click(self, on_element=None):
@@ -121,10 +135,14 @@ def context_click(self, on_element=None):
121135
- on_element: The element to context-click.
122136
If None, clicks on current mouse position.
123137
"""
124-
if on_element:
125-
self.move_to_element(on_element)
126-
self._actions.append(lambda: self._driver.execute(
127-
Command.CLICK, {'button': 2}))
138+
if self._driver.w3c:
139+
self.w3c_actions.pointer_action.context_click(on_element)
140+
self.w3c_actions.key_action.pause()
141+
else:
142+
if on_element:
143+
self.move_to_element(on_element)
144+
self._actions.append(lambda: self._driver.execute(
145+
Command.CLICK, {'button': 2}))
128146
return self
129147

130148
def double_click(self, on_element=None):
@@ -135,10 +153,15 @@ def double_click(self, on_element=None):
135153
- on_element: The element to double-click.
136154
If None, clicks on current mouse position.
137155
"""
138-
if on_element:
139-
self.move_to_element(on_element)
140-
self._actions.append(lambda: self._driver.execute(
141-
Command.DOUBLE_CLICK, {}))
156+
if self._driver.w3c:
157+
self.w3c_actions.pointer_action.double_click(on_element)
158+
for _ in range(4):
159+
self.w3c_actions.key_action.pause()
160+
else:
161+
if on_element:
162+
self.move_to_element(on_element)
163+
self._actions.append(lambda: self._driver.execute(
164+
Command.DOUBLE_CLICK, {}))
142165
return self
143166

144167
def drag_and_drop(self, source, target):
@@ -150,8 +173,15 @@ def drag_and_drop(self, source, target):
150173
- source: The element to mouse down.
151174
- target: The element to mouse up.
152175
"""
153-
self.click_and_hold(source)
154-
self.release(target)
176+
if self._driver.w3c:
177+
self.w3c_actions.pointer_action.click_and_hold(source) \
178+
.move_to(target) \
179+
.release()
180+
for _ in range(3):
181+
self.w3c_actions.key_action.pause()
182+
else:
183+
self.click_and_hold(source)
184+
self.release(target)
155185
return self
156186

157187
def drag_and_drop_by_offset(self, source, xoffset, yoffset):
@@ -164,9 +194,16 @@ def drag_and_drop_by_offset(self, source, xoffset, yoffset):
164194
- xoffset: X offset to move to.
165195
- yoffset: Y offset to move to.
166196
"""
167-
self.click_and_hold(source)
168-
self.move_by_offset(xoffset, yoffset)
169-
self.release()
197+
if self._driver.w3c:
198+
self.w3c_actions.pointer_action.click_and_hold(source) \
199+
.move_to_location(xoffset, yoffset) \
200+
.release()
201+
for _ in range(3):
202+
self.w3c_actions.key_action.pause()
203+
else:
204+
self.click_and_hold(source)
205+
self.move_by_offset(xoffset, yoffset)
206+
self.release()
170207
return self
171208

172209
def key_down(self, value, element=None):
@@ -188,6 +225,7 @@ def key_down(self, value, element=None):
188225
self.click(element)
189226
if self._driver.w3c:
190227
self.w3c_actions.key_action.key_down(value)
228+
self.w3c_actions.pointer_action.pause()
191229
else:
192230
self._actions.append(lambda: self._driver.execute(
193231
Command.SEND_KEYS_TO_ACTIVE_ELEMENT,
@@ -212,6 +250,7 @@ def key_up(self, value, element=None):
212250
self.click(element)
213251
if self._driver.w3c:
214252
self.w3c_actions.key_action.key_up(value)
253+
self.w3c_actions.pointer_action.pause()
215254
else:
216255
self._actions.append(lambda: self._driver.execute(
217256
Command.SEND_KEYS_TO_ACTIVE_ELEMENT,
@@ -239,8 +278,12 @@ def move_to_element(self, to_element):
239278
:Args:
240279
- to_element: The WebElement to move to.
241280
"""
242-
self._actions.append(lambda: self._driver.execute(
243-
Command.MOVE_TO, {'element': to_element.id}))
281+
if self._driver.w3c:
282+
self.w3c_actions.pointer_action.move_to(to_element)
283+
self.w3c_actions.key_action.pause()
284+
else:
285+
self._actions.append(lambda: self._driver.execute(
286+
Command.MOVE_TO, {'element': to_element.id}))
244287
return self
245288

246289
def move_to_element_with_offset(self, to_element, xoffset, yoffset):
@@ -253,11 +296,15 @@ def move_to_element_with_offset(self, to_element, xoffset, yoffset):
253296
- xoffset: X offset to move to.
254297
- yoffset: Y offset to move to.
255298
"""
256-
self._actions.append(
257-
lambda: self._driver.execute(Command.MOVE_TO, {
258-
'element': to_element.id,
259-
'xoffset': int(xoffset),
260-
'yoffset': int(yoffset)}))
299+
if self._driver.w3c:
300+
self.w3c_actions.pointer_action.move_to(to_element, xoffset, yoffset)
301+
self.w3c_actions.key_action.pause()
302+
else:
303+
self._actions.append(
304+
lambda: self._driver.execute(Command.MOVE_TO, {
305+
'element': to_element.id,
306+
'xoffset': int(xoffset),
307+
'yoffset': int(yoffset)}))
261308
return self
262309

263310
def release(self, on_element=None):
@@ -268,9 +315,13 @@ def release(self, on_element=None):
268315
- on_element: The element to mouse up.
269316
If None, releases on current mouse position.
270317
"""
271-
if on_element:
272-
self.move_to_element(on_element)
273-
self._actions.append(lambda: self._driver.execute(Command.MOUSE_UP, {}))
318+
if self._driver.w3c:
319+
self.w3c_actions.pointer_action.release()
320+
self.w3c_actions.key_action.pause()
321+
else:
322+
if on_element:
323+
self.move_to_element(on_element)
324+
self._actions.append(lambda: self._driver.execute(Command.MOUSE_UP, {}))
274325
return self
275326

276327
def send_keys(self, *keys_to_send):

py/selenium/webdriver/common/actions/action_builder.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
class ActionBuilder(object):
2727
def __init__(self, driver, mouse=None, keyboard=None):
2828
if mouse is None:
29-
mouse = PointerInput("mouse", "mouse")
29+
mouse = PointerInput(interaction.POINTER, "mouse")
3030
if keyboard is None:
31-
keyboard = KeyInput("keyboard")
32-
self.devices = [keyboard]
31+
keyboard = KeyInput(interaction.KEY)
32+
self.devices = [mouse, keyboard]
3333
self._key_action = KeyActions(keyboard)
3434
self._pointer_action = PointerActions(mouse)
3535
self.driver = driver
@@ -70,7 +70,9 @@ def add_pointer_input(self, type_, name):
7070
def perform(self):
7171
enc = {"actions": []}
7272
for device in self.devices:
73-
enc["actions"].append(device.encode())
73+
encoded = device.encode()
74+
if encoded['actions']:
75+
enc["actions"].append(encoded)
7476
self.driver.execute(Command.W3C_ACTIONS, enc)
7577

7678
def clear_actions(self):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class MouseButton(object):
2+
3+
LEFT = 0
4+
MIDDLE = 1
5+
RIGHT = 2

py/selenium/webdriver/common/actions/pointer_actions.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,87 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
from . import interaction
18+
1719
from .interaction import Interaction
20+
from .mouse_button import MouseButton
1821
from .pointer_input import PointerInput
1922

23+
from selenium.webdriver.remote.webelement import WebElement
24+
2025

2126
class PointerActions(Interaction):
2227

2328
def __init__(self, source=None):
2429
if source is None:
25-
source = PointerInput("mouse", "mouse")
30+
source = PointerInput(interaction.POINTER, "mouse")
2631
self.source = source
2732
super(PointerActions, self).__init__(source)
2833

29-
def pointer_down(self, button, device=None):
34+
def pointer_down(self, button=MouseButton.LEFT):
3035
self._button_action("create_pointer_down", button=button)
3136

32-
def pointer_up(self):
33-
self._button_action("create_pointer_up")
37+
def pointer_up(self, button=MouseButton.LEFT):
38+
self._button_action("create_pointer_up", button=button)
39+
40+
def move_to(self, element, x=None, y=None):
41+
if not isinstance(element, WebElement):
42+
raise AttributeError("move_to requires a WebElement")
43+
if x is not None or y is not None:
44+
el_rect = element.rect
45+
left_offset = el_rect['width'] / 2
46+
top_offset = el_rect['height'] / 2
47+
left = -left_offset + (x or 0)
48+
top = -top_offset + (y or 0)
49+
else:
50+
left = 0
51+
top = 0
52+
self.source.create_pointer_move(origin=element, x=int(left), y=int(top))
53+
return self
54+
55+
def move_by(self, x, y):
56+
self.source.create_pointer_move(origin=interaction.POINTER, x=int(x), y=int(y))
57+
return self
58+
59+
def move_to_location(self, x, y):
60+
self.source.create_pointer_move(origin='viewport', x=int(x), y=int(y))
61+
return self
62+
63+
def click(self, element=None):
64+
if element:
65+
self.move_to(element)
66+
self.pointer_down(MouseButton.LEFT)
67+
self.pointer_up(MouseButton.LEFT)
68+
return self
69+
70+
def context_click(self, element=None):
71+
if element:
72+
self.move_to(element)
73+
self.pointer_down(MouseButton.RIGHT)
74+
self.pointer_up(MouseButton.RIGHT)
75+
return self
76+
77+
def click_and_hold(self, element=None):
78+
if element:
79+
self.move_to(element)
80+
self.pointer_down()
81+
return self
82+
83+
def release(self):
84+
self.pointer_up()
85+
return self
86+
87+
def double_click(self, element=None):
88+
if element:
89+
self.move_to(element)
90+
self.click()
91+
self.click()
3492

3593
def pause(self, duration=0):
3694
self.source.create_pause(duration)
3795
return self
3896

39-
def _button_action(self, action, button=None):
97+
def _button_action(self, action, button=MouseButton.LEFT):
4098
meth = getattr(self.source, action)
4199
meth(button)
42100
return self

py/selenium/webdriver/common/actions/pointer_input.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,44 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
from .input_device import InputDevice
18-
from .interaction import Pause
18+
19+
from selenium.webdriver.remote.webelement import WebElement
1920

2021

2122
class PointerInput(InputDevice):
2223

24+
DEFAULT_MOVE_DURATION = 250
25+
2326
def __init__(self, type_, name):
2427
super(PointerInput, self).__init__()
2528
self.type = type_
2629
self.name = name
2730

28-
def create_pointer_move(self, duration=0, x=0, y=0, element=None, origin=None):
29-
self.add_action({"type": "pointerMove", "duration": duration, "x": x, "y": y})
31+
def create_pointer_move(self, duration=DEFAULT_MOVE_DURATION, x=None, y=None, origin=None):
32+
action = {"type": "pointerMove","duration": duration}
33+
action["x"] = x
34+
action["y"] = y
35+
if isinstance(origin, WebElement):
36+
action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
37+
elif origin is not None:
38+
action["origin"] = origin
39+
40+
self.add_action(action)
3041

3142
def create_pointer_down(self, button):
32-
self.add_action({"type": "pointerDown", "duration": 0})
43+
self.add_action({"type": "pointerDown", "duration": 0, "button":button})
3344

3445
def create_pointer_up(self, button):
35-
self.add_action({"type": "pointerUp", "duration": 0})
46+
self.add_action({"type": "pointerUp", "duration": 0, "button":button})
3647

3748
def create_pointer_cancel(self):
3849
self.add_action({"type": "pointerCancel"})
3950

4051
def create_pause(self, pause_duration):
41-
self.add_action(Pause(self, pause_duration))
52+
self.add_action({"type": "pause", "duration": pause_duration * 1000})
4253

4354
def encode(self):
44-
return {"type": self.type, "id": self.name, "actions": [acts.encode() for acts in self.actions]}
55+
return {"type": self.type,
56+
"parameters": {"pointerType": self.name},
57+
"id": self.name,
58+
"actions": [acts for acts in self.actions]}

0 commit comments

Comments
 (0)