watir 6.14.0 → 6.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +24 -6
  3. data/CHANGES.md +10 -0
  4. data/Gemfile +0 -2
  5. data/README.md +1 -1
  6. data/Rakefile +2 -2
  7. data/lib/watir.rb +2 -3
  8. data/lib/watir/adjacent.rb +2 -2
  9. data/lib/watir/alert.rb +5 -9
  10. data/lib/watir/attribute_helper.rb +2 -3
  11. data/lib/watir/browser.rb +5 -17
  12. data/lib/watir/capabilities.rb +11 -0
  13. data/lib/watir/cell_container.rb +2 -2
  14. data/lib/watir/container.rb +7 -8
  15. data/lib/watir/cookies.rb +2 -12
  16. data/lib/watir/element_collection.rb +2 -2
  17. data/lib/watir/elements/button.rb +2 -11
  18. data/lib/watir/elements/element.rb +43 -25
  19. data/lib/watir/elements/hidden.rb +1 -1
  20. data/lib/watir/elements/iframe.rb +7 -5
  21. data/lib/watir/elements/option.rb +2 -13
  22. data/lib/watir/elements/select.rb +6 -22
  23. data/lib/watir/elements/table.rb +2 -2
  24. data/lib/watir/exception.rb +1 -2
  25. data/lib/watir/generator/base/idl_sorter.rb +1 -1
  26. data/lib/watir/generator/base/spec_extractor.rb +2 -2
  27. data/lib/watir/generator/base/visitor.rb +1 -1
  28. data/lib/watir/generator/html/generator.rb +0 -1
  29. data/lib/watir/generator/html/spec_extractor.rb +1 -1
  30. data/lib/watir/generator/html/visitor.rb +1 -1
  31. data/lib/watir/generator/svg/spec_extractor.rb +1 -1
  32. data/lib/watir/generator/svg/visitor.rb +1 -1
  33. data/lib/watir/js_execution.rb +11 -0
  34. data/lib/watir/js_snippets.rb +1 -1
  35. data/lib/watir/js_snippets/elementObscured.js +14 -0
  36. data/lib/watir/js_snippets/selectedText.js +17 -0
  37. data/lib/watir/legacy_wait.rb +5 -5
  38. data/lib/watir/locators.rb +16 -5
  39. data/lib/watir/locators/anchor/selector_builder.rb +38 -0
  40. data/lib/watir/locators/button/locator.rb +14 -12
  41. data/lib/watir/locators/button/selector_builder.rb +0 -22
  42. data/lib/watir/locators/button/selector_builder/xpath.rb +67 -12
  43. data/lib/watir/locators/button/validator.rb +2 -1
  44. data/lib/watir/locators/cell/selector_builder.rb +0 -14
  45. data/lib/watir/locators/cell/selector_builder/xpath.rb +21 -0
  46. data/lib/watir/locators/element/locator.rb +60 -153
  47. data/lib/watir/locators/element/selector_builder.rb +103 -84
  48. data/lib/watir/locators/element/selector_builder/regexp_disassembler.rb +66 -0
  49. data/lib/watir/locators/element/selector_builder/xpath.rb +195 -82
  50. data/lib/watir/locators/element/selector_builder/xpath_support.rb +27 -0
  51. data/lib/watir/locators/element/validator.rb +2 -9
  52. data/lib/watir/locators/row/selector_builder.rb +5 -22
  53. data/lib/watir/locators/row/selector_builder/xpath.rb +53 -0
  54. data/lib/watir/locators/text_area/selector_builder.rb +1 -1
  55. data/lib/watir/locators/text_area/selector_builder/xpath.rb +19 -0
  56. data/lib/watir/locators/text_field/locator.rb +11 -8
  57. data/lib/watir/locators/text_field/selector_builder.rb +0 -23
  58. data/lib/watir/locators/text_field/selector_builder/xpath.rb +33 -4
  59. data/lib/watir/locators/text_field/validator.rb +4 -4
  60. data/lib/watir/radio_set.rb +5 -5
  61. data/lib/watir/row_container.rb +2 -2
  62. data/lib/watir/user_editable.rb +2 -2
  63. data/lib/watir/version.rb +1 -1
  64. data/lib/watir/wait.rb +24 -37
  65. data/lib/watir/window.rb +11 -8
  66. data/lib/watirspec/remote_server.rb +3 -1
  67. data/spec/locator_spec_helper.rb +1 -1
  68. data/spec/spec_helper.rb +25 -1
  69. data/spec/unit/anchor_locator_spec.rb +68 -0
  70. data/spec/unit/capabilities_spec.rb +27 -0
  71. data/spec/unit/element_locator_spec.rb +184 -101
  72. data/spec/unit/logger_spec.rb +5 -0
  73. data/spec/watirspec/adjacent_spec.rb +34 -34
  74. data/spec/watirspec/after_hooks_spec.rb +78 -35
  75. data/spec/watirspec/alert_spec.rb +10 -0
  76. data/spec/watirspec/browser_spec.rb +27 -1
  77. data/spec/watirspec/element_hidden_spec.rb +6 -0
  78. data/spec/watirspec/elements/button_spec.rb +5 -11
  79. data/spec/watirspec/elements/buttons_spec.rb +1 -1
  80. data/spec/watirspec/elements/checkbox_spec.rb +2 -15
  81. data/spec/watirspec/elements/date_time_field_spec.rb +6 -1
  82. data/spec/watirspec/elements/dd_spec.rb +0 -17
  83. data/spec/watirspec/elements/del_spec.rb +0 -14
  84. data/spec/watirspec/elements/div_spec.rb +0 -18
  85. data/spec/watirspec/elements/dl_spec.rb +0 -17
  86. data/spec/watirspec/elements/dt_spec.rb +0 -17
  87. data/spec/watirspec/elements/element_spec.rb +177 -17
  88. data/spec/watirspec/elements/elements_spec.rb +7 -6
  89. data/spec/watirspec/elements/em_spec.rb +0 -13
  90. data/spec/watirspec/elements/filefield_spec.rb +0 -11
  91. data/spec/watirspec/elements/form_spec.rb +6 -0
  92. data/spec/watirspec/elements/hn_spec.rb +0 -14
  93. data/spec/watirspec/elements/iframe_spec.rb +15 -0
  94. data/spec/watirspec/elements/ins_spec.rb +0 -14
  95. data/spec/watirspec/elements/labels_spec.rb +1 -1
  96. data/spec/watirspec/elements/li_spec.rb +0 -14
  97. data/spec/watirspec/elements/link_spec.rb +22 -14
  98. data/spec/watirspec/elements/links_spec.rb +13 -0
  99. data/spec/watirspec/elements/list_spec.rb +15 -0
  100. data/spec/watirspec/elements/ol_spec.rb +0 -14
  101. data/spec/watirspec/elements/option_spec.rb +0 -10
  102. data/spec/watirspec/elements/p_spec.rb +0 -14
  103. data/spec/watirspec/elements/pre_spec.rb +0 -14
  104. data/spec/watirspec/elements/radio_spec.rb +0 -14
  105. data/spec/watirspec/elements/select_list_spec.rb +0 -10
  106. data/spec/watirspec/elements/span_spec.rb +4 -15
  107. data/spec/watirspec/elements/strong_spec.rb +4 -15
  108. data/spec/watirspec/elements/table_nesting_spec.rb +1 -1
  109. data/spec/watirspec/elements/table_spec.rb +7 -0
  110. data/spec/watirspec/elements/text_field_spec.rb +10 -2
  111. data/spec/watirspec/elements/text_fields_spec.rb +1 -1
  112. data/spec/watirspec/elements/tr_spec.rb +1 -1
  113. data/spec/watirspec/elements/ul_spec.rb +0 -14
  114. data/spec/watirspec/html/closeable.html +8 -0
  115. data/spec/watirspec/html/forms_with_input_elements.html +28 -23
  116. data/spec/watirspec/html/nested_elements.html +9 -9
  117. data/spec/watirspec/html/obscured.html +34 -0
  118. data/spec/watirspec/html/tables.html +13 -13
  119. data/spec/watirspec/radio_set_spec.rb +5 -0
  120. data/spec/watirspec/selector_builder/button_spec.rb +254 -0
  121. data/spec/watirspec/selector_builder/cell_spec.rb +93 -0
  122. data/spec/watirspec/selector_builder/element_spec.rb +639 -0
  123. data/spec/watirspec/selector_builder/row_spec.rb +150 -0
  124. data/spec/watirspec/selector_builder/text_spec.rb +170 -0
  125. data/spec/watirspec/support/rspec_matchers.rb +6 -1
  126. data/spec/watirspec/user_editable_spec.rb +4 -0
  127. data/spec/watirspec/wait_spec.rb +65 -14
  128. data/spec/watirspec/window_switching_spec.rb +54 -1
  129. data/spec/watirspec_helper.rb +2 -0
  130. data/watir.gemspec +7 -1
  131. metadata +86 -8
  132. data/lib/watir/locators/text_area/locator.rb +0 -13
  133. data/lib/watir/xpath_support.rb +0 -18
