watir 6.15.1 → 6.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -2
  3. data/.travis.yml +2 -0
  4. data/CHANGES.md +13 -0
  5. data/Rakefile +6 -0
  6. data/lib/watir.rb +1 -0
  7. data/lib/watir/browser.rb +4 -1
  8. data/lib/watir/element_collection.rb +27 -17
  9. data/lib/watir/elements/element.rb +41 -14
  10. data/lib/watir/elements/iframe.rb +3 -1
  11. data/lib/watir/elements/radio.rb +7 -2
  12. data/lib/watir/elements/select.rb +1 -0
  13. data/lib/watir/locators.rb +21 -21
  14. data/lib/watir/locators/button/matcher.rb +40 -0
  15. data/lib/watir/locators/cell/selector_builder.rb +3 -0
  16. data/lib/watir/locators/element/locator.rb +29 -172
  17. data/lib/watir/locators/element/matcher.rb +127 -0
  18. data/lib/watir/locators/element/selector_builder.rb +69 -23
  19. data/lib/watir/locators/element/selector_builder/xpath.rb +3 -10
  20. data/lib/watir/locators/row/selector_builder.rb +5 -5
  21. data/lib/watir/locators/text_area/selector_builder.rb +0 -14
  22. data/lib/watir/locators/text_area/selector_builder/xpath.rb +2 -2
  23. data/lib/watir/locators/text_field/matcher.rb +38 -0
  24. data/lib/watir/radio_set.rb +28 -31
  25. data/lib/watir/scroll.rb +69 -0
  26. data/lib/watir/version.rb +1 -1
  27. data/spec/locator_spec_helper.rb +58 -14
  28. data/spec/unit/element_locator_spec.rb +46 -591
  29. data/spec/unit/match_elements/button_spec.rb +80 -0
  30. data/spec/unit/match_elements/element_spec.rb +368 -0
  31. data/spec/unit/match_elements/text_field_spec.rb +79 -0
  32. data/spec/unit/selector_builder/anchor_spec.rb +51 -0
  33. data/spec/unit/selector_builder/button_spec.rb +206 -0
  34. data/spec/unit/selector_builder/cell_spec.rb +63 -0
  35. data/spec/unit/selector_builder/element_spec.rb +744 -0
  36. data/spec/unit/selector_builder/row_spec.rb +111 -0
  37. data/spec/unit/selector_builder/text_field_spec.rb +189 -0
  38. data/spec/unit/selector_builder/textarea_spec.rb +25 -0
  39. data/spec/watirspec/browser_spec.rb +7 -8
  40. data/spec/watirspec/element_hidden_spec.rb +1 -2
  41. data/spec/watirspec/elements/element_spec.rb +52 -16
  42. data/spec/watirspec/elements/iframe_spec.rb +1 -1
  43. data/spec/watirspec/elements/select_list_spec.rb +1 -1
  44. data/spec/watirspec/html/obscured.html +3 -1
  45. data/spec/watirspec/html/scroll.html +32 -0
  46. data/spec/watirspec/relaxed_locate_spec.rb +6 -1
  47. data/spec/watirspec/scroll_spec.rb +106 -0
  48. data/spec/watirspec/support/rspec_matchers.rb +2 -0
  49. data/spec/watirspec/wait_spec.rb +1 -1
  50. data/watir.gemspec +2 -4
  51. metadata +36 -33
  52. data/lib/watir/locators/button/locator.rb +0 -32
  53. data/lib/watir/locators/button/validator.rb +0 -17
  54. data/lib/watir/locators/cell/locator.rb +0 -13
  55. data/lib/watir/locators/element/validator.rb +0 -11
  56. data/lib/watir/locators/row/locator.rb +0 -13
  57. data/lib/watir/locators/text_field/locator.rb +0 -31
  58. data/lib/watir/locators/text_field/validator.rb +0 -13
  59. data/spec/unit/anchor_locator_spec.rb +0 -68
  60. data/spec/watirspec/selector_builder/button_spec.rb +0 -250
  61. data/spec/watirspec/selector_builder/cell_spec.rb +0 -92
  62. data/spec/watirspec/selector_builder/element_spec.rb +0 -628
  63. data/spec/watirspec/selector_builder/row_spec.rb +0 -148
  64. data/spec/watirspec/selector_builder/text_spec.rb +0 -199
@@ -17,6 +17,7 @@ module Watir
17
17
 
18
18
  index = @selector.delete(:index)
19
19
  @adjacent = @selector.delete(:adjacent)
20
+ @scope = @selector.delete(:scope)
20
21
 
21
22
  xpath = start_string
22
23
  xpath << adjacent_string
@@ -75,7 +76,8 @@ module Watir
75
76
  end
76
77
 
77
78
  def start_string
78
- @adjacent ? './' : './/*'
79
+ start = @adjacent ? './' : './/*'
80
+ @scope ? "(#{@scope[:xpath]})[1]#{start.tr('.', '')}" : start
79
81
  end
80
82
 
81
83
  def adjacent_string
@@ -104,8 +106,6 @@ module Watir
104
106
  class_name = @selector.delete(:class)
105
107
  return '' if class_name.nil?
106
108
 
107
- deprecate_class_array(class_name) if class_name.is_a?(String) && class_name.strip.include?(' ')
108
-
109
109
  @built[:class] = []
110
110
 
111
111
  predicates = [class_name].flatten.map { |value| process_attribute(:class, value) }.compact
@@ -181,13 +181,6 @@ module Watir
181
181
  end
182
182
  end
183
183
 
184
- def deprecate_class_array(class_name)
185
- dep = "Using the :class locator to locate multiple classes with a String value (i.e. \"#{class_name}\")"
186
- Watir.logger.deprecate dep,
187
- "Array (e.g. #{class_name.split})",
188
- ids: [:class_array]
189
- end
190
-
191
184
  def visible?
192
185
  !(@built.keys & CAN_NOT_BUILD).empty?
193
186
  end
@@ -2,13 +2,13 @@ module Watir
2
2
  module Locators
3
3
  class Row
4
4
  class SelectorBuilder < Element::SelectorBuilder
