capybara 3.6.0 → 3.7.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 +16 -0
- data/README.md +5 -1
- data/lib/capybara.rb +2 -0
- data/lib/capybara/minitest/spec.rb +1 -1
- data/lib/capybara/node/actions.rb +34 -25
- data/lib/capybara/node/base.rb +15 -17
- data/lib/capybara/node/document_matchers.rb +1 -3
- data/lib/capybara/node/element.rb +11 -12
- data/lib/capybara/node/finders.rb +2 -1
- data/lib/capybara/node/simple.rb +13 -6
- data/lib/capybara/queries/base_query.rb +4 -4
- data/lib/capybara/queries/selector_query.rb +119 -94
- data/lib/capybara/queries/text_query.rb +2 -1
- data/lib/capybara/rack_test/form.rb +4 -4
- data/lib/capybara/rack_test/node.rb +5 -5
- data/lib/capybara/result.rb +23 -32
- data/lib/capybara/rspec/compound.rb +1 -1
- data/lib/capybara/rspec/matchers.rb +63 -61
- data/lib/capybara/selector.rb +28 -10
- data/lib/capybara/selector/css.rb +17 -17
- data/lib/capybara/selector/filter_set.rb +9 -9
- data/lib/capybara/selector/selector.rb +3 -4
- data/lib/capybara/selenium/driver.rb +73 -95
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +4 -4
- data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +9 -0
- data/lib/capybara/selenium/node.rb +127 -67
- data/lib/capybara/selenium/nodes/chrome_node.rb +3 -3
- data/lib/capybara/selenium/nodes/marionette_node.rb +14 -8
- data/lib/capybara/server.rb +2 -2
- data/lib/capybara/server/animation_disabler.rb +17 -3
- data/lib/capybara/server/middleware.rb +8 -4
- data/lib/capybara/session.rb +43 -37
- data/lib/capybara/session/config.rb +8 -6
- data/lib/capybara/spec/session/assert_text_spec.rb +14 -0
- data/lib/capybara/spec/session/attach_file_spec.rb +7 -0
- data/lib/capybara/spec/session/check_spec.rb +21 -0
- data/lib/capybara/spec/session/choose_spec.rb +15 -1
- data/lib/capybara/spec/session/fill_in_spec.rb +7 -0
- data/lib/capybara/spec/session/find_spec.rb +2 -1
- data/lib/capybara/spec/session/has_selector_spec.rb +18 -0
- data/lib/capybara/spec/session/has_text_spec.rb +14 -0
- data/lib/capybara/spec/session/node_spec.rb +2 -1
- data/lib/capybara/spec/session/reset_session_spec.rb +4 -4
- data/lib/capybara/spec/session/text_spec.rb +2 -1
- data/lib/capybara/spec/session/title_spec.rb +2 -1
- data/lib/capybara/spec/session/uncheck_spec.rb +8 -0
- data/lib/capybara/spec/session/within_spec.rb +2 -1
- data/lib/capybara/spec/spec_helper.rb +1 -32
- data/lib/capybara/spec/views/with_js.erb +3 -4
- data/lib/capybara/version.rb +1 -1
- data/spec/minitest_spec.rb +4 -0
- data/spec/minitest_spec_spec.rb +4 -0
- data/spec/rack_test_spec.rb +4 -4
- data/spec/rspec/shared_spec_matchers.rb +4 -2
- data/spec/selector_spec.rb +15 -1
- data/spec/selenium_spec_chrome.rb +1 -6
- data/spec/selenium_spec_chrome_remote.rb +1 -1
- data/spec/selenium_spec_firefox_remote.rb +2 -5
- data/spec/selenium_spec_ie.rb +41 -4
- data/spec/selenium_spec_marionette.rb +1 -25
- data/spec/shared_selenium_session.rb +74 -16
- data/spec/spec_helper.rb +41 -0
- metadata +2 -2
@@ -21,7 +21,7 @@ if defined?(::RSpec::Expectations::Version)
|
|
21
21
|
class CapybaraEvaluator
|
22
22
|
def initialize(actual)
|
23
23
|
@actual = actual
|
24
|
-
@match_results = Hash.new { |
|
24
|
+
@match_results = Hash.new { |hsh, matcher| hsh[matcher] = matcher.matches?(@actual) }
|
25
25
|
end
|
26
26
|
|
27
27
|
def matcher_matches?(matcher)
|
@@ -27,15 +27,15 @@ module Capybara
|
|
27
27
|
|
28
28
|
def wrap_matches?(actual)
|
29
29
|
yield(wrap(actual))
|
30
|
-
rescue Capybara::ExpectationNotMet =>
|
31
|
-
@failure_message =
|
30
|
+
rescue Capybara::ExpectationNotMet => err
|
31
|
+
@failure_message = err.message
|
32
32
|
false
|
33
33
|
end
|
34
34
|
|
35
35
|
def wrap_does_not_match?(actual)
|
36
36
|
yield(wrap(actual))
|
37
|
-
rescue Capybara::ExpectationNotMet =>
|
38
|
-
@failure_message_when_negated =
|
37
|
+
rescue Capybara::ExpectationNotMet => err
|
38
|
+
@failure_message_when_negated = err.message
|
39
39
|
false
|
40
40
|
end
|
41
41
|
|
@@ -280,28 +280,72 @@ module Capybara
|
|
280
280
|
MatchSelector.new(*args, &optional_filter_block)
|
281
281
|
end
|
282
282
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
end
|
283
|
+
%i[css xpath].each do |selector|
|
284
|
+
define_method "have_#{selector}" do |expr, **options, &optional_filter_block|
|
285
|
+
HaveSelector.new(selector, expr, options, &optional_filter_block)
|
286
|
+
end
|
288
287
|
|
289
|
-
|
290
|
-
|
291
|
-
|
288
|
+
define_method "match_#{selector}" do |expr, **options, &optional_filter_block|
|
289
|
+
MatchSelector.new(selector, expr, options, &optional_filter_block)
|
290
|
+
end
|
292
291
|
end
|
293
292
|
|
294
|
-
#
|
295
|
-
#
|
296
|
-
|
297
|
-
|
293
|
+
# @!method have_xpath(xpath, **options, &optional_filter_block)
|
294
|
+
# RSpec matcher for whether elements(s) matching a given xpath selector exist
|
295
|
+
# See {Capybara::Node::Matchers#has_xpath?}
|
296
|
+
|
297
|
+
# @!method have_css(css, **options, &optional_filter_block)
|
298
|
+
# RSpec matcher for whether elements(s) matching a given css selector exist
|
299
|
+
# See {Capybara::Node::Matchers#has_css?}
|
300
|
+
|
301
|
+
# @!method match_xpath(xpath, **options, &optional_filter_block)
|
302
|
+
# RSpec matcher for whether the current element matches a given xpath selector
|
303
|
+
# See {Capybara::Node::Matchers#matches_xpath?}
|
304
|
+
|
305
|
+
# @!method match_css(css, **options, &optional_filter_block)
|
306
|
+
# RSpec matcher for whether the current element matches a given css selector
|
307
|
+
# See {Capybara::Node::Matchers#matches_css?}
|
308
|
+
|
309
|
+
%i[link button field select table].each do |selector|
|
310
|
+
define_method "have_#{selector}" do |locator = nil, **options, &optional_filter_block|
|
311
|
+
HaveSelector.new(selector, locator, options, &optional_filter_block)
|
312
|
+
end
|
298
313
|
end
|
299
314
|
|
300
|
-
#
|
301
|
-
|
302
|
-
|
315
|
+
# @!method have_link(locator = nil, **options, &optional_filter_block)
|
316
|
+
# RSpec matcher for links
|
317
|
+
# See {Capybara::Node::Matchers#has_link?}
|
318
|
+
|
319
|
+
# @!method have_button(locator = nil, **options, &optional_filter_block)
|
320
|
+
# RSpec matcher for buttons
|
321
|
+
# See {Capybara::Node::Matchers#has_button?}
|
322
|
+
|
323
|
+
# @!method have_field(locator = nil, **options, &optional_filter_block)
|
324
|
+
# RSpec matcher for links
|
325
|
+
# See {Capybara::Node::Matchers#has_field?}
|
326
|
+
|
327
|
+
# @!method have_select(locator = nil, **options, &optional_filter_block)
|
328
|
+
# RSpec matcher for select elements
|
329
|
+
# See {Capybara::Node::Matchers#has_select?}
|
330
|
+
|
331
|
+
# @!method have_table(locator = nil, **options, &optional_filter_block)
|
332
|
+
# RSpec matcher for table elements
|
333
|
+
# See {Capybara::Node::Matchers#has_table?}
|
334
|
+
|
335
|
+
%i[checked unchecked].each do |state|
|
336
|
+
define_method "have_#{state}_field" do |locator = nil, **options, &optional_filter_block|
|
337
|
+
HaveSelector.new(:field, locator, options.merge(state => true), &optional_filter_block)
|
338
|
+
end
|
303
339
|
end
|
304
340
|
|
341
|
+
# @!method have_checked_field(locator = nil, **options, &optional_filter_block)
|
342
|
+
# RSpec matcher for checked fields
|
343
|
+
# See {Capybara::Node::Matchers#has_checked_field?}
|
344
|
+
|
345
|
+
# @!method have_unchecked_field(locator = nil, **options, &optional_filter_block)
|
346
|
+
# RSpec matcher for unchecked fields
|
347
|
+
# See {Capybara::Node::Matchers#has_unchecked_field?}
|
348
|
+
|
305
349
|
# RSpec matcher for text content
|
306
350
|
# See {Capybara::SessionMatchers#assert_text}
|
307
351
|
def have_text(*args)
|
@@ -319,48 +363,6 @@ module Capybara
|
|
319
363
|
HaveCurrentPath.new(path, options)
|
320
364
|
end
|
321
365
|
|
322
|
-
# RSpec matcher for links
|
323
|
-
# See {Capybara::Node::Matchers#has_link?}
|
324
|
-
def have_link(locator = nil, **options, &optional_filter_block)
|
325
|
-
HaveSelector.new(:link, locator, options, &optional_filter_block)
|
326
|
-
end
|
327
|
-
|
328
|
-
# RSpec matcher for buttons
|
329
|
-
# See {Capybara::Node::Matchers#has_button?}
|
330
|
-
def have_button(locator = nil, **options, &optional_filter_block)
|
331
|
-
HaveSelector.new(:button, locator, options, &optional_filter_block)
|
332
|
-
end
|
333
|
-
|
334
|
-
# RSpec matcher for links
|
335
|
-
# See {Capybara::Node::Matchers#has_field?}
|
336
|
-
def have_field(locator = nil, **options, &optional_filter_block)
|
337
|
-
HaveSelector.new(:field, locator, options, &optional_filter_block)
|
338
|
-
end
|
339
|
-
|
340
|
-
# RSpec matcher for checked fields
|
341
|
-
# See {Capybara::Node::Matchers#has_checked_field?}
|
342
|
-
def have_checked_field(locator = nil, **options, &optional_filter_block)
|
343
|
-
HaveSelector.new(:field, locator, options.merge(checked: true), &optional_filter_block)
|
344
|
-
end
|
345
|
-
|
346
|
-
# RSpec matcher for unchecked fields
|
347
|
-
# See {Capybara::Node::Matchers#has_unchecked_field?}
|
348
|
-
def have_unchecked_field(locator = nil, **options, &optional_filter_block)
|
349
|
-
HaveSelector.new(:field, locator, options.merge(unchecked: true), &optional_filter_block)
|
350
|
-
end
|
351
|
-
|
352
|
-
# RSpec matcher for select elements
|
353
|
-
# See {Capybara::Node::Matchers#has_select?}
|
354
|
-
def have_select(locator = nil, **options, &optional_filter_block)
|
355
|
-
HaveSelector.new(:select, locator, options, &optional_filter_block)
|
356
|
-
end
|
357
|
-
|
358
|
-
# RSpec matcher for table elements
|
359
|
-
# See {Capybara::Node::Matchers#has_table?}
|
360
|
-
def have_table(locator = nil, **options, &optional_filter_block)
|
361
|
-
HaveSelector.new(:table, locator, options, &optional_filter_block)
|
362
|
-
end
|
363
|
-
|
364
366
|
# RSpec matcher for element style
|
365
367
|
# See {Capybara::Node::Matchers#has_style?}
|
366
368
|
def have_style(styles, **options)
|
data/lib/capybara/selector.rb
CHANGED
@@ -194,8 +194,10 @@ end
|
|
194
194
|
Capybara.add_selector(:fillable_field) do
|
195
195
|
label 'field'
|
196
196
|
|
197
|
-
xpath do |locator, **options|
|
198
|
-
xpath = XPath.
|
197
|
+
xpath(:allow_self) do |locator, **options|
|
198
|
+
xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input, :textarea)[
|
199
|
+
!XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
|
200
|
+
]
|
199
201
|
locate_field(xpath, locator, options)
|
200
202
|
end
|
201
203
|
|
@@ -223,8 +225,10 @@ end
|
|
223
225
|
Capybara.add_selector(:radio_button) do
|
224
226
|
label 'radio button'
|
225
227
|
|
226
|
-
xpath do |locator, **options|
|
227
|
-
xpath = XPath.
|
228
|
+
xpath(:allow_self) do |locator, **options|
|
229
|
+
xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[
|
230
|
+
XPath.attr(:type) == 'radio'
|
231
|
+
]
|
228
232
|
locate_field(xpath, locator, options)
|
229
233
|
end
|
230
234
|
|
@@ -239,8 +243,10 @@ Capybara.add_selector(:radio_button) do
|
|
239
243
|
end
|
240
244
|
|
241
245
|
Capybara.add_selector(:checkbox) do
|
242
|
-
xpath do |locator, **options|
|
243
|
-
xpath = XPath.
|
246
|
+
xpath(:allow_self) do |locator, **options|
|
247
|
+
xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[
|
248
|
+
XPath.attr(:type) == 'checkbox'
|
249
|
+
]
|
244
250
|
locate_field(xpath, locator, options)
|
245
251
|
end
|
246
252
|
|
@@ -375,8 +381,10 @@ end
|
|
375
381
|
|
376
382
|
Capybara.add_selector(:file_field) do
|
377
383
|
label 'file field'
|
378
|
-
xpath do |locator, options|
|
379
|
-
xpath = XPath.
|
384
|
+
xpath(:allow_self) do |locator, **options|
|
385
|
+
xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[
|
386
|
+
XPath.attr(:type) == 'file'
|
387
|
+
]
|
380
388
|
locate_field(xpath, locator, options)
|
381
389
|
end
|
382
390
|
|
@@ -460,13 +468,17 @@ end
|
|
460
468
|
|
461
469
|
Capybara.add_selector(:element) do
|
462
470
|
xpath do |locator, **|
|
463
|
-
XPath.descendant(
|
471
|
+
locator ? XPath.descendant(locator.to_sym) : XPath.descendant
|
464
472
|
end
|
465
473
|
|
466
474
|
expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
|
467
475
|
case val
|
468
476
|
when Regexp
|
469
477
|
xpath
|
478
|
+
when true
|
479
|
+
xpath[XPath.attr(name)]
|
480
|
+
when false
|
481
|
+
xpath[!XPath.attr(name)]
|
470
482
|
when XPath::Expression
|
471
483
|
xpath[XPath.attr(name)[val]]
|
472
484
|
else
|
@@ -478,6 +490,12 @@ Capybara.add_selector(:element) do
|
|
478
490
|
val.is_a?(Regexp) ? node[name] =~ val : true
|
479
491
|
end
|
480
492
|
|
481
|
-
describe_expression_filters
|
493
|
+
describe_expression_filters do |**options|
|
494
|
+
booleans, values = options.partition { |_k, v| [true, false].include? v }.map(&:to_h)
|
495
|
+
desc = describe_all_expression_filters(values)
|
496
|
+
desc + booleans.map do |k, v|
|
497
|
+
v ? " with #{k} attribute" : "without #{k} attribute"
|
498
|
+
end.join
|
499
|
+
end
|
482
500
|
end
|
483
501
|
# rubocop:enable Metrics/BlockLength
|
@@ -8,12 +8,12 @@ module Capybara
|
|
8
8
|
out = +''
|
9
9
|
out << value.slice!(0...1) if value =~ /^[-_]/
|
10
10
|
out << (value[0] =~ NMSTART ? value.slice!(0...1) : escape_char(value.slice!(0...1)))
|
11
|
-
out << value.gsub(/[^a-zA-Z0-9_-]/) { |
|
11
|
+
out << value.gsub(/[^a-zA-Z0-9_-]/) { |char| escape_char char }
|
12
12
|
out
|
13
13
|
end
|
14
14
|
|
15
|
-
def self.escape_char(
|
16
|
-
|
15
|
+
def self.escape_char(char)
|
16
|
+
char =~ %r{[ -/:-~]} ? "\\#{char}" : format('\\%06x', char.ord)
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.split(css)
|
@@ -32,21 +32,21 @@ module Capybara
|
|
32
32
|
selectors = []
|
33
33
|
StringIO.open(css) do |str|
|
34
34
|
selector = ''
|
35
|
-
while (
|
36
|
-
case
|
35
|
+
while (char = str.getc)
|
36
|
+
case char
|
37
37
|
when '['
|
38
38
|
selector += parse_square(str)
|
39
39
|
when '('
|
40
40
|
selector += parse_paren(str)
|
41
41
|
when '"', "'"
|
42
|
-
selector += parse_string(
|
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
47
|
selector = ''
|
48
48
|
else
|
49
|
-
selector +=
|
49
|
+
selector += char
|
50
50
|
end
|
51
51
|
end
|
52
52
|
selectors << selector.strip
|
@@ -66,16 +66,16 @@ module Capybara
|
|
66
66
|
|
67
67
|
def parse_block(start, final, strio)
|
68
68
|
block = start
|
69
|
-
while (
|
70
|
-
case
|
69
|
+
while (char = strio.getc)
|
70
|
+
case char
|
71
71
|
when final
|
72
|
-
return block +
|
72
|
+
return block + char
|
73
73
|
when '\\'
|
74
|
-
block +=
|
74
|
+
block += char + strio.getc
|
75
75
|
when '"', "'"
|
76
|
-
block += parse_string(
|
76
|
+
block += parse_string(char, strio)
|
77
77
|
else
|
78
|
-
block +=
|
78
|
+
block += char
|
79
79
|
end
|
80
80
|
end
|
81
81
|
raise ArgumentError, "Invalid CSS Selector - Block end '#{final}' not found"
|
@@ -83,9 +83,9 @@ module Capybara
|
|
83
83
|
|
84
84
|
def parse_string(quote, strio)
|
85
85
|
string = quote
|
86
|
-
while (
|
87
|
-
string +=
|
88
|
-
case
|
86
|
+
while (char = strio.getc)
|
87
|
+
string += char
|
88
|
+
case char
|
89
89
|
when quote
|
90
90
|
return string
|
91
91
|
when '\\'
|
@@ -11,7 +11,7 @@ module Capybara
|
|
11
11
|
@name = name
|
12
12
|
@node_filters = {}
|
13
13
|
@expression_filters = {}
|
14
|
-
@descriptions = Hash.new { |
|
14
|
+
@descriptions = Hash.new { |hsh, key| hsh[key] = [] }
|
15
15
|
instance_eval(&block)
|
16
16
|
end
|
17
17
|
|
@@ -39,11 +39,11 @@ module Capybara
|
|
39
39
|
|
40
40
|
def description(node_filters: true, expression_filters: true, **options)
|
41
41
|
opts = options_with_defaults(options)
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
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
|
+
description
|
47
47
|
end
|
48
48
|
|
49
49
|
def descriptions
|
@@ -53,7 +53,7 @@ module Capybara
|
|
53
53
|
|
54
54
|
def import(name, filters = nil)
|
55
55
|
f_set = self.class.all[name]
|
56
|
-
filter_selector = filters.nil? ? ->(*) { true } : ->(
|
56
|
+
filter_selector = filters.nil? ? ->(*) { true } : ->(filter_name, _) { filters.include? filter_name }
|
57
57
|
|
58
58
|
expression_filters.merge!(f_set.expression_filters.select(&filter_selector))
|
59
59
|
node_filters.merge!(f_set.node_filters.select(&filter_selector))
|
@@ -96,7 +96,7 @@ module Capybara
|
|
96
96
|
def options_with_defaults(options)
|
97
97
|
options = options.dup
|
98
98
|
[expression_filters, node_filters].each do |filters|
|
99
|
-
filters.select { |_n,
|
99
|
+
filters.select { |_n, filter| filter.default? }.each do |name, filter|
|
100
100
|
options[name] = filter.default unless options.key?(name)
|
101
101
|
end
|
102
102
|
end
|
@@ -104,7 +104,7 @@ module Capybara
|
|
104
104
|
end
|
105
105
|
|
106
106
|
def add_filter(name, filter_class, *types, matcher: nil, **options, &block)
|
107
|
-
types.each { |
|
107
|
+
types.each { |type| options[type] = true }
|
108
108
|
raise 'ArgumentError', ':default option is not supported for filters with a :matcher option' if matcher && options[:default]
|
109
109
|
if filter_class <= Filters::ExpressionFilter
|
110
110
|
@expression_filters[name] = filter_class.new(name, matcher, block, options)
|
@@ -185,7 +185,6 @@ module Capybara
|
|
185
185
|
@match = nil
|
186
186
|
@label = nil
|
187
187
|
@failure_message = nil
|
188
|
-
@description = nil
|
189
188
|
@format = nil
|
190
189
|
@expression = nil
|
191
190
|
@expression_filters = {}
|
@@ -337,7 +336,7 @@ module Capybara
|
|
337
336
|
#
|
338
337
|
# Define an expression filter for use with this selector
|
339
338
|
#
|
340
|
-
# @!method expression_filter(name, *types, options
|
339
|
+
# @!method expression_filter(name, *types, matcher: nil, **options, &block)
|
341
340
|
# @param [Symbol, Regexp] name The filter name
|
342
341
|
# @param [Regexp] matcher (nil) A Regexp used to check whether a specific option is handled by this filter
|
343
342
|
# @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
|
@@ -419,8 +418,8 @@ module Capybara
|
|
419
418
|
def describe_all_expression_filters(**opts)
|
420
419
|
expression_filters.map do |ef_name, ef|
|
421
420
|
if ef.matcher?
|
422
|
-
opts.keys.map do |
|
423
|
-
" with #{ef_name}[#{
|
421
|
+
opts.keys.map do |key|
|
422
|
+
" with #{ef_name}[#{key} => #{opts[key]}]" if ef.handles_option?(key) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key)
|
424
423
|
end.join
|
425
424
|
elsif opts.key?(ef_name)
|
426
425
|
" with #{ef_name} #{opts[ef_name]}"
|
@@ -15,33 +15,19 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
15
15
|
def self.load_selenium
|
16
16
|
require 'selenium-webdriver'
|
17
17
|
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')
|
18
|
-
rescue LoadError =>
|
19
|
-
raise
|
18
|
+
rescue LoadError => err
|
19
|
+
raise err if err.message !~ /selenium-webdriver/
|
20
20
|
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."
|
21
21
|
end
|
22
22
|
|
23
23
|
def browser
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
@processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
|
31
|
-
@browser = Selenium::WebDriver.for(options[:browser], @processed_options)
|
32
|
-
|
33
|
-
extend ChromeDriver if chrome?
|
34
|
-
extend MarionetteDriver if marionette?
|
35
|
-
|
36
|
-
main = Process.pid
|
37
|
-
at_exit do
|
38
|
-
# Store the exit status of the test run since it goes away after calling the at_exit proc...
|
39
|
-
@exit_status = $ERROR_INFO.status if $ERROR_INFO.is_a?(SystemExit)
|
40
|
-
quit if Process.pid == main
|
41
|
-
exit @exit_status if @exit_status # Force exit with stored status
|
24
|
+
@browser ||= begin
|
25
|
+
processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
|
26
|
+
Selenium::WebDriver.for(options[:browser], processed_options).tap do |driver|
|
27
|
+
specialize_driver(driver)
|
28
|
+
setup_exit_handler
|
42
29
|
end
|
43
30
|
end
|
44
|
-
@browser
|
45
31
|
end
|
46
32
|
|
47
33
|
def initialize(app, **options)
|
@@ -49,7 +35,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
49
35
|
@app = app
|
50
36
|
@browser = nil
|
51
37
|
@exit_status = nil
|
52
|
-
@frame_handles = {}
|
38
|
+
@frame_handles = Hash.new { |hash, handle| hash[handle] = [] }
|
53
39
|
@options = DEFAULT_OPTIONS.merge(options)
|
54
40
|
@node_class = ::Capybara::Selenium::Node
|
55
41
|
end
|
@@ -122,18 +108,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
122
108
|
unless navigated
|
123
109
|
# Only trigger a navigation if we haven't done it already, otherwise it
|
124
110
|
# can trigger an endless series of unload modals
|
125
|
-
|
126
|
-
@browser.manage.delete_all_cookies
|
127
|
-
clear_storage
|
128
|
-
# rescue Selenium::WebDriver::Error::NoSuchAlertError
|
129
|
-
# # Handle a bug in Firefox/Geckodriver where it thinks it needs an alert modal to exist
|
130
|
-
# # for no good reason
|
131
|
-
# retry
|
132
|
-
rescue Selenium::WebDriver::Error::UnhandledError # rubocop:disable Lint/HandleExceptions
|
133
|
-
# delete_all_cookies fails when we've previously gone
|
134
|
-
# to about:blank, so we rescue this error and do nothing
|
135
|
-
# instead.
|
136
|
-
end
|
111
|
+
clear_browser_state
|
137
112
|
@browser.navigate.to('about:blank')
|
138
113
|
end
|
139
114
|
navigated = true
|
@@ -151,18 +126,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
151
126
|
@browser.switch_to.alert.accept
|
152
127
|
sleep 0.25 # allow time for the modal to be handled
|
153
128
|
rescue modal_error
|
154
|
-
# The alert is now gone
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
# since FF may have dismissed the alert at first attempt
|
159
|
-
@browser.navigate.to('about:blank')
|
160
|
-
sleep 0.1 # slight wait for alert
|
161
|
-
@browser.switch_to.alert.accept
|
162
|
-
rescue modal_error # rubocop:disable Metrics/BlockNesting, Lint/HandleExceptions
|
163
|
-
# alert now gone, should mean navigation happened
|
164
|
-
end
|
165
|
-
end
|
129
|
+
# The alert is now gone.
|
130
|
+
# If navigation has not occurred attempt again and accept alert
|
131
|
+
# since FF may have dismissed the alert at first attempt.
|
132
|
+
navigate_with_accept('about:blank') if current_url != 'about:blank'
|
166
133
|
end
|
167
134
|
# try cleaning up the browser again
|
168
135
|
retry
|
@@ -170,19 +137,19 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
170
137
|
end
|
171
138
|
|
172
139
|
def switch_to_frame(frame)
|
140
|
+
handles = @frame_handles[current_window_handle]
|
173
141
|
case frame
|
174
142
|
when :top
|
175
|
-
|
143
|
+
handles.clear
|
176
144
|
browser.switch_to.default_content
|
177
145
|
when :parent
|
178
146
|
# would love to use browser.switch_to.parent_frame here
|
179
147
|
# but it has an issue if the current frame is removed from within it
|
180
|
-
|
148
|
+
handles.pop
|
181
149
|
browser.switch_to.default_content
|
182
|
-
|
150
|
+
handles.each { |fh| browser.switch_to.frame(fh) }
|
183
151
|
else
|
184
|
-
|
185
|
-
@frame_handles[browser.window_handle] << frame.native
|
152
|
+
handles << frame.native
|
186
153
|
browser.switch_to.frame(frame.native)
|
187
154
|
end
|
188
155
|
end
|
@@ -259,10 +226,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
259
226
|
@browser&.quit
|
260
227
|
rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/HandleExceptions
|
261
228
|
# Browser must have already gone
|
262
|
-
rescue Selenium::WebDriver::Error::UnknownError =>
|
263
|
-
unless silenced_unknown_error_message?(
|
229
|
+
rescue Selenium::WebDriver::Error::UnknownError => err
|
230
|
+
unless silenced_unknown_error_message?(err.message) # Most likely already gone
|
264
231
|
# probably already gone but not sure - so warn
|
265
|
-
warn "Ignoring Selenium UnknownError during driver quit: #{
|
232
|
+
warn "Ignoring Selenium UnknownError during driver quit: #{err.message}"
|
266
233
|
end
|
267
234
|
ensure
|
268
235
|
@browser = nil
|
@@ -290,54 +257,46 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
290
257
|
|
291
258
|
private
|
292
259
|
|
293
|
-
def
|
294
|
-
|
295
|
-
end
|
296
|
-
|
297
|
-
def marionette?
|
298
|
-
firefox? && w3c?
|
299
|
-
end
|
300
|
-
|
301
|
-
def firefox?
|
302
|
-
browser_name == :firefox
|
303
|
-
end
|
304
|
-
|
305
|
-
def chrome?
|
306
|
-
browser_name == :chrome
|
307
|
-
end
|
308
|
-
|
309
|
-
def edge?
|
310
|
-
browser_name == :edge
|
260
|
+
def native_args(args)
|
261
|
+
args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
|
311
262
|
end
|
312
263
|
|
313
|
-
def
|
314
|
-
|
264
|
+
def clear_browser_state
|
265
|
+
@browser.manage.delete_all_cookies
|
266
|
+
clear_storage
|
267
|
+
rescue Selenium::WebDriver::Error::UnhandledError # rubocop:disable Lint/HandleExceptions
|
268
|
+
# delete_all_cookies fails when we've previously gone
|
269
|
+
# to about:blank, so we rescue this error and do nothing
|
270
|
+
# instead.
|
315
271
|
end
|
316
272
|
|
317
|
-
def
|
318
|
-
|
273
|
+
def clear_storage
|
274
|
+
clear_session_storage if options[:clear_session_storage]
|
275
|
+
clear_local_storage if options[:clear_local_storage]
|
319
276
|
end
|
320
277
|
|
321
|
-
def
|
322
|
-
|
278
|
+
def clear_session_storage
|
279
|
+
if @browser.respond_to? :session_storage
|
280
|
+
@browser.session_storage.clear
|
281
|
+
else
|
282
|
+
warn 'sessionStorage clear requested but is not available for this driver'
|
283
|
+
end
|
323
284
|
end
|
324
285
|
|
325
|
-
def
|
326
|
-
if
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
warn 'sessionStorage clear requested but is not available for this driver'
|
331
|
-
end
|
286
|
+
def clear_local_storage
|
287
|
+
if @browser.respond_to? :local_storage
|
288
|
+
@browser.local_storage.clear
|
289
|
+
else
|
290
|
+
warn 'localStorage clear requested but is not available for this driver'
|
332
291
|
end
|
292
|
+
end
|
333
293
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
end
|
294
|
+
def navigate_with_accept(url)
|
295
|
+
@browser.navigate.to(url)
|
296
|
+
sleep 0.1 # slight wait for alert
|
297
|
+
@browser.switch_to.alert.accept
|
298
|
+
rescue modal_error # rubocop:disable Lint/HandleExceptions
|
299
|
+
# alert now gone, should mean navigation happened
|
341
300
|
end
|
342
301
|
|
343
302
|
def modal_error
|
@@ -375,7 +334,7 @@ private
|
|
375
334
|
end
|
376
335
|
|
377
336
|
def silenced_unknown_error_message?(msg)
|
378
|
-
silenced_unknown_error_messages.any? { |
|
337
|
+
silenced_unknown_error_messages.any? { |regex| msg =~ regex }
|
379
338
|
end
|
380
339
|
|
381
340
|
def silenced_unknown_error_messages
|
@@ -385,9 +344,9 @@ private
|
|
385
344
|
def unwrap_script_result(arg)
|
386
345
|
case arg
|
387
346
|
when Array
|
388
|
-
arg.map { |
|
347
|
+
arg.map { |arr| unwrap_script_result(arr) }
|
389
348
|
when Hash
|
390
|
-
arg.each { |
|
349
|
+
arg.each { |key, value| arg[key] = unwrap_script_result(value) }
|
391
350
|
when Selenium::WebDriver::Element
|
392
351
|
build_node(arg)
|
393
352
|
else
|
@@ -398,6 +357,25 @@ private
|
|
398
357
|
def build_node(native_node)
|
399
358
|
::Capybara::Selenium::Node.new(self, native_node)
|
400
359
|
end
|
360
|
+
|
361
|
+
def specialize_driver(sel_driver)
|
362
|
+
case sel_driver.browser
|
363
|
+
when :chrome
|
364
|
+
extend ChromeDriver
|
365
|
+
when :firefox
|
366
|
+
extend MarionetteDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def setup_exit_handler
|
371
|
+
main = Process.pid
|
372
|
+
at_exit do
|
373
|
+
# Store the exit status of the test run since it goes away after calling the at_exit proc...
|
374
|
+
@exit_status = $ERROR_INFO.status if $ERROR_INFO.is_a?(SystemExit)
|
375
|
+
quit if Process.pid == main
|
376
|
+
exit @exit_status if @exit_status # Force exit with stored status
|
377
|
+
end
|
378
|
+
end
|
401
379
|
end
|
402
380
|
|
403
381
|
require 'capybara/selenium/driver_specializations/chrome_driver'
|