@@ -6,7 +6,7 @@ module Watir
6
6
 
7
7
  def execute_js(function_name, *arguments)
8
8
  file = File.expand_path("../js_snippets/#{function_name}.js", __FILE__)
9
- raise Watir::Error, "Can not excute script as #{function_name}.js does not exist" unless File.exist?(file)
9
+ raise Exception::Error, "Can not excute script as #{function_name}.js does not exist" unless File.exist?(file)
10
10
 
11
11
  js = File.read(file)
12
12
  script = "return (#{js}).apply(null, arguments)"
@@ -0,0 +1,14 @@
1
+ // Original Author: Florent B.
2
+ // Source: https://stackoverflow.com/a/45244889/1200545
3
+ function() {
4
+ var elem = arguments[0],
5
+ box = elem.getBoundingClientRect(),
6
+ cx = box.left + box.width / 2,
7
+ cy = box.top + box.height / 2,
8
+ e = document.elementFromPoint(cx, cy);
9
+ for (; e; e = e.parentElement) {
10
+ if (e === elem)
11
+ return false;
12
+ }
13
+ return true;
14
+ }
@@ -0,0 +1,17 @@
1
+ // Code from https://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text
2
+
3
+ function() {
4
+ var text = "";
5
+ var activeEl = document.activeElement;
6
+ var activeElTagName = activeEl ? activeEl.tagName.toLowerCase() : null;
7
+ if (
8
+ (activeElTagName == "textarea") || (activeElTagName == "input" &&
9
+ /^(?:text|search|password|tel|url)$/i.test(activeEl.type)) &&
10
+ (typeof activeEl.selectionStart == "number")
11
+ ) {
12
+ text = activeEl.value.slice(activeEl.selectionStart, activeEl.selectionEnd);
13
+ } else if (window.getSelection) {
14
+ text = window.getSelection().toString();
15
+ }
16
+ return text;
17
+ }
@@ -16,7 +16,7 @@ module Watir
16
16
  def method_missing(method, *args, &block)