5
- def initialize(valid_attributes, scope_tag_name)
6
- @scope_tag_name = scope_tag_name
7
- super(valid_attributes)
5
+ def build_wd_selector(selector)
6
+ scope_tag_name = @query_scope.selector[:tag_name] || @query_scope.tag_name
7
+ Kernel.const_get("#{self.class.name}::XPath").new.build(selector, scope_tag_name)
8
8
  end
9
9
 
10
- def build_wd_selector(selector)
11
- Kernel.const_get("#{self.class.name}::XPath").new.build(selector, @scope_tag_name)
10
+ def use_scope?
11
+ false
12
12
  end
13
13
  end
14
14
  end
@@ -2,20 +2,6 @@ module Watir
2
2
  module Locators
3
3
  class TextArea
4
4
  class SelectorBuilder < Element::SelectorBuilder
5
- private
6
-
7
- def normalize_locator(how, what)
8
- # We need to iterate through located elements and fetch
9
- # attribute value using Selenium because XPath doesn't understand
10
- # difference between IDL vs content attribute.
11
- # Current Element design doesn't allow to do that in any
12
- # obvious way except to use regular expression.
13
- if how == :value && what.is_a?(String)
14
- [how, Regexp.new('^' + Regexp.escape(what) + '$')]
15
- else
16
- super
17
- end
18
- end
19
5
  end
20
6
  end
21
7
  end
@@ -6,10 +6,10 @@ module Watir
6
6
  private
7
7
 
8
8
  # value always requires a wire call since we want the property not the attribute
9
- def predicate_conversion(key, regexp)
9
+ def process_attribute(key, value)
10
10
  return super unless key == :value
11
11
 
12
- @built[:value] = regexp
12
+ @built[:value] = value
13
13
  nil
14
14
  end
15
15
  end
@@ -0,0 +1,38 @@
1
+ module Watir
2
+ module Locators
3
+ class TextField
4
+ class Matcher < Element::Matcher
5
+ private
6
+
7
+ def elements_match?(element, values_to_match)
8
+ case element.tag_name.downcase
9
+ when 'input'
10
+ %i[text label visible_text].each do |key|
11
+ next unless values_to_match.key?(key)
12
+
13
+ values_to_match[:value] = values_to_match.delete(key)
14
+ end
15
+ when 'label'
16
+ %i[value label].each do |key|
17
+ next unless values_to_match.key?(key)
18
+
19
+ values_to_match[:text] = values_to_match.delete(key)
20
+ end
21
+ else
22
+ return
23
+ end
24
+
25
+ super
26
+ end
27
+
28
+ def text_regexp_deprecation(*)
29
+ # does not apply to text_field
30
+ end
31
+
32
+ def validate_tag(element, _tag_name)
33
+ matches_values?(element.tag_name.downcase, 'input')
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -46,13 +46,12 @@ module Watir
46
46
  #
47
47
 
48
48
  def radio(opt = {})
49
- n = name
50
- if !n.empty? && (!opt[:name] || opt[:name] == n)
51
- frame.radio(opt.merge(name: n))
52
- elsif n.empty?
53
- return source
49
+ if !name.empty? && (!opt[:name] || opt[:name] == name)
50
+ frame.radio(opt.merge(name: name))
51
+ elsif name.empty?
52
+ source
54
53
  else
55
- raise UnknownObjectException, "#{opt[:name]} does not match name of RadioSet: #{n}"
54
+ raise UnknownObjectException, "#{opt[:name]} does not match name of RadioSet: #{name}"
56
55
  end
57
56
  end
58
57
 
@@ -61,13 +60,12 @@ module Watir
61
60
  #
62
61
 
63
62
  def radios(opt = {})
64
- n = name
65
- if !n.empty? && (!opt[:name] || opt[:name] == n)
66
- element_call(:wait_for_present) { frame.radios(opt.merge(name: n)) }
67
- elsif n.empty?
68
- RadioCollection.new(frame, element: source.wd)
63
+ if !name.empty? && (!opt[:name] || opt[:name] == name)
64
+ element_call(:wait_for_present) { frame.radios(opt.merge(name: name)) }
65
+ elsif name.empty?
66
+ single_radio_collection
69
67
  else
70
- raise UnknownObjectException, "#{opt[:name]} does not match name of RadioSet: #{n}"
68
+ raise UnknownObjectException, "#{opt[:name]} does not match name of RadioSet: #{name}"
71
69
  end
72
70
  end
73
71
 
@@ -132,18 +130,14 @@ module Watir
132
130
  #
133
131
 
134
132
  def select(str_or_rx)
135
- found_by_value = radio(value: str_or_rx)
136
- found_by_text = radio(label: str_or_rx)
137
-
138
- if found_by_value.exist?
139
- found_by_value.click unless found_by_value.selected?
140
- return found_by_value.value
141
- elsif found_by_text.exist?
142
- found_by_text.click unless found_by_text.selected?
143
- return found_by_text.text
144
- else
145
- raise UnknownObjectException, "Unable to locate radio matching #{str_or_rx.inspect}"
133
+ %i[value label].each do |key|
134
+ radio = radio(key => str_or_rx)
135
+ next unless radio.exist?
136
+
137
+ radio.click unless radio.selected?
138
+ return key == :value ? radio.value : radio.text
146
139
  end
140
+ raise UnknownObjectException, "Unable to locate radio matching #{str_or_rx.inspect}"
147
141
  end
148
142
 
149
143
  #
@@ -156,7 +150,6 @@ module Watir
156
150
 
157
151
  def selected?(str_or_rx)
158
152
  found = frame.radio(label: str_or_rx)
159
-
160
153
  return found.selected? if found.exist?
161
154
 
162
155
  raise UnknownObjectException, "Unable to locate radio matching #{str_or_rx.inspect}"
@@ -170,8 +163,7 @@ module Watir
170
163
  #
171
164
 
172
165
  def value
173
- sel = selected
174
- sel&.value
166
+ selected&.value
175
167
  end
176
168
 
177
169
  #
@@ -182,8 +174,7 @@ module Watir
182
174
  #
