Skip to content

Commit 122c9f9

Browse files
[py] Add Scroll via Wheel Inputs and Wheel Actions
1 parent a22205a commit 122c9f9

7 files changed

Lines changed: 277 additions & 3 deletions

File tree

common/src/web/scrollingPage.html

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<!--Copied from https://github.com/web-platform-tests/wpt/blob/master/webdriver/tests/perform_actions/support/test_actions_scroll_wdspec.html-->
2+
3+
<!doctype html>
4+
<meta charset=utf-8>
5+
<html>
6+
7+
<head>
8+
<title>Test Actions</title>
9+
<style>
10+
div {
11+
padding: 0px;
12+
margin: 0px;
13+
}
14+
15+
.area {
16+
width: 100px;
17+
height: 50px;
18+
background-color: #ccc;
19+
}
20+
21+
#scrollable {
22+
width: 100px;
23+
height: 100px;
24+
overflow: scroll;
25+
}
26+
27+
#scrollContent {
28+
width: 600px;
29+
height: 1000px;
30+
background-color: blue;
31+
}
32+
33+
#subframe {
34+
width: 100px;
35+
height: 100px;
36+
}
37+
</style>
38+
<script>
39+
"use strict";
40+
var els = {};
41+
var allEvents = { events: [] };
42+
function displayMessage(message) {
43+
document.getElementById("events").innerHTML = "<p>" + message + "</p>";
44+
}
45+
46+
function appendMessage(message) {
47+
document.getElementById("events").innerHTML += "<p>" + message + "</p>";
48+
}
49+
50+
function recordWheelEvent(event) {
51+
allEvents.events.push({
52+
"type": event.type,
53+
"button": event.button,
54+
"buttons": event.buttons,
55+
"deltaX": event.deltaX,
56+
"deltaY": event.deltaY,
57+
"deltaZ": event.deltaZ,
58+
"deltaMode": event.deltaMode,
59+
"target": event.target.id
60+
});
61+
appendMessage(event.type + " " +
62+
"button: " + event.button + ", " +
63+
"pageX: " + event.pageX + ", " +
64+
"pageY: " + event.pageY + ", " +
65+
"button: " + event.button + ", " +
66+
"buttons: " + event.buttons + ", " +
67+
"deltaX: " + event.deltaX + ", " +
68+
"deltaY: " + event.deltaY + ", " +
69+
"deltaZ: " + event.deltaZ + ", " +
70+
"deltaMode: " + event.deltaMode + ", " +
71+
"target id: " + event.target.id);
72+
}
73+
74+
function resetEvents() {
75+
allEvents.events.length = 0;
76+
displayMessage("");
77+
}
78+
79+
document.addEventListener("DOMContentLoaded", function () {
80+
var outer = document.getElementById("outer");
81+
outer.addEventListener("wheel", recordWheelEvent);
82+
83+
var scrollable = document.getElementById("scrollable");
84+
scrollable.addEventListener("wheel", recordWheelEvent);
85+
});
86+
</script>
87+
</head>
88+
89+
<body>
90+
<div>
91+
<h2>ScrollReporter</h2>
92+
<div id="outer" class="area">
93+
</div>
94+
</div>
95+
<div>
96+
<h2>OverflowScrollReporter</h2>
97+
<div id="scrollable">
98+
<div id="scrollContent"></div>
99+
</div>
100+
</div>
101+
<div>
102+
<h2>IframeScrollReporter</h2>
103+
<iframe id='subframe' srcdoc='
104+
<script>
105+
document.scrollingElement.addEventListener("wheel",
106+
function(event) {
107+
window.parent.allEvents.events.push({
108+
"type": event.type,
109+
"button": event.button,
110+
"buttons": event.buttons,
111+
"deltaX": event.deltaX,
112+
"deltaY": event.deltaY,
113+
"deltaZ": event.deltaZ,
114+
"deltaMode": event.deltaMode,
115+
"target": event.target.id
116+
});
117+
}
118+
);
119+
</script>
120+
<div id="iframeContent"
121+
style="width: 7500px; height: 7500px; background-color:blue" ></div>'>
122+
</iframe>
123+
</div>
124+
<div id="resultContainer">
125+
<h2>Events</h2>
126+
<div id="events"></div>
127+
</div>
128+
</body>
129+
130+
</html>