17
17
  return super unless @element.respond_to?(method)
18
18
 
19
- Watir::Wait.until(@timeout, @message) { wait_until }
19
+ Wait.until(@timeout, @message) { wait_until }
20
20
 
21
21
  @element.__send__(method, *args, &block)
22
22
  end
@@ -29,9 +29,9 @@ module Watir
29
29
 
30
30
  class WhenPresentDecorator < BaseDecorator
31
31
  def present?
32
- Watir::Wait.until(@timeout, @message) { wait_until }
32
+ Wait.until(@timeout, @message) { wait_until }
33
33
  true
34
- rescue Watir::Wait::TimeoutError
34
+ rescue Wait::TimeoutError
35
35
  false
36
36
  end
37
37
 
@@ -85,7 +85,7 @@ module Watir
85
85
  message = "waiting for #{selector_string} to become present"
86
86
 
87
87
  if block_given?
88
- Watir::Wait.until(timeout, message) { present? }
88
+ Wait.until(timeout, message) { present? }
89
89
  yield self
90
90
  else
91
91
  WhenPresentDecorator.new(self, timeout, message)
@@ -112,7 +112,7 @@ module Watir
112
112
  message = "waiting for #{selector_string} to become enabled"
113
113
 
114
114
  if block_given?
115
- Watir::Wait.until(timeout, message) { enabled? }
115
+ Wait.until(timeout, message) { enabled? }
116
116
  yield self
117
117
  else
118
118
  WhenEnabledDecorator.new(self, timeout, message)
@@ -1,8 +1,12 @@
1
1
  require 'watir/locators/element/locator'
2
2
  require 'watir/locators/element/selector_builder'
3
+ require 'watir/locators/element/selector_builder/xpath_support'
4
+ require 'watir/locators/element/selector_builder/regexp_disassembler'
3
5
  require 'watir/locators/element/selector_builder/xpath'
4
6
  require 'watir/locators/element/validator'
5
7
 
8
+ require 'watir/locators/anchor/selector_builder'
9
+
6
10
  require 'watir/locators/button/locator'
7
11
  require 'watir/locators/button/selector_builder'
8
12
  require 'watir/locators/button/selector_builder/xpath'
@@ -10,12 +14,14 @@ require 'watir/locators/button/validator'
10
14
 
11
15
  require 'watir/locators/cell/locator'
12
16
  require 'watir/locators/cell/selector_builder'
17
+ require 'watir/locators/cell/selector_builder/xpath'
13
18
 
14
19
  require 'watir/locators/row/locator'
15
20
  require 'watir/locators/row/selector_builder'
21
+ require 'watir/locators/row/selector_builder/xpath'
16
22
 
17
- require 'watir/locators/text_area/locator'
18
23
  require 'watir/locators/text_area/selector_builder'
24
+ require 'watir/locators/text_area/selector_builder/xpath'
19
25
 
20
26
  require 'watir/locators/text_field/locator'
21
27
  require 'watir/locators/text_field/selector_builder'
@@ -29,21 +35,21 @@ module Watir
29
35
  class_from_string("#{browser.locator_namespace}::#{element_class_name}::Locator") ||
30
36
  class_from_string("Watir::Locators::#{element_class_name}::Locator") ||
31
37
  class_from_string("#{browser.locator_namespace}::Element::Locator") ||
32
- Watir::Locators::Element::Locator
38
+ Locators::Element::Locator
33
39
  end
34
40
 
35
41
  def element_validator_class
36
42
  class_from_string("#{browser.locator_namespace}::#{element_class_name}::Validator") ||
