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
@@ -2,78 +2,113 @@ module Watir
2
2
  module Locators
3
3
  class Element
4
4
  class SelectorBuilder
5
+ include Exception
5
6
  attr_reader :custom_attributes
6
7
 
7
- VALID_WHATS = [Array, String, Regexp, TrueClass, FalseClass, ::Symbol].freeze
8
+ VALID_WHATS = [String, Regexp, TrueClass, FalseClass].freeze
8
9
  WILDCARD_ATTRIBUTE = /^(aria|data)_(.+)$/
9
10
 
10
- def initialize(query_scope, selector, valid_attributes)
11
- @query_scope = query_scope # either element or browser
12
- @selector = selector
11
+ def initialize(valid_attributes)
13
12
  @valid_attributes = valid_attributes
14
13
  @custom_attributes = []
15
14
  end
16
15
 
17
- def normalized_selector
18
- selector = {}
16
+ def build(selector)
17
+ inspected = selector.inspect
18
+ @selector = selector
19
+ normalize_selector
20
+
21
+ xpath_css = (@selector.keys & %i[xpath css]).each_with_object({}) do |key, hash|
22
+ hash[key] = @selector.delete(key)
23
+ end
24
+
25
+ built = if xpath_css.empty?
26
+ build_wd_selector(@selector)
27
+ else
28
+ process_xpath_css(xpath_css)
29
+ xpath_css
30
+ end
19
31
 
20
- @selector.each do |how, what|
21
- check_type(how, what)
32
+ @selector.delete(:index) if @selector[:index]&.zero?
22
33
 
23
- how, what = normalize_selector(how, what)
24
- selector[how] = what
34
+ Watir.logger.debug "Converted #{inspected} to #{built}, with #{@selector.inspect} to match"
35
+ [built, @selector]
36
+ end
37
+
38
+ def normalize_selector
39
+ if @selector.key?(:class) && @selector.key?(:class_name)
40
+ raise LocatorException, 'Can not use both :class and :class_name locators'
25
41
  end
26
42
 
27
- selector
43
+ if @selector[:adjacent] == :ancestor && @selector.key?(:text)
44
+ raise LocatorException, 'Can not find parent element with text locator'
45
+ end
46
+
47
+ @selector.keys.each do |key|
48
+ check_type(key, @selector[key])
49
+
50
+ how, what = normalize_locator(key, @selector.delete(key))
51
+ @selector[how] = what
52
+ end
28
53
  end
29
54
 
30
55
  def check_type(how, what)
31
56
  case how
57
+ when :adjacent
58
+ return raise_unless(what, ::Symbol)
59
+ when :xpath, :css
60
+ return raise_unless(what, String)
32
61
  when :index
33
- raise_unless_int(what)
62
+ return raise_unless(what, Integer)
34
63
  when :visible
35
- raise_unless_boolean(what)
36
- when :visible_text
37
- raise_unless_str_regex(what)
38
- else
39
- if what.is_a?(Array) && !%i[class class_name].include?(how)
40
- raise TypeError, 'Only :class locator can have a value of an Array'
64
+ return raise_unless(what, :boolean)
65
+ when :tag_name
66
+ return raise_unless(what, :string_or_regexp_or_symbol)
67
+ when :visible_text, :text
68
+ return raise_unless(what, :string_or_regexp)
69
+ when :class, :class_name
70
+ if what.is_a?(Array)
71
+ raise LocatorException, 'Can not locate elements with an empty Array for :class' if what.empty?
72
+
73
+ what.each do |klass|
74
+ raise_unless(klass, :string_or_regexp)
75
+ end
76
+ return
41
77
  end
42
- raise TypeError, 'Symbol is not a valid value' if what.is_a?(Symbol) && how != :adjacent
43
- return if VALID_WHATS.any? { |t| what.is_a? t }
44
-
45
- raise TypeError, "expected one of #{VALID_WHATS.inspect}, got #{what.inspect}:#{what.class}"
46
78
  end
