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