Skip to content

Commit a47dade

Browse files
committed
[rb] implement wheel input device for actions
1 parent 231acf6 commit a47dade

12 files changed

Lines changed: 558 additions & 1 deletion

File tree

common/src/web/scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
<div>
99
<iframe name="nested_scrolling_frame" src="frame_with_height_above_200.html" width="800" height="200" ></iframe>
1010
</div>
11+
<footer>IFrame Above</footer>
1112
</body>
1213
</html>

rb/lib/selenium/webdriver/common.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
require 'selenium/webdriver/common/interactions/none_input'
5353
require 'selenium/webdriver/common/interactions/key_input'
5454
require 'selenium/webdriver/common/interactions/pointer_input'
55+
require 'selenium/webdriver/common/interactions/scroll'
56+
require 'selenium/webdriver/common/interactions/wheel_input'
57+
require 'selenium/webdriver/common/interactions/wheel_actions'
5558
require 'selenium/webdriver/common/action_builder'
5659
require 'selenium/webdriver/common/html5/shared_web_storage'
5760
require 'selenium/webdriver/common/html5/local_storage'

rb/lib/selenium/webdriver/common/action_builder.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module WebDriver
2222
class ActionBuilder
2323
include KeyActions # Actions specific to key inputs
2424
include PointerActions # Actions specific to pointer inputs
25+
include WheelActions # Actions specific to wheel inputs
2526

2627
attr_reader :devices
2728

@@ -100,6 +101,22 @@ def add_key_input(name)
100101
add_input(Interactions.key(name))
101102
end
102103

104+
#
105+
# Adds a WheelInput device
106+
#
107+
# @example Add a wheel input device
108+
#
109+
# builder = device.action
110+
# builder.add_wheel_input('wheel2')
111+
#
112+
# @param [String] name name for the device
113+
# @return [Interactions::WheelInput] The wheel input added
114+
#
115+
116+
def add_wheel_input(name)
117+
add_input(Interactions.wheel(name))
118+
end
119+
103120
#
104121
# Retrieves the input device for the given name
105122
#
@@ -150,6 +167,16 @@ def key_inputs
150167
@devices.select { |device| device.type == Interactions::KEY }
151168
end
152169

170+
#
171+
# Retrieves the current WheelInput device
172+
#
173+
# @return [Selenium::WebDriver::Interactions::InputDevice] current WheelInput devices
174+
#
175+
176+
def wheel_inputs
177+
@devices.select { |device| device.type == Interactions::WHEEL }
178+
end
179+
153180
#
154181
# Creates a pause for the given device of the given duration. If no duration is given, the pause will only wait
155182
# for all actions to complete in that tick.

rb/lib/selenium/webdriver/common/interactions/interactions.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module Interactions
2323
KEY = :key
2424
POINTER = :pointer
2525
NONE = :none
26+
WHEEL = :wheel
2627

2728
#
2829
# Class methods for initializing known Input devices
@@ -40,6 +41,10 @@ def pointer(kind = :mouse, name: nil)
4041
def none(name = nil)
4142
NoneInput.new(name)
4243
end
44+
45+
def wheel(name = nil)
46+
WheelInput.new(name)
47+
end
4348
end
4449
end # Interactions
4550
end # WebDriver

