Add Session#scroll_to/scroll_by and Element#scroll_to/scroll_by

This commit is contained in:
Thomas Walpole 2018-12-17 10:09:53 -08:00
parent 6105d95051
commit f30472fd4c
11 changed files with 263 additions and 3 deletions

View File

@ -68,6 +68,14 @@ module Capybara
raise NotImplementedError
end
def scroll_by(x, y)
raise NotImplementedError
end
def scroll_to(element, location = :top)
raise NotImplementedError
end
def tag_name
raise NotImplementedError
end

View File

@ -31,6 +31,22 @@ module Capybara
def title
session.driver.title
end
def execute_script(*args)
find(:xpath, '/html').execute_script(*args)
end
def evaluate_script(*args)
find(:xpath, '/html').evaluate_script(*args)
end
def scroll_to(*args)
find(:xpath, '//body').scroll_to(*args)
end
def scroll_by(*args)
find(:xpath, '//body').scroll_by(*args)
end
end
end
end

View File

@ -381,6 +381,17 @@ module Capybara
self
end
def scroll_to(pos_or_el, position = :top)
pos_or_el = pos_or_el.base if pos_or_el.is_a? Capybara::Node::Element
synchronize { base.scroll_to(pos_or_el, position) }
self
end
def scroll_by(x, y)
synchronize { base.scroll_by(x, y) }
self
end
##
#
# Execute the given JS in the context of the element not returning a result. This is useful for scripts that return

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
module Capybara
module Selenium
module Scroll
def scroll_by(x, y)
driver.execute_script <<~JS, self, x, y
var el = arguments[0];
if (el.scrollBy){
el.scrollBy(arguments[1], arguments[2]);
} else {
el.scrollTop = el.scrollTop + arguments[2];
el.scrollLeft = el.scrollLeft + arguments[1];
}
JS
end
def scroll_to(element, location = :top)
location, element = element, nil if element.is_a? Symbol
if element.is_a? Capybara::Selenium::Node
scroll_element_to_location(element, location)
elsif location.is_a? Symbol
scroll_to_location(location)
else
x, y = element, location
scroll_to_coords(x, y)
end
self
end
private
def scroll_element_to_location(element, location)
scroll_opts = case location
when :top
'true'
when :bottom
'false'
when :center
"{behavior: 'instant', block: 'center'}"
else
raise ArgumentError, "Invalid scroll_to location: #{location}"
end
driver.execute_script <<~JS, element
arguments[0].scrollIntoView(#{scroll_opts})
JS
end
def scroll_to_location(location)
scroll_y = case location
when :top
'0'
when :bottom
'arguments[0].scrollHeight'
when :center
'(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
end
driver.execute_script <<~JS, self
if (arguments[0].scrollTo){
arguments[0].scrollTo(0, #{scroll_y});
} else {
arguments[0].scrollTop = #{scroll_y};
}
JS
end
def scroll_to_coords(x, y)
driver.execute_script <<~JS, self, x, y
if (this.scrollTo){
arguments[0].scrollTo(arguments[1], arguments[2]);
} else {
arguments[0].scrollTop = arguments[2];
arguments[0].scrollLeft = arguments[1];
}
JS
end
end
end
end

View File

@ -1,7 +1,12 @@
# frozen_string_literal: true
# Selenium specific implementation of the Capybara::Driver::Node API
require 'capybara/selenium/extensions/scroll'
class Capybara::Selenium::Node < Capybara::Driver::Node
include Capybara::Selenium::Scroll
def visible_text
native.text
end

View File

@ -39,7 +39,7 @@ module Capybara
include Capybara::SessionMatchers
NODE_METHODS = %i[
all first attach_file text check choose
all first attach_file text check choose scroll_to scroll_by
click_link_or_button click_button click_link
fill_in find find_all find_button find_by_id find_field find_link
has_content? has_text? has_css? has_no_content? has_no_text?

View File

@ -0,0 +1,119 @@
# frozen_string_literal: true
Capybara::SpecHelper.spec '#scroll_to', requires: [:scroll] do
before do
@session.visit('/scroll')
end
it 'can scroll an element to the top of the viewport' do
el = @session.find(:css, '#scroll')
@session.scroll_to(el, :top)
expect(el.evaluate_script('this.getBoundingClientRect().top')).to be_within(1).of(0)
end
it 'can scroll an element to the bottom of the viewport' do
el = @session.find(:css, '#scroll')
@session.scroll_to(el, :bottom)
el_bottom = el.evaluate_script('this.getBoundingClientRect().bottom')
viewport_bottom = el.evaluate_script('document.body.clientHeight')
expect(el_bottom).to be_within(1).of(viewport_bottom)
end
it 'can scroll an element to the center of the viewport' do
el = @session.find(:css, '#scroll')
@session.scroll_to(el, :center)
el_center = el.evaluate_script('(function(rect){return (rect.top + rect.bottom)/2})(this.getBoundingClientRect())')
viewport_bottom = el.evaluate_script('document.body.clientHeight')
expect(el_center).to be_within(1).of(viewport_bottom / 2)
end
it 'can scroll the window to the vertical top' do
@session.scroll_to :bottom
@session.scroll_to :top
expect(@session.evaluate_script('[window.scrollX, window.scrollY]')).to eq [0, 0]
end
it 'can scroll the window to the vertical bottom' do
@session.scroll_to :bottom
max_scroll = @session.evaluate_script('document.body.scrollHeight - document.body.clientHeight')
expect(@session.evaluate_script('[window.scrollX, window.scrollY]')).to eq [0, max_scroll]
end
it 'can scroll the window to the vertical center' do
@session.scroll_to :center
max_scroll = @session.evaluate_script('document.documentElement.scrollHeight - document.body.clientHeight')
expect(@session.evaluate_script('[window.scrollX, window.scrollY]')).to eq [0, max_scroll / 2]
end
it 'can scroll the window to specifc location' do
@session.scroll_to 100, 100
expect(@session.evaluate_script('[window.scrollX, window.scrollY]')).to eq [100, 100]
end
it 'can scroll an element to the top of the scrolling element' do
scrolling_element = @session.find(:css, '#scrollable')
el = scrolling_element.find(:css, '#inner')
scrolling_element.scroll_to(el, :top)
scrolling_element_top = scrolling_element.evaluate_script('this.getBoundingClientRect().top')
expect(el.evaluate_script('this.getBoundingClientRect().top')).to be_within(1).of(scrolling_element_top)
end
it 'can scroll an element to the bottom of the scrolling element' do
scrolling_element = @session.find(:css, '#scrollable')
el = scrolling_element.find(:css, '#inner')
scrolling_element.scroll_to(el, :bottom)
el_bottom = el.evaluate_script('this.getBoundingClientRect().bottom')
scroller_bottom = scrolling_element.evaluate_script('this.getBoundingClientRect().top + this.clientHeight')
expect(el_bottom).to be_within(1).of(scroller_bottom)
end
it 'can scroll an element to the center of the scrolling element' do
scrolling_element = @session.find(:css, '#scrollable')
el = scrolling_element.find(:css, '#inner')
scrolling_element.scroll_to(el, :center)
el_center = el.evaluate_script('(function(rect){return (rect.top + rect.bottom)/2})(this.getBoundingClientRect())')
scrollable_center = scrolling_element.evaluate_script('(this.clientHeight / 2) + this.getBoundingClientRect().top')
expect(el_center).to be_within(1).of(scrollable_center)
end
it 'can scroll the scrolling element to the top' do
scrolling_element = @session.find(:css, '#scrollable')
scrolling_element.scroll_to :bottom
scrolling_element.scroll_to :top
expect(scrolling_element.evaluate_script('[this.scrollLeft, this.scrollTop]')).to eq [0, 0]
end
it 'can scroll the scrolling element to the bottom' do
scrolling_element = @session.find(:css, '#scrollable')
scrolling_element.scroll_to :bottom
max_scroll = scrolling_element.evaluate_script('this.scrollHeight - this.clientHeight')
expect(scrolling_element.evaluate_script('[this.scrollLeft, this.scrollTop]')).to eq [0, max_scroll]
end
it 'can scroll the scrolling element to the vertical center' do
scrolling_element = @session.find(:css, '#scrollable')
scrolling_element.scroll_to :center
max_scroll = scrolling_element.evaluate_script('this.scrollHeight - this.clientHeight')
expect(scrolling_element.evaluate_script('[this.scrollLeft, this.scrollTop]')).to eq [0, max_scroll / 2]
end
it 'can scroll the scrolling element to specifc location' do
scrolling_element = @session.find(:css, '#scrollable')
scrolling_element.scroll_to 100, 100
expect(scrolling_element.evaluate_script('[this.scrollLeft, this.scrollTop]')).to eq [100, 100]
end
context 'scroll_by' do
it 'can scroll the window by a specifc amount' do
@session.scroll_by 100, 100
expect(@session.evaluate_script('[window.scrollX, window.scrollY]')).to eq [100, 100]
end
it 'can scroll the scroll the scrolling element by a specifc amount' do
scrolling_element = @session.find(:css, '#scrollable')
scrolling_element.scroll_to 100, 100
scrolling_element.scroll_by(-50, 50)
expect(scrolling_element.evaluate_script('[this.scrollLeft, this.scrollTop]')).to eq [50, 150]
end
end
end

View File

@ -0,0 +1,20 @@
<html style="height: 250%; width: 150%">
<head>
<style>
* { padding: 0; }
div { position: absolute; }
#scroll { top: 125%; }
#scrollable { top: 150%; overflow-y: scroll; overflow-x: scroll; height: 50px; width: 50px; position: relative; border: 1px solid black;}
#inner { top: 50%; }
</style>
</head>
<body style="height: 100%; width: 100%">
<div id="scroll" style = "top: 125%">scroll</div>
<div id="scrollable">
<div style="height: 200px; width: 200px; poistion: relative">
<div id="inner">inner</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -8,7 +8,7 @@ class TestClass
end
Capybara::SpecHelper.run_specs TestClass.new, 'DSL', capybara_skip: %i[
js modals screenshot frames windows send_keys server hover about_scheme psc download css driver
js modals screenshot frames windows send_keys server hover about_scheme psc download css driver scroll
] do |example|
case example.metadata[:full_description]
when /has_css\? should support case insensitive :class and :id options/

View File

@ -24,6 +24,7 @@ skipped_tests = %i[
about_scheme
download
css
scroll
]
Capybara::SpecHelper.run_specs TestSessions::RackTest, 'RackTest', capybara_skip: skipped_tests do |example|
case example.metadata[:full_description]

View File

@ -51,6 +51,6 @@ end
RSpec.configure do |config|
Capybara::SpecHelper.configure(config)
config.filter_run_including focus_: true unless ENV['CI']
config.filter_run_including focus_: true # unless ENV['CI']
config.run_all_when_everything_filtered = true
end