watir 6.14.0 → 6.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +24 -6
- data/CHANGES.md +10 -0
- data/Gemfile +0 -2
- data/README.md +1 -1
- data/Rakefile +2 -2
- data/lib/watir.rb +2 -3
- data/lib/watir/adjacent.rb +2 -2
- data/lib/watir/alert.rb +5 -9
- data/lib/watir/attribute_helper.rb +2 -3
- data/lib/watir/browser.rb +5 -17
- data/lib/watir/capabilities.rb +11 -0
- data/lib/watir/cell_container.rb +2 -2
- data/lib/watir/container.rb +7 -8
- data/lib/watir/cookies.rb +2 -12
- data/lib/watir/element_collection.rb +2 -2
- data/lib/watir/elements/button.rb +2 -11
- data/lib/watir/elements/element.rb +43 -25
- data/lib/watir/elements/hidden.rb +1 -1
- data/lib/watir/elements/iframe.rb +7 -5
- data/lib/watir/elements/option.rb +2 -13
- data/lib/watir/elements/select.rb +6 -22
- data/lib/watir/elements/table.rb +2 -2
- data/lib/watir/exception.rb +1 -2
- data/lib/watir/generator/base/idl_sorter.rb +1 -1
- data/lib/watir/generator/base/spec_extractor.rb +2 -2
- data/lib/watir/generator/base/visitor.rb +1 -1
- data/lib/watir/generator/html/generator.rb +0 -1
- data/lib/watir/generator/html/spec_extractor.rb +1 -1
- data/lib/watir/generator/html/visitor.rb +1 -1
- data/lib/watir/generator/svg/spec_extractor.rb +1 -1
- data/lib/watir/generator/svg/visitor.rb +1 -1
- data/lib/watir/js_execution.rb +11 -0
- data/lib/watir/js_snippets.rb +1 -1
- data/lib/watir/js_snippets/elementObscured.js +14 -0
- data/lib/watir/js_snippets/selectedText.js +17 -0
- data/lib/watir/legacy_wait.rb +5 -5
- data/lib/watir/locators.rb +16 -5
- data/lib/watir/locators/anchor/selector_builder.rb +38 -0
- data/lib/watir/locators/button/locator.rb +14 -12
- data/lib/watir/locators/button/selector_builder.rb +0 -22
- data/lib/watir/locators/button/selector_builder/xpath.rb +67 -12
- data/lib/watir/locators/button/validator.rb +2 -1
- data/lib/watir/locators/cell/selector_builder.rb +0 -14
- data/lib/watir/locators/cell/selector_builder/xpath.rb +21 -0
- data/lib/watir/locators/element/locator.rb +60 -153
- data/lib/watir/locators/element/selector_builder.rb +103 -84
- data/lib/watir/locators/element/selector_builder/regexp_disassembler.rb +66 -0
- data/lib/watir/locators/element/selector_builder/xpath.rb +195 -82
- data/lib/watir/locators/element/selector_builder/xpath_support.rb +27 -0
- data/lib/watir/locators/element/validator.rb +2 -9
- data/lib/watir/locators/row/selector_builder.rb +5 -22
- data/lib/watir/locators/row/selector_builder/xpath.rb +53 -0
- data/lib/watir/locators/text_area/selector_builder.rb +1 -1
- data/lib/watir/locators/text_area/selector_builder/xpath.rb +19 -0
- data/lib/watir/locators/text_field/locator.rb +11 -8
- data/lib/watir/locators/text_field/selector_builder.rb +0 -23
- data/lib/watir/locators/text_field/selector_builder/xpath.rb +33 -4
- data/lib/watir/locators/text_field/validator.rb +4 -4
- data/lib/watir/radio_set.rb +5 -5
- data/lib/watir/row_container.rb +2 -2
- data/lib/watir/user_editable.rb +2 -2
- data/lib/watir/version.rb +1 -1
- data/lib/watir/wait.rb +24 -37
- data/lib/watir/window.rb +11 -8
- data/lib/watirspec/remote_server.rb +3 -1
- data/spec/locator_spec_helper.rb +1 -1
- data/spec/spec_helper.rb +25 -1
- data/spec/unit/anchor_locator_spec.rb +68 -0
- data/spec/unit/capabilities_spec.rb +27 -0
- data/spec/unit/element_locator_spec.rb +184 -101
- data/spec/unit/logger_spec.rb +5 -0
- data/spec/watirspec/adjacent_spec.rb +34 -34
- data/spec/watirspec/after_hooks_spec.rb +78 -35
- data/spec/watirspec/alert_spec.rb +10 -0
- data/spec/watirspec/browser_spec.rb +27 -1
- data/spec/watirspec/element_hidden_spec.rb +6 -0
- data/spec/watirspec/elements/button_spec.rb +5 -11
- data/spec/watirspec/elements/buttons_spec.rb +1 -1
- data/spec/watirspec/elements/checkbox_spec.rb +2 -15
- data/spec/watirspec/elements/date_time_field_spec.rb +6 -1
- data/spec/watirspec/elements/dd_spec.rb +0 -17
- data/spec/watirspec/elements/del_spec.rb +0 -14
- data/spec/watirspec/elements/div_spec.rb +0 -18
- data/spec/watirspec/elements/dl_spec.rb +0 -17
- data/spec/watirspec/elements/dt_spec.rb +0 -17
- data/spec/watirspec/elements/element_spec.rb +177 -17
- data/spec/watirspec/elements/elements_spec.rb +7 -6
- data/spec/watirspec/elements/em_spec.rb +0 -13
- data/spec/watirspec/elements/filefield_spec.rb +0 -11
- data/spec/watirspec/elements/form_spec.rb +6 -0
- data/spec/watirspec/elements/hn_spec.rb +0 -14
- data/spec/watirspec/elements/iframe_spec.rb +15 -0
- data/spec/watirspec/elements/ins_spec.rb +0 -14
- data/spec/watirspec/elements/labels_spec.rb +1 -1
- data/spec/watirspec/elements/li_spec.rb +0 -14
- data/spec/watirspec/elements/link_spec.rb +22 -14
- data/spec/watirspec/elements/links_spec.rb +13 -0
- data/spec/watirspec/elements/list_spec.rb +15 -0
- data/spec/watirspec/elements/ol_spec.rb +0 -14
- data/spec/watirspec/elements/option_spec.rb +0 -10
- data/spec/watirspec/elements/p_spec.rb +0 -14
- data/spec/watirspec/elements/pre_spec.rb +0 -14
- data/spec/watirspec/elements/radio_spec.rb +0 -14
- data/spec/watirspec/elements/select_list_spec.rb +0 -10
- data/spec/watirspec/elements/span_spec.rb +4 -15
- data/spec/watirspec/elements/strong_spec.rb +4 -15
- data/spec/watirspec/elements/table_nesting_spec.rb +1 -1
- data/spec/watirspec/elements/table_spec.rb +7 -0
- data/spec/watirspec/elements/text_field_spec.rb +10 -2
- data/spec/watirspec/elements/text_fields_spec.rb +1 -1
- data/spec/watirspec/elements/tr_spec.rb +1 -1
- data/spec/watirspec/elements/ul_spec.rb +0 -14
- data/spec/watirspec/html/closeable.html +8 -0
- data/spec/watirspec/html/forms_with_input_elements.html +28 -23
- data/spec/watirspec/html/nested_elements.html +9 -9
- data/spec/watirspec/html/obscured.html +34 -0
- data/spec/watirspec/html/tables.html +13 -13
- data/spec/watirspec/radio_set_spec.rb +5 -0
- data/spec/watirspec/selector_builder/button_spec.rb +254 -0
- data/spec/watirspec/selector_builder/cell_spec.rb +93 -0
- data/spec/watirspec/selector_builder/element_spec.rb +639 -0
- data/spec/watirspec/selector_builder/row_spec.rb +150 -0
- data/spec/watirspec/selector_builder/text_spec.rb +170 -0
- data/spec/watirspec/support/rspec_matchers.rb +6 -1
- data/spec/watirspec/user_editable_spec.rb +4 -0
- data/spec/watirspec/wait_spec.rb +65 -14
- data/spec/watirspec/window_switching_spec.rb +54 -1
- data/spec/watirspec_helper.rb +2 -0
- data/watir.gemspec +7 -1
- metadata +86 -8
- data/lib/watir/locators/text_area/locator.rb +0 -13
- 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 = [
|
8
|
+
VALID_WHATS = [String, Regexp, TrueClass, FalseClass].freeze
|
8
9
|
WILDCARD_ATTRIBUTE = /^(aria|data)_(.+)$/
|
9
10
|
|
10
|
-
def initialize(
|
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
|
18
|
-
|
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.
|
21
|
-
check_type(how, what)
|
32
|
+
@selector.delete(:index) if @selector[:index]&.zero?
|
22
33
|
|
23
|
-
|
24
|
-
|
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
|
-
|
62
|
+
return raise_unless(what, Integer)
|
34
63
|
when :visible
|
35
|
-
|
36
|
-
when :
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
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
|
63
|
-
|
85
|
+
def should_use_label_element?
|
86
|
+
!valid_attribute?(:label)
|
64
87
|
end
|
65
88
|
|
66
89
|
private
|
67
90
|
|
68
|
-
def
|
91
|
+
def normalize_locator(how, what)
|
69
92
|
case how
|
70
|
-
when
|
71
|
-
|
72
|
-
|
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
|
91
|
-
|
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
|
128
|
+
return if combine_with_xpath_or_css?(@selector)
|
99
129
|
|
100
|
-
msg = "#{
|
101
|
-
raise
|
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
|
-
|
105
|
-
|
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
|
143
|
+
def combine_with_xpath_or_css?(selector)
|
115
144
|
keys = selector.keys
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
146
|
-
|
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
|
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
|
-
|
7
|
-
|
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
|
11
|
-
|
12
|
-
|
105
|
+
def class_string
|
106
|
+
class_name = @selector.delete(:class)
|
107
|
+
return '' if class_name.nil?
|
13
108
|
|
14
|
-
|
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
|
-
|
111
|
+
@requires_matches[:class] = []
|
18
112
|
|
19
|
-
|
20
|
-
xpath << '[' << attribute_expression(nil, selectors) << ']' unless selectors.empty?
|
113
|
+
predicates = [class_name].flatten.map { |value| process_attribute(:class, value) }.compact
|
21
114
|
|
22
|
-
|
115
|
+
@requires_matches.delete(:class) if @requires_matches[:class].empty?
|
23
116
|
|
24
|
-
|
117
|
+
predicates.empty? ? '' : "[#{predicates.join(' and ')}]"
|
118
|
+
end
|
119
|
+
|
120
|
+
def text_string
|
121
|
+
text = @selector.delete :text
|
25
122
|
|
26
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
183
|
+
@requires_matches[key] = regexp
|
63
184
|
end
|
64
185
|
end
|
65
186
|
|
66
|
-
|
67
|
-
|
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 :
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
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(
|
212
|
+
lhs_for(attribute, false)
|
116
213
|
end
|
117
214
|
|
118
215
|
def attribute_absence(attribute)
|
119
|
-
|
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
|