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
@@ -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