183
175
 
184
176
  def text
185
- sel = selected
186
- sel&.text
177
+ selected&.text
187
178
  end
188
179
 
189
180
  #
@@ -206,9 +197,7 @@ module Watir
206
197
  #
207
198
 
208
199
  def ==(other)
209
- return false unless other.is_a?(self.class)
210
-
211
- radios == other.radios
200
+ other.is_a?(self.class) && radios == other.radios
212
201
  end
213
202
  alias eql? ==
214
203
 
@@ -218,6 +207,14 @@ module Watir
218
207
  source.send(method, *args, &blk)
219
208
  end
220
209
  end
210
+
211
+ private
212
+
213
+ def single_radio_collection
214
+ collection = RadioCollection.new(frame, source.selector)
215
+ collection.first.cache = source.wd
216
+ collection
217
+ end
221
218
  end # RadioSet
222
219
 
223
220
  module Container
@@ -0,0 +1,69 @@
1
+ module Watir
2
+ module Scrolling
3
+ def scroll
4
+ Scroll.new(self)
5
+ end
6
+ end
7
+
8
+ class Scroll
9
+ def initialize(object)
10
+ @object = object
11
+ end
12
+
13
+ # Scrolls by offset.
14
+ # @param [Fixnum] left Horizontal offset
15
+ # @param [Fixnum] top Vertical offset
16
+ #
17
+ def by(left, top)
18
+ @object.browser.execute_script('window.scrollBy(arguments[0], arguments[1]);', Integer(left), Integer(top))
19
+ self
20
+ end
21
+
22
+ #
23
+ # Scrolls to specified location.
24
+ # @param [Symbol] param
25
+ #
26
+ def to(param = :top)
27
+ args = @object.is_a?(Watir::Element) ? element_scroll(param) : browser_scroll(param)
28
+ raise ArgumentError, "Don't know how to scroll #{@object} to: #{param}!" if args.nil?
29
+
30
+ @object.browser.execute_script(*args)
31
+ self
32
+ end
33
+
34
+ private
35
+
36
+ def element_scroll(param)
37
+ script = case param
38
+ when :top, :start
39
+ 'arguments[0].scrollIntoView();'
40
+ when :center
41
+ <<-JS
42
+ var bodyRect = document.body.getBoundingClientRect();
43
+ var elementRect = arguments[0].getBoundingClientRect();
44
+ var left = (elementRect.left - bodyRect.left) - (window.innerWidth / 2);
45
+ var top = (elementRect.top - bodyRect.top) - (window.innerHeight / 2);
46
+ window.scrollTo(left, top);
47
+ JS
48
+ when :bottom, :end
49
+ 'arguments[0].scrollIntoView(false);'
50
+ else
51
+ return nil
52
+ end
53
+ [script, @object]
54
+ end
55
+
56
+ def browser_scroll(param)
57
+ case param
58
+ when :top, :start
59
+ 'window.scrollTo(0, 0);'
60
+ when :center
61
+ 'window.scrollTo(window.outerWidth / 2, window.outerHeight / 2);'
62
+ when :bottom, :end
63
+ 'window.scrollTo(0, document.body.scrollHeight);'
64
+ when Array
65
+ ['window.scrollTo(arguments[0], arguments[1]);', Integer(param[0]), Integer(param[1])]
66
+ end
67
+ end
68
+ end
69
+ end
data/lib/watir/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Watir
2
- VERSION = '6.15.1'.freeze
2
+ VERSION = '6.16.0'.freeze
3
3
  end
@@ -1,17 +1,40 @@
1
1
  module LocatorSpecHelper
2
2
  def browser
3
- @browser ||= double(Watir::Browser, wd: driver)
3
+ @browser ||= instance_double(Watir::Browser, wd: driver)
4
+ allow(@browser).to receive(:browser).and_return(@browser)
5
+ allow(@browser).to receive(:is_a?).with(Watir::Browser).and_return(true)
6
+ allow(@browser).to receive(:locator_namespace).and_return(@locator_namespace || Watir::Locators)
7
+ @browser
4
8
  end
5
9
 
6
10
  def driver
7
- @driver ||= double(Selenium::WebDriver::Driver)
11
+ @driver ||= instance_double(Selenium::WebDriver::Driver)
8
12
  end
9
13
 
10
- def locator(selector, attrs)
11
- attrs ||= Watir::HTMLElement.attributes
12
- element_validator = Watir::Locators::Element::Validator.new
13
- selector_builder = Watir::Locators::Element::SelectorBuilder.new(attrs)
14
- Watir::Locators::Element::Locator.new(browser, selector, selector_builder, element_validator)
14
+ def selector_builder
15
+ @built ||= {xpath: ''}
16
+ @selector_builder = instance_double(Watir::Locators::Element::SelectorBuilder)
17
+ allow(@selector_builder).to receive(:build).and_return(@built)
18
+ @selector_builder
19
+ end
20
+
21
+ def attributes
22
+ @attributes ||= Watir::HTMLElement.attribute_list
23
+ end
24
+
25
+ def query_scope
26
+ @query_scope ||= browser
27
+ end
28
+
29
+ def element_matcher
30
+ @element_matcher ||= instance_double(Watir::Locators::Element::Matcher)
31
+ allow(@element_matcher).to receive(:query_scope).and_return(browser)
32
+ allow(@element_matcher).to receive(:selector).and_return(@locator || {})
33
+ @element_matcher
34
+ end
35
+
36
+ def locator
37
+ Watir::Locators::Element::Locator.new(element_matcher)
15
38
  end
16
39
 
17
40
  def expect_one(*args)
@@ -22,25 +45,46 @@ module LocatorSpecHelper
22
45
  expect(driver).to receive(:find_elements).with(*args)
23
46
  end
24
47
 
25
- def locate_one(selector, attrs = nil)
26
- locator(ordered_hash(selector), attrs).locate
48
+ def locate_one(selector = nil)
49
+ selector ||= @locator || {}
50
+ locator.locate ordered_hash(selector)
51
+ end
52
+
53
+ def locate_all(selector = nil)
54
+ selector ||= @locator || {}
55
+ locator.locate_all ordered_hash(selector)
27
56
  end
