watir 6.14.0 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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