py/selenium/webdriver/common/action_chains.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,22 @@ def send_keys_to_element(self, element, *keys_to_send):
321321
self.send_keys(*keys_to_send)
322322
return self
323323

324+
def scroll(self, x: int, y: int, delta_x: int, delta_y: int, duration: int = 0, origin: str = "viewport"):
325+
"""
326+
Sends wheel scroll information to the browser to be processed.
327+
328+
:Args:
329+
- x: starting X coordinate
330+
- y: starting Y coordinate
331+
- delta_x: the distance the mouse will scroll on the x axis
332+
- delta_y: the distance the mouse will scroll on the y axis
333+
"""
334+
self.w3c_actions.wheel_action.scroll(x=x, y=y, delta_x=delta_x, delta_y=delta_y,
335+
duration=duration, origin=origin)
336+
return self
337+
324338
# Context manager so ActionChains can be used in a 'with .. as' statements.
339+
325340
def __enter__(self):
326341
return self # Return created instance of self.
327342

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,22 @@
2121
from .key_input import KeyInput
2222
from .pointer_actions import PointerActions
2323
from .pointer_input import PointerInput
24+
from .wheel_input import WheelInput
25+
from .wheel_actions import WheelActions
2426

2527

2628
class ActionBuilder(object):
27-
def __init__(self, driver, mouse=None, keyboard=None, duration=250):
29+
def __init__(self, driver, mouse=None, wheel=None, keyboard=None, duration=250):
2830
if not mouse:
2931
mouse = PointerInput(interaction.POINTER_MOUSE, "mouse")
3032
if not keyboard:
3133
keyboard = KeyInput(interaction.KEY)
32-
self.devices = [mouse, keyboard]
34+
if not wheel:
35+
wheel = WheelInput(interaction.WHEEL)
36+
self.devices = [mouse, keyboard, wheel]
3337
self._key_action = KeyActions(keyboard)
3438
self._pointer_action = PointerActions(mouse, duration=duration)
39+
self._wheel_action = WheelActions(wheel)
3540
self.driver = driver
3641

3742
def get_device_with(self, name):
@@ -57,6 +62,10 @@ def key_action(self):
5762
def pointer_action(self):
5863
return self._pointer_action
5964

65+
@property
66+
def wheel_action(self):
67+
return self._wheel_action
68+
6069
def add_key_input(self, name):
6170
new_input = KeyInput(name)
6271
self._add_input(new_input)
@@ -67,6 +76,11 @@ def add_pointer_input(self, kind, name):
6776
self._add_input(new_input)
6877
return new_input
6978

79+
def add_wheel_input(self, kind, name):
80+
new_input = WheelInput(kind, name)
81+
self._add_input(new_input)
82+
return new_input
83+
7084
def perform(self):
7185
enc = {"actions": []}
7286
for device in self.devices:

py/selenium/webdriver/common/actions/interaction.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
KEY = "key"
2020
POINTER = "pointer"
2121
NONE = "none"
22+
WHEEL = "wheel"
2223
SOURCE_TYPES = set([KEY, POINTER, NONE])
2324