47
- end
48
-
49
- def should_use_label_element?
50
- !valid_attribute?(:label)
51
- end
52
79
 
53
- def build(selector)
54
- inspect = selector.inspect
55
- return given_xpath_or_css(selector) if selector.key?(:xpath) || selector.key?(:css)
80
+ return if VALID_WHATS.any? { |t| what.is_a? t }
56
81
 
57
- built = build_wd_selector(selector)
58
- Watir.logger.debug "Converted #{inspect} to #{built}"
59
- built
82
+ raise TypeError, "expected one of #{VALID_WHATS.inspect}, got #{what.inspect}:#{what.class}"
60
83
  end
61
84
 
62
- def xpath_builder
63
- @xpath_builder ||= xpath_builder_class.new(should_use_label_element?)
85
+ def should_use_label_element?
86
+ !valid_attribute?(:label)
64
87
  end
65
88
 
66
89
  private
67
90
 
68
- def normalize_selector(how, what)
91
+ def normalize_locator(how, what)
69
92
  case how
70
- when :tag_name, :text, :xpath, :index, :class, :label, :css, :visible, :visible_text, :adjacent
71
- # include :class since the valid attribute is 'class_name'
72
- # include :for since the valid attribute is 'html_for'
93
+ when 'text'
94
+ Watir.logger.deprecate "String 'text' as a locator", 'Symbol :text', ids: ['text_string']
95
+ [:text, what]
96
+ when :tag_name
97
+ what = what.to_s if what.is_a?(::Symbol)
98
+ [how, what]
99
+ when :text, :xpath, :index, :class, :css, :visible, :visible_text, :adjacent
73
100
  [how, what]
101
+ when :label
102
+ if should_use_label_element?
103
+ ["#{how}_element".to_sym, what]
104
+ else
105
+ [how, what]
106
+ end
74
107
  when :class_name
75
108
  [:class, what]
76
109
  when :caption
110
+ # This allows any element to be located with 'caption' instead of 'text'
111
+ Watir.logger.deprecate('Locating elements with :caption', ':text locator', ids: [:caption])
77
112
  [:text, what]
78
113
  else
79
114
  check_custom_attribute how
@@ -87,65 +122,49 @@ module Watir
87
122
  @custom_attributes << attribute.to_s
88
123
  end
89
124
 
90
- def given_xpath_or_css(selector)
91
- locator = {}
92
- locator[:xpath] = selector.delete(:xpath) if selector.key?(:xpath)
93
- locator[:css] = selector.delete(:css) if selector.key?(:css)
94
-
95
- return if locator.empty?
96
- raise ArgumentError, ":xpath and :css cannot be combined (#{selector.inspect})" if locator.size > 1
125
+ def process_xpath_css(xpath_css)
126
+ raise LocatorException, ":xpath and :css cannot be combined (#{xpath_css})" if xpath_css.size > 1
97
127
 
98
- return locator.first unless selector.any? && !can_be_combined_with_xpath_or_css?(selector)
128
+ return if combine_with_xpath_or_css?(@selector)
99
129
 
100
- msg = "#{locator.keys.first} cannot be combined with other selectors (#{selector.inspect})"
101
- raise ArgumentError, msg
130
+ msg = "#{xpath_css.keys.first} cannot be combined with all of these locators (#{@selector.inspect})"
131
+ raise LocatorException, msg
102
132
  end
103
133
 
104
- def build_wd_selector(selectors)
105
- return if selectors.values.any? { |e| e.is_a? Regexp }
106
-
107
- build_xpath(selectors)
134
+ # Implement this method when creating a different selector builder
135
+ def build_wd_selector(selector)
136
+ Kernel.const_get("#{self.class.name}::XPath").new.build(selector)
108
137
  end
109
138
 
110
139
  def valid_attribute?(attribute)
111
140
  @valid_attributes&.include?(attribute)
112
141
  end
113
142
 