37
43
  class_from_string("Watir::Locators::#{element_class_name}::Validator") ||
38
44
  class_from_string("#{browser.locator_namespace}::Element::Validator") ||
39
- Watir::Locators::Element::Validator
45
+ Locators::Element::Validator
40
46
  end
41
47
 
42
48
  def selector_builder_class
43
49
  class_from_string("#{browser.locator_namespace}::#{element_class_name}::SelectorBuilder") ||
44
50
  class_from_string("Watir::Locators::#{element_class_name}::SelectorBuilder") ||
45
51
  class_from_string("#{browser.locator_namespace}::Element::SelectorBuilder") ||
46
- Watir::Locators::Element::SelectorBuilder
52
+ Locators::Element::SelectorBuilder
47
53
  end
48
54
 
49
55
  def class_from_string(string)
@@ -57,8 +63,13 @@ module Watir
57
63
  end
58
64
 
59
65
  def build_locator
66
+ selector_builder = if element_class == Watir::Row
67
+ scope_tag_name = @query_scope.selector[:tag_name]
68
+ selector_builder_class.new(element_class.attribute_list, scope_tag_name)
69
+ else
70
+ selector_builder_class.new(element_class.attribute_list)
71
+ end
60
72
  element_validator = element_validator_class.new
61
- selector_builder = selector_builder_class.new(@query_scope, @selector.dup, element_class.attribute_list)
62
73
  locator_class.new(@query_scope, @selector.dup, selector_builder, element_validator)
63
74
  end
64
75
  end
@@ -0,0 +1,38 @@
1
+ module Watir
2
+ module Locators
3
+ class Anchor
4
+ class SelectorBuilder < Element::SelectorBuilder
5
+ private
6
+
7
+ def build_wd_selector(selector)
8
+ build_link_text(selector) || build_partial_link_text(selector) || super
9
+ end
10
+
11
+ def build_link_text(selector)
12
+ return unless can_convert_to_link_text?(selector)
13
+
14
+ selector.delete(:tag_name)
15
+ {link_text: selector.delete(:visible_text)}
16
+ end
17
+
18
+ def can_convert_to_link_text?(selector)
19
+ selector.keys.sort == %i[tag_name visible_text] &&
20
+ selector[:visible_text].is_a?(String)
21
+ end
22
+
23
+ def build_partial_link_text(selector)
24
+ return unless convert_to_partial_link_text?(selector)
25
+
26
+ selector.delete(:tag_name)
27
+ {partial_link_text: selector.delete(:visible_text).source}
28
+ end
29
+
30
+ def convert_to_partial_link_text?(selector)
31
+ regex = selector[:visible_text]
32
+ selector.keys.sort == %i[tag_name visible_text] && !regex.casefold? &&
33
+ RegexpDisassembler.new(regex).substrings.first == regex.source
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -8,21 +8,23 @@ module Watir
8
8
  # force watir usage
9
9
  end
10
10
 
11
- def can_convert_regexp_to_contains?
12
- # regexp conversion won't work with the complex xpath selector
13
- false
14
- end
11
+ def matches_values?(element, values_to_match)
12
+ return super unless values_to_match.key?(:value)
13
+
14
+ copy = values_to_match.dup
15
+ value = copy.delete(:value)
15
16
 
16
- def matches_selector?(element, selector)
17
- if selector.key?(:value)
18
- copy = selector.dup
19
- value = copy.delete(:value)
17
+ everything_except_value = super(element, copy)
20
18
 
