capybara 3.15.0 → 3.17.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 +39 -0
- data/README.md +3 -3
- data/lib/capybara/config.rb +1 -2
- data/lib/capybara/helpers.rb +1 -1
- data/lib/capybara/node/actions.rb +6 -6
- data/lib/capybara/node/base.rb +5 -5
- data/lib/capybara/node/element.rb +5 -5
- data/lib/capybara/node/finders.rb +7 -5
- data/lib/capybara/node/simple.rb +3 -2
- data/lib/capybara/queries/base_query.rb +1 -1
- data/lib/capybara/queries/selector_query.rb +7 -7
- data/lib/capybara/queries/style_query.rb +1 -1
- data/lib/capybara/rack_test/form.rb +1 -1
- data/lib/capybara/registrations/drivers.rb +36 -0
- data/lib/capybara/registrations/servers.rb +38 -0
- data/lib/capybara/result.rb +2 -2
- data/lib/capybara/rspec/matcher_proxies.rb +2 -2
- data/lib/capybara/rspec/matchers/base.rb +2 -2
- data/lib/capybara/rspec/matchers.rb +1 -1
- data/lib/capybara/selector/css.rb +3 -3
- data/lib/capybara/selector/filters/base.rb +1 -1
- data/lib/capybara/selector/selector.rb +1 -0
- data/lib/capybara/selector/xpath_extensions.rb +8 -0
- data/lib/capybara/selector.rb +117 -102
- data/lib/capybara/selenium/driver.rb +97 -48
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +16 -4
- data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +23 -0
- data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +5 -0
- data/lib/capybara/selenium/driver_specializations/safari_driver.rb +13 -0
- data/lib/capybara/selenium/extensions/find.rb +48 -37
- data/lib/capybara/selenium/extensions/html5_drag.rb +3 -1
- data/lib/capybara/selenium/logger_suppressor.rb +29 -0
- data/lib/capybara/selenium/node.rb +22 -8
- data/lib/capybara/selenium/nodes/chrome_node.rb +8 -4
- data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
- data/lib/capybara/server/animation_disabler.rb +1 -1
- data/lib/capybara/server/checker.rb +1 -1
- data/lib/capybara/server/middleware.rb +3 -3
- data/lib/capybara/session/config.rb +2 -8
- data/lib/capybara/session.rb +9 -5
- data/lib/capybara/spec/session/attach_file_spec.rb +1 -1
- data/lib/capybara/spec/session/check_spec.rb +4 -4
- data/lib/capybara/spec/session/choose_spec.rb +2 -2
- data/lib/capybara/spec/session/click_button_spec.rb +28 -1
- data/lib/capybara/spec/session/fill_in_spec.rb +2 -2
- data/lib/capybara/spec/session/find_spec.rb +1 -1
- data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
- data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
- data/lib/capybara/spec/session/has_css_spec.rb +9 -2
- data/lib/capybara/spec/session/has_table_spec.rb +4 -4
- data/lib/capybara/spec/session/node_spec.rb +18 -6
- data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
- data/lib/capybara/spec/session/unselect_spec.rb +1 -1
- data/lib/capybara/spec/spec_helper.rb +1 -1
- data/lib/capybara/spec/views/frame_child.erb +2 -1
- data/lib/capybara/spec/views/react.erb +45 -0
- data/lib/capybara/version.rb +1 -1
- data/lib/capybara/window.rb +1 -1
- data/lib/capybara.rb +2 -70
- data/spec/minitest_spec_spec.rb +1 -1
- data/spec/result_spec.rb +10 -6
- data/spec/rspec/shared_spec_matchers.rb +8 -4
- data/spec/sauce_spec_chrome.rb +42 -0
- data/spec/selector_spec.rb +4 -0
- data/spec/selenium_spec_safari.rb +2 -3
- data/spec/session_spec.rb +7 -0
- data/spec/shared_selenium_session.rb +14 -12
- data/spec/spec_helper.rb +2 -1
- metadata +37 -17
data/lib/capybara/selector.rb
CHANGED
|
@@ -7,20 +7,28 @@ Capybara::Selector::FilterSet.add(:_field) do
|
|
|
7
7
|
node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
|
|
8
8
|
node_filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
|
|
9
9
|
node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
|
|
10
|
-
node_filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
|
|
11
10
|
|
|
12
11
|
expression_filter(:name) { |xpath, val| xpath[XPath.attr(:name) == val] }
|
|
13
12
|
expression_filter(:placeholder) { |xpath, val| xpath[XPath.attr(:placeholder) == val] }
|
|
13
|
+
expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
|
|
14
|
+
expression_filter(:multiple) { |xpath, val| xpath[val ? XPath.attr(:multiple) : ~XPath.attr(:multiple)] }
|
|
14
15
|
|
|
15
|
-
describe(:
|
|
16
|
+
describe(:expression_filters) do |name: nil, placeholder: nil, disabled: nil, multiple: nil, **|
|
|
17
|
+
desc = +''
|
|
18
|
+
desc << ' that is not disabled' if disabled == false
|
|
19
|
+
desc << " with name #{name}" if name
|
|
20
|
+
desc << " with placeholder #{placeholder}" if placeholder
|
|
21
|
+
desc << ' with the multiple attribute' if multiple == true
|
|
22
|
+
desc << ' without the multiple attribute' if multiple == false
|
|
23
|
+
desc
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, **|
|
|
16
27
|
desc, states = +'', []
|
|
17
28
|
states << 'checked' if checked || (unchecked == false)
|
|
18
29
|
states << 'not checked' if unchecked || (checked == false)
|
|
19
30
|
states << 'disabled' if disabled == true
|
|
20
|
-
states << 'not disabled' if disabled == false
|
|
21
31
|
desc << " that is #{states.join(' and ')}" unless states.empty?
|
|
22
|
-
desc << ' with the multiple attribute' if multiple == true
|
|
23
|
-
desc << ' without the multiple attribute' if multiple == false
|
|
24
32
|
desc
|
|
25
33
|
end
|
|
26
34
|
end
|
|
@@ -37,7 +45,7 @@ end
|
|
|
37
45
|
|
|
38
46
|
Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
|
|
39
47
|
xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
|
|
40
|
-
locator_filter { |node, id| id.is_a?(Regexp) ? node[:id]
|
|
48
|
+
locator_filter { |node, id| id.is_a?(Regexp) ? id.match?(node[:id]) : true }
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
Capybara.add_selector(:field, locator_type: [String, Symbol]) do
|
|
@@ -63,16 +71,13 @@ Capybara.add_selector(:field, locator_type: [String, Symbol]) do
|
|
|
63
71
|
node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
|
|
64
72
|
node_filter(:with) do |node, with|
|
|
65
73
|
val = node.value
|
|
66
|
-
(with.is_a?(Regexp) ? val
|
|
74
|
+
(with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
|
|
67
75
|
add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
|
|
68
76
|
end
|
|
69
77
|
end
|
|
70
78
|
|
|
71
|
-
describe_expression_filters do |type: nil,
|
|
72
|
-
|
|
73
|
-
(expression_filters.keys & options.keys).each { |ef| desc << " with #{ef} #{options[ef]}" }
|
|
74
|
-
desc << " of type #{type.inspect}" if type
|
|
75
|
-
desc
|
|
79
|
+
describe_expression_filters do |type: nil, **|
|
|
80
|
+
" of type #{type.inspect}" if type
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
describe_node_filters do |**options|
|
|
@@ -90,6 +95,7 @@ Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
|
|
|
90
95
|
end
|
|
91
96
|
|
|
92
97
|
node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
|
|
98
|
+
expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
|
|
93
99
|
end
|
|
94
100
|
|
|
95
101
|
Capybara.add_selector(:link, locator_type: [String, Symbol]) do
|
|
@@ -123,26 +129,28 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
|
|
|
123
129
|
builder(expr).add_attribute_conditions(download: download)
|
|
124
130
|
end
|
|
125
131
|
|
|
126
|
-
describe_expression_filters do
|
|
132
|
+
describe_expression_filters do |download: nil, **options|
|
|
127
133
|
desc = +''
|
|
128
134
|
if (href = options[:href])
|
|
129
135
|
desc << " with href #{'matching ' if href.is_a? Regexp}#{href.inspect}"
|
|
130
136
|
elsif options.key?(:href) # is nil/false specified?
|
|
131
137
|
desc << ' with no href attribute'
|
|
132
138
|
end
|
|
139
|
+
desc << " with download attribute#{" #{download}" if download.is_a? String}" if download
|
|
140
|
+
desc << ' without download attribute' if download == false
|
|
133
141
|
desc
|
|
134
142
|
end
|
|
135
143
|
end
|
|
136
144
|
|
|
137
145
|
Capybara.add_selector(:button, locator_type: [String, Symbol]) do
|
|
138
|
-
xpath(:value, :title, :type) do |locator, **options|
|
|
146
|
+
xpath(:value, :title, :type, :name) do |locator, **options|
|
|
139
147
|
input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
|
|
140
148
|
btn_xpath = XPath.descendant(:button)
|
|
141
149
|
image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
|
|
142
150
|
|
|
143
151
|
unless locator.nil?
|
|
144
152
|
locator = locator.to_s
|
|
145
|
-
locator_matchers = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
|
|
153
|
+
locator_matchers = XPath.attr(:id).equals(locator) | XPath.attr(:name).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
|
|
146
154
|
locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
|
|
147
155
|
locator_matchers |= XPath.attr(test_id) == locator if test_id
|
|
148
156
|
|
|
@@ -155,14 +163,20 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
|
|
|
155
163
|
image_btn_xpath = image_btn_xpath[alt_matches]
|
|
156
164
|
end
|
|
157
165
|
|
|
158
|
-
%i[value title type].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
|
|
166
|
+
%i[value title type name].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
|
|
159
167
|
memo[find_by_attr(ef, options[ef])]
|
|
160
168
|
end
|
|
161
169
|
end
|
|
162
170
|
|
|
163
171
|
node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
|
|
172
|
+
expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
|
|
173
|
+
|
|
174
|
+
describe_expression_filters do |disabled: nil, **options|
|
|
175
|
+
desc = +''
|
|
176
|
+
desc << ' that is not disabled' if disabled == false
|
|
177
|
+
desc << describe_all_expression_filters(options)
|
|
178
|
+
end
|
|
164
179
|
|
|
165
|
-
describe_expression_filters
|
|
166
180
|
describe_node_filters do |disabled: nil, **|
|
|
167
181
|
' that is disabled' if disabled == true
|
|
168
182
|
end
|
|
@@ -176,7 +190,7 @@ Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
|
|
|
176
190
|
end.reduce(:union)
|
|
177
191
|
end
|
|
178
192
|
|
|
179
|
-
node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value|
|
|
193
|
+
node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
|
|
180
194
|
|
|
181
195
|
describe_node_filters do |disabled: nil, **|
|
|
182
196
|
' that is disabled' if disabled == true
|
|
@@ -205,12 +219,11 @@ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
|
|
|
205
219
|
|
|
206
220
|
node_filter(:with) do |node, with|
|
|
207
221
|
val = node.value
|
|
208
|
-
(with.is_a?(Regexp) ? val
|
|
222
|
+
(with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
|
|
209
223
|
add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
|
|
210
224
|
end
|
|
211
225
|
end
|
|
212
226
|
|
|
213
|
-
describe_expression_filters
|
|
214
227
|
describe_node_filters do |**options|
|
|
215
228
|
" with value #{options[:with].to_s.inspect}" if options.key?(:with)
|
|
216
229
|
end
|
|
@@ -234,7 +247,6 @@ Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
|
|
|
234
247
|
end
|
|
235
248
|
end
|
|
236
249
|
|
|
237
|
-
describe_expression_filters
|
|
238
250
|
describe_node_filters do |option: nil, **|
|
|
239
251
|
" with value #{option.inspect}" if option
|
|
240
252
|
end
|
|
@@ -257,7 +269,6 @@ Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
|
|
|
257
269
|
end
|
|
258
270
|
end
|
|
259
271
|
|
|
260
|
-
describe_expression_filters
|
|
261
272
|
describe_node_filters do |option: nil, **|
|
|
262
273
|
" with value #{option.inspect}" if option
|
|
263
274
|
end
|
|
@@ -304,18 +315,18 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
|
|
|
304
315
|
end
|
|
305
316
|
end
|
|
306
317
|
|
|
307
|
-
describe_expression_filters do |with_options: nil,
|
|
318
|
+
describe_expression_filters do |with_options: nil, **|
|
|
308
319
|
desc = +''
|
|
309
320
|
desc << " with at least options #{with_options.inspect}" if with_options
|
|
310
|
-
desc << describe_all_expression_filters(opts)
|
|
311
321
|
desc
|
|
312
322
|
end
|
|
313
323
|
|
|
314
|
-
describe_node_filters do |options: nil, selected: nil, with_selected: nil, **|
|
|
324
|
+
describe_node_filters do |options: nil, selected: nil, with_selected: nil, disabled: nil, **|
|
|
315
325
|
desc = +''
|
|
316
326
|
desc << " with options #{options.inspect}" if options
|
|
317
327
|
desc << " with #{selected.inspect} selected" if selected
|
|
318
328
|
desc << " with at least #{with_selected.inspect} selected" if with_selected
|
|
329
|
+
desc << ' which is disabled' if disabled
|
|
319
330
|
desc
|
|
320
331
|
end
|
|
321
332
|
end
|
|
@@ -343,10 +354,9 @@ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
|
|
|
343
354
|
end
|
|
344
355
|
end
|
|
345
356
|
|
|
346
|
-
describe_expression_filters do |with_options: nil,
|
|
357
|
+
describe_expression_filters do |with_options: nil, **|
|
|
347
358
|
desc = +''
|
|
348
359
|
desc << " with at least options #{with_options.inspect}" if with_options
|
|
349
|
-
desc << describe_all_expression_filters(opts)
|
|
350
360
|
desc
|
|
351
361
|
end
|
|
352
362
|
|
|
@@ -363,11 +373,19 @@ Capybara.add_selector(:option, locator_type: [String, Symbol]) do
|
|
|
363
373
|
end
|
|
364
374
|
|
|
365
375
|
node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
|
|
376
|
+
expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
|
|
377
|
+
|
|
366
378
|
node_filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }
|
|
367
379
|
|
|
380
|
+
describe_expression_filters do |disabled: nil, **options|
|
|
381
|
+
desc = +''
|
|
382
|
+
desc << ' that is not disabled' if disabled == false
|
|
383
|
+
(expression_filters.keys & options.keys).inject(desc) { |memo, ef| memo << " with #{ef} #{options[ef]}" }
|
|
384
|
+
end
|
|
385
|
+
|
|
368
386
|
describe_node_filters do |**options|
|
|
369
387
|
desc = +''
|
|
370
|
-
desc <<
|
|
388
|
+
desc << ' that is disabled' if options[:disabled]
|
|
371
389
|
desc << " that is#{' not' unless options[:selected]} selected" if options.key?(:selected)
|
|
372
390
|
desc
|
|
373
391
|
end
|
|
@@ -384,9 +402,16 @@ Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
|
|
|
384
402
|
end
|
|
385
403
|
|
|
386
404
|
node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
|
|
405
|
+
expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
|
|
406
|
+
|
|
407
|
+
describe_expression_filters do |disabled: nil, **options|
|
|
408
|
+
desc = +''
|
|
409
|
+
desc << ' that is not disabled' if disabled == false
|
|
410
|
+
desc << describe_all_expression_filters(options)
|
|
411
|
+
end
|
|
387
412
|
|
|
388
413
|
describe_node_filters do |**options|
|
|
389
|
-
|
|
414
|
+
' that is disabled' if options[:disabled]
|
|
390
415
|
end
|
|
391
416
|
end
|
|
392
417
|
|
|
@@ -400,8 +425,6 @@ Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
|
|
|
400
425
|
end
|
|
401
426
|
|
|
402
427
|
filter_set(:_field, %i[disabled multiple name])
|
|
403
|
-
|
|
404
|
-
describe_expression_filters
|
|
405
428
|
end
|
|
406
429
|
|
|
407
430
|
Capybara.add_selector(:label, locator_type: [String, Symbol]) do
|
|
@@ -459,100 +482,92 @@ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
|
|
|
459
482
|
expression_filter(:with_cols, valid_values: [Array]) do |xpath, cols|
|
|
460
483
|
col_conditions = cols.map do |col|
|
|
461
484
|
if col.is_a? Hash
|
|
462
|
-
col.reduce(nil) do |xp, (header,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
xp = XPath::Expression.new(:join, table_ancestor, xp)
|
|
469
|
-
cell_xp[XPath.string.n.is(cell) & XPath.position.equals(xp.preceding_sibling(:td).count.plus(1))]
|
|
485
|
+
col.reduce(nil) do |xp, (header, cell_str)|
|
|
486
|
+
header = XPath.descendant(:th)[XPath.string.n.is(header)]
|
|
487
|
+
td = XPath.descendant(:tr)[header].descendant(:td)
|
|
488
|
+
cell_condition = XPath.string.n.is(cell_str)
|
|
489
|
+
cell_condition &= prev_col_position?(XPath.ancestor(:table)[1].join(xp)) if xp
|
|
490
|
+
td[cell_condition]
|
|
470
491
|
end
|
|
471
492
|
else
|
|
472
|
-
cells_xp = col.reduce(nil) do |
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
493
|
+
cells_xp = col.reduce(nil) do |prev_cell, cell_str|
|
|
494
|
+
cell_condition = XPath.string.n.is(cell_str)
|
|
495
|
+
|
|
496
|
+
if prev_cell
|
|
497
|
+
prev_cell = XPath.ancestor(:tr)[1].preceding_sibling(:tr).join(prev_cell)
|
|
498
|
+
cell_condition &= prev_col_position?(prev_cell)
|
|
477
499
|
end
|
|
478
|
-
|
|
500
|
+
|
|
501
|
+
XPath.descendant(:td)[cell_condition]
|
|
479
502
|
end
|
|
480
|
-
XPath
|
|
503
|
+
XPath.descendant(:tr).join(cells_xp)
|
|
481
504
|
end
|
|
482
505
|
end.reduce(:&)
|
|
483
506
|
xpath[col_conditions]
|
|
484
507
|
end
|
|
485
508
|
|
|
486
509
|
expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
|
|
487
|
-
raise ArgumentError,
|
|
510
|
+
raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all? { |col| col.is_a? Array }
|
|
488
511
|
|
|
489
512
|
rows = cols.transpose
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
col_conditions = rows.map do |row|
|
|
493
|
-
row_conditions = row.map do |cell|
|
|
494
|
-
XPath.self(:td)[XPath.string.n.is(cell)]
|
|
495
|
-
end
|
|
496
|
-
row_conditions = row_conditions.reverse.reduce do |cond, cell|
|
|
497
|
-
cell[XPath.following_sibling[cond]]
|
|
498
|
-
end
|
|
499
|
-
row_xpath = XPath.descendant(:tr)[XPath.descendant(:td)[row_conditions]]
|
|
500
|
-
row_xpath[XPath.descendant(:td).count.equals(row.size)]
|
|
501
|
-
end.reduce(:&)
|
|
502
|
-
|
|
503
|
-
xpath[col_conditions]
|
|
513
|
+
col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
|
|
514
|
+
xpath[match_row_count(rows.size)][col_conditions]
|
|
504
515
|
end
|
|
505
516
|
|
|
506
517
|
expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows|
|
|
507
|
-
rows_conditions = rows.map
|
|
508
|
-
if row.is_a? Hash
|
|
509
|
-
row_conditions = row.map do |header, cell|
|
|
510
|
-
header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
|
|
511
|
-
XPath.descendant(:td)[
|
|
512
|
-
XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
|
|
513
|
-
]
|
|
514
|
-
end.reduce(:&)
|
|
515
|
-
XPath.descendant(:tr)[row_conditions]
|
|
516
|
-
else
|
|
517
|
-
row_conditions = row.map do |cell|
|
|
518
|
-
XPath.self(:td)[XPath.string.n.is(cell)]
|
|
519
|
-
end
|
|
520
|
-
row_conditions = row_conditions.reverse.reduce do |cond, cell|
|
|
521
|
-
cell[XPath.following_sibling[cond]]
|
|
522
|
-
end
|
|
523
|
-
XPath.descendant(:tr)[XPath.descendant(:td)[row_conditions]]
|
|
524
|
-
end
|
|
525
|
-
end.reduce(:&)
|
|
518
|
+
rows_conditions = rows.map { |row| match_row(row) }.reduce(:&)
|
|
526
519
|
xpath[rows_conditions]
|
|
527
520
|
end
|
|
528
521
|
|
|
529
522
|
expression_filter(:rows, valid_values: [Array]) do |xpath, rows|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
523
|
+
rows_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
|
|
524
|
+
xpath[match_row_count(rows.size)][rows_conditions]
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
describe_expression_filters do |caption: nil, **|
|
|
528
|
+
" with caption \"#{caption}\"" if caption
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def prev_col_position?(cell)
|
|
532
|
+
XPath.position.equals(cell_position(cell))
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def cell_position(cell)
|
|
536
|
+
cell.preceding_sibling(:td).count.plus(1)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def match_row(row, match_size: false)
|
|
540
|
+
xp = XPath.descendant(:tr)[
|
|
541
|
+
if row.is_a? Hash
|
|
542
|
+
row_match_cells_to_headers(row)
|
|
540
543
|
else
|
|
541
|
-
|
|
542
|
-
XPath.self(:td)[XPath.string.n.is(cell)]
|
|
543
|
-
end
|
|
544
|
-
row_conditions = row_conditions.reverse.reduce do |cond, cell|
|
|
545
|
-
cell[XPath.following_sibling[cond]]
|
|
546
|
-
end
|
|
547
|
-
XPath.descendant(:tr)[XPath.descendant(:td)[row_conditions]]
|
|
544
|
+
XPath.descendant(:td)[row_match_ordered_cells(row)]
|
|
548
545
|
end
|
|
549
|
-
|
|
546
|
+
]
|
|
547
|
+
xp = xp[XPath.descendant(:td).count.equals(row.size)] if match_size
|
|
548
|
+
xp
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def match_row_count(size)
|
|
552
|
+
XPath.descendant(:tbody).descendant(:tr).count.equals(size) | (XPath.descendant(:tr).count.equals(size) & ~XPath.descendant(:tbody))
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def row_match_cells_to_headers(row)
|
|
556
|
+
row.map do |header, cell|
|
|
557
|
+
header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
|
|
558
|
+
XPath.descendant(:td)[
|
|
559
|
+
XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
|
|
560
|
+
]
|
|
550
561
|
end.reduce(:&)
|
|
551
|
-
xpath[rows_conditions]
|
|
552
562
|
end
|
|
553
563
|
|
|
554
|
-
|
|
555
|
-
|
|
564
|
+
def row_match_ordered_cells(row)
|
|
565
|
+
row_conditions = row.map do |cell|
|
|
566
|
+
XPath.self(:td)[XPath.string.n.is(cell)]
|
|
567
|
+
end
|
|
568
|
+
row_conditions.reverse.reduce do |cond, cell|
|
|
569
|
+
cell[XPath.following_sibling[cond]]
|
|
570
|
+
end
|
|
556
571
|
end
|
|
557
572
|
end
|
|
558
573
|
|
|
@@ -603,7 +618,7 @@ Capybara.add_selector(:element, locator_type: [String, Symbol]) do
|
|
|
603
618
|
node_filter(:attributes, matcher: /.+/) do |node, name, val|
|
|
604
619
|
next true unless val.is_a?(Regexp)
|
|
605
620
|
|
|
606
|
-
(node[name]
|
|
621
|
+
(val.match? node[name]).tap do |res|
|
|
607
622
|
add_error("Expected #{name} to match #{val.inspect} but it was #{node[name]}") unless res
|
|
608
623
|
end
|
|
609
624
|
end
|
|
@@ -14,25 +14,40 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
14
14
|
SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout].freeze
|
|
15
15
|
attr_reader :app, :options
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
class << self
|
|
18
|
+
def load_selenium
|
|
19
|
+
require 'selenium-webdriver'
|
|
20
|
+
require 'capybara/selenium/logger_suppressor'
|
|
21
|
+
warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade." if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
|
|
22
|
+
rescue LoadError => e
|
|
23
|
+
raise e unless e.message.match?(/selenium-webdriver/)
|
|
24
|
+
|
|
25
|
+
raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :specializations
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
def register_specialization(browser_name, specialization)
|
|
31
|
+
@specializations ||= {}
|
|
32
|
+
@specializations[browser_name] = specialization
|
|
33
|
+
end
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
def browser
|
|
27
|
-
@browser
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
unless @browser
|
|
38
|
+
options[:http_client] ||= begin
|
|
39
|
+
require 'capybara/selenium/patches/persistent_client'
|
|
40
|
+
if options[:timeout]
|
|
41
|
+
::Capybara::Selenium::PersistentClient.new(read_timeout: options[:timeout])
|
|
42
|
+
else
|
|
43
|
+
::Capybara::Selenium::PersistentClient.new
|
|
44
|
+
end
|
|
30
45
|
end
|
|
31
46
|
processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
|
|
32
|
-
Selenium::WebDriver.for(options[:browser], processed_options)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
@browser = Selenium::WebDriver.for(options[:browser], processed_options)
|
|
48
|
+
|
|
49
|
+
specialize_driver
|
|
50
|
+
setup_exit_handler
|
|
36
51
|
end
|
|
37
52
|
@browser
|
|
38
53
|
end
|
|
@@ -110,7 +125,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
110
125
|
navigated = true
|
|
111
126
|
# Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
|
|
112
127
|
wait_for_empty_page(timer)
|
|
113
|
-
rescue
|
|
128
|
+
rescue *unhandled_alert_errors
|
|
114
129
|
# This error is thrown if an unhandled alert is on the page
|
|
115
130
|
# Firefox appears to automatically dismiss this alert, chrome does not
|
|
116
131
|
# We'll try to accept it
|
|
@@ -177,7 +192,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
177
192
|
browser.window_handles
|
|
178
193
|
end
|
|
179
194
|
|
|
180
|
-
def open_new_window
|
|
195
|
+
def open_new_window(kind = :tab)
|
|
196
|
+
browser.manage.new_window(kind)
|
|
197
|
+
rescue NoMethodError, Selenium::WebDriver::Error::WebDriverError
|
|
198
|
+
# If not supported by the driver or browser default to using JS
|
|
181
199
|
browser.execute_script('window.open();')
|
|
182
200
|
end
|
|
183
201
|
|
|
@@ -218,19 +236,27 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
218
236
|
end
|
|
219
237
|
|
|
220
238
|
def invalid_element_errors
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
239
|
+
@invalid_element_errors ||= begin
|
|
240
|
+
[
|
|
241
|
+
::Selenium::WebDriver::Error::StaleElementReferenceError,
|
|
242
|
+
::Selenium::WebDriver::Error::ElementNotInteractableError,
|
|
243
|
+
::Selenium::WebDriver::Error::InvalidSelectorError, # Work around chromedriver go_back/go_forward race condition
|
|
244
|
+
::Selenium::WebDriver::Error::ElementClickInterceptedError,
|
|
245
|
+
::Selenium::WebDriver::Error::NoSuchElementError, # IE
|
|
246
|
+
::Selenium::WebDriver::Error::InvalidArgumentError # IE
|
|
247
|
+
].tap do |errors|
|
|
248
|
+
unless selenium_4?
|
|
249
|
+
::Selenium::WebDriver.logger.suppress_deprecations do
|
|
250
|
+
errors.concat [
|
|
251
|
+
::Selenium::WebDriver::Error::UnhandledError,
|
|
252
|
+
::Selenium::WebDriver::Error::ElementNotVisibleError,
|
|
253
|
+
::Selenium::WebDriver::Error::InvalidElementStateError,
|
|
254
|
+
::Selenium::WebDriver::Error::ElementNotSelectableError
|
|
255
|
+
]
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
234
260
|
end
|
|
235
261
|
|
|
236
262
|
def no_such_window_error
|
|
@@ -239,6 +265,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
239
265
|
|
|
240
266
|
private
|
|
241
267
|
|
|
268
|
+
def selenium_4?
|
|
269
|
+
defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)
|
|
270
|
+
end
|
|
271
|
+
|
|
242
272
|
def native_args(args)
|
|
243
273
|
args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
|
|
244
274
|
end
|
|
@@ -246,12 +276,32 @@ private
|
|
|
246
276
|
def clear_browser_state
|
|
247
277
|
delete_all_cookies
|
|
248
278
|
clear_storage
|
|
249
|
-
rescue
|
|
279
|
+
rescue *clear_browser_state_errors # rubocop:disable Lint/HandleExceptions
|
|
250
280
|
# delete_all_cookies fails when we've previously gone
|
|
251
281
|
# to about:blank, so we rescue this error and do nothing
|
|
252
282
|
# instead.
|
|
253
283
|
end
|
|
254
284
|
|
|
285
|
+
def clear_browser_state_errors
|
|
286
|
+
@clear_browser_state_errors ||= [Selenium::WebDriver::Error::UnknownError].tap do |errors|
|
|
287
|
+
unless selenium_4?
|
|
288
|
+
::Selenium::WebDriver.logger.suppress_deprecations do
|
|
289
|
+
errors << Selenium::WebDriver::Error::UnhandledError
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def unhandled_alert_errors
|
|
296
|
+
@unhandled_alert_errors ||= [Selenium::WebDriver::Error::UnexpectedAlertOpenError].tap do |errors|
|
|
297
|
+
unless selenium_4?
|
|
298
|
+
::Selenium::WebDriver.logger.suppress_deprecations do
|
|
299
|
+
errors << Selenium::WebDriver::Error::UnhandledAlertError
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
255
305
|
def delete_all_cookies
|
|
256
306
|
@browser.manage.delete_all_cookies
|
|
257
307
|
end
|
|
@@ -322,15 +372,25 @@ private
|
|
|
322
372
|
wait.until do
|
|
323
373
|
alert = @browser.switch_to.alert
|
|
324
374
|
regexp = text.is_a?(Regexp) ? text : Regexp.escape(text.to_s)
|
|
325
|
-
alert.text.match(regexp) ? alert : nil
|
|
375
|
+
alert.text.match?(regexp) ? alert : nil
|
|
326
376
|
end
|
|
327
|
-
rescue
|
|
377
|
+
rescue *find_modal_errors
|
|
328
378
|
raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
|
|
329
379
|
end
|
|
330
380
|
end
|
|
331
381
|
|
|
382
|
+
def find_modal_errors
|
|
383
|
+
@find_modal_errors ||= [Selenium::WebDriver::Error::TimeoutError].tap do |errors|
|
|
384
|
+
unless selenium_4?
|
|
385
|
+
::Selenium::WebDriver.logger.suppress_deprecations do
|
|
386
|
+
errors << Selenium::WebDriver::Error::TimeOutError
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
332
392
|
def silenced_unknown_error_message?(msg)
|
|
333
|
-
silenced_unknown_error_messages.any? { |regex| msg
|
|
393
|
+
silenced_unknown_error_messages.any? { |regex| msg.match? regex }
|
|
334
394
|
end
|
|
335
395
|
|
|
336
396
|
def silenced_unknown_error_messages
|
|
@@ -342,7 +402,7 @@ private
|
|
|
342
402
|
when Array
|
|
343
403
|
arg.map { |arr| unwrap_script_result(arr) }
|
|
344
404
|
when Hash
|
|
345
|
-
arg.
|
|
405
|
+
arg.transform_values! { |value| unwrap_script_result(value) }
|
|
346
406
|
when Selenium::WebDriver::Element
|
|
347
407
|
build_node(arg)
|
|
348
408
|
else
|
|
@@ -358,24 +418,13 @@ private
|
|
|
358
418
|
::Capybara::Selenium::Node.new(self, native_node, initial_cache)
|
|
359
419
|
end
|
|
360
420
|
|
|
361
|
-
def specialize_driver
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
extend
|
|
365
|
-
when :firefox
|
|
366
|
-
require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(sel_driver)
|
|
367
|
-
extend FirefoxDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
|
|
368
|
-
when :ie, :internet_explorer
|
|
369
|
-
extend InternetExplorerDriver
|
|
370
|
-
when :safari, :Safari_Technology_Preview
|
|
371
|
-
extend SafariDriver
|
|
421
|
+
def specialize_driver
|
|
422
|
+
browser_type = browser.browser
|
|
423
|
+
self.class.specializations.select { |k, _v| k === browser_type }.each_value do |specialization| # rubocop:disable Style/CaseEquality
|
|
424
|
+
extend specialization
|
|
372
425
|
end
|
|
373
426
|
end
|
|
374
427
|
|
|
375
|
-
def pause_broken?(driver)
|
|
376
|
-
driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
|
|
377
|
-
end
|
|
378
|
-
|
|
379
428
|
def setup_exit_handler
|
|
380
429
|
main = Process.pid
|
|
381
430
|
at_exit do
|