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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +16 -0
  3. data/README.md +5 -1
  4. data/lib/capybara.rb +2 -0
  5. data/lib/capybara/minitest/spec.rb +1 -1
  6. data/lib/capybara/node/actions.rb +34 -25
  7. data/lib/capybara/node/base.rb +15 -17
  8. data/lib/capybara/node/document_matchers.rb +1 -3
  9. data/lib/capybara/node/element.rb +11 -12
  10. data/lib/capybara/node/finders.rb +2 -1
  11. data/lib/capybara/node/simple.rb +13 -6
  12. data/lib/capybara/queries/base_query.rb +4 -4
  13. data/lib/capybara/queries/selector_query.rb +119 -94
  14. data/lib/capybara/queries/text_query.rb +2 -1
  15. data/lib/capybara/rack_test/form.rb +4 -4
  16. data/lib/capybara/rack_test/node.rb +5 -5
  17. data/lib/capybara/result.rb +23 -32
  18. data/lib/capybara/rspec/compound.rb +1 -1
  19. data/lib/capybara/rspec/matchers.rb +63 -61
  20. data/lib/capybara/selector.rb +28 -10
  21. data/lib/capybara/selector/css.rb +17 -17
  22. data/lib/capybara/selector/filter_set.rb +9 -9
  23. data/lib/capybara/selector/selector.rb +3 -4
  24. data/lib/capybara/selenium/driver.rb +73 -95
  25. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +4 -4
  26. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +9 -0
  27. data/lib/capybara/selenium/node.rb +127 -67
  28. data/lib/capybara/selenium/nodes/chrome_node.rb +3 -3
  29. data/lib/capybara/selenium/nodes/marionette_node.rb +14 -8
  30. data/lib/capybara/server.rb +2 -2
  31. data/lib/capybara/server/animation_disabler.rb +17 -3
  32. data/lib/capybara/server/middleware.rb +8 -4
  33. data/lib/capybara/session.rb +43 -37
  34. data/lib/capybara/session/config.rb +8 -6
  35. data/lib/capybara/spec/session/assert_text_spec.rb +14 -0
  36. data/lib/capybara/spec/session/attach_file_spec.rb +7 -0
  37. data/lib/capybara/spec/session/check_spec.rb +21 -0
  38. data/lib/capybara/spec/session/choose_spec.rb +15 -1
  39. data/lib/capybara/spec/session/fill_in_spec.rb +7 -0
  40. data/lib/capybara/spec/session/find_spec.rb +2 -1
  41. data/lib/capybara/spec/session/has_selector_spec.rb +18 -0
  42. data/lib/capybara/spec/session/has_text_spec.rb +14 -0
  43. data/lib/capybara/spec/session/node_spec.rb +2 -1
  44. data/lib/capybara/spec/session/reset_session_spec.rb +4 -4
  45. data/lib/capybara/spec/session/text_spec.rb +2 -1
  46. data/lib/capybara/spec/session/title_spec.rb +2 -1
  47. data/lib/capybara/spec/session/uncheck_spec.rb +8 -0
  48. data/lib/capybara/spec/session/within_spec.rb +2 -1
  49. data/lib/capybara/spec/spec_helper.rb +1 -32
  50. data/lib/capybara/spec/views/with_js.erb +3 -4
  51. data/lib/capybara/version.rb +1 -1
  52. data/spec/minitest_spec.rb +4 -0
  53. data/spec/minitest_spec_spec.rb +4 -0
  54. data/spec/rack_test_spec.rb +4 -4
  55. data/spec/rspec/shared_spec_matchers.rb +4 -2
  56. data/spec/selector_spec.rb +15 -1
  57. data/spec/selenium_spec_chrome.rb +1 -6
  58. data/spec/selenium_spec_chrome_remote.rb +1 -1
  59. data/spec/selenium_spec_firefox_remote.rb +2 -5
  60. data/spec/selenium_spec_ie.rb +41 -4
  61. data/spec/selenium_spec_marionette.rb +1 -25
  62. data/spec/shared_selenium_session.rb +74 -16
  63. data/spec/spec_helper.rb +41 -0
  64. 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 { |h, matcher| h[matcher] = matcher.matches?(@actual) }
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 => e
31
- @failure_message = e.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 => e
38
- @failure_message_when_negated = e.message
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
- # RSpec matcher for whether elements(s) matching a given xpath selector exist
284
- # See {Capybara::Node::Matchers#has_xpath?}
285
- def have_xpath(xpath, **options, &optional_filter_block)
286
- HaveSelector.new(:xpath, xpath, options, &optional_filter_block)
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
- # RSpec matcher for whether the current element matches a given xpath selector
290
- def match_xpath(xpath, **options, &optional_filter_block)
291
- MatchSelector.new(:xpath, xpath, options, &optional_filter_block)
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
- # RSpec matcher for whether elements(s) matching a given css selector exist
295
- # See {Capybara::Node::Matchers#has_css?}
296
- def have_css(css, **options, &optional_filter_block)
297
- HaveSelector.new(:css, css, options, &optional_filter_block)
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
- # RSpec matcher for whether the current element matches a given css selector
301
- def match_css(css, **options, &optional_filter_block)
302
- MatchSelector.new(:css, css, options, &optional_filter_block)
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)
@@ -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.descendant(:input, :textarea)[!XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
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.descendant(:input)[XPath.attr(:type) == 'radio']
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.descendant(:input)[XPath.attr(:type) == 'checkbox']
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.descendant(:input)[XPath.attr(:type) == 'file']
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((locator || '@').to_sym)
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_-]/) { |c| escape_char c }
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(c)
16
- c =~ %r{[ -/:-~]} ? "\\#{c}" : format('\\%06x', c.ord)
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 (c = str.getc)
36
- case c
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(c, str)
42
+ selector += parse_string(char, str)
43
43
  when '\\'
