Skip to content

Commit

Permalink
Implement basic support for relative locators
Browse files Browse the repository at this point in the history
A typical example for using relative locators is to find all elements of
tag name in the relative direction. For example, you can find all table
cells to the right of the element:

  driver.find_elements(relative: {tag_name: 'td', right: my_element})

Supported relative locators are:
  * above
  * below
  * left
  * right
  * near (within 50px or custom distance in any direction)

See tests for more examples.
  • Loading branch information
p0deje committed Nov 22, 2019
1 parent 541e06a commit df6be2e
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 12 deletions.
1 change: 1 addition & 0 deletions rb/build.desc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ruby_library(name = "common",
resources = [
{ "../LICENSE" : "rb/LICENSE" },
{ "//javascript/webdriver/atoms:get-attribute": "rb/lib/selenium/webdriver/atoms/getAttribute.js"},
{ "//javascript/atoms/fragments:find-elements": "rb/lib/selenium/webdriver/atoms/findElements.js"},
{ "//javascript/atoms/fragments:is-displayed": "rb/lib/selenium/webdriver/atoms/isDisplayed.js"}
]
)
Expand Down
122 changes: 122 additions & 0 deletions rb/lib/selenium/webdriver/atoms/findElements.js

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions rb/lib/selenium/webdriver/common/search_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module SearchContext
link_text: 'link text',
name: 'name',
partial_link_text: 'partial link text',
relative: 'relative',
tag_name: 'tag name',
xpath: 'xpath'
}.freeze
Expand Down Expand Up @@ -59,7 +60,7 @@ def find_element(*args)
by = FINDERS[how.to_sym]
raise ArgumentError, "cannot find element by #{how.inspect}" unless by

bridge.find_element_by by, what.to_s, ref
bridge.find_element_by by, what, ref
rescue Selenium::WebDriver::Error::TimeoutError
# Implicit Wait times out in Edge
raise Selenium::WebDriver::Error::NoSuchElementError
Expand All @@ -77,7 +78,7 @@ def find_elements(*args)
by = FINDERS[how.to_sym]
raise ArgumentError, "cannot find elements by #{how.inspect}" unless by

bridge.find_elements_by by, what.to_s, ref
bridge.find_elements_by by, what, ref
rescue Selenium::WebDriver::Error::TimeoutError
# Implicit Wait times out in Edge
[]
Expand Down
35 changes: 25 additions & 10 deletions rb/lib/selenium/webdriver/remote/bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -512,23 +512,28 @@ def active_element
alias_method :switch_to_active_element, :active_element

def find_element_by(how, what, parent = nil)
how, what = convert_locators(how, what)
how, what = convert_locator(how, what)

return execute_atom(:findElements, Support::RelativeLocator.new(what).as_json).first if how == 'relative'

id = if parent
execute :find_child_element, {id: parent}, {using: how, value: what}
execute :find_child_element, {id: parent}, {using: how, value: what.to_s}
else
execute :find_element, {}, {using: how, value: what}
execute :find_element, {}, {using: how, value: what.to_s}
end

Element.new self, element_id_from(id)
end

def find_elements_by(how, what, parent = nil)
how, what = convert_locators(how, what)
how, what = convert_locator(how, what)

return execute_atom :findElements, Support::RelativeLocator.new(what).as_json if how == 'relative'

ids = if parent
execute :find_child_elements, {id: parent}, {using: how, value: what}
execute :find_child_elements, {id: parent}, {using: how, value: what.to_s}
else
execute :find_elements, {}, {using: how, value: what}
execute :find_elements, {}, {using: how, value: what.to_s}
end

ids.map { |id| Element.new self, element_id_from(id) }
Expand Down Expand Up @@ -594,20 +599,30 @@ def element_id_from(id)
id['ELEMENT'] || id['element-6066-11e4-a52e-4f735466cecf']
end

def convert_locators(how, what)
def convert_locator(how, what)
how = SearchContext::FINDERS[how.to_sym] || how

case how
when 'class name'
how = 'css selector'
what = ".#{escape_css(what)}"
what = ".#{escape_css(what.to_s)}"
when 'id'
how = 'css selector'
what = "##{escape_css(what)}"
what = "##{escape_css(what.to_s)}"
when 'name'
how = 'css selector'
what = "*[name='#{escape_css(what)}']"
what = "*[name='#{escape_css(what.to_s)}']"
when 'tag name'
how = 'css selector'
end