rb/lib/selenium/webdriver/common/interactions/pointer_move.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ module Selenium
2121
module WebDriver
2222
module Interactions
2323
#
24-
# Actions related to moving the pointer.
24+
# Action related to moving the pointer.
2525
#
2626
# @api private
2727
#
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed to the Software Freedom Conservancy (SFC) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The SFC licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
module Selenium
21+
module WebDriver
22+
module Interactions
23+
#
24+
# Action related to scrolling a wheel.
25+
#
26+
# @api private
27+
#
28+
29+
class Scroll < Interaction
30+
VIEWPORT = :viewport
31+
POINTER = :pointer
32+
33+
def initialize(source, duration, origin, x, y, delta_x, delta_y)
34+
super(source)
35+
@type = :scroll
36+
@duration = duration * 1000
37+
@origin = origin
38+
@x_offset = x
39+
@y_offset = y
40+
@delta_x = delta_x
41+
@delta_y = delta_y
42+
end
43+
44+
def assert_source(source)
45+
raise TypeError, "#{source.type} is not a valid input type" unless source.is_a? WheelInput
46+
end
47+
48+
def encode
49+
{'type' => type.to_s,
50+
'duration' => @duration.to_i,
51+
'x' => @x_offset,
52+
'y' => @y_offset,
53+
'deltaX' => @delta_x,
54+
'deltaY' => @delta_y,
55+
'origin' => @origin.is_a?(Element) ? @origin : @origin.to_s}
56+
end
57+
end # PointerPress
58+
end # Interactions
59+
end # WebDriver
60+
end # Selenium
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed to the Software Freedom Conservancy (SFC) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The SFC licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
module Selenium
21+
module WebDriver
22+
module WheelActions
23+
24+
# This scrolls an element to the bottom of the viewport
25+
def scroll_to(element, device: nil)
26+
scroll_from(element, device: device)
27+
end
28+
29+
# This scrolls from the provided element
30+
# The origin of the scroll is the center of the element plus the
31+
# offset amounts in origin_offset_x and origin_offset_y
32+
# The amount of scrolling is the value of right_by and down_by
33+
def scroll_from(element, right_by = 0, down_by = 0, origin_offset_x: 0, origin_offset_y: 0, device: nil)
34+
wheel = wheel_input(device)
35+
wheel.create_scroll(x: Integer(origin_offset_x),
36+
y: Integer(origin_offset_y),
37+
delta_x: Integer(right_by),
38+
delta_y: Integer(down_by),
39+
origin: element,
40+
duration: default_move_duration)
41+
tick(wheel)
42+
self
43+
end
44+
45+
# The origin of the scroll will the upper left corner of the viewport plus the
46+
# offset amounts in origin_x and origin_y
47+
# The amount of scrolling is the value of right_by and down_by
48+
def scroll_by(right_by = 0, down_by = 0, origin_x: 0, origin_y: 0, device: nil)
49+
wheel = wheel_input(device)
50+
wheel.create_scroll(x: Integer(origin_x),
51+
y: Integer(origin_y),
52+
delta_x: Integer(right_by),
53+
delta_y: Integer(down_by),
54+
origin: Interactions::Scroll::VIEWPORT,
55+
duration: default_move_duration)
56+
tick(wheel)
57+
self
58+
end
59+
60+
private
61+
62+
def wheel_input(name = nil)
63+
device(name: name, type: Interactions::WHEEL) || add_wheel_input('wheel')
64+
end
65+
end # KeyActions
66+
end # WebDriver
67+
end # Selenium
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed to the Software Freedom Conservancy (SFC) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The SFC licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
module Selenium
21+
module WebDriver
22+
module Interactions
23+
#
24+
# Creates actions specific to Pointer Input devices
25+
#
26+
# @api private
27+
#
28+
29+
class WheelInput < InputDevice
30+
def initialize(name = nil)
31+
super(name)
32+
@type = Interactions::WHEEL
33+
end
34+
35+
def create_scroll(duration: 0, x: 0, y: 0, delta_x: 0, delta_y: 0, origin: nil)
36+
add_action(Scroll.new(self, duration, origin, x, y, delta_x, delta_y))
37+
end
38+
end # PointerInput
39+
end # Interactions
40+
end # WebDriver
41+
end # Selenium

