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
data/lib/watir/js_snippets.rb
CHANGED
@@ -6,7 +6,7 @@ module Watir
|
|
6
6
|
|
7
7
|
def execute_js(function_name, *arguments)
|
8
8
|
file = File.expand_path("../js_snippets/#{function_name}.js", __FILE__)
|
9
|
-
raise
|
9
|
+
raise Exception::Error, "Can not excute script as #{function_name}.js does not exist" unless File.exist?(file)
|
10
10
|
|
11
11
|
js = File.read(file)
|
12
12
|
script = "return (#{js}).apply(null, arguments)"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
// Original Author: Florent B.
|
2
|
+
// Source: https://stackoverflow.com/a/45244889/1200545
|
3
|
+
function() {
|
4
|
+
var elem = arguments[0],
|
5
|
+
box = elem.getBoundingClientRect(),
|
6
|
+
cx = box.left + box.width / 2,
|
7
|
+
cy = box.top + box.height / 2,
|
8
|
+
e = document.elementFromPoint(cx, cy);
|
9
|
+
for (; e; e = e.parentElement) {
|
10
|
+
if (e === elem)
|
11
|
+
return false;
|
12
|
+
}
|
13
|
+
return true;
|
14
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
// Code from https://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text
|
2
|
+
|
3
|
+
function() {
|
4
|
+
var text = "";
|
5
|
+
var activeEl = document.activeElement;
|
6
|
+
var activeElTagName = activeEl ? activeEl.tagName.toLowerCase() : null;
|
7
|
+
if (
|
8
|
+
(activeElTagName == "textarea") || (activeElTagName == "input" &&
|
9
|
+
/^(?:text|search|password|tel|url)$/i.test(activeEl.type)) &&
|
10
|
+
(typeof activeEl.selectionStart == "number")
|
11
|
+
) {
|
12
|
+
text = activeEl.value.slice(activeEl.selectionStart, activeEl.selectionEnd);
|
13
|
+
} else if (window.getSelection) {
|
14
|
+
text = window.getSelection().toString();
|
15
|
+
}
|
16
|
+
return text;
|
17
|
+
}
|
data/lib/watir/legacy_wait.rb
CHANGED
@@ -16,7 +16,7 @@ module Watir
|
|
16
16
|
def method_missing(method, *args, &block)
|
17
17
|
return super unless @element.respond_to?(method)
|
18
18
|
|
19
|
-
|
19
|
+
Wait.until(@timeout, @message) { wait_until }
|
20
20
|
|
21
21
|
@element.__send__(method, *args, &block)
|
22
22
|
end
|
@@ -29,9 +29,9 @@ module Watir
|
|
29
29
|
|
30
30
|
class WhenPresentDecorator < BaseDecorator
|
31
31
|
def present?
|
32
|
-
|
32
|
+
Wait.until(@timeout, @message) { wait_until }
|
33
33
|
true
|
34
|
-
rescue
|
34
|
+
rescue Wait::TimeoutError
|
35
35
|
false
|
36
36
|
end
|
37
37
|
|
@@ -85,7 +85,7 @@ module Watir
|
|
85
85
|
message = "waiting for #{selector_string} to become present"
|
86
86
|
|
87
87
|
if block_given?
|
88
|
-
|
88
|
+
Wait.until(timeout, message) { present? }
|
89
89
|
yield self
|
90
90
|
else
|
91
91
|
WhenPresentDecorator.new(self, timeout, message)
|
@@ -112,7 +112,7 @@ module Watir
|
|
112
112
|
message = "waiting for #{selector_string} to become enabled"
|
113
113
|
|
114
114
|
if block_given?
|
115
|
-
|
115
|
+
Wait.until(timeout, message) { enabled? }
|
116
116
|
yield self
|
117
117
|
else
|
118
118
|
WhenEnabledDecorator.new(self, timeout, message)
|
data/lib/watir/locators.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
require 'watir/locators/element/locator'
|
2
2
|
require 'watir/locators/element/selector_builder'
|
3
|
+
require 'watir/locators/element/selector_builder/xpath_support'
|
4
|
+
require 'watir/locators/element/selector_builder/regexp_disassembler'
|
3
5
|
require 'watir/locators/element/selector_builder/xpath'
|
4
6
|
require 'watir/locators/element/validator'
|
5
7
|
|
8
|
+
require 'watir/locators/anchor/selector_builder'
|
9
|
+
|
6
10
|
require 'watir/locators/button/locator'
|
7
11
|
require 'watir/locators/button/selector_builder'
|
8
12
|
require 'watir/locators/button/selector_builder/xpath'
|
@@ -10,12 +14,14 @@ require 'watir/locators/button/validator'
|
|
10
14
|
|
11
15
|
require 'watir/locators/cell/locator'
|
12
16
|
require 'watir/locators/cell/selector_builder'
|
17
|
+
require 'watir/locators/cell/selector_builder/xpath'
|
13
18
|
|
14
19
|
require 'watir/locators/row/locator'
|
15
20
|
require 'watir/locators/row/selector_builder'
|
21
|
+
require 'watir/locators/row/selector_builder/xpath'
|
16
22
|
|
17
|
-
require 'watir/locators/text_area/locator'
|
18
23
|
require 'watir/locators/text_area/selector_builder'
|
24
|
+
require 'watir/locators/text_area/selector_builder/xpath'
|
19
25
|
|
20
26
|
require 'watir/locators/text_field/locator'
|
21
27
|
require 'watir/locators/text_field/selector_builder'
|
@@ -29,21 +35,21 @@ module Watir
|
|
29
35
|
class_from_string("#{browser.locator_namespace}::#{element_class_name}::Locator") ||
|
30
36
|
class_from_string("Watir::Locators::#{element_class_name}::Locator") ||
|
31
37
|
class_from_string("#{browser.locator_namespace}::Element::Locator") ||
|
32
|
-
|
38
|
+
Locators::Element::Locator
|
33
39
|
end
|
34
40
|
|
35
41
|
def element_validator_class
|
36
42
|
class_from_string("#{browser.locator_namespace}::#{element_class_name}::Validator") ||
|
37
43
|
class_from_string("Watir::Locators::#{element_class_name}::Validator") ||
|
38
44
|
class_from_string("#{browser.locator_namespace}::Element::Validator") ||
|
39
|
-
|
45
|
+
Locators::Element::Validator
|
40
46
|
end
|
41
47
|
|
42
48
|
def selector_builder_class
|
43
49
|
class_from_string("#{browser.locator_namespace}::#{element_class_name}::SelectorBuilder") ||
|
44
50
|
class_from_string("Watir::Locators::#{element_class_name}::SelectorBuilder") ||
|
45
51
|
class_from_string("#{browser.locator_namespace}::Element::SelectorBuilder") ||
|
46
|
-
|
52
|
+
Locators::Element::SelectorBuilder
|
47
53
|
end
|
48
54
|
|
49
55
|
def class_from_string(string)
|
@@ -57,8 +63,13 @@ module Watir
|
|
57
63
|
end
|
58
64
|
|
59
65
|
def build_locator
|
66
|
+
selector_builder = if element_class == Watir::Row
|
67
|
+
scope_tag_name = @query_scope.selector[:tag_name]
|
68
|
+
selector_builder_class.new(element_class.attribute_list, scope_tag_name)
|
69
|
+
else
|
70
|
+
selector_builder_class.new(element_class.attribute_list)
|
71
|
+
end
|
60
72
|
element_validator = element_validator_class.new
|
61
|
-
selector_builder = selector_builder_class.new(@query_scope, @selector.dup, element_class.attribute_list)
|
62
73
|
locator_class.new(@query_scope, @selector.dup, selector_builder, element_validator)
|
63
74
|
end
|
64
75
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Watir
|
2
|
+
module Locators
|
3
|
+
class Anchor
|
4
|
+
class SelectorBuilder < Element::SelectorBuilder
|
5
|
+
private
|
6
|
+
|
7
|
+
def build_wd_selector(selector)
|
8
|
+
build_link_text(selector) || build_partial_link_text(selector) || super
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_link_text(selector)
|
12
|
+
return unless can_convert_to_link_text?(selector)
|
13
|
+
|
14
|
+
selector.delete(:tag_name)
|
15
|
+
{link_text: selector.delete(:visible_text)}
|
16
|
+
end
|
17
|
+
|
18
|
+
def can_convert_to_link_text?(selector)
|
19
|
+
selector.keys.sort == %i[tag_name visible_text] &&
|
20
|
+
selector[:visible_text].is_a?(String)
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_partial_link_text(selector)
|
24
|
+
return unless convert_to_partial_link_text?(selector)
|
25
|
+
|
26
|
+
selector.delete(:tag_name)
|
27
|
+
{partial_link_text: selector.delete(:visible_text).source}
|
28
|
+
end
|
29
|
+
|
30
|
+
def convert_to_partial_link_text?(selector)
|
31
|
+
regex = selector[:visible_text]
|
32
|
+
selector.keys.sort == %i[tag_name visible_text] && !regex.casefold? &&
|
33
|
+
RegexpDisassembler.new(regex).substrings.first == regex.source
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -8,21 +8,23 @@ module Watir
|
|
8
8
|
# force watir usage
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
def matches_values?(element, values_to_match)
|
12
|
+
return super unless values_to_match.key?(:value)
|
13
|
+
|
14
|
+
copy = values_to_match.dup
|
15
|
+
value = copy.delete(:value)
|
15
16
|
|
16
|
-
|
17
|
-
if selector.key?(:value)
|
18
|
-
copy = selector.dup
|
19
|
-
value = copy.delete(:value)
|
17
|
+
everything_except_value = super(element, copy)
|
20
18
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
matches_value = fetch_value(element, :value) =~ /#{value}/
|
20
|
+
matches_text = fetch_value(element, :text) =~ /#{value}/
|
21
|
+
if matches_text
|
22
|
+
Watir.logger.deprecate(':value locator key for finding button text',
|
23
|
+
'use :text locator',
|
24
|
+
ids: [:value_button])
|
25
25
|
end
|
26
|
+
|
27
|
+
everything_except_value && (matches_value || matches_text)
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|
@@ -2,28 +2,6 @@ module Watir
|
|
2
2
|
module Locators
|
3
3
|
class Button
|
4
4
|
class SelectorBuilder < Element::SelectorBuilder
|
5
|
-
def build_wd_selector(selectors)
|
6
|
-
return if selectors.values.any? { |e| e.is_a? Regexp }
|
7
|
-
|
8
|
-
selectors.delete(:tag_name) || raise('internal error: no tag_name?!')
|
9
|
-
|
10
|
-
button_attr_exp = xpath_builder.attribute_expression(:button, selectors)
|
11
|
-
|
12
|
-
xpath = './/button'
|
13
|
-
xpath << "[#{button_attr_exp}]" unless button_attr_exp.empty?
|
14
|
-
|
15
|
-
unless selectors[:type].eql? false
|
16
|
-
selectors[:type] = Watir::Button::VALID_TYPES if [nil, true].include?(selectors[:type])
|
17
|
-
input_attr_exp = xpath_builder.attribute_expression(:input, selectors)
|
18
|
-
|
19
|
-
xpath << ' | .//input'
|
20
|
-
xpath << "[#{input_attr_exp}]"
|
21
|
-
end
|
22
|
-
|
23
|
-
p build_wd_selector: xpath if $DEBUG
|
24
|
-
|
25
|
-
[:xpath, xpath]
|
26
|
-
end
|
27
5
|
end
|
28
6
|
end
|
29
7
|
end
|
@@ -3,25 +3,80 @@ module Watir
|
|
3
3
|
class Button
|
4
4
|
class SelectorBuilder
|
5
5
|
class XPath < Element::SelectorBuilder::XPath
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
private
|
7
|
+
|
8
|
+
def tag_string
|
9
|
+
return super if @adjacent
|
10
|
+
|
11
|
+
# Selector builder ignores tag name and builds for both button elements and input elements of type button
|
12
|
+
@selector.delete(:tag_name)
|
13
|
+
|
14
|
+
type = @selector.delete(:type)
|
15
|
+
text = @selector.delete(:text)
|
16
|
+
|
17
|
+
string = "(#{button_string(text: text, type: type)})"
|
18
|
+
string << " or (#{input_string(text: text, type: type)})" unless type.eql?(false)
|
19
|
+
"[#{string}]"
|
20
|
+
end
|
21
|
+
|
22
|
+
def button_string(text: nil, type: nil)
|
23
|
+
string = process_attribute(:tag_name, 'button')
|
24
|
+
string << " and #{process_attribute(:text, text)}" unless text.nil?
|
25
|
+
string << " and #{input_types(type)}" unless type.nil?
|
26
|
+
string
|
27
|
+
end
|
28
|
+
|
29
|
+
def input_string(text: nil, type: nil)
|
30
|
+
string = process_attribute(:tag_name, 'input')
|
31
|
+
type = nil if type.eql?(true)
|
32
|
+
string << " and (#{input_types(type)})"
|
33
|
+
if text
|
34
|
+
string << " and #{process_attribute(:value, text)}"
|
35
|
+
@requires_matches.delete(:value)
|
11
36
|
end
|
37
|
+
string
|
12
38
|
end
|
13
39
|
|
14
|
-
|
40
|
+
# value locator needs to match input value, button text or button value
|
41
|
+
def text_string
|
42
|
+
return super if @adjacent
|
43
|
+
|
44
|
+
# :text locator is already dealt with in #tag_name_string
|
45
|
+
value = @selector.delete(:value)
|
15
46
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
"(text
|
47
|
+
case value
|
48
|
+
when nil
|
49
|
+
''
|
50
|
+
when Regexp
|
51
|
+
res = "[#{predicate_conversion(:text, value)} or #{predicate_conversion(:value, value)}]"
|
52
|
+
@requires_matches.delete(:text)
|
53
|
+
res
|
21
54
|
else
|
22
|
-
|
55
|
+
"[#{predicate_expression(:text, value)} or #{predicate_expression(:value, value)}]"
|
23
56
|
end
|
24
57
|
end
|
58
|
+
|
59
|
+
def predicate_conversion(key, regexp)
|
60
|
+
res = key == :text ? super(:contains_text, regexp) : super
|
61
|
+
@requires_matches[key] = @requires_matches.delete(:contains_text) if @requires_matches.key?(:contains_text)
|
62
|
+
res
|
63
|
+
end
|
64
|
+
|
65
|
+
def input_types(type = nil)
|
66
|
+
types = if type.eql?(nil)
|
67
|
+
Watir::Button::VALID_TYPES
|
68
|
+
elsif Watir::Button::VALID_TYPES.include?(type)
|
69
|
+
[type]
|
70
|
+
elsif type.eql?(true) || type.eql?(false)
|
71
|
+
[type]
|
72
|
+
else
|
73
|
+
msg = "Button Elements can not be located by input type: #{type}"
|
74
|
+
raise LocatorException, msg
|
75
|
+
end
|
76
|
+
types.map { |button_type|
|
77
|
+
predicate_expression(:type, button_type)
|
78
|
+
}.compact.join(' or ')
|
79
|
+
end
|
25
80
|
end
|
26
81
|
end
|
27
82
|
end
|
@@ -2,9 +2,10 @@ module Watir
|
|
2
2
|
module Locators
|
3
3
|
class Button
|
4
4
|
class Validator < Element::Validator
|
5
|
-
def validate(element,
|
5
|
+
def validate(element, _tag_name)
|
6
6
|
tag_name = element.tag_name.downcase
|
7
7
|
return unless %w[input button].include?(tag_name)
|
8
|
+
|
8
9
|
# TODO: - Verify this is desired behavior based on https://bugzilla.mozilla.org/show_bug.cgi?id=1290963
|
9
10
|
return if tag_name == 'input' && !Watir::Button::VALID_TYPES.include?(element.attribute(:type).downcase)
|
10
11
|
|
@@ -2,20 +2,6 @@ module Watir
|
|
2
2
|
module Locators
|
3
3
|
class Cell
|
4
4
|
class SelectorBuilder < Element::SelectorBuilder
|
5
|
-
def build_wd_selector(selectors)
|
6
|
-
return if selectors.values.any? { |e| e.is_a? Regexp }
|
7
|
-
|
8
|
-
expressions = %w[./th ./td]
|
9
|
-
attr_expr = xpath_builder.attribute_expression(nil, selectors)
|
10
|
-
|
11
|
-
expressions.map! { |e| "#{e}[#{attr_expr}]" } unless attr_expr.empty?
|
12
|
-
|
13
|
-
xpath = expressions.join(' | ')
|
14
|
-
|
15
|
-
p build_wd_selector: xpath if $DEBUG
|
16
|
-
|
17
|
-
[:xpath, xpath]
|
18
|
-
end
|
19
5
|
end
|
20
6
|
end
|
21
7
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Watir
|
2
|
+
module Locators
|
3
|
+
class Cell
|
4
|
+
class SelectorBuilder
|
5
|
+
class XPath < Element::SelectorBuilder::XPath
|
6
|
+
private
|
7
|
+
|
8
|
+
def start_string
|
9
|
+
@adjacent ? './' : './*'
|
10
|
+
end
|
11
|
+
|
12
|
+
def tag_string
|
13
|
+
return super if @adjacent
|
14
|
+
|
15
|
+
"[#{process_attribute(:tag_name, 'th')} or #{process_attribute(:tag_name, 'td')}]"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,6 +2,8 @@ module Watir
|
|
2
2
|
module Locators
|
3
3
|
class Element
|
4
4
|
class Locator
|
5
|
+
include Exception
|
6
|
+
|
5
7
|
attr_reader :selector_builder
|
6
8
|
attr_reader :element_validator
|
7
9
|
|
@@ -14,19 +16,9 @@ module Watir
|
|
14
16
|
xpath
|
15
17
|
].freeze
|
16
18
|
|
17
|
-
# Regular expressions that can be reliably converted to xpath `contains`
|
18
|
-
# expressions in order to optimize the locator.
|
19
|
-
CONVERTABLE_REGEXP = /
|
20
|
-
\A
|
21
|
-
([^\[\]\\^$.|?*+()]*) # leading literal characters
|
22
|
-
[^|]*? # do not try to convert expressions with alternates
|
23
|
-
([^\[\]\\^$.|?*+()]*) # trailing literal characters
|
24
|
-
\z
|
25
|
-
/x
|
26
|
-
|
27
19
|
def initialize(query_scope, selector, selector_builder, element_validator)
|
28
20
|
@query_scope = query_scope # either element or browser
|
29
|
-
@selector = selector
|
21
|
+
@selector = selector
|
30
22
|
@selector_builder = selector_builder
|
31
23
|
@element_validator = element_validator
|
32
24
|
end
|
@@ -46,11 +38,12 @@ module Watir
|
|
46
38
|
private
|
47
39
|
|
48
40
|
def using_selenium(filter = :first)
|
49
|
-
|
50
|
-
|
41
|
+
selector = @selector.dup
|
42
|
+
tag = selector[:tag_name].is_a?(::Symbol) ? selector.delete(:tag_name).to_s : selector.delete(:tag_name)
|
43
|
+
return if selector.size > 1
|
51
44
|
|
52
|
-
how =
|
53
|
-
what =
|
45
|
+
how = selector.keys.first || :tag_name
|
46
|
+
what = selector.values.first || tag
|
54
47
|
|
55
48
|
return unless wd_supported?(how, what, tag)
|
56
49
|
|
@@ -58,27 +51,33 @@ module Watir
|
|
58
51
|
end
|
59
52
|
|
60
53
|
def using_watir(filter = :first)
|
61
|
-
|
62
|
-
return unless @normalized_selector
|
63
|
-
|
64
|
-
create_filter_selector
|
54
|
+
raise ArgumentError, "can't locate all elements by :index" if @selector.key?(:index) && filter == :all
|
65
55
|
|
66
|
-
|
67
|
-
|
68
|
-
|
56
|
+
begin
|
57
|
+
generate_scope
|
58
|
+
rescue LocatorException
|
59
|
+
return nil
|
69
60
|
end
|
70
61
|
|
71
|
-
|
62
|
+
selector, values_to_match = selector_builder.build(@selector)
|
63
|
+
|
64
|
+
validate_built_selector(selector, values_to_match)
|
72
65
|
|
73
|
-
if filter == :all ||
|
74
|
-
|
66
|
+
if filter == :all || values_to_match.any?
|
67
|
+
locate_matching_elements(selector, values_to_match, filter)
|
75
68
|
else
|
76
|
-
locate_element(
|
69
|
+
locate_element(selector.keys.first, selector.values.first, @driver_scope)
|
77
70
|
end
|
78
71
|
end
|
79
72
|
|
80
|
-
def
|
81
|
-
|
73
|
+
def validate_built_selector(selector, values_to_match)
|
74
|
+
if selector.nil?
|
75
|
+
msg = "#{selector_builder.class} was unable to build selector from #{@selector.inspect}"
|
76
|
+
raise LocatorException, msg
|
77
|
+
elsif values_to_match.nil?
|
78
|
+
msg = "#{selector_builder.class}#build is not returning expected responses for the current version of Watir"
|
79
|
+
raise LocatorException, msg
|
80
|
+
end
|
82
81
|
end
|
83
82
|
|
84
83
|
def fetch_value(element, how)
|
@@ -93,121 +92,61 @@ module Watir
|
|
93
92
|
element.tag_name.downcase
|
94
93
|
when :href
|
95
94
|
element.attribute('href')&.strip
|
96
|
-
|
95
|
+
else
|
97
96
|
how = how.to_s.tr('_', '-') if how.is_a?(::Symbol)
|
98
97
|
element.attribute(how)
|
99
|
-
else
|
100
|
-
raise Error::Exception, "Unable to fetch value for #{how}"
|
101
98
|
end
|
102
99
|
end
|
103
100
|
|
104
|
-
def
|
105
|
-
selector = @filter_selector.dup
|
101
|
+
def matching_elements(elements, values_to_match, filter: :first)
|
106
102
|
if filter == :first
|
107
|
-
idx = element_index(elements,
|
103
|
+
idx = element_index(elements, values_to_match)
|
108
104
|
counter = 0
|
109
105
|
|
110
106
|
# Lazy evaluation to avoid fetching values for elements that will be discarded
|
111
107
|
matches = elements.lazy.select do |el|
|
112
108
|
counter += 1
|
113
|
-
|
109
|
+
matches_values?(el, values_to_match)
|
114
110
|
end
|
115
|
-
msg = "
|
111
|
+
msg = "iterated through #{counter} elements to locate #{@selector.inspect}"
|
116
112
|
matches.take(idx + 1).to_a[idx].tap { Watir.logger.debug msg }
|
117
113
|
else
|
118
114
|
Watir.logger.debug "Iterated through #{elements.size} elements to locate all #{@selector.inspect}"
|
119
|
-
elements.select { |el|
|
115
|
+
elements.select { |el| matches_values?(el, values_to_match) }
|
120
116
|
end
|
121
117
|
end
|
122
118
|
|
123
|
-
def element_index(elements,
|
124
|
-
idx =
|
119
|
+
def element_index(elements, values_to_match)
|
120
|
+
idx = values_to_match.delete(:index) || 0
|
125
121
|
return idx unless idx.negative?
|
126
122
|
|
127
123
|
elements.reverse!
|
128
124
|
idx.abs - 1
|
129
125
|
end
|
130
126
|
|
131
|
-
def
|
132
|
-
return @
|
127
|
+
def generate_scope
|
128
|
+
return @driver_scope if @driver_scope
|
133
129
|
|
134
130
|
@driver_scope = @query_scope.wd
|
135
131
|
|
136
|
-
@
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
elsif @normalized_selector.key?(:visible_label)
|
141
|
-
label_key = :visible_label
|
142
|
-
end
|
143
|
-
|
144
|
-
if label_key
|
145
|
-
process_label(label_key)
|
146
|
-
return if @normalized_selector.nil?
|
147
|
-
end
|
148
|
-
|
149
|
-
if @normalized_selector.key?(:index) && filter == :all
|
150
|
-
raise ArgumentError, "can't locate all elements by :index"
|
151
|
-
end
|
152
|
-
|
153
|
-
@normalized_selector
|
154
|
-
end
|
155
|
-
|
156
|
-
def create_filter_selector
|
157
|
-
return @filter_selector if @filter_selector
|
158
|
-
|
159
|
-
@filter_selector = {}
|
160
|
-
|
161
|
-
# Remove selectors that can never be used in XPath builder
|
162
|
-
%i[visible visible_text].each do |how|
|
163
|
-
next unless @normalized_selector.key?(how)
|
164
|
-
|
165
|
-
@filter_selector[how] = @normalized_selector.delete(how)
|
166
|
-
end
|
167
|
-
|
168
|
-
set_tag_validation if tag_validation_required?(@normalized_selector)
|
169
|
-
|
170
|
-
# Regexp locators currently need to be validated even if they are included in the XPath builder
|
171
|
-
# TODO: Identify Regexp that can have an exact equivalent using XPath contains (ie would not require
|
172
|
-
# filtering) vs approximations (ie would still requiring filtering)
|
173
|
-
@normalized_selector.each do |how, what|
|
174
|
-
next unless what.is_a?(Regexp)
|
175
|
-
|
176
|
-
@filter_selector[how] = @normalized_selector.delete(how)
|
177
|
-
end
|
178
|
-
|
179
|
-
if @normalized_selector[:index] && !@normalized_selector[:adjacent]
|
180
|
-
idx = @normalized_selector.delete(:index)
|
181
|
-
|
182
|
-
# Do not add {index: 0} filter if the only filter.
|
183
|
-
# This will allow using #find_element instead of #find_elements.
|
184
|
-
implicit_idx_filter = @filter_selector.empty? && idx.zero?
|
185
|
-
@filter_selector[:index] = idx unless implicit_idx_filter
|
132
|
+
if @selector.key?(:label)
|
133
|
+
process_label :label
|
134
|
+
elsif @selector.key?(:visible_label)
|
135
|
+
process_label :visible_label
|
186
136
|
end
|
187
|
-
|
188
|
-
@filter_selector
|
189
|
-
end
|
190
|
-
|
191
|
-
def set_tag_validation
|
192
|
-
@filter_selector[:tag_name] = if @normalized_selector[:tag_name].is_a?(::Symbol)
|
193
|
-
@normalized_selector[:tag_name].to_s
|
194
|
-
else
|
195
|
-
@normalized_selector[:tag_name]
|
196
|
-
end
|
197
137
|
end
|
198
138
|
|
199
139
|
def process_label(label_key)
|
200
|
-
regexp = @
|
140
|
+
regexp = @selector[label_key].is_a?(Regexp)
|
201
141
|
return unless (regexp || label_key == :visible_label) && selector_builder.should_use_label_element?
|
202
142
|
|
203
143
|
label = label_from_text(label_key)
|
204
|
-
|
205
|
-
|
206
|
-
return
|
207
|
-
end
|
144
|
+
msg = "Unable to locate element with label #{label_key}: #{@selector[label_key]}"
|
145
|
+
raise LocatorException, msg unless label
|
208
146
|
|
209
|
-
|
210
|
-
|
147
|
+
id = label.attribute('for')
|
148
|
+
if id
|
149
|
+
@selector[:id] = id
|
211
150
|
else
|
212
151
|
@driver_scope = label
|
213
152
|
end
|
@@ -216,24 +155,24 @@ module Watir
|
|
216
155
|
def label_from_text(label_key)
|
217
156
|
# TODO: this won't work correctly if @wd is a sub-element, write spec
|
218
157
|
# TODO: Figure out how to do this with find_element
|
219
|
-
label_text = @
|
220
|
-
locator_key = label_key.to_s.gsub('label', 'text').to_sym
|
158
|
+
label_text = @selector.delete(label_key)
|
159
|
+
locator_key = label_key.to_s.gsub('label', 'text').gsub('_element', '').to_sym
|
221
160
|
locate_elements(:tag_name, 'label', @driver_scope).find do |el|
|
222
|
-
|
161
|
+
matches_values?(el, locator_key => label_text)
|
223
162
|
end
|
224
163
|
end
|
225
164
|
|
226
|
-
def
|
227
|
-
matches =
|
165
|
+
def matches_values?(element, values_to_match)
|
166
|
+
matches = values_to_match.all? do |how, what|
|
228
167
|
if how == :tag_name && what.is_a?(String)
|
229
|
-
element_validator.validate(element,
|
168
|
+
element_validator.validate(element, what)
|
230
169
|
else
|
231
170
|
val = fetch_value(element, how)
|
232
171
|
what == val || val =~ /#{what}/
|
233
172
|
end
|
234
173
|
end
|
235
174
|
|
236
|
-
text_regexp_deprecation(element,
|
175
|
+
text_regexp_deprecation(element, values_to_match, matches) if values_to_match[:text]
|
237
176
|
|
238
177
|
matches
|
239
178
|
end
|
@@ -250,38 +189,6 @@ module Watir
|
|
250
189
|
Watir.logger.deprecate(dep, ":visible_#{key}", ids: [:text_regexp])
|
251
190
|
end
|
252
191
|
|
253
|
-
def can_convert_regexp_to_contains?
|
254
|
-
true
|
255
|
-
end
|
256
|
-
|
257
|
-
def add_regexp_predicates(what)
|
258
|
-
return what unless can_convert_regexp_to_contains?
|
259
|
-
|
260
|
-
@filter_selector.each do |key, value|
|
261
|
-
next if %i[tag_name text visible_text visible index].include?(key)
|
262
|
-
|
263
|
-
predicates = regexp_selector_to_predicates(key, value)
|
264
|
-
what = "(#{what})[#{predicates.join(' and ')}]" unless predicates.empty?
|
265
|
-
end
|
266
|
-
what
|
267
|
-
end
|
268
|
-
|
269
|
-
def regexp_selector_to_predicates(key, regexp)
|
270
|
-
return [] if regexp.casefold?
|
271
|
-
|
272
|
-
match = regexp.source.match(CONVERTABLE_REGEXP)
|
273
|
-
return [] unless match
|
274
|
-
|
275
|
-
lhs = selector_builder.xpath_builder.lhs_for(nil, key)
|
276
|
-
match.captures.reject(&:empty?).map do |literals|
|
277
|
-
"contains(#{lhs}, #{XpathSupport.escape(literals)})"
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
def tag_validation_required?(selector)
|
282
|
-
(selector.key?(:css) || selector.key?(:xpath)) && selector.key?(:tag_name)
|
283
|
-
end
|
284
|
-
|
285
192
|
def locate_element(how, what, scope = @query_scope.wd)
|
286
193
|
scope.find_element(how, what)
|
287
194
|
end
|
@@ -290,17 +197,17 @@ module Watir
|
|
290
197
|
scope.find_elements(how, what)
|
291
198
|
end
|
292
199
|
|
293
|
-
def
|
200
|
+
def locate_matching_elements(selector, values_to_match, filter)
|
294
201
|
retries = 0
|
295
202
|
begin
|
296
|
-
elements = locate_elements(
|
297
|
-
|
203
|
+
elements = locate_elements(selector.keys.first, selector.values.first, @driver_scope) || []
|
204
|
+
matching_elements(elements, values_to_match, filter: filter)
|
298
205
|
rescue Selenium::WebDriver::Error::StaleElementReferenceError
|
299
206
|
retries += 1
|
300
207
|
sleep 0.5
|
301
208
|
retry unless retries > 2
|
302
209
|
target = filter == :all ? 'element collection' : 'element'
|
303
|
-
raise
|
210
|
+
raise LocatorException, "Unable to locate #{target} from #{@selector} due to changing page"
|
304
211
|
end
|
305
212
|
end
|
306
213
|
|
@@ -309,10 +216,10 @@ module Watir
|
|
309
216
|
return false unless what.is_a?(String)
|
310
217
|
|
311
218
|
if %i[partial_link_text link_text link].include?(how)
|
312
|
-
Watir.logger.deprecate(":#{how} locator", ':visible_text', ids: [:
|
219
|
+
Watir.logger.deprecate(":#{how} locator", ':visible_text', ids: [:link_text])
|
313
220
|
return true if [:a, :link, nil].include?(tag)
|
314
221
|
|
315
|
-
raise
|
222
|
+
raise LocatorException, "Can not use #{how} locator to find a #{what} element"
|
316
223
|
elsif how == :tag_name
|
317
224
|
return true
|
318
225
|
else
|