114
- def can_be_combined_with_xpath_or_css?(selector)
143
+ def combine_with_xpath_or_css?(selector)
115
144
  keys = selector.keys
116
- return true if keys == [:tag_name]
117
-
118
- return keys.sort == %i[tag_name type] if selector[:tag_name] == 'input'
119
-
120
- false
121
- end
122
-
123
- def build_xpath(selectors)
124
- xpath_builder.build(selectors)
125
- end
126
-
127
- def xpath_builder_class
128
- Kernel.const_get("#{self.class.name}::XPath")
129
- rescue StandardError
130
- XPath
131
- end
132
-
133
- def raise_unless_int(what)
134
- return if what.is_a?(Integer)
135
-
136
- raise TypeError, "expected Integer, got #{what.inspect}:#{what.class}"
137
- end
138
-
139
- def raise_unless_boolean(what)
140
- return if what.is_a?(TrueClass) || what.is_a?(FalseClass)
141
-
142
- raise TypeError, "expected TrueClass or FalseClass, got #{what.inspect}:#{what.class}"
145
+ keys.reject! { |key| %i[visible visible_text index].include? key }
146
+ if (keys - [:tag_name]).empty?
147
+ true
148
+ elsif selector[:tag_name] == 'input' && keys == %i[tag_name type]
149
+ true
150
+ else
151
+ false
152
+ end
143
153
  end
144
154
 
145
- def raise_unless_str_regex(what)
146
- return if what.is_a?(String) || what.is_a?(Regexp)
155
+ def raise_unless(what, type)
156
+ valid = if type == :boolean
157
+ [TrueClass, FalseClass].include?(what.class)
158
+ elsif type == :string_or_regexp
159
+ [String, Regexp].include?(what.class)
160
+ elsif type == :string_or_regexp_or_symbol
161
+ [String, Regexp, ::Symbol].include?(what.class)
162
+ else
163
+ what.is_a?(type)
164
+ end
165
+ return if valid
147
166
 
148
- raise TypeError, "expected String or Regexp, got #{what.inspect}:#{what.class}"
167
+ raise TypeError, "expected #{type}, got #{what.inspect}:#{what.class}"
149
168
  end
150
169
  end
151
170
  end
@@ -0,0 +1,66 @@
1
+ # Source: https://github.com/teamcapybara/capybara/blob/xpath_regexp/lib/capybara/selector/regexp_disassembler.rb
2
+ require 'regexp_parser'
3
+
4
+ module Watir
5
+ module Locators
6
+ class Element
7
+ class SelectorBuilder
8
+ class RegexpDisassembler
9
+ def initialize(regexp)
10
+ @regexp = regexp
11
+ @regexp_source = regexp.source
12
+ end
13
+
14
+ def substrings
15
+ @substrings ||= begin
16
+ strs = extract_strings(Regexp::Parser.parse(@regexp), [+''])
17
+ strs.map!(&:downcase) if @regexp.casefold?
18
+ strs.reject(&:empty?).uniq
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def min_repeat(exp)
25
+ exp.quantifier&.min || 1
26
+ end
27
+
28
+ def fixed_repeat?(exp)
29
+ min_repeat(exp) == (exp.quantifier&.max || 1)
30
+ end
31
+
32
+ def optional?(exp)
33
+ min_repeat(exp).zero?
34
+ end
35
+
36
+ def extract_strings(expression, strings)
37
+ expression.each do |exp|
38
+ if optional?(exp)
39
+ strings.push(+'')
40
+ next
41
+ end
42
+ if %i[meta set].include?(exp.type)
43
+ strings.push(+'')
44
+ next
45
+ end
46
+ if exp.terminal?
47
+ case exp.type
48
+ when :literal
49
+ strings.last << (exp.text * min_repeat(exp))
50
+ when :escape
51
+ strings.last << (exp.char * min_repeat(exp))
52
+ else
53
+ strings.push(+'')
54
+ end
55
+ else
56
+ min_repeat(exp).times { extract_strings(exp, strings) }
57
+ end
58
+ strings.push(+'') unless fixed_repeat?(exp)
59
+ end
60
+ strings
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,120 +3,233 @@ module Watir
3
3
  class Element