rb/spec/integration/selenium/webdriver/action_builder_spec.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,95 @@ module WebDriver
257257
expect(element.attribute(:value)).to eq('Clicked')
258258
end
259259
end
260+
261+
def in_viewport?(element)
262+
in_viewport = <<-IN_VIEWPORT
263+
for(var e=arguments[0],f=e.offsetTop,t=e.offsetLeft,o=e.offsetWidth,n=e.offsetHeight;
264+
e.offsetParent;)f+=(e=e.offsetParent).offsetTop,t+=e.offsetLeft;
265+
return f<window.pageYOffset+window.innerHeight&&t<window.pageXOffset+window.innerWidth&&f+n>
266+
window.pageYOffset&&t+o>window.pageXOffset
267+
IN_VIEWPORT
268+
269+
driver.execute_script(in_viewport, element)
270+
end
271+
272+
describe '#scroll_to' do
273+
it 'scrolls to element' do
274+
driver.navigate.to url_for('scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html')
275+
iframe = driver.find_element(tag_name: 'iframe')
276+
expect(in_viewport?(iframe)).to eq false
277+
278+
driver.action.scroll_to(iframe).perform
279+
280+
expect(in_viewport?(iframe)).to eq true
281+
end
282+
end
283+
284+
describe '#scroll_from' do
285+
it 'scrolls from element by the provided amount' do
286+
driver.navigate.to url_for('scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html')
287+
288+
iframe = driver.find_element(tag_name: 'iframe')
289+
driver.action.scroll_from(iframe, 0, 200).perform
290+
291+
driver.switch_to.frame(iframe)
292+
checkbox = driver.find_element(name: 'scroll_checkbox')
293+
expect(in_viewport?(checkbox)).to eq true
294+
end
295+
296+
it 'scrolls from element with offset by the provided amount' do
297+
driver.navigate.to url_for('scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html')
298+
299+
footer = driver.find_element(tag_name: 'footer')
300+
driver.action.scroll_from(footer, 0, 200, origin_offset_x: 0, origin_offset_y: -50).perform
301+
302+
driver.switch_to.frame(driver.find_element(tag_name: 'iframe'))
303+
checkbox = driver.find_element(name: 'scroll_checkbox')
304+
expect(in_viewport?(checkbox)).to eq true
305+
end
306+
307+
it 'throws MoveTargetOutOfBoundsError when origin offset is out of viewport' do
308+
driver.navigate.to url_for('scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html')
309+
310+
footer = driver.find_element(tag_name: 'footer')
311+
312+
expect {
313+
driver.action.scroll_from(footer, 0, 200, origin_offset_x: 0, origin_offset_y: 50).perform
314+
}.to raise_error(Error::MoveTargetOutOfBoundsError)
315+
end
316+
end
317+
318+
describe '#scroll_by' do
319+
it 'scrolls by amount provided' do
320+
driver.navigate.to url_for('scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html')
321+
322+
footer = driver.find_element(tag_name: 'footer')
323+
y = footer.rect.y
324+
325+
driver.action.scroll_by(0, y).perform
326+
327+
expect(in_viewport?(footer)).to eq true
328+
end
329+
end
330+
331+
it 'scrolls by amount provided from provided origin' do
332+
driver.navigate.to url_for('scrolling_tests/frame_with_nested_scrolling_frame.html')
333+
334+
iframe = driver.find_element(tag_name: 'iframe')
335+
driver.action.scroll_by(0, 200, origin_x: 10, origin_y: 10).perform
336+
337+
driver.switch_to.frame(iframe)
338+
checkbox = driver.find_element(name: 'scroll_checkbox')
339+
expect(in_viewport?(checkbox)).to eq true
340+
end
341+
342+
it 'throws MoveTargetOutOfBoundsError when origin offset is out of viewport' do
343+
driver.navigate.to url_for('scrolling_tests/frame_with_nested_scrolling_frame.html')
344+
345+
expect {
346+
driver.action.scroll_by(0, 200, origin_x: -10, origin_y: -10).perform
347+
}.to raise_error(Error::MoveTargetOutOfBoundsError)
348+
end
260349
end # ActionBuilder
261350
end # WebDriver
262351
end # Selenium

0 commit comments

Comments
 (0)