21
- super(element, copy) &&
22
- (fetch_value(element, :value) =~ /#{value}/ || fetch_value(element, :text) =~ /#{value}/)
23
- else
24
- super
19
+ matches_value = fetch_value(element, :value) =~ /#{value}/
20
+ matches_text = fetch_value(element, :text) =~ /#{value}/
21
+ if matches_text
22
+ Watir.logger.deprecate(':value locator key for finding button text',
23
+ 'use :text locator',
24
+ ids: [:value_button])
25
25
  end
26
+
27
+ everything_except_value && (matches_value || matches_text)
26
28
  end
27
29
  end
28
30
  end
@@ -2,28 +2,6 @@ module Watir
2
2
  module Locators
3
3
  class Button
4
4
  class SelectorBuilder < Element::SelectorBuilder
5
- def build_wd_selector(selectors)
6
- return if selectors.values.any? { |e| e.is_a? Regexp }
7
-
8
- selectors.delete(:tag_name) || raise('internal error: no tag_name?!')
9
-
10
- button_attr_exp = xpath_builder.attribute_expression(:button, selectors)
11
-
12
- xpath = './/button'
13
- xpath << "[#{button_attr_exp}]" unless button_attr_exp.empty?
14
-
15
- unless selectors[:type].eql? false
16
- selectors[:type] = Watir::Button::VALID_TYPES if [nil, true].include?(selectors[:type])
17
- input_attr_exp = xpath_builder.attribute_expression(:input, selectors)
18
-
19
- xpath << ' | .//input'
20
- xpath << "[#{input_attr_exp}]"
21
- end
22
-
23
- p build_wd_selector: xpath if $DEBUG
24
-
25
- [:xpath, xpath]
26
- end
27
5
  end
28
6
  end
29
7
  end
@@ -3,25 +3,80 @@ module Watir
3
3
  class Button
4
4
  class SelectorBuilder
5
5
  class XPath < Element::SelectorBuilder::XPath
6
- def lhs_for(building, key)
7
- if building == :input && key == :text
8
- '@value'
9
- else
10
- super
6
+ private
7
+
8
+ def tag_string
9
+ return super if @adjacent
10
+
11
+ # Selector builder ignores tag name and builds for both button elements and input elements of type button
12
+ @selector.delete(:tag_name)
13
+
14
+ type = @selector.delete(:type)
15
+ text = @selector.delete(:text)
16
+
17
+ string = "(#{button_string(text: text, type: type)})"
18
+ string << " or (#{input_string(text: text, type: type)})" unless type.eql?(false)
19
+ "[#{string}]"
20
+ end
21
+
22
+ def button_string(text: nil, type: nil)
23
+ string = process_attribute(:tag_name, 'button')
24
+ string << " and #{process_attribute(:text, text)}" unless text.nil?
25
+ string << " and #{input_types(type)}" unless type.nil?
26
+ string
27
+ end
28
+
29
+ def input_string(text: nil, type: nil)
30
+ string = process_attribute(:tag_name, 'input')
31
+ type = nil if type.eql?(true)
32
+ string << " and (#{input_types(type)})"
33
+ if text
34
+ string << " and #{process_attribute(:value, text)}"
35
+ @requires_matches.delete(:value)
11
36
  end
37
+ string
12
38
  end
13
39
 
14
- private
40
+ # value locator needs to match input value, button text or button value
41
+ def text_string
42
+ return super if @adjacent
43
+
44
+ # :text locator is already dealt with in #tag_name_string
45
+ value = @selector.delete(:value)
15
46
 
16
- def equal_pair(building, key, value)
17
- if building == :button && key == :value
18
- # :value should look for both node text and @value attribute
19
- text = XpathSupport.escape(value)
20
- "(text()=#{text} or @value=#{text})"
47
+ case value
48
+ when nil
49
+ ''
50
+ when Regexp
51
+ res = "[#{predicate_conversion(:text, value)} or #{predicate_conversion(:value, value)}]"
52
+ @requires_matches.delete(:text)
53
+ res
21
54
  else
22
- super
55
+ "[#{predicate_expression(:text, value)} or #{predicate_expression(:value, value)}]"
23
56
  end
24
57
  end
58
+
59
+ def predicate_conversion(key, regexp)
60
+ res = key == :text ? super(:contains_text, regexp) : super
61
+ @requires_matches[key] = @requires_matches.delete(:contains_text) if @requires_matches.key?(:contains_text)
62
+ res
63
+ end
64
+
65
+ def input_types(type = nil)
66
+ types = if type.eql?(nil)
67
+ Watir::Button::VALID_TYPES
68
+ elsif Watir::Button::VALID_TYPES.include?(type)
69
+ [type]
70
+ elsif type.eql?(true) || type.eql?(false)
71
+ [type]
72
+ else
73
+ msg = "Button Elements can not be located by input type: #{type}"
74
+ raise LocatorException, msg
75
+ end
76
+ types.map { |button_type|
77
+ predicate_expression(:type, button_type)
78
+ }.compact.join(' or ')
79
+ end
25
80
  end
26
81
  end
27
82
  end
@@ -2,9 +2,10 @@ module Watir
2
2
  module Locators
3
3
  class Button
4
4
  class Validator < Element::Validator
5
- def validate(element, _selector)
5
+ def validate(element, _tag_name)
6
6
  tag_name = element.tag_name.downcase
7
7
  return unless %w[input button].include?(tag_name)
8
+
8
9
  # TODO: - Verify this is desired behavior based on https://bugzilla.mozilla.org/show_bug.cgi?id=1290963
9
10
  return if tag_name == 'input' && !Watir::Button::VALID_TYPES.include?(element.attribute(:type).downcase)
10
11
 
@@ -2,20 +2,6 @@ module Watir
2
2
  module Locators
3
3
  class Cell
4
4
  class SelectorBuilder < Element::SelectorBuilder
5
- def build_wd_selector(selectors)
6
- return if selectors.values.any? { |e| e.is_a? Regexp }
7
-
8
- expressions = %w[./th ./td]
9
- attr_expr = xpath_builder.attribute_expression(nil, selectors)
10
-
11
- expressions.map! { |e| "#{e}[#{attr_expr}]" } unless attr_expr.empty?
12
-
13
- xpath = expressions.join(' | ')
14
-
15
- p build_wd_selector: xpath if $DEBUG
16
-
17
- [:xpath, xpath]
18
- end
19
5
  end
20
6
  end
21
7
  end
@@ -0,0 +1,21 @@
1
+ module Watir
2
+ module Locators
3
+ class Cell
4
+ class SelectorBuilder
5
+ class XPath < Element::SelectorBuilder::XPath
6
+ private
7
+
8
+ def start_string
9
+ @adjacent ? './' : './*'
10
+ end
11
+
12
+ def tag_string
13
+ return super if @adjacent
14
+
15
+ "[#{process_attribute(:tag_name, 'th')} or #{process_attribute(:tag_name, 'td')}]"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,6 +2,8 @@ module Watir
2
2
  module Locators
3
3
  class Element
4
4
  class Locator
5
+ include Exception
6
+
5
7
  attr_reader :selector_builder
6
8
  attr_reader :element_validator
7
9
 
@@ -14,19 +16,9 @@ module Watir
14
16
  xpath
15
17
  ].freeze