44
- selector += c + str.getc
44
+ selector += char + str.getc
45
45
  when ','
46
46
  selectors << selector.strip
47
47
  selector = ''
48
48
  else
49
- selector += c
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 (c = strio.getc)
70
- case c
69
+ while (char = strio.getc)
70
+ case char
71
71
  when final
72
- return block + c
72
+ return block + char
73
73
  when '\\'
74
- block += c + strio.getc
74
+ block += char + strio.getc
75
75
  when '"', "'"
76
- block += parse_string(c, strio)
76
+ block += parse_string(char, strio)
77
77
  else
78
- block += c
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 (c = strio.getc)
87
- string += c
88
- case c
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 { |h, k| h[k] = [] }
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
- d = +''
43
- d += undeclared_descriptions.map { |desc| desc.call(opts).to_s }.join
44
- d += expression_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if expression_filters
45
- d += node_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if node_filters
46
- d
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 } : ->(n, _) { filters.include? n }
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, f| f.default? }.each do |name, filter|
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 { |k| options[k] = true }
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={}, &block)
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 |k|
423
- " with #{ef_name}[#{k} => #{opts[k]}]" if ef.handles_option?(k) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(k)
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 => e
19
- raise e if e.message !~ /selenium-webdriver/
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
- unless @browser
25
- # if firefox?
26
- # options[:desired_capabilities] ||= {}
27
- # options[:desired_capabilities][:unexpectedAlertBehaviour] = "ignore"
28
- # end
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
- begin
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
- if current_url != 'about:blank'
156
- begin
157
- # If navigation has not occurred attempt again and accept alert
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
- @frame_handles[browser.window_handle] = []
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
- @frame_handles[browser.window_handle].pop
148
+ handles.pop
181
149
  browser.switch_to.default_content
182
- @frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
150
+ handles.each { |fh| browser.switch_to.frame(fh) }
183
151
  else
184
- @frame_handles[browser.window_handle] ||= []
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 => e
263
- unless silenced_unknown_error_message?(e.message) # Most likely already gone
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: #{e.message}"
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 w3c?
294
- browser && browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3C::Capabilities)
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 ie?
314
- %i[internet_explorer ie].include?(browser_name)
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 browser_name
318
- browser.browser
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 native_args(args)
322
- args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
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 clear_storage
326
- if options[:clear_session_storage]
327
- if @browser.respond_to? :session_storage
328
- @browser.session_storage.clear
329
- else
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
- if options[:clear_local_storage] # rubocop:disable Style/GuardClause
335
- if @browser.respond_to? :local_storage
336
- @browser.local_storage.clear
337
- else
338
- warn 'localStorage clear requested but is not available for this driver'
339
- end
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? { |r| msg =~ r }
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 { |e| unwrap_script_result(e) }
347
+ arg.map { |arr| unwrap_script_result(arr) }
389
348
  when Hash
390
- arg.each { |k, v| arg[k] = unwrap_script_result(v) }
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'