if what.is_a?(Hash)
what = what.each_with_object({}) do |(h, w), hash|
h, w = convert_locator(h.to_s, w)
hash[h] = w
end
end

[how, what]
end

Expand Down
1 change: 1 addition & 0 deletions rb/lib/selenium/webdriver/support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@
require 'selenium/webdriver/support/escaper'
require 'selenium/webdriver/support/select'
require 'selenium/webdriver/support/color'
require 'selenium/webdriver/support/relative_locator'
51 changes: 51 additions & 0 deletions rb/lib/selenium/webdriver/support/relative_locator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

module Selenium
module WebDriver
module Support

#
# @api private
#

class RelativeLocator
KEYS = %w[above below left right near distance].freeze

def initialize(locator)
@filters, @root = locator.partition { |how, _| KEYS.include?(how) }.map(&:to_h)
end

def as_json
{
relative: {
root: @root,
filters: @filters.map do |kind, filter|
{
kind: kind,
args: [filter]
}
end
}
}
end
end
end
end
end
73 changes: 73 additions & 0 deletions rb/spec/integration/selenium/webdriver/driver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ def save_screenshot_and_assert(path)
expect(driver.find_element(tag_name: 'div').attribute('class')).to eq('navigation')
end

it 'should find above another' do
driver.navigate.to url_for('relative_locators.html')

above = driver.find_element(relative: {tag_name: 'td', above: {id: 'center'}})
expect(above.attribute('id')).to eq('first')
end

it 'should find child element' do
driver.navigate.to url_for('nestedElements.html')

Expand Down Expand Up @@ -174,6 +181,72 @@ def save_screenshot_and_assert(path)
driver.find_elements(css: 'p')
end

it 'should find above element' do
driver.navigate.to url_for('relative_locators.html')

lowest = driver.find_element(id: 'below')
above = driver.find_elements(relative: {tag_name: 'p', above: lowest})
expect(above.map { |e| e.attribute('id') }).to eq(%w[above mid])
end

it 'should find above another' do
driver.navigate.to url_for('relative_locators.html')

above = driver.find_elements(relative: {tag_name: 'td', above: {id: 'center'}})
expect(above.map { |e| e.attribute('id') }).to eq(%w[first second third])
end

it 'should find below element' do
driver.navigate.to url_for('relative_locators.html')

midpoint = driver.find_element(id: 'mid')
above = driver.find_elements(relative: {tag_name: 'p', below: midpoint})
expect(above.map { |e| e.attribute('id') }).to eq(['below'])
end

it 'should find near another within default distance' do
driver.navigate.to url_for('relative_locators.html')

near = driver.find_elements(relative: {tag_name: 'td', near: {id: 'sixth'}})
expect(near.map { |e| e.attribute('id') }).to eq(%w[second third center eighth ninth])
end

it 'should find near another within custom distance' do
driver.navigate.to url_for('relative_locators.html')

near = driver.find_elements(relative: {tag_name: 'td', near: {id: 'sixth', distance: 100}})
expect(near.map { |e| e.attribute('id') }).to eq(%w[second third center eighth ninth])
end

it 'should find to the left of another' do
driver.navigate.to url_for('relative_locators.html')

left = driver.find_elements(relative: {tag_name: 'td', left: {id: 'center'}})
expect(left.map { |e| e.attribute('id') }).to eq(%w[first fourth seventh])
end

it 'should find to the right of another' do
driver.navigate.to url_for('relative_locators.html')

right = driver.find_elements(relative: {tag_name: 'td', right: {id: 'center'}})
expect(right.map { |e| e.attribute('id') }).to eq(%w[third sixth ninth])
end

it 'should find by combined relative locators' do
driver.navigate.to url_for('relative_locators.html')

found = driver.find_elements(relative: {tag_name: 'td', right: {id: 'second'}, above: {id: 'center'}})
expect(found.map { |e| e.attribute('id') }).to eq(['third'])
end

it 'should find all by empty relative locator' do
driver.navigate.to url_for('relative_locators.html')

expected = driver.find_elements(tag_name: 'p')
actual = driver.find_elements(relative: {tag_name: 'p'})
expect(actual).to eq(expected)
end

it 'should find children by field name' do
driver.navigate.to url_for('nestedElements.html')
element = driver.find_element(name: 'form2')
Expand Down

0 comments on commit df6be2e

Please sign in to comment.