16
18
 
17
- # Regular expressions that can be reliably converted to xpath `contains`
18
- # expressions in order to optimize the locator.
19
- CONVERTABLE_REGEXP = /
20
- \A
21
- ([^\[\]\\^$.|?*+()]*) # leading literal characters
22
- [^|]*? # do not try to convert expressions with alternates
23
- ([^\[\]\\^$.|?*+()]*) # trailing literal characters
24
- \z
25
- /x
26
-
27
19
  def initialize(query_scope, selector, selector_builder, element_validator)
28
20
  @query_scope = query_scope # either element or browser
29
- @selector = selector.dup
21
+ @selector = selector
30
22
  @selector_builder = selector_builder
31
23
  @element_validator = element_validator
32
24
  end
@@ -46,11 +38,12 @@ module Watir
46
38
  private
47
39
 
48
40
  def using_selenium(filter = :first)
49
- tag = @selector[:tag_name].is_a?(::Symbol) ? @selector.delete(:tag_name).to_s : @selector.delete(:tag_name)
50
- return if @selector.size > 1
41
+ selector = @selector.dup
42
+ tag = selector[:tag_name].is_a?(::Symbol) ? selector.delete(:tag_name).to_s : selector.delete(:tag_name)
43
+ return if selector.size > 1
51
44
 
52
- how = @selector.keys.first || :tag_name
53
- what = @selector.values.first || tag
45
+ how = selector.keys.first || :tag_name
46
+ what = selector.values.first || tag
54
47
 
55
48
  return unless wd_supported?(how, what, tag)
56
49
 
@@ -58,27 +51,33 @@ module Watir
58
51
  end
59
52
 
60
53
  def using_watir(filter = :first)
61
- create_normalized_selector(filter)
62
- return unless @normalized_selector
63
-
64
- create_filter_selector
54
+ raise ArgumentError, "can't locate all elements by :index" if @selector.key?(:index) && filter == :all
65
55
 
66
- how, what = selector_builder.build(@normalized_selector.dup)
67
- unless how
68
- raise Error, "internal error: unable to build Selenium selector from #{@normalized_selector.inspect}"
56
+ begin
57
+ generate_scope
58
+ rescue LocatorException
59
+ return nil
69
60
  end
70
61
 
71
- what = add_regexp_predicates(what) if how == :xpath
62
+ selector, values_to_match = selector_builder.build(@selector)
63
+
64
+ validate_built_selector(selector, values_to_match)
72
65
 
73
- if filter == :all || !@filter_selector.empty?
74
- locate_filtered_elements(how, what, filter)
66
+ if filter == :all || values_to_match.any?
67
+ locate_matching_elements(selector, values_to_match, filter)
75
68
  else
76
- locate_element(how, what, @driver_scope)
69
+ locate_element(selector.keys.first, selector.values.first, @driver_scope)
77
70
  end
78
71
  end
79
72
 
80
- def validate(elements, tag_name)
81
- elements.compact.all? { |element| element_validator.validate(element, tag_name: tag_name) }
73
+ def validate_built_selector(selector, values_to_match)
74
+ if selector.nil?
75
+ msg = "#{selector_builder.class} was unable to build selector from #{@selector.inspect}"
76
+ raise LocatorException, msg
77
+ elsif values_to_match.nil?
78
+ msg = "#{selector_builder.class}#build is not returning expected responses for the current version of Watir"
79
+ raise LocatorException, msg
80
+ end
82
81
  end
83
82
 
84
83
  def fetch_value(element, how)
@@ -93,121 +92,61 @@ module Watir
93
92
  element.tag_name.downcase
94
93
  when :href
95
94
  element.attribute('href')&.strip
96
- when String, ::Symbol
95
+ else
97
96
  how = how.to_s.tr('_', '-') if how.is_a?(::Symbol)
98
97
  element.attribute(how)
99
- else
100
- raise Error::Exception, "Unable to fetch value for #{how}"
101
98
  end