28
57
 
29
- def locate_all(selector, attrs = nil)
30
- locator(ordered_hash(selector), attrs).locate_all
58
+ def selector_build(selector)
59
+ selector_builder.build(selector)
31
60
  end
32
61
 
33
62
  def element(opts = {})
34
- attrs = opts.delete(:attributes)
35
- el = double(Watir::HTMLElement, opts)
63
+ raise unless opts.delete(:attributes).nil?
64
+
65
+ klass = opts.delete(:watir_element) || Watir::HTMLElement
66
+ el = instance_double(klass, opts)
67
+
68
+ allow(el).to receive(:enabled?).and_return true
69
+ allow(el).to receive(:selector_builder).and_return(selector_builder)
70
+ allow(el).to receive(:wd).and_return wd_element unless opts.key?(:wd)
71
+ allow(el).to receive(:selector).and_return(@selector || {})
72
+ el
73
+ end
36
74
 
75
+ def wd_element(opts = {})
76
+ attrs = opts.delete(:attributes)
77
+ el = instance_double(Selenium::WebDriver::Element, opts)
37
78
  attrs&.each do |key, value|
38
79
  allow(el).to receive(:attribute).with(key.to_s).and_return(value)
39
80
  end
40
- allow(el).to receive(:enabled?).and_return true
41
81
  el
42
82
  end
43
83
 
84
+ def el
85
+ @el ||= wd_element
86
+ end
87
+
44
88
  def ordered_hash(selector)
45
89
  case selector
46
90
  when Hash
@@ -3,627 +3,82 @@ require_relative 'unit_helper'
3
3
  describe Watir::Locators::Element::Locator do
4
4
  include LocatorSpecHelper
5
5
 
