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