102
99
  end
103
100
 
104
- def filter_elements(elements, filter: :first)
105
- selector = @filter_selector.dup
101
+ def matching_elements(elements, values_to_match, filter: :first)
106
102
  if filter == :first
107
- idx = element_index(elements, selector)
103
+ idx = element_index(elements, values_to_match)
108
104
  counter = 0
109
105
 
110
106
  # Lazy evaluation to avoid fetching values for elements that will be discarded
111
107
  matches = elements.lazy.select do |el|
112
108
  counter += 1
113
- matches_selector?(el, selector)
109
+ matches_values?(el, values_to_match)
114
110
  end
115
- msg = "Filtered through #{counter} elements to locate #{@selector.inspect}"
111
+ msg = "iterated through #{counter} elements to locate #{@selector.inspect}"
116
112
  matches.take(idx + 1).to_a[idx].tap { Watir.logger.debug msg }
117
113
  else
118
114
  Watir.logger.debug "Iterated through #{elements.size} elements to locate all #{@selector.inspect}"
119
- elements.select { |el| matches_selector?(el, selector) }
115
+ elements.select { |el| matches_values?(el, values_to_match) }
120
116
  end
121
117
  end
122
118
 
123
- def element_index(elements, selector)
124
- idx = selector.delete(:index) || 0
119
+ def element_index(elements, values_to_match)
120
+ idx = values_to_match.delete(:index) || 0
125
121
  return idx unless idx.negative?
126
122
 
127
123
  elements.reverse!
128
124
  idx.abs - 1
129
125
  end
130
126
 
131
- def create_normalized_selector(filter)
132
- return @normalized_selector if @normalized_selector
127
+ def generate_scope
128
+ return @driver_scope if @driver_scope
133
129
 
134
130
  @driver_scope = @query_scope.wd
135
131
 
136
- @normalized_selector = selector_builder.normalized_selector
137
-
138
- if @normalized_selector.key?(:label)
139
- label_key = :label
140
- elsif @normalized_selector.key?(:visible_label)
141
- label_key = :visible_label
142
- end
143
-
144
- if label_key
145
- process_label(label_key)
146
- return if @normalized_selector.nil?
147
- end
148
-
149
- if @normalized_selector.key?(:index) && filter == :all
150
- raise ArgumentError, "can't locate all elements by :index"
151
- end
152
-
153
- @normalized_selector
154
- end
155
-
156
- def create_filter_selector
157
- return @filter_selector if @filter_selector
158
-
159
- @filter_selector = {}
160
-
161
- # Remove selectors that can never be used in XPath builder
162
- %i[visible visible_text].each do |how|
163
- next unless @normalized_selector.key?(how)
164
-
165
- @filter_selector[how] = @normalized_selector.delete(how)
166
- end
167
-
168
- set_tag_validation if tag_validation_required?(@normalized_selector)
169
-
170
- # Regexp locators currently need to be validated even if they are included in the XPath builder
171
- # TODO: Identify Regexp that can have an exact equivalent using XPath contains (ie would not require
172
- # filtering) vs approximations (ie would still requiring filtering)
173
- @normalized_selector.each do |how, what|
174
- next unless what.is_a?(Regexp)
175
-
176
- @filter_selector[how] = @normalized_selector.delete(how)
177
- end
178
-
179
- if @normalized_selector[:index] && !@normalized_selector[:adjacent]
180
- idx = @normalized_selector.delete(:index)
181
-
182
- # Do not add {index: 0} filter if the only filter.
183
- # This will allow using #find_element instead of #find_elements.
184
- implicit_idx_filter = @filter_selector.empty? && idx.zero?
185
- @filter_selector[:index] = idx unless implicit_idx_filter
132
+ if @selector.key?(:label)
133
+ process_label :label
134
+ elsif @selector.key?(:visible_label)
135
+ process_label :visible_label
186
136
  end
187
-
188
- @filter_selector
189
- end
190
-
191
- def set_tag_validation
192
- @filter_selector[:tag_name] = if @normalized_selector[:tag_name].is_a?(::Symbol)
193
- @normalized_selector[:tag_name].to_s
194
- else
195
- @normalized_selector[:tag_name]
196
- end
197
137
  end
198
138
 
199
139
  def process_label(label_key)
200
- regexp = @normalized_selector[label_key].is_a?(Regexp)
140
+ regexp = @selector[label_key].is_a?(Regexp)
201
141
  return unless (regexp || label_key == :visible_label) && selector_builder.should_use_label_element?
202
142
 
203
143
  label = label_from_text(label_key)
204
- unless label # label not found, stop looking for element
205
- @normalized_selector = nil
206
- return
207
- end
144
+ msg = "Unable to locate element with label #{label_key}: #{@selector[label_key]}"
145
+ raise LocatorException, msg unless label
208
146
 