6
- describe 'finds a single element' do
7
- describe 'by delegating to Selenium' do
8
- SELENIUM_SELECTORS.each do |loc|
9
- it "delegates to Selenium's #{loc} locator" do
10
- expect_one(loc, 'bar').and_return(element(tag_name: 'div'))
11
- match = %i[link link_text partial_link_text].include?(loc) ? :to : :to_not
12
- msg = /:#{loc} locator is deprecated\. Use :visible_text instead/
13
- expect { locate_one loc => 'bar' }.send(match, output(msg).to_stdout_from_any_process)
14
- end
15
- end
16
-
17
- it 'raises exception if locating a non-link element by link locator' do
18
- selector = {tag_name: 'div', link_text: 'foo'}
19
- msg = 'Can not use link_text locator to find a foo element'
20
- expect {
21
- expect { locate_one(selector) }.to raise_exception(StandardError, msg)
22
- }.to have_deprecated_link_text
23
- end
24
- end
25
-
26
- describe 'with selectors not supported by Selenium' do
27
- it 'handles selector with tag name and a single attribute' do
28
- expect_one :xpath, ".//*[local-name()='div'][@title='foo']"
29
-
30
- locate_one tag_name: 'div',
31
- title: 'foo'
32
- end
33
-
34
- it 'handles selector with no tag name and and a single attribute' do
35
- expect_one :xpath, ".//*[@title='foo']"
36
-
37
- locate_one title: 'foo'
38
- end
39
-
40
- it 'handles single quotes in the attribute string' do
41
- expect_one :xpath, %{.//*[@title=concat('foo and ',"'",'bar',"'",'')]}
42
-
43
- locate_one title: "foo and 'bar'"
44
- end
45
-
46
- it 'handles selector with tag name and multiple attributes' do
47
- expect_one :xpath, ".//*[local-name()='div'][@title='foo' and @dir='bar']"
48
-
49
- locate_one [:tag_name, 'div',
50
- :title, 'foo',
51
- :dir, 'bar']
52
- end
53
-
54
- it 'handles selector with no tag name and multiple attributes' do
55
- expect_one :xpath, ".//*[@dir='foo' and @title='bar']"
56
-
57
- locate_one [:dir, 'foo',
58
- :title, 'bar']
59
- end
60
-
61
- it 'handles selector with attribute presence' do
62
- expect_one :xpath, './/*[@data-view]'
63
-
64
- locate_one [:data_view, true]
65
- end
66
-
67
- it 'handles selector with attribute absence' do
68
- expect_one :xpath, './/*[not(@data-view)]'
69
-
70
- locate_one [:data_view, false]
71
- end
72
-
73
- it 'handles selector with class attribute presence' do
74
- expect_one :xpath, './/*[@class]'
75
-
76
- locate_one class: true
77
- end
78
-
79
- it 'handles selector with multiple classes in array' do
80
- xpath = ".//*[contains(concat(' ', @class, ' '), ' a ') and contains(concat(' ', @class, ' '), ' b ')]"
81
- expect_one :xpath, xpath
82
-
83
- locate_one class: %w[a b]
84
- end
85
-
86
- it 'handles selector with multiple classes in string' do
87
- expect_one :xpath, ".//*[contains(concat(' ', @class, ' '), ' a b ')]"
88
-
89
- expect { locate_one class: 'a b' }.to have_deprecated_class_array
90
- end
91
-
92
- it 'handles selector with xpath and tag_name String' do
93
- elements = [
94
- element(tag_name: 'div', attributes: {class: 'foo'}),
95
- element(tag_name: 'span', attributes: {class: 'foo'}),
96
- element(tag_name: 'div', attributes: {class: 'foo'})
97
- ]
98
-
99
- expect_all(:xpath, './/*[@class="foo"]').and_return(elements)
100
-
101
- selector = {
102
- xpath: './/*[@class="foo"]',
103
- tag_name: 'span'
104
- }
105
-
106
- expect(locate_one(selector).tag_name).to eq 'span'
107
- end
108
-
109
- it 'handles selector with xpath and tag_name Symbol' do
110
- elements = [
111
- element(tag_name: 'div', attributes: {class: 'foo'}),
112
- element(tag_name: 'span', attributes: {class: 'foo'}),
113
- element(tag_name: 'div', attributes: {class: 'foo'})
114
- ]
115
-
116
- expect_all(:xpath, './/*[@class="foo"]').and_return(elements)
117
-
118
- selector = {
119
- xpath: './/*[@class="foo"]',
120
- tag_name: 'span'
121
- }
122
-
123
- expect(locate_one(selector).tag_name).to eq 'span'
124
- end
125
-
126
- it 'handles custom attributes' do
127
- elements = [
128
- element(tag_name: 'div', attributes: {custom_attribute: 'foo'}),
129
- element(tag_name: 'span', attributes: {custom_attribute: 'foo'}),
130
- element(tag_name: 'div', attributes: {custom_attribute: 'foo'})
131
- ]
132
-
133
- expect_one(:xpath, ".//*[local-name()='span'][@custom-attribute='foo']").and_return(elements[1])
134
-
135
- selector = {
136
- custom_attribute: 'foo',
137
- tag_name: 'span'
138
- }
139
-
140
- expect(locate_one(selector).tag_name).to eq 'span'
141
- end
142
- end
143
-
144
- describe 'with special cased selectors' do
145
- it 'normalizes space for :text' do
146
- expect_one :xpath, ".//*[local-name()='div'][normalize-space()='foo']"
147
- locate_one tag_name: 'div',
148
- text: 'foo'
149
- end
150
-
151
- # TODO: This is deprecated by 'text_string'
152
- it "handles 'text' key when it's a string" do
153
- expect_one :xpath, ".//*[local-name()='div'][normalize-space()='foo']"
154
- locate_one tag_name: 'div',
155
- 'text' => 'foo'
156
- end
157
-
158
- it 'translates :caption to :text' do
159
- expect_one :xpath, ".//*[local-name()='div'][normalize-space()='foo']"
160
-
161
- locate_one tag_name: 'div',
162
- caption: 'foo'
163
- end
164
-
165
- it 'handles data-* attributes' do
166
- expect_one :xpath, ".//*[local-name()='div'][@data-name='foo']"
167
-
168
- locate_one tag_name: 'div',
169
- data_name: 'foo'
170
- end
171
-
172
- it 'handles aria-* attributes' do
173
- expect_one :xpath, ".//*[local-name()='div'][@aria-label='foo']"
174
-
175
- locate_one tag_name: 'div',
176
- aria_label: 'foo'
177
- end
178
-
179
- it "doesn't modify attribute name when the attribute key is a string" do
180
- expect_one :xpath, ".//*[local-name()='div'][@_ngcontent-c24]"
181
-
182
- locate_one tag_name: 'div',
183
- '_ngcontent-c24' => true
184
- end
185
-
186
- it 'normalizes space for the :href attribute' do
187
- expect_one :xpath, ".//*[local-name()='a'][normalize-space(@href)='foo']"
188
-
189
- selector = {
190
- tag_name: 'a',
191
- href: 'foo'
192
- }
193
-
194
- locate_one selector, Watir::Anchor.attributes
195
- end
196
-
197
- it 'wraps :type attribute with translate() for upper case values' do
198
- translated_type = "translate(@type,'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \
199
- "'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ')"
200
- expect_one :xpath, ".//*[local-name()='input'][#{translated_type}='file']"
201
-
202
- selector = [
203
- :tag_name, 'input',
204
- :type, 'file'
205
- ]
206
-
207
- locate_one selector, Watir::Input.attributes
208
- end
209
-
210
- it "uses the corresponding <label>'s @for attribute or parent::label when locating by label" do
211
- translated_type = "translate(@type,'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \
212
- "'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ')"
213
- xpath = ".//*[local-name()='input'][@id=//label[normalize-space()='foo']/@for " \
214
- "or parent::label[normalize-space()='foo']][#{translated_type}='text']"
215
- expect_one :xpath, xpath
216
-
217
- selector = [
218
- :tag_name, 'input',
219
- :type, 'text',
220
- :label, 'foo'
221
- ]
222
-
223
- locate_one selector, Watir::Input.attributes
224
- end
225
-
226
- it 'uses label attribute if it is valid for element' do
227
- expect_one :xpath, ".//*[local-name()='option'][@label='foo']"
228
-
229
- selector = {tag_name: 'option', label: 'foo'}
230
- locate_one selector, Watir::Option.attributes
231
- end
232
-
233
- it 'translates ruby attribute names to content attribute names' do
234
- expect_one :xpath, ".//*[local-name()='meta'][@http-equiv='foo']"
235
-
236
- selector = {
237
- tag_name: 'meta',
238
- http_equiv: 'foo'
239
- }
240
-
241
- locate_one selector, Watir::Meta.attributes
242
- end
243
- end
244
-
245
- describe 'with simple regexp selectors' do
246
- it 'handles selector with tag name and a simple regexp attribute' do
247
- element = element(tag_name: 'div', attributes: {class: 'foob'})
248
-
249
- expect_one(:xpath, ".//*[local-name()='div'][contains(@class, 'oob')]").and_return(element)
250
-
251
- expect(locate_one(tag_name: 'div', class: /oob/)).to eq element
252
- end
253
-
254
- it 'handles :tag_name, :index and a simple regexp attribute' do
255
- element = element(tag_name: 'div', attributes: {class: 'foo'})
256
-
257
- expect_one(:xpath, "(.//*[local-name()='div'][contains(@class, 'foo')])[2]").and_return(element)
258
-
259
- selector = {
260
- tag_name: 'div',
261
- class: /foo/,
262
- index: 1
263
- }
264
-
265
- expect(locate_one(selector)).to eq element
266
- end
267
-
268
- it 'handles :xpath and :index selectors' do
269
- elements = [
270
- element(tag_name: 'div', attributes: {class: 'foo'}),
271
- element(tag_name: 'div', attributes: {class: 'foo'})
272
- ]
273
-
274
- expect_all(:xpath, './/div[@class="foo"]').and_return(elements)
275
-
276
- selector = {
277
- xpath: './/div[@class="foo"]',
278
- index: 1
279
- }
280
-
281
- expect(locate_one(selector)).to eq elements[1]
282
- end
283
-
284
- it 'handles :css and :index selectors' do
285
- elements = [
286
- element(tag_name: 'div', attributes: {class: 'foo'}),
287
- element(tag_name: 'div', attributes: {class: 'foo'})
288
- ]
289
-
290
- expect_all(:css, 'div[class="foo"]').and_return(elements)
291
-
292
- selector = {
293
- css: 'div[class="foo"]',
294
- index: 1
295
- }
296
-
297
- expect(locate_one(selector)).to eq elements[1]
298
- end
299
-
300
- it 'handles mix of string and regexp attributes' do
301
- element = element(tag_name: 'div', attributes: {dir: 'foo', title: 'baz'})
6
+ describe '#locate' do
7
+ context 'when XPath can be built to represent entire selector' do
8
+ it 'locates without using match' do
9
+ @locator = {xpath: './/div'}
302
10
 
303
- expect_one(:xpath, ".//*[local-name()='div'][@dir='foo' and contains(@title, 'baz')]").and_return(element)
11
+ expect_one(*@locator.to_a.flatten).and_return(el)
12
+ expect(element_matcher).not_to receive(:match)
304
13
 
305
- selector = {
306
- tag_name: 'div',
307
- dir: 'foo',
308
- title: /baz/
309
- }
310
-
311
- expect(locate_one(selector)).to eq element
312
- end
313
-
314
- it 'handles data-* attributes with regexp' do
315
- element = element(tag_name: 'div', attributes: {'data-automation-id': 'bar'})
316
-
317
- expect_one(:xpath, ".//*[local-name()='div'][contains(@data-automation-id, 'bar')]").and_return(element)
318
-
319
- selector = {
320
- tag_name: 'div',
321
- data_automation_id: /bar/
322
- }
323
-
324
- expect(locate_one(selector)).to eq element
14
+ expect(locate_one).to eq el
325
15
  end
326
16
 
327
- # TODO: I can not figure out how to mock this out properly with the new implementation
328
- xit 'handles :label => /regexp/ selector' do
329
- label_elements = [
330
- element(tag_name: 'label', text: 'foo', attributes: {'for' => 'bar'}),
331
- element(tag_name: 'label', text: 'foob', attributes: {'for' => 'baz'})
332
- ]
333
- div_elements = [element(tag_name: 'div')]
17
+ it 'returns nil if not found' do
18
+ @locator = {xpath: './/div'}
334
19
 
335
- expect_all(:tag_name, 'label').ordered.and_return(label_elements)
336
- expect_one(:xpath, ".//*[local-name()='div'][@id='baz']").ordered.and_return(div_elements.first)
337
-
338
- allow(browser).to receive(:ensure_context).and_return(nil)
339
- allow(browser).to receive(:execute_script).and_return('foo', 'foob')
340
-
341
- expect(locate_one(tag_name: 'div', label: /oob/)).to eq div_elements.first
342
- end
343
-
344
- # TODO: I can not figure out how to mock this out properly with the new implementation
345
- xit 'returns nil when no label matching the regexp is found' do
346
- expect_all(:tag_name, 'label').and_return([])
347
- expect(locate_one(tag_name: 'div', label: /foo/)).to be_nil
348
- end
349
-
350
- it 'relocates an element that goes stale during filtering' do
351
- element1 = element(tag_name: 'div', attributes: {class: 'foo'})
352
- element2 = element(tag_name: 'div', attributes: {class: 'foob'})
353
-
354
- elements1 = [element1.clone, element2.clone]
355
- elements2 = [element1.clone, element2.clone]
356
-
357
- allow(elements1.first).to receive(:attribute).and_raise(Selenium::WebDriver::Error::StaleElementReferenceError)
358
-
359
- expect_all(:xpath, ".//*[contains(@class, 'foo')]").and_return(elements1, elements2)
360
-
361
- expect(locate_one(class: /foo$/)).to eq elements2[0]
362
- end
363
-
364
- it 'raises error if too many attempts to relocate a stale element during filtering' do
365
- element1 = element(tag_name: 'div', attributes: {class: 'foo'})
366
- element2 = element(tag_name: 'div', attributes: {class: 'foob'})
367
-
368
- elements1 = [element1.clone, element2.clone]
369
- elements2 = [element1.clone, element2.clone]
370
- elements3 = [element1.clone, element2.clone]
371
-
372
- allow(elements1.first).to receive(:attribute).and_raise(Selenium::WebDriver::Error::StaleElementReferenceError)
373
- allow(elements2.first).to receive(:attribute).and_raise(Selenium::WebDriver::Error::StaleElementReferenceError)
374
- allow(elements3.first).to receive(:attribute).and_raise(Selenium::WebDriver::Error::StaleElementReferenceError)
375
-
376
- expect_all(:xpath, ".//*[contains(@class, 'foo')]").and_return(elements1, elements2, elements3)
377
-
378
- msg = 'Unable to locate element from {:class=>/foo$/} due to changing page'
379
- expect { locate_one(class: /foo$/) }.to raise_exception(Watir::Exception::LocatorException, msg)
20
+ expect_one(*@locator.to_a.flatten).and_raise(Selenium::WebDriver::Error::NoSuchElementError)
21
+ expect(locate_one).to eq nil
380
22
  end
381
23
  end
382
24
 
383
- it "returns nil if found element didn't match the selector tag_name" do
384
- expect_all(:xpath, '//div').and_return([element(tag_name: 'div')])
385
-
386
- selector = {
387
- tag_name: 'input',
388
- xpath: '//div'
389
- }
25
+ context 'when SelectorBuilder result has additional locators to match' do
26
+ it 'locates using match' do
27
+ @locator = {xpath: './/div', id: 'foo'}
390
28
 
391
- expect(locate_one(selector, Watir::Input.attributes)).to be_nil
392
- end
29
+ expect_all(*@locator.to_a.first.flatten).and_return([el])
30
+ expect(element_matcher).to receive(:match).and_return(el)
393
31
 
394
- it 'allows tag_name values to be Symbols when combined with xpath' do
395
- expect_all(:xpath, '//div').and_return([element(tag_name: 'div')])
396
-
397
- selector = {
398
- tag_name: :input,
399
- xpath: '//div'
400
- }
401
-
402
- expect(locate_one(selector, Watir::Input.attributes)).to be_nil
403
- end
404
-
405
- describe 'errors' do
406
- it 'raises a TypeError if :index is not a Integer' do
407
- msg = /expected one of \[(Integer|Fixnum)\], got "bar":String/
408
- expect { locate_one(tag_name: 'div', index: 'bar') }.to raise_error TypeError, msg
32
+ expect(locate_one).to eq el
409
33
  end
410
34
 
411
- it 'raises a TypeError if selector value is not a String, Regexp or Boolean' do
412
- msg = /expected one of \[String, Regexp, TrueClass, FalseClass\], got 123:(Integer|Fixnum)/
413
- expect { locate_one(foo: 123) }.to raise_error TypeError, msg
414
- end
35
+ it 'relocates if element goes stale' do
36
+ @locator = {xpath: './/div', id: 'foo'}
415
37
 
416
- it 'raises a Error if selector key is not a String or a Symbol' do
417
- msg = /Unable to build XPath using 7:(Integer|Fixnum)/
418
- expect { locate_one(7 => 'bad') }.to raise_exception(Watir::Exception::Error, msg)
419
- end
38
+ expect_all(*@locator.to_a.first.flatten).exactly(2).times.and_return([el])
39
+ stale_exception = Selenium::WebDriver::Error::StaleElementReferenceError
40
+ expect(element_matcher).to receive(:match).and_raise(stale_exception)
41
+ expect(element_matcher).to receive(:match).and_return(el)
420
42
 
421
- it 'raises a Error if selector key is not a String or a Symbol' do
422
- msg = /Unable to build XPath using 7:(Integer|Fixnum)/
423
- expect { locate_one(7 => 'bad') }.to raise_exception(Watir::Exception::Error, msg)
43
+ expect(locate_one).to eq el
424
44
  end
425
45
 
426
- it 'raises an Error if unable to build selector' do
427
- module Foo
428
- class SelectorBuilder < Watir::Locators::Element::SelectorBuilder
429
- def build(*_args)
430
- nil
431
- end
432
- end
433
- end
46
+ it 'Raises Exception if element continues to go stale' do
47
+ @locator = {xpath: './/div', id: 'foo'}
434
48
 
435
- selector = {name: 'foo'}
436
- element_validator = Watir::Locators::Element::Validator.new
437
- selector_builder = Foo::SelectorBuilder.new(Watir::HTMLElement.attributes)
438
- locator = Watir::Locators::Element::Locator.new(browser, selector, selector_builder, element_validator)
49
+ expect_all(*@locator.to_a.first.flatten).exactly(3).times.and_return([el])
50
+ stale_exception = Selenium::WebDriver::Error::StaleElementReferenceError
51
+ expect(element_matcher).to receive(:match).and_raise(stale_exception).exactly(3).times
439
52
 
440
- msg = 'Foo::SelectorBuilder was unable to build selector from {:name=>"foo"}'
441
- expect { locator.locate }.to raise_exception(Watir::Exception::LocatorException, msg)
442
- end
443
-
444
- it 'raises an Error if unable to build values to match' do
445
- module Foo
446
- class SelectorBuilder < Watir::Locators::Element::SelectorBuilder
447
- def build(*_args)
448
- {}
449
- end
450
- end
451
- end
452
-
453
- selector = {name: 'foo'}
454
- element_validator = Watir::Locators::Element::Validator.new
455
- selector_builder = Foo::SelectorBuilder.new(Watir::HTMLElement.attributes)
456
- locator = Watir::Locators::Element::Locator.new(browser, selector, selector_builder, element_validator)
457
-
458
- msg = 'Foo::SelectorBuilder was unable to build selector from {:name=>"foo"}'
459
- expect { locator.locate }.to raise_exception(Watir::Exception::LocatorException, msg)
53
+ msg = 'Unable to locate element from {:xpath=>".//div", :id=>"foo"} due to changing page'
54
+ expect { locate_one }.to raise_exception Watir::Exception::LocatorException, msg
460
55
  end
461
56
  end
462
57
  end
463
58
 
464
- describe 'finds several elements' do
465
- describe 'by delegating to Selenium' do
466
- SELENIUM_SELECTORS.each do |loc|
467
- it "delegates to Selenium's #{loc} locator" do
468
- expect_all(loc, 'bar').and_return([element(tag_name: 'div')])
469
- match = %i[link link_text partial_link_text].include?(loc) ? :to : :to_not
470
- msg = /:#{loc} locator is deprecated\. Use :visible_text instead/
471
- expect { locate_all loc => 'bar' }.send(match, output(msg).to_stdout_from_any_process)
472
- end
473
- end
474
- end
475
-
476
- describe 'with an empty selector' do
477
- it 'finds all when an empty selctor is given' do
478
- expect_all :xpath, './/*'
479
- locate_all({})
480
- end
481
- end
482
-
483
- describe 'with selectors not supported by Selenium' do
484
- it 'handles selector with tag name and a single attribute' do
485
- expect_all :xpath, ".//*[local-name()='div'][@dir='foo']"
486
- locate_all tag_name: 'div',
487
- dir: 'foo'
488
- end
489
-
490
- it 'handles selector with tag name and multiple attributes' do
491
- expect_all :xpath, ".//*[local-name()='div'][@dir='foo' and @title='bar']"
492
- locate_all [:tag_name, 'div',
493
- :dir, 'foo',
494
- :title, 'bar']
495
- end
496
-
497
- it 'handles selector with class attribute presence' do
498
- expect_all :xpath, './/*[@class]'
499
-
500
- locate_all class: true
501
- end
502
-
503
- it 'handles selector with multiple classes in array' do
504
- xpath = ".//*[contains(concat(' ', @class, ' '), ' a ') and contains(concat(' ', @class, ' '), ' b ')]"
505
- expect_all :xpath, xpath
59
+ describe '#locate_all' do
60
+ it 'locates using match' do
61
+ @locator = {xpath: './/div', id: 'foo'}
506
62
 
507
- locate_all class: %w[a b]
508
- end
509
-
510
- it 'handles selector with multiple classes in string' do
511
- expect_all :xpath, ".//*[contains(concat(' ', @class, ' '), ' a b ')]"
63
+ expect_all(*@locator.to_a.first.flatten).and_return([el])
64
+ expect(element_matcher).to receive(:match).and_return([el])
512
65
 
513
- expect { locate_all class: 'a b' }.to have_deprecated_class_array
514
- end
66
+ expect(locate_all).to eq [el]
515
67
  end
516
68
 
517
- describe 'with regexp selectors' do
518
- it 'handles selector with tag name and a single regexp attribute' do
519
- elements = [
520
- element(tag_name: 'div', attributes: {class: 'foob'}),
521
- element(tag_name: 'div', attributes: {class: 'doob'}),
522
- element(tag_name: 'div', attributes: {class: 'noob'})
523
- ]
524
-
525
- expect_all(:xpath, ".//*[local-name()='div'][contains(@class, 'oob')]").and_return(elements)
526
- expect(locate_all(tag_name: 'div', class: /oob/)).to eq elements.last(3)
527
- end
528
-
529
- it 'handles mix of string and regexp attributes' do
530
- elements = [
531
- element(tag_name: 'div', attributes: {dir: 'foo', title: 'baz'}),
532
- element(tag_name: 'div', attributes: {dir: 'foo', title: 'bazt'})
533
- ]
69
+ it 'raises LocatorException if element continues to go stale' do
70
+ @locator = {xpath: './/div', id: 'foo'}
534
71
 
535
- expect_all(:xpath, ".//*[local-name()='div'][@dir='foo' and contains(@title, 'baz')]").and_return(elements)
72
+ expect_all(*@locator.to_a.first.flatten).exactly(3).times.and_return([el])
73
+ stale_exception = Selenium::WebDriver::Error::StaleElementReferenceError
74
+ expect(element_matcher).to receive(:match).and_raise(stale_exception).exactly(3).times
536
75
 
537
- selector = {
538
- tag_name: 'div',
539
- dir: 'foo',
540
- title: /baz/
541
- }
542
-
543
- expect(locate_all(selector)).to eq elements.last(2)
544
- end
76
+ msg = 'Unable to locate element collection from {:xpath=>".//div", :id=>"foo"} due to changing page'
77
+ expect { locate_all }.to raise_exception Watir::Exception::LocatorException, msg
545
78
  end
546
79
 
547
- it 'with :index' do
548
- element = element(tag_name: 'div')
549
-
550
- expect_one(:xpath, "(.//*[local-name()='div'][@dir='foo'])[2]").and_return(element)
551
-
552
- selector = {
553
- tag_name: 'div',
554
- dir: 'foo',
555
- index: 1
556
- }
557
-
558
- expect(locate_one(selector)).to eq element
559
- end
560
-
561
- context 'and xpath' do
562
- it 'converts a leading run of regexp literals to a contains() expression' do
563
- elements = [
564
- element(tag_name: 'div', attributes: {foo: 'foo'}),
565
- element(tag_name: 'div', attributes: {foo: 'foob'}),
566
- element(tag_name: 'div', attributes: {foo: 'bar'})
567
- ]
568
-
569
- expect_all(:xpath, ".//*[local-name()='div'][contains(@foo, 'fo') and contains(@foo, 'b')]")
570
- .and_return(elements.first(2))
571
-
572
- expect(locate_one(tag_name: 'div', foo: /fo.b$/)).to eq elements[1]
573
- end
574
-
575
- it 'converts a trailing run of regexp literals to a contains() expression' do
576
- elements = [
577
- element(tag_name: 'div', attributes: {foo: 'foo'}),
578
- element(tag_name: 'div', attributes: {foo: 'foob'})
579
- ]
580
-
581
- expect_all(:xpath, ".//*[local-name()='div'][contains(@foo, 'fo') and contains(@foo, 'b')]")
582
- .and_return(elements.last(1))
583
-
584
- expect(locate_one(tag_name: 'div', foo: /^fo.b/)).to eq elements[1]
585
- end
586
-
587
- it 'converts a leading and a trailing run of regexp literals to a contains() expression' do
588
- elements = [
589
- element(tag_name: 'div', attributes: {foo: 'foo'}),
590
- element(tag_name: 'div', attributes: {foo: 'foob'})
591
- ]
592
-
593
- expect_all(:xpath, ".//*[local-name()='div'][contains(@foo, 'fo') and contains(@foo, 'b')]")
594
- .and_return(elements.last(1))
595
-
596
- expect(locate_one(tag_name: 'div', foo: /fo.b/)).to eq elements[1]
597
- end
598
-
599
- it 'does not try to convert case insensitive expressions' do
600
- element = element(tag_name: 'div', attributes: {foo: 'foo'})
601
-
602
- xpath = ".//*[local-name()='div'][contains(translate" \
603
- "(@foo,'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \
604
- "'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ'), 'foob')]"
605
- expect_one(:xpath, xpath).and_return(element)
606
-
607
- expect(locate_one(tag_name: 'div', foo: /FOOB/i)).to eq element
608
- end
609
-
610
- it "does not try to convert expressions containing '|'" do
611
- elements = [
612
- element(tag_name: 'div', attributes: {foo: 'foo'}),
613
- element(tag_name: 'div', attributes: {foo: 'foob'})
614
- ]
615
-
616
- expect_all(:xpath, ".//*[local-name()='div'][@foo]").and_return(elements.last(1))
617
-
618
- expect(locate_one(tag_name: 'div', foo: /x|b/)).to eq elements[1]
619
- end
620
- end
621
-
622
- describe 'errors' do
623
- it 'raises ArgumentError if :index is given' do
624
- expect { locate_all(tag_name: 'div', index: 1) }.to \
625
- raise_error(ArgumentError, "can't locate all elements by :index")
626
- end
80
+ it 'raises Argument error if using index key' do
81
+ expect { locate_all(index: 2) }.to raise_exception(ArgumentError, "can't locate all elements by :index")
627
82
  end
628
83
  end
629
84
  end