4
4
  class SelectorBuilder
5
5
  class XPath
6
- def initialize(should_use_label_element)
7
- @should_use_label_element = should_use_label_element
6
+ include Exception
7
+ include XpathSupport
8
+
9
+ CAN_NOT_BUILD = %i[visible visible_text].freeze
10
+
11
+ def build(selector)
12
+ @selector = selector
13
+
14
+ @requires_matches = (@selector.keys & CAN_NOT_BUILD).each_with_object({}) do |key, hash|
15
+ hash[key] = @selector.delete(key)
16
+ end
17
+
18
+ index = @selector.delete(:index)
19
+ @adjacent = @selector.delete(:adjacent)
20
+
21
+ xpath = start_string
22
+ xpath << adjacent_string
23
+ xpath << tag_string
24
+ xpath << class_string
25
+ xpath << text_string
26
+ xpath << additional_string
27
+ xpath << attribute_string
28
+
29
+ xpath = index ? add_index(xpath, index) : xpath
30
+
31
+ @selector.merge! @requires_matches
32
+
33
+ {xpath: xpath}
34
+ end
35
+
36
+ private
37
+
38
+ def process_attribute(key, value)
39
+ if value.is_a? Regexp
40
+ predicate_conversion(key, value)
41
+ else
42
+ predicate_expression(key, value)
43
+ end
44
+ end
45
+
46
+ def predicate_expression(key, val)
47
+ if val.eql? true
48
+ attribute_presence(key)
49
+ elsif val.eql? false
50
+ attribute_absence(key)
51
+ else
52
+ equal_pair(key, val)
53
+ end
54
+ end
55
+
56
+ def predicate_conversion(key, regexp)
57
+ # type attributes can be upper case - downcase them
58
+ # https://github.com/watir/watir/issues/72
59
+ downcase = key == :type || regexp.casefold?
60
+
61
+ lhs = lhs_for(key, downcase)
62
+
63
+ results = RegexpDisassembler.new(regexp).substrings
64
+
65
+ if results.empty?
66
+ add_to_matching(key, regexp)
67
+ return lhs
68
+ elsif results.size == 1 && starts_with?(results, regexp) && !visible?
69
+ return "starts-with(#{lhs}, '#{results.first}')"
70
+ end
71
+
72
+ add_to_matching(key, regexp, results)
73
+
74
+ results.map { |substring|
75
+ "contains(#{lhs}, '#{substring}')"
76
+ }.flatten.compact.join(' and ')
77
+ end
78
+
79
+ def start_string
80
+ @adjacent ? './' : './/*'
81
+ end
82
+
83
+ def adjacent_string
84
+ case @adjacent
85
+ when nil
86
+ ''
87
+ when :ancestor
88
+ 'ancestor::*'
89
+ when :preceding
90
+ 'preceding-sibling::*'
91
+ when :following
92
+ 'following-sibling::*'
93
+ when :child
94
+ 'child::*'
95
+ else
96
+ raise LocatorException, "Unable to process adjacent locator with #{@adjacent}"
97
+ end
98
+ end
99
+
100
+ def tag_string
101
+ tag_name = @selector.delete(:tag_name)
102
+ tag_name.nil? ? '' : "[#{process_attribute(:tag_name, tag_name)}]"
8
103
  end
9
104
 
10
- def build(selectors)
11
- adjacent = selectors.delete :adjacent
12
- xpath = adjacent ? process_adjacent(adjacent) : './/*'
105
+ def class_string
106
+ class_name = @selector.delete(:class)
107
+ return '' if class_name.nil?
13
108
 
