capybara 3.12.0 → 3.13.2
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 +27 -0
- data/README.md +13 -3
- data/lib/capybara/config.rb +3 -1
- data/lib/capybara/driver/base.rb +2 -2
- data/lib/capybara/driver/node.rb +11 -2
- data/lib/capybara/minitest/spec.rb +10 -3
- data/lib/capybara/minitest.rb +3 -3
- data/lib/capybara/node/actions.rb +4 -0
- data/lib/capybara/node/base.rb +13 -5
- data/lib/capybara/node/document.rb +12 -0
- data/lib/capybara/node/element.rb +37 -0
- data/lib/capybara/node/finders.rb +1 -0
- data/lib/capybara/node/matchers.rb +19 -4
- data/lib/capybara/node/simple.rb +7 -2
- data/lib/capybara/queries/selector_query.rb +93 -9
- data/lib/capybara/rspec/matchers/become_closed.rb +1 -1
- data/lib/capybara/rspec/matchers/match_style.rb +38 -0
- data/lib/capybara/rspec/matchers.rb +11 -3
- data/lib/capybara/selector/selector.rb +47 -3
- data/lib/capybara/selector.rb +30 -30
- data/lib/capybara/selenium/driver.rb +12 -11
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +3 -3
- data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
- data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +13 -0
- data/lib/capybara/selenium/extensions/find.rb +87 -0
- data/lib/capybara/selenium/extensions/scroll.rb +78 -0
- data/lib/capybara/selenium/node.rb +52 -28
- data/lib/capybara/session.rb +13 -1
- data/lib/capybara/spec/public/test.js +1 -0
- data/lib/capybara/spec/session/assert_style_spec.rb +4 -4
- data/lib/capybara/spec/session/attach_file_spec.rb +6 -6
- data/lib/capybara/spec/session/click_button_spec.rb +1 -1
- data/lib/capybara/spec/session/evaluate_script_spec.rb +1 -0
- data/lib/capybara/spec/session/execute_script_spec.rb +1 -0
- data/lib/capybara/spec/session/fill_in_spec.rb +7 -1
- data/lib/capybara/spec/session/find_field_spec.rb +1 -1
- data/lib/capybara/spec/session/find_spec.rb +11 -0
- data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +0 -1
- data/lib/capybara/spec/session/has_css_spec.rb +32 -0
- data/lib/capybara/spec/session/has_select_spec.rb +2 -2
- data/lib/capybara/spec/session/has_selector_spec.rb +7 -0
- data/lib/capybara/spec/session/has_text_spec.rb +1 -1
- data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
- data/lib/capybara/spec/session/scroll_spec.rb +117 -0
- data/lib/capybara/spec/session/select_spec.rb +5 -0
- data/lib/capybara/spec/spec_helper.rb +1 -0
- data/lib/capybara/spec/views/obscured.erb +1 -1
- data/lib/capybara/spec/views/scroll.erb +20 -0
- data/lib/capybara/spec/views/with_html.erb +1 -1
- data/lib/capybara/version.rb +1 -1
- data/lib/capybara/window.rb +1 -1
- data/lib/capybara.rb +8 -4
- data/spec/basic_node_spec.rb +11 -0
- data/spec/dsl_spec.rb +3 -1
- data/spec/minitest_spec.rb +2 -2
- data/spec/minitest_spec_spec.rb +1 -1
- data/spec/rack_test_spec.rb +4 -1
- data/spec/result_spec.rb +2 -2
- data/spec/selector_spec.rb +11 -1
- data/spec/selenium_spec_firefox.rb +1 -1
- data/spec/selenium_spec_ie.rb +70 -9
- data/spec/session_spec.rb +9 -0
- data/spec/shared_selenium_session.rb +4 -3
- data/spec/spec_helper.rb +2 -0
- metadata +38 -6
- data/lib/capybara/rspec/matchers/have_style.rb +0 -23
- data/lib/capybara/spec/session/has_style_spec.rb +0 -25
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'capybara/rspec/matchers/have_selector'
|
|
4
4
|
require 'capybara/rspec/matchers/match_selector'
|
|
5
5
|
require 'capybara/rspec/matchers/have_current_path'
|
|
6
|
-
require 'capybara/rspec/matchers/
|
|
6
|
+
require 'capybara/rspec/matchers/match_style'
|
|
7
7
|
require 'capybara/rspec/matchers/have_text'
|
|
8
8
|
require 'capybara/rspec/matchers/have_title'
|
|
9
9
|
require 'capybara/rspec/matchers/become_closed'
|
|
@@ -124,9 +124,17 @@ module Capybara
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
# RSpec matcher for element style
|
|
127
|
-
# See {Capybara::Node::Matchers#
|
|
127
|
+
# See {Capybara::Node::Matchers#matches_style?}
|
|
128
|
+
def match_style(styles, **options)
|
|
129
|
+
Matchers::MatchStyle.new(styles, options)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
##
|
|
133
|
+
# @deprecated
|
|
134
|
+
#
|
|
128
135
|
def have_style(styles, **options)
|
|
129
|
-
|
|
136
|
+
warn 'DEPRECATED: have_style is deprecated, please use match_style'
|
|
137
|
+
match_style(styles, **options)
|
|
130
138
|
end
|
|
131
139
|
|
|
132
140
|
%w[selector css xpath text title current_path link button
|
|
@@ -35,6 +35,7 @@ module Capybara
|
|
|
35
35
|
# * :unchecked (Boolean) — Match unchecked fields?
|
|
36
36
|
# * :disabled (Boolean) — Match disabled field?
|
|
37
37
|
# * :multiple (Boolean) — Match fields that accept multiple values
|
|
38
|
+
# * :style (String, Regexp, Hash)
|
|
38
39
|
#
|
|
39
40
|
# * **:fieldset** - Select fieldset elements
|
|
40
41
|
# * Locator: Matches id or contents of wrapped legend
|
|
@@ -42,6 +43,7 @@ module Capybara
|
|
|
42
43
|
# * :id (String, Regexp, XPath::Expression) — Matches id attribute
|
|
43
44
|
# * :legend (String) — Matches contents of wrapped legend
|
|
44
45
|
# * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
|
|
46
|
+
# * :style (String, Regexp, Hash)
|
|
45
47
|
#
|
|
46
48
|
# * **:link** - Find links ( <a> elements with an href attribute )
|
|
47
49
|
# * Locator: Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
|
|
@@ -51,6 +53,7 @@ module Capybara
|
|
|
51
53
|
# * :alt (String) — Matches the alt attribute of a contained img element
|
|
52
54
|
# * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
|
|
53
55
|
# * :href (String, Regexp, nil) — Matches the normalized href of the link, if nil will find <a> elements with no href attribute
|
|
56
|
+
# * :style (String, Regexp, Hash)
|
|
54
57
|
#
|
|
55
58
|
# * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
|
|
56
59
|
# * Locator: Matches the id, Capybara.test_id attribute, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
|
|
@@ -60,6 +63,7 @@ module Capybara
|
|
|
60
63
|
# * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
|
|
61
64
|
# * :value (String) — Matches the value of an input button
|
|
62
65
|
# * :type
|
|
66
|
+
# * :style (String, Regexp, Hash)
|
|
63
67
|
#
|
|
64
68
|
# * **:link_or_button** - Find links or buttons
|
|
65
69
|
# * Locator: See :link and :button selectors
|
|
@@ -75,6 +79,7 @@ module Capybara
|
|
|
75
79
|
# * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
|
|
76
80
|
# * :disabled (Boolean) — Match disabled field?
|
|
77
81
|
# * :multiple (Boolean) — Match fields that accept multiple values
|
|
82
|
+
# * :style (String, Regexp, Hash)
|
|
78
83
|
#
|
|
79
84
|
# * **:radio_button** - Find radio buttons
|
|
80
85
|
# * Locator: Match id, Capybara.test_id attribute, name, or associated label text
|
|
@@ -86,6 +91,7 @@ module Capybara
|
|
|
86
91
|
# * :unchecked (Boolean) — Match unchecked fields?
|
|
87
92
|
# * :disabled (Boolean) — Match disabled field?
|
|
88
93
|
# * :option (String) — Match the value
|
|
94
|
+
# * :style (String, Regexp, Hash)
|
|
89
95
|
#
|
|
90
96
|
# * **:checkbox** - Find checkboxes
|
|
91
97
|
# * Locator: Match id, Capybara.test_id attribute, name, or associated label text
|
|
@@ -97,6 +103,7 @@ module Capybara
|
|
|
97
103
|
# * *:unchecked (Boolean) — Match unchecked fields?
|
|
98
104
|
# * *:disabled (Boolean) — Match disabled field?
|
|
99
105
|
# * *:option (String) — Match the value
|
|
106
|
+
# * :style (String, Regexp, Hash)
|
|
100
107
|
#
|
|
101
108
|
# * **:select** - Find select elements
|
|
102
109
|
# * Locator: Match id, Capybara.test_id attribute, name, placeholder, or associated label text
|
|
@@ -111,6 +118,7 @@ module Capybara
|
|
|
111
118
|
# * :with_options (Array<String>) — Partial match options
|
|
112
119
|
# * :selected (String, Array<String>) — Match the selection(s)
|
|
113
120
|
# * :with_selected (String, Array<String>) — Partial match the selection(s)
|
|
121
|
+
# * :style (String, Regexp, Hash)
|
|
114
122
|
#
|
|
115
123
|
# * **:option** - Find option elements
|
|
116
124
|
# * Locator: Match text of option
|
|
@@ -136,6 +144,7 @@ module Capybara
|
|
|
136
144
|
# * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
|
|
137
145
|
# * :disabled (Boolean) — Match disabled field?
|
|
138
146
|
# * :multiple (Boolean) — Match field that accepts multiple values
|
|
147
|
+
# * :style (String, Regexp, Hash)
|
|
139
148
|
#
|
|
140
149
|
# * **:label** - Find label elements
|
|
141
150
|
# * Locator: Match id or text contents
|
|
@@ -148,6 +157,7 @@ module Capybara
|
|
|
148
157
|
# * :id (String, Regexp, XPath::Expression) — Match id attribute of table
|
|
149
158
|
# * :caption (String) — Match text of associated caption
|
|
150
159
|
# * :class ((String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
|
|
160
|
+
# * :style (String, Regexp, Hash)
|
|
151
161
|
#
|
|
152
162
|
# * **:frame** - Find frame/iframe elements
|
|
153
163
|
# * Locator: Match id or name
|
|
@@ -155,6 +165,7 @@ module Capybara
|
|
|
155
165
|
# * :id (String, Regexp, XPath::Expression) — Match id attribute
|
|
156
166
|
# * :name (String) — Match name attribute
|
|
157
167
|
# * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
|
|
168
|
+
# * :style (String, Regexp, Hash)
|
|
158
169
|
#
|
|
159
170
|
# * **:element**
|
|
160
171
|
# * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
|
|
@@ -173,8 +184,8 @@ module Capybara
|
|
|
173
184
|
all.fetch(name.to_sym) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
|
|
174
185
|
end
|
|
175
186
|
|
|
176
|
-
def add(name, &block)
|
|
177
|
-
all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
|
|
187
|
+
def add(name, **options, &block)
|
|
188
|
+
all[name.to_sym] = Capybara::Selector.new(name.to_sym, **options, &block)
|
|
178
189
|
end
|
|
179
190
|
|
|
180
191
|
def update(name, &block)
|
|
@@ -190,7 +201,7 @@ module Capybara
|
|
|
190
201
|
end
|
|
191
202
|
end
|
|
192
203
|
|
|
193
|
-
def initialize(name, &block)
|
|
204
|
+
def initialize(name, locator_type: nil, raw_locator: false, &block)
|
|
194
205
|
@name = name
|
|
195
206
|
@filter_set = FilterSet.add(name) {}
|
|
196
207
|
@match = nil
|
|
@@ -201,6 +212,8 @@ module Capybara
|
|
|
201
212
|
@expression_filters = {}
|
|
202
213
|
@locator_filter = nil
|
|
203
214
|
@default_visibility = nil
|
|
215
|
+
@locator_type = locator_type
|
|
216
|
+
@raw_locator = raw_locator
|
|
204
217
|
@config = {
|
|
205
218
|
enable_aria_label: false,
|
|
206
219
|
test_id: nil
|
|
@@ -301,6 +314,8 @@ module Capybara
|
|
|
301
314
|
else
|
|
302
315
|
warn 'Selector has no format'
|
|
303
316
|
end
|
|
317
|
+
ensure
|
|
318
|
+
warn "Locator #{locator.inspect} must #{locator_description}. This will raise an error in a future version of Capybara." unless locator_valid?(locator)
|
|
304
319
|
end
|
|
305
320
|
|
|
306
321
|
##
|
|
@@ -428,8 +443,37 @@ module Capybara
|
|
|
428
443
|
Thread.current["capybara_#{object_id}_errors"] = nil
|
|
429
444
|
end
|
|
430
445
|
|
|
446
|
+
# @api private
|
|
447
|
+
def raw_locator?
|
|
448
|
+
!!@raw_locator
|
|
449
|
+
end
|
|
450
|
+
|
|
431
451
|
private
|
|
432
452
|
|
|
453
|
+
def locator_types
|
|
454
|
+
return nil unless @locator_type
|
|
455
|
+
|
|
456
|
+
Array(@locator_type)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def locator_valid?(locator)
|
|
460
|
+
return true unless locator && locator_types
|
|
461
|
+
|
|
462
|
+
locator_types&.any? do |type_or_method|
|
|
463
|
+
type_or_method.is_a?(Symbol) ? locator.respond_to?(type_or_method) : type_or_method === locator # rubocop:disable Style/CaseEquality
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def locator_description
|
|
468
|
+
locator_types.group_by { |lt| lt.is_a? Symbol }.map do |symbol, types_or_methods|
|
|
469
|
+
if symbol
|
|
470
|
+
"respond to #{types_or_methods.join(' or ')}"
|
|
471
|
+
else
|
|
472
|
+
"be an instance of #{types_or_methods.join(' or ')}"
|
|
473
|
+
end
|
|
474
|
+
end.join(' or ')
|
|
475
|
+
end
|
|
476
|
+
|
|
433
477
|
def errors
|
|
434
478
|
Thread.current["capybara_#{object_id}_errors"] || []
|
|
435
479
|
end
|
data/lib/capybara/selector.rb
CHANGED
|
@@ -25,24 +25,24 @@ end
|
|
|
25
25
|
|
|
26
26
|
# rubocop:disable Metrics/BlockLength
|
|
27
27
|
|
|
28
|
-
Capybara.add_selector(:xpath) do
|
|
28
|
+
Capybara.add_selector(:xpath, locator_type: [:to_xpath, String], raw_locator: true) do
|
|
29
29
|
xpath { |xpath| xpath }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
Capybara.add_selector(:css) do
|
|
32
|
+
Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
|
|
33
33
|
css { |css| css }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
Capybara.add_selector(:id) do
|
|
36
|
+
Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
|
|
37
37
|
xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
|
|
38
38
|
locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true }
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
Capybara.add_selector(:field) do
|
|
42
|
-
visible { |options| :hidden if options[:type] == 'hidden' }
|
|
41
|
+
Capybara.add_selector(:field, locator_type: [String, Symbol]) do
|
|
42
|
+
visible { |options| :hidden if options[:type].to_s == 'hidden' }
|
|
43
43
|
xpath do |locator, **options|
|
|
44
44
|
invalid_types = %w[submit image]
|
|
45
|
-
invalid_types << 'hidden' unless options[:type] == 'hidden'
|
|
45
|
+
invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
|
|
46
46
|
xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
|
|
47
47
|
locate_field(xpath, locator, options)
|
|
48
48
|
end
|
|
@@ -78,7 +78,7 @@ Capybara.add_selector(:field) do
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
Capybara.add_selector(:fieldset) do
|
|
81
|
+
Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
|
|
82
82
|
xpath do |locator, legend: nil, **|
|
|
83
83
|
locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]
|
|
84
84
|
locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
|
|
@@ -90,7 +90,7 @@ Capybara.add_selector(:fieldset) do
|
|
|
90
90
|
node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
Capybara.add_selector(:link) do
|
|
93
|
+
Capybara.add_selector(:link, locator_type: [String, Symbol]) do
|
|
94
94
|
xpath do |locator, href: true, alt: nil, title: nil, **|
|
|
95
95
|
xpath = builder(XPath.descendant(:a)).add_attribute_conditions(href: href)
|
|
96
96
|
|
|
@@ -132,7 +132,7 @@ Capybara.add_selector(:link) do
|
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
-
Capybara.add_selector(:button) do
|
|
135
|
+
Capybara.add_selector(:button, locator_type: [String, Symbol]) do
|
|
136
136
|
xpath(:value, :title, :type) do |locator, **options|
|
|
137
137
|
input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
|
|
138
138
|
btn_xpath = XPath.descendant(:button)
|
|
@@ -166,7 +166,7 @@ Capybara.add_selector(:button) do
|
|
|
166
166
|
end
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
Capybara.add_selector(:link_or_button) do
|
|
169
|
+
Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
|
|
170
170
|
label 'link or button'
|
|
171
171
|
xpath do |locator, **options|
|
|
172
172
|
self.class.all.values_at(:link, :button).map do |selector|
|
|
@@ -181,9 +181,8 @@ Capybara.add_selector(:link_or_button) do
|
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
-
Capybara.add_selector(:fillable_field) do
|
|
184
|
+
Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
|
|
185
185
|
label 'field'
|
|
186
|
-
|
|
187
186
|
xpath do |locator, allow_self: nil, **options|
|
|
188
187
|
xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
|
|
189
188
|
!XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
|
|
@@ -215,9 +214,8 @@ Capybara.add_selector(:fillable_field) do
|
|
|
215
214
|
end
|
|
216
215
|
end
|
|
217
216
|
|
|
218
|
-
Capybara.add_selector(:radio_button) do
|
|
217
|
+
Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
|
|
219
218
|
label 'radio button'
|
|
220
|
-
|
|
221
219
|
xpath do |locator, allow_self: nil, **options|
|
|
222
220
|
xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
|
|
223
221
|
XPath.attr(:type) == 'radio'
|
|
@@ -240,7 +238,7 @@ Capybara.add_selector(:radio_button) do
|
|
|
240
238
|
end
|
|
241
239
|
end
|
|
242
240
|
|
|
243
|
-
Capybara.add_selector(:checkbox) do
|
|
241
|
+
Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
|
|
244
242
|
xpath do |locator, allow_self: nil, **options|
|
|
245
243
|
xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
|
|
246
244
|
XPath.attr(:type) == 'checkbox'
|
|
@@ -263,7 +261,7 @@ Capybara.add_selector(:checkbox) do
|
|
|
263
261
|
end
|
|
264
262
|
end
|
|
265
263
|
|
|
266
|
-
Capybara.add_selector(:select) do
|
|
264
|
+
Capybara.add_selector(:select, locator_type: [String, Symbol]) do
|
|
267
265
|
label 'select box'
|
|
268
266
|
|
|
269
267
|
xpath do |locator, **options|
|
|
@@ -320,7 +318,7 @@ Capybara.add_selector(:select) do
|
|
|
320
318
|
end
|
|
321
319
|
end
|
|
322
320
|
|
|
323
|
-
Capybara.add_selector(:datalist_input) do
|
|
321
|
+
Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
|
|
324
322
|
label 'input box with datalist completion'
|
|
325
323
|
|
|
326
324
|
xpath do |locator, **options|
|
|
@@ -355,7 +353,7 @@ Capybara.add_selector(:datalist_input) do
|
|
|
355
353
|
end
|
|
356
354
|
end
|
|
357
355
|
|
|
358
|
-
Capybara.add_selector(:option) do
|
|
356
|
+
Capybara.add_selector(:option, locator_type: [String, Symbol]) do
|
|
359
357
|
xpath do |locator|
|
|
360
358
|
xpath = XPath.descendant(:option)
|
|
361
359
|
xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
|
|
@@ -373,7 +371,7 @@ Capybara.add_selector(:option) do
|
|
|
373
371
|
end
|
|
374
372
|
end
|
|
375
373
|
|
|
376
|
-
Capybara.add_selector(:datalist_option) do
|
|
374
|
+
Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
|
|
377
375
|
label 'datalist option'
|
|
378
376
|
visible(:all)
|
|
379
377
|
|
|
@@ -390,7 +388,7 @@ Capybara.add_selector(:datalist_option) do
|
|
|
390
388
|
end
|
|
391
389
|
end
|
|
392
390
|
|
|
393
|
-
Capybara.add_selector(:file_field) do
|
|
391
|
+
Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
|
|
394
392
|
label 'file field'
|
|
395
393
|
xpath do |locator, allow_self: nil, **options|
|
|
396
394
|
xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
|
|
@@ -404,7 +402,7 @@ Capybara.add_selector(:file_field) do
|
|
|
404
402
|
describe_expression_filters
|
|
405
403
|
end
|
|
406
404
|
|
|
407
|
-
Capybara.add_selector(:label) do
|
|
405
|
+
Capybara.add_selector(:label, locator_type: [String, Symbol]) do
|
|
408
406
|
label 'label'
|
|
409
407
|
xpath(:for) do |locator, options|
|
|
410
408
|
xpath = XPath.descendant(:label)
|
|
@@ -413,12 +411,14 @@ Capybara.add_selector(:label) do
|
|
|
413
411
|
locator_matchers |= XPath.attr(test_id) == locator if test_id
|
|
414
412
|
xpath = xpath[locator_matchers]
|
|
415
413
|
end
|
|
416
|
-
if options.key?(:for)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
414
|
+
if options.key?(:for)
|
|
415
|
+
if (for_option = options[:for].is_a?(Capybara::Node::Element) ? options[:for][:id] : options[:for])
|
|
416
|
+
with_attr = XPath.attr(:for) == for_option.to_s
|
|
417
|
+
labelable_elements = %i[button input keygen meter output progress select textarea]
|
|
418
|
+
wrapped = !XPath.attr(:for) &
|
|
419
|
+
XPath.descendant(*labelable_elements)[XPath.attr(:id) == for_option.to_s]
|
|
420
|
+
xpath = xpath[with_attr | wrapped]
|
|
421
|
+
end
|
|
422
422
|
end
|
|
423
423
|
xpath
|
|
424
424
|
end
|
|
@@ -442,7 +442,7 @@ Capybara.add_selector(:label) do
|
|
|
442
442
|
end
|
|
443
443
|
end
|
|
444
444
|
|
|
445
|
-
Capybara.add_selector(:table) do
|
|
445
|
+
Capybara.add_selector(:table, locator_type: [String, Symbol]) do
|
|
446
446
|
xpath do |locator, caption: nil, **|
|
|
447
447
|
xpath = XPath.descendant(:table)
|
|
448
448
|
unless locator.nil?
|
|
@@ -459,7 +459,7 @@ Capybara.add_selector(:table) do
|
|
|
459
459
|
end
|
|
460
460
|
end
|
|
461
461
|
|
|
462
|
-
Capybara.add_selector(:frame) do
|
|
462
|
+
Capybara.add_selector(:frame, locator_type: [String, Symbol]) do
|
|
463
463
|
xpath do |locator, name: nil, **|
|
|
464
464
|
xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
|
|
465
465
|
unless locator.nil?
|
|
@@ -475,7 +475,7 @@ Capybara.add_selector(:frame) do
|
|
|
475
475
|
end
|
|
476
476
|
end
|
|
477
477
|
|
|
478
|
-
Capybara.add_selector(:element) do
|
|
478
|
+
Capybara.add_selector(:element, locator_type: [String, Symbol]) do
|
|
479
479
|
xpath do |locator, **|
|
|
480
480
|
XPath.descendant.where(locator ? XPath.local_name == locator.to_s : nil)
|
|
481
481
|
end
|
|
@@ -4,6 +4,8 @@ require 'uri'
|
|
|
4
4
|
require 'English'
|
|
5
5
|
|
|
6
6
|
class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
7
|
+
include Capybara::Selenium::Find
|
|
8
|
+
|
|
7
9
|
DEFAULT_OPTIONS = {
|
|
8
10
|
browser: :firefox,
|
|
9
11
|
clear_local_storage: nil,
|
|
@@ -73,14 +75,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
73
75
|
browser.current_url
|
|
74
76
|
end
|
|
75
77
|
|
|
76
|
-
def find_xpath(selector)
|
|
77
|
-
browser.find_elements(:xpath, selector).map(&method(:build_node))
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def find_css(selector)
|
|
81
|
-
browser.find_elements(:css, selector).map(&method(:build_node))
|
|
82
|
-
end
|
|
83
|
-
|
|
84
78
|
def wait?; true; end
|
|
85
79
|
def needs_server?; true; end
|
|
86
80
|
|
|
@@ -348,8 +342,12 @@ private
|
|
|
348
342
|
end
|
|
349
343
|
end
|
|
350
344
|
|
|
351
|
-
def
|
|
352
|
-
|
|
345
|
+
def find_context
|
|
346
|
+
browser
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def build_node(native_node, initial_cache = {})
|
|
350
|
+
::Capybara::Selenium::Node.new(self, native_node, initial_cache)
|
|
353
351
|
end
|
|
354
352
|
|
|
355
353
|
def specialize_driver(sel_driver)
|
|
@@ -359,6 +357,8 @@ private
|
|
|
359
357
|
when :firefox
|
|
360
358
|
require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(sel_driver)
|
|
361
359
|
extend FirefoxDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
|
|
360
|
+
when :ie, :internet_explorer
|
|
361
|
+
extend InternetExplorerDriver
|
|
362
362
|
end
|
|
363
363
|
end
|
|
364
364
|
|
|
@@ -385,7 +385,7 @@ private
|
|
|
385
385
|
until find_xpath('/html/body/*').empty?
|
|
386
386
|
raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if timer.expired?
|
|
387
387
|
|
|
388
|
-
sleep 0.
|
|
388
|
+
sleep 0.01
|
|
389
389
|
end
|
|
390
390
|
end
|
|
391
391
|
|
|
@@ -402,3 +402,4 @@ end
|
|
|
402
402
|
|
|
403
403
|
require 'capybara/selenium/driver_specializations/chrome_driver'
|
|
404
404
|
require 'capybara/selenium/driver_specializations/firefox_driver'
|
|
405
|
+
require 'capybara/selenium/driver_specializations/internet_explorer_driver'
|
|
@@ -23,7 +23,7 @@ module Capybara::Selenium::Driver::ChromeDriver
|
|
|
23
23
|
|
|
24
24
|
# Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
|
|
25
25
|
# and raises unnecessary error. Wait a bit and try again.
|
|
26
|
-
sleep 0.
|
|
26
|
+
sleep 0.25
|
|
27
27
|
super
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -51,8 +51,8 @@ private
|
|
|
51
51
|
result['value']
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def build_node(native_node)
|
|
55
|
-
::Capybara::Selenium::ChromeNode.new(self, native_node)
|
|
54
|
+
def build_node(native_node, initial_cache = {})
|
|
55
|
+
::Capybara::Selenium::ChromeNode.new(self, native_node, initial_cache)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def bridge
|
|
@@ -44,7 +44,7 @@ module Capybara::Selenium::Driver::FirefoxDriver
|
|
|
44
44
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
|
-
def build_node(native_node)
|
|
48
|
-
::Capybara::Selenium::FirefoxNode.new(self, native_node)
|
|
47
|
+
def build_node(native_node, initial_cache = {})
|
|
48
|
+
::Capybara::Selenium::FirefoxNode.new(self, native_node, initial_cache)
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara::Selenium::Driver::InternetExplorerDriver
|
|
4
|
+
def switch_to_frame(frame)
|
|
5
|
+
return super unless frame == :parent
|
|
6
|
+
|
|
7
|
+
# iedriverserver has an issue if the current frame is removed from within it
|
|
8
|
+
# so we have to move to the default_content and iterate back through the frames
|
|
9
|
+
handles = @frame_handles[current_window_handle]
|
|
10
|
+
browser.switch_to.default_content
|
|
11
|
+
handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Selenium
|
|
5
|
+
module Find
|
|
6
|
+
def find_xpath(selector, uses_visibility: false, styles: nil, **_options)
|
|
7
|
+
find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find_css(selector, uses_visibility: false, texts: [], styles: nil, **_options)
|
|
11
|
+
find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def find_by(format, selector, uses_visibility:, texts:, styles:)
|
|
17
|
+
els = find_context.find_elements(format, selector)
|
|
18
|
+
hints = []
|
|
19
|
+
|
|
20
|
+
if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
|
|
21
|
+
els = filter_by_text(els, texts) unless texts.empty?
|
|
22
|
+
|
|
23
|
+
hints_js = +''
|
|
24
|
+
functions = []
|
|
25
|
+
if uses_visibility && !is_displayed_atom.empty?
|
|
26
|
+
hints_js << <<~VISIBILITY_JS
|
|
27
|
+
var vis_func = #{is_displayed_atom};
|
|
28
|
+
VISIBILITY_JS
|
|
29
|
+
functions << 'vis_func'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if styles.is_a? Hash
|
|
33
|
+
hints_js << <<~STYLE_JS
|
|
34
|
+
var style_func = function(el){
|
|
35
|
+
var el_styles = window.getComputedStyle(el);
|
|
36
|
+
return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
|
|
37
|
+
res[style] = el_styles[style];
|
|
38
|
+
return res;
|
|
39
|
+
}, {});
|
|
40
|
+
};
|
|
41
|
+
STYLE_JS
|
|
42
|
+
functions << 'style_func'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
unless functions.empty?
|
|
46
|
+
hints_js << <<~EACH_JS
|
|
47
|
+
return arguments[0].map(function(el){
|
|
48
|
+
return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) }); });
|
|
49
|
+
EACH_JS
|
|
50
|
+
|
|
51
|
+
hints = es_context.execute_script hints_js, els
|
|
52
|
+
hints.map! do |results|
|
|
53
|
+
result = {}
|
|
54
|
+
result[:style] = results.pop if styles.is_a? Hash
|
|
55
|
+
result[:visible] = results.pop if uses_visibility
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def filter_by_text(elements, texts)
|
|
64
|
+
es_context.execute_script <<~JS, elements, texts
|
|
65
|
+
var texts = arguments[1]
|
|
66
|
+
return arguments[0].filter(function(el){
|
|
67
|
+
var content = el.textContent.toLowerCase();
|
|
68
|
+
return texts.every(function(txt){ return content.indexOf(txt.toLowerCase()) != -1 });
|
|
69
|
+
})
|
|
70
|
+
JS
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def es_context
|
|
74
|
+
respond_to?(:execute_script) ? self : driver
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def is_displayed_atom # rubocop:disable Naming/PredicateName
|
|
78
|
+
@@is_displayed_atom ||= begin
|
|
79
|
+
browser.send(:bridge).send(:read_atom, 'isDisplayed')
|
|
80
|
+
rescue StandardError
|
|
81
|
+
# If the atom doesn't exist or other error
|
|
82
|
+
""
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Selenium
|
|
5
|
+
module Scroll
|
|
6
|
+
def scroll_by(x, y)
|
|
7
|
+
driver.execute_script <<~JS, self, x, y
|
|
8
|
+
var el = arguments[0];
|
|
9
|
+
if (el.scrollBy){
|
|
10
|
+
el.scrollBy(arguments[1], arguments[2]);
|
|
11
|
+
} else {
|
|
12
|
+
el.scrollTop = el.scrollTop + arguments[2];
|
|
13
|
+
el.scrollLeft = el.scrollLeft + arguments[1];
|
|
14
|
+
}
|
|
15
|
+
JS
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def scroll_to(element, location, position = nil)
|
|
19
|
+
# location, element = element, nil if element.is_a? Symbol
|
|
20
|
+
if element.is_a? Capybara::Selenium::Node
|
|
21
|
+
scroll_element_to_location(element, location)
|
|
22
|
+
elsif location.is_a? Symbol
|
|
23
|
+
scroll_to_location(location)
|
|
24
|
+
else
|
|
25
|
+
scroll_to_coords(*position)
|
|
26
|
+
end
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def scroll_element_to_location(element, location)
|
|
33
|
+
scroll_opts = case location
|
|
34
|
+
when :top
|
|
35
|
+
'true'
|
|
36
|
+
when :bottom
|
|
37
|
+
'false'
|
|
38
|
+
when :center
|
|
39
|
+
"{behavior: 'instant', block: 'center'}"
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "Invalid scroll_to location: #{location}"
|
|
42
|
+
end
|
|
43
|
+
driver.execute_script <<~JS, element
|
|
44
|
+
arguments[0].scrollIntoView(#{scroll_opts})
|
|
45
|
+
JS
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def scroll_to_location(location)
|
|
49
|
+
scroll_y = case location
|
|
50
|
+
when :top
|
|
51
|
+
'0'
|
|
52
|
+
when :bottom
|
|
53
|
+
'arguments[0].scrollHeight'
|
|
54
|
+
when :center
|
|
55
|
+
'(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
|
|
56
|
+
end
|
|
57
|
+
driver.execute_script <<~JS, self
|
|
58
|
+
if (arguments[0].scrollTo){
|
|
59
|
+
arguments[0].scrollTo(0, #{scroll_y});
|
|
60
|
+
} else {
|
|
61
|
+
arguments[0].scrollTop = #{scroll_y};
|
|
62
|
+
}
|
|
63
|
+
JS
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def scroll_to_coords(x, y)
|
|
67
|
+
driver.execute_script <<~JS, self, x, y
|
|
68
|
+
if (arguments[0].scrollTo){
|
|
69
|
+
arguments[0].scrollTo(arguments[1], arguments[2]);
|
|
70
|
+
} else {
|
|
71
|
+
arguments[0].scrollTop = arguments[2];
|
|
72
|
+
arguments[0].scrollLeft = arguments[1];
|
|
73
|
+
}
|
|
74
|
+
JS
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|