209
- if (id = label.attribute('for'))
210
- @normalized_selector[:id] = id
147
+ id = label.attribute('for')
148
+ if id
149
+ @selector[:id] = id
211
150
  else
212
151
  @driver_scope = label
213
152
  end
@@ -216,24 +155,24 @@ module Watir
216
155
  def label_from_text(label_key)
217
156
  # TODO: this won't work correctly if @wd is a sub-element, write spec
218
157
  # TODO: Figure out how to do this with find_element
219
- label_text = @normalized_selector.delete(label_key)
220
- locator_key = label_key.to_s.gsub('label', 'text').to_sym
158
+ label_text = @selector.delete(label_key)
159
+ locator_key = label_key.to_s.gsub('label', 'text').gsub('_element', '').to_sym
221
160
  locate_elements(:tag_name, 'label', @driver_scope).find do |el|
222
- matches_selector?(el, locator_key => label_text)
161
+ matches_values?(el, locator_key => label_text)
223
162
  end
224
163
  end
225
164
 
226
- def matches_selector?(element, selector)
227
- matches = selector.all? do |how, what|
165
+ def matches_values?(element, values_to_match)
166
+ matches = values_to_match.all? do |how, what|
228
167
  if how == :tag_name && what.is_a?(String)
229
- element_validator.validate(element, tag_name: what)
168
+ element_validator.validate(element, what)
230
169
  else
231
170
  val = fetch_value(element, how)
232
171
  what == val || val =~ /#{what}/
233
172
  end
234
173
  end
235
174
 
236
- text_regexp_deprecation(element, selector, matches) if selector[:text]
175
+ text_regexp_deprecation(element, values_to_match, matches) if values_to_match[:text]
237
176
 
238
177
  matches
239
178
  end
@@ -250,38 +189,6 @@ module Watir
250
189
  Watir.logger.deprecate(dep, ":visible_#{key}", ids: [:text_regexp])
251
190
  end
252
191
 
253
- def can_convert_regexp_to_contains?
254
- true
255
- end
256
-
257
- def add_regexp_predicates(what)
258
- return what unless can_convert_regexp_to_contains?
259
-
260
- @filter_selector.each do |key, value|
261
- next if %i[tag_name text visible_text visible index].include?(key)
262
-
263
- predicates = regexp_selector_to_predicates(key, value)
264
- what = "(#{what})[#{predicates.join(' and ')}]" unless predicates.empty?
265
- end
266
- what
267
- end
268
-
269
- def regexp_selector_to_predicates(key, regexp)
270
- return [] if regexp.casefold?
271
-
272
- match = regexp.source.match(CONVERTABLE_REGEXP)
273
- return [] unless match
274
-
275
- lhs = selector_builder.xpath_builder.lhs_for(nil, key)
276
- match.captures.reject(&:empty?).map do |literals|
277
- "contains(#{lhs}, #{XpathSupport.escape(literals)})"
278
- end
279
- end
280
-
281
- def tag_validation_required?(selector)
282
- (selector.key?(:css) || selector.key?(:xpath)) && selector.key?(:tag_name)
283
- end
284
-
285
192
  def locate_element(how, what, scope = @query_scope.wd)
286
193
  scope.find_element(how, what)
287
194
  end
@@ -290,17 +197,17 @@ module Watir
290
197
  scope.find_elements(how, what)
291
198
  end
292
199
 
293
- def locate_filtered_elements(how, what, filter)
200
+ def locate_matching_elements(selector, values_to_match, filter)
294
201
  retries = 0
295
202
  begin
296
- elements = locate_elements(how, what, @driver_scope) || []
297
- filter_elements(elements, filter: filter)
203
+ elements = locate_elements(selector.keys.first, selector.values.first, @driver_scope) || []
204
+ matching_elements(elements, values_to_match, filter: filter)
298
205
  rescue Selenium::WebDriver::Error::StaleElementReferenceError
299
206
  retries += 1
300
207
  sleep 0.5
301
208
  retry unless retries > 2
302
209
  target = filter == :all ? 'element collection' : 'element'
303
- raise StandardError, "Unable to locate #{target} from #{@selector} due to changing page"
210
+ raise LocatorException, "Unable to locate #{target} from #{@selector} due to changing page"
304
211
  end
305
212
  end
306
213
 
@@ -309,10 +216,10 @@ module Watir
309
216
  return false unless what.is_a?(String)
310
217
 
311
218
  if %i[partial_link_text link_text link].include?(how)
312
- Watir.logger.deprecate(":#{how} locator", ':visible_text', ids: [:visible_text])
219
+ Watir.logger.deprecate(":#{how} locator", ':visible_text', ids: [:link_text])
313
220
  return true if [:a, :link, nil].include?(tag)
314
221
 
315
- raise StandardError, "Can not use #{how} locator to find a #{what} element"
222
+ raise LocatorException, "Can not use #{how} locator to find a #{what} element"
316
223
  elsif how == :tag_name
317
224
  return true
318
225
  else