14
- tag_name = selectors.delete(:tag_name).to_s
15
- xpath << "[local-name()='#{tag_name}']" unless tag_name.empty?
109
+ deprecate_class_array(class_name) if class_name.is_a?(String) && class_name.strip.include?(' ')
16
110
 
17
- index = selectors.delete(:index)
111
+ @requires_matches[:class] = []
18
112
 
19
- # the remaining entries should be attributes
20
- xpath << '[' << attribute_expression(nil, selectors) << ']' unless selectors.empty?
113
+ predicates = [class_name].flatten.map { |value| process_attribute(:class, value) }.compact
21
114
 
22
- xpath << "[#{index + 1}]" if adjacent && index
115
+ @requires_matches.delete(:class) if @requires_matches[:class].empty?
23
116
 
24
- p xpath: xpath, selectors: selectors if $DEBUG
117
+ predicates.empty? ? '' : "[#{predicates.join(' and ')}]"
118
+ end
119
+
120
+ def text_string
121
+ text = @selector.delete :text
25
122
 
26
- [:xpath, xpath]
123
+ case text
124
+ when nil
125
+ ''
126
+ when Regexp
127
+ @requires_matches[:text] = text
128
+ ''
129
+ else
130
+ "[#{predicate_expression(:text, text)}]"
131
+ end
27
132
  end
28
133
 
29
- # @todo Get rid of building
30
- def attribute_expression(building, selectors)
31
- f = selectors.map do |key, val|
32
- if val.is_a?(Array) && key == :class
33
- '(' + val.map { |v| build_class_match(v) }.join(' and ') + ')'
34
- elsif val.is_a?(Array)
35
- '(' + val.map { |v| equal_pair(building, key, v) }.join(' or ') + ')'
36
- elsif val.eql? true
37
- attribute_presence(key)
38
- elsif val.eql? false
39
- attribute_absence(key)
40
- else
41
- equal_pair(building, key, val)
42
- end
134
+ def attribute_string
135
+ attributes = @selector.keys.map { |key|
136
+ process_attribute(key, @selector.delete(key))
137
+ }.flatten.compact
138
+ attributes.empty? ? '' : "[#{attributes.join(' and ')}]"
139
+ end
140
+
141
+ def additional_string
142
+ # to be used by subclasses as necessary
143
+ ''
144
+ end
145
+
146
+ # TODO: Remove this on refactor of index
147
+ def add_index(xpath, index)
148
+ if @adjacent
149
+ "#{xpath}[#{index + 1}]"
150
+ elsif index&.positive? && @requires_matches.empty?
151
+ "(#{xpath})[#{index + 1}]"
152
+ elsif index&.negative? && @requires_matches.empty?
153
+ last_value = 'last()'
154
+ last_value << (index + 1).to_s if index < -1
155
+ "(#{xpath})[#{last_value}]"
156
+ else
157
+ @requires_matches[:index] = index
158
+ xpath
43
159
  end
44
- f.join(' and ')
45
160
  end
46
161
 
47
- # @todo Get rid of building
48
- def equal_pair(building, key, value)
162
+ def deprecate_class_array(class_name)
163
+ dep = "Using the :class locator to locate multiple classes with a String value (i.e. \"#{class_name}\")"
164
+ Watir.logger.deprecate dep,
165
+ "Array (e.g. #{class_name.split})",
166
+ ids: [:class_array]
167
+ end
168
+
169
+ def visible?
170
+ !(@requires_matches.keys & CAN_NOT_BUILD).empty?
171
+ end
172
+
173
+ def starts_with?(results, regexp)
174
+ regexp.source[0] == '^' && results.first == regexp.source[1..-1]
175
+ end
176
+
177
+ def add_to_matching(key, regexp, results = nil)
178
+ return unless results.nil? || requires_matching?(results, regexp)
179
+
49
180
  if key == :class