2425
POINTER_MOUSE = "mouse"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
from . import interaction
18+
from .wheel_input import WheelInput
19+
from .interaction import Interaction
20+
from selenium.webdriver.remote.webelement import WebElement
21+
22+
23+
class WheelActions(Interaction):
24+
25+
def __init__(self, source: WheelInput = None):
26+
if not source:
27+
source = WheelInput("wheel")
28+
super(WheelActions, self).__init__(source)
29+
30+
def pause(self, duration=0):
31+
self.source.create_pause(duration)
32+
return self
33+
34+
def scroll(self, x, y, delta_x, delta_y, duration, origin):
35+
self.source.create_scroll(x, y, delta_x, delta_y, duration, origin)
36+
return self
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
from . import interaction
18+
from .input_device import InputDevice
19+
20+
from selenium.common.exceptions import InvalidArgumentException
21+
from selenium.webdriver.remote.webelement import WebElement
22+
23+
24+
class WheelInput(InputDevice):
25+
26+
def __init__(self, name):
27+
super().__init__(name=name)
28+
self.name = name
29+
self.type = interaction.WHEEL
30+
31+
def encode(self):
32+
return {"type": self.type,
33+
"id": self.name,
34+
"actions": [acts for acts in self.actions]}
35+
36+
def create_scroll(self, x, y, delta_x, delta_y, duration, origin):
37+
if isinstance(origin, WebElement):
38+
origin = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
39+
self.add_action({"type": "scroll", "x": x, "y": y, "deltaX": delta_x, "deltaY": delta_y, "duration": duration, "origin": origin})
40+
41+
def create_pause(self, pause_duration):
42+
self.add_action({"type": "pause", "duration": int(pause_duration * 1000)})

py/test/selenium/webdriver/common/interactions_tests.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from selenium.webdriver.common.by import By
2222
from selenium.webdriver.common.keys import Keys
2323
from selenium.webdriver.common.action_chains import ActionChains
24+
from selenium.webdriver.common.actions import interaction
2425
from selenium.webdriver.support.ui import WebDriverWait
2526

2627

@@ -233,7 +234,7 @@ def test_can_reset_interactions(driver, pages):
233234
actions = ActionChains(driver)
234235
actions.click()
235236
actions.key_down('A')
236-
assert all((len(device.actions) > 0 for device in actions.w3c_actions.devices))
237+
assert all((len(device.actions) > 0 for device in actions.w3c_actions.devices if device.type != interaction.WHEEL))
237238

238239
actions.reset_actions()
239240

@@ -257,3 +258,38 @@ def test_can_pause(driver, pages):
257258
assert pause_time < end - start
258259
assert "Clicked" == toClick.get_attribute('value')
259260
assert "Clicked" == toDoubleClick.get_attribute('value')
261+
262+
263+
def test_can_scroll_mouse_wheel(driver, pages):
264+
pages.load("scrollingPage.html")
265+
driver.execute_script("document.scrollingElement.scrollTop = 0")
266+
267+
scrollable = driver.find_element(By.CSS_SELECTOR, "#scrollable")
268+
ActionChains(driver).scroll(0, 0, 5, 10, origin=scrollable).perform()
269+
#wheel_chain.scroll(0, 0, 5, 10, origin=scrollable).perform()
270+
events = _get_events(driver)
271+
assert len(events) == 1
272+
assert events[0]["type"] == "wheel"
273+
assert events[0]["deltaX"] >= 5
274+
assert events[0]["deltaY"] >= 10
275+
assert events[0]["deltaZ"] == 0
276+
assert events[0]["target"] == "scrollContent"
277+
278+
279+
def _get_events(driver):
280+
"""Return list of key events recorded in the test_keys_page fixture."""
281+
events = driver.execute_script("return allEvents.events;") or []
282+
# `key` values in `allEvents` may be escaped (see `escapeSurrogateHalf` in
283+
# test_keys_wdspec.html), so this converts them back into unicode literals.
284+
for e in events:
285+
# example: turn "U+d83d" (6 chars) into u"\ud83d" (1 char)
286+
if "key" in e and e["key"].startswith(u"U+"):
287+
key = e["key"]
288+
hex_suffix = key[key.index("+") + 1:]
289+
e["key"] = unichr(int(hex_suffix, 16))
290+
291+
# WebKit sets code as 'Unidentified' for unidentified key codes, but
292+
# tests expect ''.
293+
if "code" in e and e["code"] == "Unidentified":
294+
e["code"] = ""
295+
return events

0 commit comments

Comments
 (0)