capybara 3.11.1 → 3.12.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/History.md +18 -1
- data/README.md +4 -4
- data/lib/capybara.rb +7 -0
- data/lib/capybara/node/element.rb +7 -1
- data/lib/capybara/node/matchers.rb +0 -18
- data/lib/capybara/queries/base_query.rb +1 -1
- data/lib/capybara/queries/selector_query.rb +24 -39
- data/lib/capybara/result.rb +4 -4
- data/lib/capybara/selector.rb +7 -15
- data/lib/capybara/selector/builders/css_builder.rb +59 -27
- data/lib/capybara/selector/builders/xpath_builder.rb +50 -35
- data/lib/capybara/selector/css.rb +7 -7
- data/lib/capybara/selector/filter.rb +1 -0
- data/lib/capybara/selector/filter_set.rb +17 -15
- data/lib/capybara/selector/filters/locator_filter.rb +19 -0
- data/lib/capybara/selector/regexp_disassembler.rb +104 -61
- data/lib/capybara/selector/selector.rb +14 -5
- data/lib/capybara/selenium/driver.rb +14 -9
- data/lib/capybara/selenium/driver_specializations/{marionette_driver.rb → firefox_driver.rb} +3 -3
- data/lib/capybara/selenium/nodes/{marionette_node.rb → firefox_node.rb} +1 -1
- data/lib/capybara/spec/session/all_spec.rb +8 -1
- data/lib/capybara/spec/session/assert_selector_spec.rb +0 -10
- data/lib/capybara/spec/session/click_button_spec.rb +5 -3
- data/lib/capybara/spec/session/click_link_spec.rb +5 -5
- data/lib/capybara/spec/session/find_spec.rb +1 -1
- data/lib/capybara/spec/session/first_spec.rb +1 -1
- data/lib/capybara/spec/session/has_css_spec.rb +7 -0
- data/lib/capybara/spec/session/has_xpath_spec.rb +17 -0
- data/lib/capybara/spec/session/node_spec.rb +10 -3
- data/lib/capybara/spec/session/window/window_spec.rb +2 -2
- data/lib/capybara/spec/spec_helper.rb +1 -2
- data/lib/capybara/spec/views/obscured.erb +3 -0
- data/lib/capybara/version.rb +1 -1
- data/spec/css_builder_spec.rb +99 -0
- data/spec/result_spec.rb +6 -0
- data/spec/selector_spec.rb +26 -1
- data/spec/selenium_spec_chrome.rb +18 -16
- data/spec/selenium_spec_chrome_remote.rb +0 -2
- data/spec/{selenium_spec_marionette.rb → selenium_spec_firefox.rb} +31 -25
- data/spec/selenium_spec_firefox_remote.rb +4 -6
- data/spec/shared_selenium_session.rb +2 -2
- data/spec/spec_helper.rb +5 -5
- data/spec/xpath_builder_spec.rb +91 -0
- metadata +10 -7
@@ -6,49 +6,64 @@ module Capybara
|
|
6
6
|
class Selector
|
7
7
|
# @api private
|
8
8
|
class XPathBuilder
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
XPath.attr(attribute)[value]
|
15
|
-
when Regexp
|
16
|
-
XPath.attr(attribute)[regexp_to_xpath_conditions(value)]
|
17
|
-
when true
|
18
|
-
XPath.attr(attribute)
|
19
|
-
when false, nil
|
20
|
-
!XPath.attr(attribute)
|
21
|
-
else
|
22
|
-
XPath.attr(attribute) == value.to_s
|
23
|
-
end
|
24
|
-
end.reduce(:&)
|
25
|
-
end
|
9
|
+
def initialize(expression)
|
10
|
+
@expression = expression || ''
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :expression
|
26
14
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
15
|
+
def add_attribute_conditions(**conditions)
|
16
|
+
@expression = conditions.inject(expression) do |xp, (name, value)|
|
17
|
+
conditions = name == :class ? class_conditions(value) : attribute_conditions(name => value)
|
18
|
+
if xp.is_a? XPath::Expression
|
19
|
+
xp[conditions]
|
31
20
|
else
|
32
|
-
|
33
|
-
if klass.start_with?('!') && !klass.start_with?('!!!')
|
34
|
-
!XPath.attr(:class).contains_word(klass.slice(1..-1))
|
35
|
-
else
|
36
|
-
XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
|
37
|
-
end
|
38
|
-
end.reduce(:&)
|
21
|
+
"(#{xp})[#{conditions}]"
|
39
22
|
end
|
40
23
|
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
41
27
|
|
42
|
-
|
28
|
+
def attribute_conditions(attributes)
|
29
|
+
attributes.map do |attribute, value|
|
30
|
+
case value
|
31
|
+
when XPath::Expression
|
32
|
+
XPath.attr(attribute)[value]
|
33
|
+
when Regexp
|
34
|
+
XPath.attr(attribute)[regexp_to_xpath_conditions(value)]
|
35
|
+
when true
|
36
|
+
XPath.attr(attribute)
|
37
|
+
when false, nil
|
38
|
+
!XPath.attr(attribute)
|
39
|
+
else
|
40
|
+
XPath.attr(attribute) == value.to_s
|
41
|
+
end
|
42
|
+
end.reduce(:&)
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
def class_conditions(classes)
|
46
|
+
case classes
|
47
|
+
when XPath::Expression, Regexp
|
48
|
+
attribute_conditions(class: classes)
|
49
|
+
else
|
50
|
+
Array(classes).map do |klass|
|
51
|
+
if klass.start_with?('!') && !klass.start_with?('!!!')
|
52
|
+
!XPath.attr(:class).contains_word(klass.slice(1..-1))
|
53
|
+
else
|
54
|
+
XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
|
55
|
+
end
|
56
|
+
end.reduce(:&)
|
50
57
|
end
|
51
58
|
end
|
59
|
+
|
60
|
+
def regexp_to_xpath_conditions(regexp)
|
61
|
+
condition = XPath.current
|
62
|
+
condition = condition.uppercase if regexp.casefold?
|
63
|
+
Selector::RegexpDisassembler.new(regexp).alternated_substrings.map do |strs|
|
64
|
+
strs.map { |str| condition.contains(str) }.reduce(:&)
|
65
|
+
end.reduce(:|)
|
66
|
+
end
|
52
67
|
end
|
53
68
|
end
|
54
69
|
end
|
@@ -31,22 +31,22 @@ module Capybara
|
|
31
31
|
def split(css)
|
32
32
|
selectors = []
|
33
33
|
StringIO.open(css.to_s) do |str|
|
34
|
-
selector = ''
|
34
|
+
selector = +''
|
35
35
|
while (char = str.getc)
|
36
36
|
case char
|
37
37
|
when '['
|
38
|
-
selector
|
38
|
+
selector << parse_square(str)
|
39
39
|
when '('
|
40
|
-
selector
|
40
|
+
selector << parse_paren(str)
|
41
41
|
when '"', "'"
|
42
|
-
selector
|
42
|
+
selector << parse_string(char, str)
|
43
43
|
when '\\'
|
44
|
-
selector
|
44
|
+
selector << char + str.getc
|
45
45
|
when ','
|
46
46
|
selectors << selector.strip
|
47
|
-
selector
|
47
|
+
selector.clear
|
48
48
|
else
|
49
|
-
selector
|
49
|
+
selector << char
|
50
50
|
end
|
51
51
|
end
|
52
52
|
selectors << selector.strip
|
@@ -40,9 +40,9 @@ module Capybara
|
|
40
40
|
def description(node_filters: true, expression_filters: true, **options)
|
41
41
|
opts = options_with_defaults(options)
|
42
42
|
description = +''
|
43
|
-
description
|
44
|
-
description
|
45
|
-
description
|
43
|
+
description << undeclared_descriptions.map { |desc| desc.call(opts).to_s }.join
|
44
|
+
description << expression_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if expression_filters
|
45
|
+
description << node_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if node_filters
|
46
46
|
description
|
47
47
|
end
|
48
48
|
|
@@ -52,15 +52,16 @@ module Capybara
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def import(name, filters = nil)
|
55
|
-
f_set = self.class.all[name]
|
56
55
|
filter_selector = filters.nil? ? ->(*) { true } : ->(filter_name, _) { filters.include? filter_name }
|
57
56
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
self.class[name].tap do |f_set|
|
58
|
+
expression_filters.merge!(f_set.expression_filters.select(&filter_selector))
|
59
|
+
node_filters.merge!(f_set.node_filters.select(&filter_selector))
|
60
|
+
f_set.undeclared_descriptions.each { |desc| describe(&desc) }
|
61
|
+
f_set.expression_filter_descriptions.each { |desc| describe(:expression_filters, &desc) }
|
62
|
+
f_set.node_filter_descriptions.each { |desc| describe(:node_filters, &desc) }
|
63
|
+
end
|
64
|
+
self
|
64
65
|
end
|
65
66
|
|
66
67
|
class << self
|
@@ -68,6 +69,10 @@ module Capybara
|
|
68
69
|
@filter_sets ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
|
69
70
|
end
|
70
71
|
|
72
|
+
def [](name)
|
73
|
+
all.fetch(name.to_sym) { |set_name| raise ArgumentError, "Unknown filter set (:#{set_name})" }
|
74
|
+
end
|
75
|
+
|
71
76
|
def add(name, &block)
|
72
77
|
all[name.to_sym] = FilterSet.new(name.to_sym, &block)
|
73
78
|
end
|
@@ -107,11 +112,8 @@ module Capybara
|
|
107
112
|
types.each { |type| options[type] = true }
|
108
113
|
raise 'ArgumentError', ':default option is not supported for filters with a :matcher option' if matcher && options[:default]
|
109
114
|
|
110
|
-
|
111
|
-
|
112
|
-
else
|
113
|
-
@node_filters[name] = filter_class.new(name, matcher, block, options)
|
114
|
-
end
|
115
|
+
filter = filter_class.new(name, matcher, block, options)
|
116
|
+
(filter_class <= Filters::ExpressionFilter ? @expression_filters : @node_filters)[name] = filter
|
115
117
|
end
|
116
118
|
end
|
117
119
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/selector/filters/base'
|
4
|
+
|
5
|
+
module Capybara
|
6
|
+
class Selector
|
7
|
+
module Filters
|
8
|
+
class LocatorFilter < NodeFilter
|
9
|
+
def initialize(block, **options)
|
10
|
+
super(nil, nil, block, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(node, value, context = nil)
|
14
|
+
super(node, nil, value, context)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -65,22 +65,6 @@ module Capybara
|
|
65
65
|
strs
|
66
66
|
end
|
67
67
|
|
68
|
-
def min_repeat(exp)
|
69
|
-
exp.quantifier&.min || 1
|
70
|
-
end
|
71
|
-
|
72
|
-
def max_repeat(exp)
|
73
|
-
exp.quantifier&.max || 1
|
74
|
-
end
|
75
|
-
|
76
|
-
def fixed_repeat?(exp)
|
77
|
-
min_repeat(exp) == max_repeat(exp)
|
78
|
-
end
|
79
|
-
|
80
|
-
def optional?(exp)
|
81
|
-
min_repeat(exp).zero?
|
82
|
-
end
|
83
|
-
|
84
68
|
def combine(strs)
|
85
69
|
suffixes = [[]]
|
86
70
|
strs.reverse_each do |str|
|
@@ -91,9 +75,7 @@ module Capybara
|
|
91
75
|
prefixes.product(suffixes) { |pair| result << pair.flatten(1) }
|
92
76
|
suffixes = result
|
93
77
|
else
|
94
|
-
suffixes.each
|
95
|
-
arr.unshift str
|
96
|
-
end
|
78
|
+
suffixes.each { |arr| arr.unshift str }
|
97
79
|
end
|
98
80
|
end
|
99
81
|
suffixes
|
@@ -106,59 +88,120 @@ module Capybara
|
|
106
88
|
end
|
107
89
|
|
108
90
|
def extract_strings(expression, alternation: false)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
91
|
+
Expression.new(expression).extract_strings(alternation)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @api private
|
95
|
+
class Expression
|
96
|
+
def initialize(exp)
|
97
|
+
@exp = exp
|
98
|
+
end
|
99
|
+
|
100
|
+
def extract_strings(process_alternatives)
|
101
|
+
strings = []
|
102
|
+
each do |exp|
|
103
|
+
next strings.push(nil) if exp.optional? && !process_alternatives
|
115
104
|
|
116
|
-
|
117
|
-
|
118
|
-
|
105
|
+
next strings.push(exp.alternative_strings) if exp.alternation? && process_alternatives
|
106
|
+
|
107
|
+
strings.concat(exp.strings(process_alternatives))
|
119
108
|
end
|
109
|
+
strings
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def alternation?
|
115
|
+
(type == :meta) && !terminal?
|
116
|
+
end
|
117
|
+
|
118
|
+
def optional?
|
119
|
+
min_repeat.zero?
|
120
|
+
end
|
121
|
+
|
122
|
+
def terminal?
|
123
|
+
@exp.terminal?
|
124
|
+
end
|
120
125
|
|
121
|
-
|
122
|
-
|
123
|
-
|
126
|
+
def strings(process_alternatives)
|
127
|
+
if indeterminate?
|
128
|
+
[nil]
|
129
|
+
elsif terminal?
|
130
|
+
terminal_strings
|
131
|
+
elsif optional?
|
132
|
+
optional_strings
|
133
|
+
else
|
134
|
+
repeated_strings(process_alternatives)
|
124
135
|
end
|
136
|
+
end
|
125
137
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
else
|
131
|
-
strings.push(nil)
|
132
|
-
next
|
133
|
-
end
|
134
|
-
|
135
|
-
if optional?(exp)
|
136
|
-
strings.push(Set.new([[''], [text]]))
|
137
|
-
strings.push(nil) unless max_repeat(exp) == 1
|
138
|
-
next
|
139
|
-
else
|
140
|
-
strings.push(text * min_repeat(exp))
|
141
|
-
end
|
142
|
-
elsif optional?(exp)
|
143
|
-
strings.push(Set.new([[''], extract_strings(exp, alternation: true)]))
|
144
|
-
strings.push(nil) unless max_repeat(exp) == 1
|
145
|
-
next
|
138
|
+
def terminal_strings
|
139
|
+
text = case @exp.type
|
140
|
+
when :literal then @exp.text
|
141
|
+
when :escape then @exp.char
|
146
142
|
else
|
147
|
-
|
143
|
+
return [nil]
|
148
144
|
end
|
149
|
-
|
145
|
+
|
146
|
+
optional? ? options_set(text) : repeat_set(text)
|
147
|
+
end
|
148
|
+
|
149
|
+
def optional_strings
|
150
|
+
options_set(extract_strings(true))
|
151
|
+
end
|
152
|
+
|
153
|
+
def repeated_strings(process_alternatives)
|
154
|
+
repeat_set extract_strings(process_alternatives)
|
155
|
+
end
|
156
|
+
|
157
|
+
def alternative_strings
|
158
|
+
alts = alternatives.map { |sub_exp| sub_exp.extract_strings(alternation: true) }
|
159
|
+
alts.all?(&:any?) ? Set.new(alts) : nil
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def indeterminate?
|
165
|
+
%i[meta set].include?(type)
|
166
|
+
end
|
167
|
+
|
168
|
+
def min_repeat
|
169
|
+
@exp.quantifier&.min || 1
|
170
|
+
end
|
171
|
+
|
172
|
+
def max_repeat
|
173
|
+
@exp.quantifier&.max || 1
|
174
|
+
end
|
175
|
+
|
176
|
+
def fixed_repeat?
|
177
|
+
min_repeat == max_repeat
|
178
|
+
end
|
179
|
+
|
180
|
+
def type
|
181
|
+
@exp.type
|
182
|
+
end
|
183
|
+
|
184
|
+
def repeat_set(str)
|
185
|
+
strs = Array(str * min_repeat)
|
186
|
+
strs.push(nil) unless fixed_repeat?
|
187
|
+
strs
|
188
|
+
end
|
189
|
+
|
190
|
+
def options_set(strs)
|
191
|
+
strs = [Set.new([[''], Array(strs)])]
|
192
|
+
strs.push(nil) unless max_repeat == 1
|
193
|
+
strs
|
194
|
+
end
|
195
|
+
|
196
|
+
def alternatives
|
197
|
+
@exp.alternatives.map { |exp| Expression.new(exp) }
|
150
198
|
end
|
151
|
-
strings
|
152
|
-
end
|
153
199
|
|
154
|
-
|
155
|
-
|
156
|
-
if alternatives.all?(&:any?)
|
157
|
-
Set.new(alternatives)
|
158
|
-
else
|
159
|
-
nil
|
200
|
+
def each
|
201
|
+
@exp.each { |exp| yield Expression.new(exp) }
|
160
202
|
end
|
161
203
|
end
|
204
|
+
private_constant :Expression
|
162
205
|
end
|
163
206
|
end
|
164
207
|
end
|
@@ -169,17 +169,25 @@ module Capybara
|
|
169
169
|
@selectors ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
|
170
170
|
end
|
171
171
|
|
172
|
+
def [](name)
|
173
|
+
all.fetch(name.to_sym) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
|
174
|
+
end
|
175
|
+
|
172
176
|
def add(name, &block)
|
173
177
|
all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
|
174
178
|
end
|
175
179
|
|
176
180
|
def update(name, &block)
|
177
|
-
|
181
|
+
self[name].instance_eval(&block)
|
178
182
|
end
|
179
183
|
|
180
184
|
def remove(name)
|
181
185
|
all.delete(name.to_sym)
|
182
186
|
end
|
187
|
+
|
188
|
+
def for(locator)
|
189
|
+
all.values.find { |sel| sel.match?(locator) }
|
190
|
+
end
|
183
191
|
end
|
184
192
|
|
185
193
|
def initialize(name, &block)
|
@@ -348,8 +356,9 @@ module Capybara
|
|
348
356
|
|
349
357
|
def_delegators :@filter_set, :node_filter, :expression_filter, :filter
|
350
358
|
|
351
|
-
def locator_filter(&block)
|
352
|
-
|
359
|
+
def locator_filter(*types, **options, &block)
|
360
|
+
types.each { |type| options[type] = true }
|
361
|
+
@locator_filter = Filters::LocatorFilter.new(block, options) if block
|
353
362
|
@locator_filter
|
354
363
|
end
|
355
364
|
|
@@ -400,7 +409,7 @@ module Capybara
|
|
400
409
|
end
|
401
410
|
|
402
411
|
# @api private
|
403
|
-
def builder
|
412
|
+
def builder(expr = nil)
|
404
413
|
case format
|
405
414
|
when :css
|
406
415
|
Capybara::Selector::CSSBuilder
|
@@ -408,7 +417,7 @@ module Capybara
|
|
408
417
|
Capybara::Selector::XPathBuilder
|
409
418
|
else
|
410
419
|
raise NotImplementedError, "No builder exists for selector of type #{format}"
|
411
|
-
end
|
420
|
+
end.new(expr)
|
412
421
|
end
|
413
422
|
|
414
423
|
# @api private
|