50
- if value.strip.include?(' ')
51
- dep = "Using the :class locator to locate multiple classes with a String value (i.e. \"#{value}\")"
52
- Watir.logger.deprecate dep,
53
- "Array (e.g. #{value.split})",
54
- ids: [:class_array]
55
- end
56
- build_class_match(value)
57
- elsif key == :label && @should_use_label_element
58
- # we assume :label means a corresponding label element, not the attribute
59
- text = "normalize-space()=#{XpathSupport.escape value}"
60
- "(@id=//label[#{text}]/@for or parent::label[#{text}])"
181
+ @requires_matches[key] << regexp
61
182
  else
62
- "#{lhs_for(building, key)}=#{XpathSupport.escape value}"
183
+ @requires_matches[key] = regexp
63
184
  end
64
185
  end
65
186
 
66
- # @todo Get rid of building
67
- def lhs_for(_building, key)
187
+ def requires_matching?(results, regexp)
188
+ regexp.casefold? ? !results.first.casecmp(regexp.source).zero? : results.first != regexp.source
189
+ end
190
+
191
+ def lhs_for(key, downcase = false)
68
192
  case key
69
- when :text, 'text'
70
- 'normalize-space()'
71
193
  when String
72
194
  "@#{key}"
195
+ when :tag_name
196
+ 'local-name()'
73
197
  when :href
74
- # TODO: change this behaviour?
75
198
  'normalize-space(@href)'
76
- when :type
77
- # type attributes can be upper case - downcase them
78
- # https://github.com/watir/watir/issues/72
79
- XpathSupport.downcase('@type')
199
+ when :text
200
+ 'normalize-space()'
201
+ when :contains_text
202
+ 'text()'
80
203
  when ::Symbol
81
- "@#{key.to_s.tr('_', '-')}"
204
+ lhs = "@#{key.to_s.tr('_', '-')}"
205
+ downcase ? XpathSupport.downcase(lhs) : lhs
82
206
  else
83
- raise Error::Exception, "Unable to build XPath using #{key}"
84
- end
85
- end
86
-
87
- private
88
-
89
- def process_adjacent(adjacent)
90
- xpath = './'
91
- xpath << case adjacent
92
- when :ancestor
93
- 'ancestor::*'
94
- when :preceding
95
- 'preceding-sibling::*'
96
- when :following
97
- 'following-sibling::*'
98
- when :child
99
- 'child::*'
100
- end
101
- xpath
102
- end
103
-
104
- def build_class_match(value)
105
- if value =~ /^!/
106
- klass = XpathSupport.escape " #{value[1..-1]} "
107
- "not(contains(concat(' ', @class, ' '), #{klass}))"
108
- else
109
- klass = XpathSupport.escape " #{value} "
110
- "contains(concat(' ', @class, ' '), #{klass})"
207
+ raise LocatorException, "Unable to build XPath using #{key}:#{key.class}"
111
208
  end
112
209
  end
113
210
 
114
211
  def attribute_presence(attribute)
115
- lhs_for(nil, attribute)
212
+ lhs_for(attribute, false)
116
213
  end
117
214
 
118
215
  def attribute_absence(attribute)
119
- "not(#{lhs_for(nil, attribute)})"
216
+ lhs = lhs_for(attribute, false)
217
+ "not(#{lhs})"
218
+ end
219
+
220
+ def equal_pair(key, value)
221
+ if key == :label_element
222
+ # we assume :label means a corresponding label element, not the attribute
223
+ text = "#{lhs_for(:text)}=#{XpathSupport.escape value}"
224
+ "(@id=//label[#{text}]/@for or parent::label[#{text}])"
225
+ elsif key == :class
226
+ negate_xpath = value =~ /^!/ && value.slice!(0)
227
+ expression = "contains(concat(' ', @class, ' '), #{XpathSupport.escape " #{value} "})"
228
+
229
+ negate_xpath ? "not(#{expression})" : expression
230
+ else
231
+ "#{lhs_for(key, key == :type)}=#{XpathSupport.escape value}"
232
+ end
120
233
  end
121
234
  end
122
235
  end