capybara 3.1.1 → 3.2.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +19 -0
  3. data/README.md +1 -1
  4. data/lib/capybara.rb +2 -0
  5. data/lib/capybara/config.rb +2 -1
  6. data/lib/capybara/driver/base.rb +1 -1
  7. data/lib/capybara/driver/node.rb +3 -3
  8. data/lib/capybara/node/actions.rb +90 -92
  9. data/lib/capybara/node/base.rb +2 -2
  10. data/lib/capybara/node/document_matchers.rb +5 -5
  11. data/lib/capybara/node/element.rb +47 -16
  12. data/lib/capybara/node/finders.rb +13 -13
  13. data/lib/capybara/node/matchers.rb +18 -17
  14. data/lib/capybara/node/simple.rb +6 -2
  15. data/lib/capybara/queries/ancestor_query.rb +1 -1
  16. data/lib/capybara/queries/base_query.rb +3 -3
  17. data/lib/capybara/queries/current_path_query.rb +1 -1
  18. data/lib/capybara/queries/match_query.rb +8 -0
  19. data/lib/capybara/queries/selector_query.rb +97 -42
  20. data/lib/capybara/queries/sibling_query.rb +1 -1
  21. data/lib/capybara/queries/text_query.rb +12 -7
  22. data/lib/capybara/rack_test/browser.rb +9 -7
  23. data/lib/capybara/rack_test/form.rb +15 -17
  24. data/lib/capybara/rack_test/node.rb +12 -12
  25. data/lib/capybara/result.rb +26 -15
  26. data/lib/capybara/rspec.rb +1 -2
  27. data/lib/capybara/rspec/compound.rb +4 -4
  28. data/lib/capybara/rspec/matchers.rb +2 -2
  29. data/lib/capybara/selector.rb +75 -225
  30. data/lib/capybara/selector/css.rb +2 -2
  31. data/lib/capybara/selector/filter_set.rb +17 -21
  32. data/lib/capybara/selector/filters/base.rb +24 -1
  33. data/lib/capybara/selector/filters/expression_filter.rb +3 -5
  34. data/lib/capybara/selector/filters/node_filter.rb +4 -4
  35. data/lib/capybara/selector/selector.rb +221 -69
  36. data/lib/capybara/selenium/driver.rb +15 -88
  37. data/lib/capybara/selenium/node.rb +25 -28
  38. data/lib/capybara/server.rb +10 -54
  39. data/lib/capybara/server/animation_disabler.rb +43 -0
  40. data/lib/capybara/server/middleware.rb +55 -0
  41. data/lib/capybara/session.rb +29 -30
  42. data/lib/capybara/session/config.rb +11 -1
  43. data/lib/capybara/session/matchers.rb +5 -5
  44. data/lib/capybara/spec/session/assert_text_spec.rb +1 -1
  45. data/lib/capybara/spec/session/body_spec.rb +10 -12
  46. data/lib/capybara/spec/session/click_link_spec.rb +3 -3
  47. data/lib/capybara/spec/session/element/assert_match_selector_spec.rb +1 -1
  48. data/lib/capybara/spec/session/fill_in_spec.rb +9 -0
  49. data/lib/capybara/spec/session/find_field_spec.rb +1 -1
  50. data/lib/capybara/spec/session/find_spec.rb +8 -3
  51. data/lib/capybara/spec/session/has_link_spec.rb +2 -2
  52. data/lib/capybara/spec/session/node_spec.rb +50 -0
  53. data/lib/capybara/spec/session/node_wrapper_spec.rb +5 -5
  54. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +1 -1
  55. data/lib/capybara/spec/session/window/windows_spec.rb +3 -5
  56. data/lib/capybara/spec/spec_helper.rb +4 -2
  57. data/lib/capybara/spec/views/with_animation.erb +46 -0
  58. data/lib/capybara/version.rb +1 -1
  59. data/lib/capybara/window.rb +3 -2
  60. data/spec/filter_set_spec.rb +19 -2
  61. data/spec/result_spec.rb +33 -1
  62. data/spec/rspec/features_spec.rb +6 -10
  63. data/spec/rspec/shared_spec_matchers.rb +4 -4
  64. data/spec/selector_spec.rb +74 -4
  65. data/spec/selenium_spec_marionette.rb +2 -0
  66. data/spec/server_spec.rb +1 -1
  67. data/spec/session_spec.rb +12 -0
  68. data/spec/shared_selenium_session.rb +30 -0
  69. metadata +8 -9
  70. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  71. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  72. data/.yard/yard_extensions.rb +0 -78
  73. data/.yardopts +0 -1
@@ -101,7 +101,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
101
101
  def needs_server?; true; end
102
102
 
103
103
  def execute_script(script, *args)
104
- browser.execute_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg })
104
+ browser.execute_script(script, *native_args(args))
105
105
  end
106
106
 
107
107
  def evaluate_script(script, *args)
@@ -111,7 +111,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
111
111
 
112
112
  def evaluate_async_script(script, *args)
113
113
  browser.manage.timeouts.script_timeout = Capybara.default_max_wait_time
114
- result = browser.execute_async_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg })
114
+ result = browser.execute_async_script(script, *native_args(args))
115
115
  unwrap_script_result(result)
116
116
  end
117
117
 
@@ -137,7 +137,11 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
137
137
  begin
138
138
  @browser.manage.delete_all_cookies
139
139
  clear_storage
140
- rescue Selenium::WebDriver::Error::UnhandledError
140
+ # rescue Selenium::WebDriver::Error::NoSuchAlertError
141
+ # # Handle a bug in Firefox/Geckodriver where it thinks it needs an alert modal to exist
142
+ # # for no good reason
143
+ # retry
144
+ rescue Selenium::WebDriver::Error::UnhandledError # rubocop:disable Lint/HandleExceptions
141
145
  # delete_all_cookies fails when we've previously gone
142
146
  # to about:blank, so we rescue this error and do nothing
143
147
  # instead.
@@ -167,7 +171,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
167
171
  @browser.navigate.to("about:blank")
168
172
  sleep 0.1 # slight wait for alert
169
173
  @browser.switch_to.alert.accept
170
- rescue modal_error # rubocop:disable Metrics/BlockNesting
174
+ rescue modal_error # rubocop:disable Metrics/BlockNesting, Lint/HandleExceptions
171
175
  # alert now gone, should mean navigation happened
172
176
  end
173
177
  end
@@ -263,8 +267,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
263
267
  end
264
268
 
265
269
  def quit
266
- @browser.quit if @browser
267
- rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
270
+ @browser&.quit
271
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/HandleExceptions
268
272
  # Browser must have already gone
269
273
  rescue Selenium::WebDriver::Error::UnknownError => e
270
274
  unless silenced_unknown_error_message?(e.message) # Most likely already gone
@@ -327,6 +331,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
327
331
 
328
332
  private
329
333
 
334
+ def native_args(args)
335
+ args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
336
+ end
337
+
330
338
  def clear_storage
331
339
  if options[:clear_session_storage]
332
340
  if @browser.respond_to? :session_storage
@@ -335,6 +343,7 @@ private
335
343
  warn "sessionStorage clear requested but is not available for this driver"
336
344
  end
337
345
  end
346
+
338
347
  if options[:clear_local_storage]
339
348
  if @browser.respond_to? :local_storage
340
349
  @browser.local_storage.clear
@@ -352,60 +361,6 @@ private
352
361
  end
353
362
  end
354
363
 
355
- def insert_modal_handlers(accept, response_text)
356
- prompt_response = if accept
357
- if response_text.nil?
358
- "default_text"
359
- else
360
- "'#{response_text.gsub('\\', '\\\\\\').gsub("'", "\\\\'")}'"
361
- end
362
- else
363
- 'null'
364
- end
365
-
366
- script = <<-JS
367
- if (typeof window.capybara === 'undefined') {
368
- window.capybara = {
369
- modal_handlers: [],
370
- current_modal_status: function() {
371
- return [this.modal_handlers[0].called, this.modal_handlers[0].modal_text];
372
- },
373
- add_handler: function(handler) {
374
- this.modal_handlers.unshift(handler);
375
- },
376
- remove_handler: function(handler) {
377
- window.alert = handler.alert;
378
- window.confirm = handler.confirm;
379
- window.prompt = handler.prompt;
380
- },
381
- handler_called: function(handler, str) {
382
- handler.called = true;
383
- handler.modal_text = str;
384
- this.remove_handler(handler);
385
- }
386
- };
387
- };
388
-
389
- var modal_handler = {
390
- prompt: window.prompt,
391
- confirm: window.confirm,
392
- alert: window.alert,
393
- called: false
394
- }
395
- window.capybara.add_handler(modal_handler);
396
-
397
- window.alert = window.confirm = function(str = "") {
398
- window.capybara.handler_called(modal_handler, str.toString());
399
- return #{accept ? 'true' : 'false'};
400
- }
401
- window.prompt = function(str = "", default_text = "") {
402
- window.capybara.handler_called(modal_handler, str.toString());
403
- return #{prompt_response};
404
- }
405
- JS
406
- execute_script script
407
- end
408
-
409
364
  def within_given_window(handle)
410
365
  original_handle = current_window_handle
411
366
  if handle == original_handle
@@ -436,34 +391,6 @@ private
436
391
  end
437
392
  end
438
393
 
439
- def find_headless_modal(text: nil, **options)
440
- # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
441
- # Actual wait time may be longer than specified
442
- wait = Selenium::WebDriver::Wait.new(
443
- timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0,
444
- ignore: modal_error
445
- )
446
- begin
447
- wait.until do
448
- called, alert_text = evaluate_script('window.capybara && window.capybara.current_modal_status()')
449
- if called
450
- execute_script('window.capybara && window.capybara.modal_handlers.shift()')
451
- regexp = text.is_a?(Regexp) ? text : Regexp.escape(text.to_s)
452
- raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}" unless alert_text.match(regexp)
453
- alert_text
454
- elsif called.nil?
455
- # page changed so modal_handler data has gone away
456
- warn "Can't verify modal text when page change occurs - ignoring" if options[:text]
457
- ""
458
- else
459
- nil
460
- end
461
- end
462
- rescue Selenium::WebDriver::Error::TimeOutError
463
- raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}"
464
- end
465
- end
466
-
467
394
  def silenced_unknown_error_message?(msg)
468
395
  silenced_unknown_error_messages.any? { |r| msg =~ r }
469
396
  end
@@ -21,7 +21,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
21
21
  end
22
22
 
23
23
  def value
24
- if tag_name == "select" and multiple?
24
+ if tag_name == "select" && multiple?
25
25
  native.find_elements(:css, "option:checked").map { |n| n[:value] || n.text }
26
26
  else
27
27
  native[:value]
@@ -76,51 +76,36 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
76
76
  native.click if selected?
77
77
  end
78
78
 
79
- def click(keys = [], options = {})
80
- if keys.empty? && !(options[:x] && options[:y])
79
+ def click(keys = [], **options)
80
+ if keys.empty? && !coords?(options)
81
81
  native.click
82
82
  else
83
83
  scroll_if_needed do
84
84
  action_with_modifiers(keys, options) do |a|
85
- if options[:x] && options[:y]
86
- a.click
87
- else
88
- a.click(native)
89
- end
85
+ coords?(options) ? a.click : a.click(native)
90
86
  end
91
87
  end
92
88
  end
93
- rescue => e
89
+ rescue StandardError => e
94
90
  if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
95
91
  e.message =~ /Other element would receive the click/
96
- begin
97
- driver.execute_script("arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'})", self)
98
- rescue # Swallow error if scrollIntoView with options isn't supported
99
- end
92
+ scroll_to_center
100
93
  end
101
94
  raise e
102
95
  end
103
96
 
104
- def right_click(keys = [], options = {})
97
+ def right_click(keys = [], **options)
105
98
  scroll_if_needed do
106
99
  action_with_modifiers(keys, options) do |a|
107
- if options[:x] && options[:y]
108
- a.context_click
109
- else
110
- a.context_click(native)
111
- end
100
+ coords?(options) ? a.context_click : a.context_click(native)
112
101
  end
113
102
  end
114
103
  end
115
104
 
116
- def double_click(keys = [], options = {})
105
+ def double_click(keys = [], **options)
117
106
  scroll_if_needed do
118
107
  action_with_modifiers(keys, options) do |a|
119
- if options[:x] && options[:y]
120
- a.double_click
121
- else
122
- a.double_click(native)
123
- end
108
+ coords?(options) ? a.double_click : a.double_click(native)
124
109
  end
125
110
  end
126
111
  end
@@ -197,8 +182,12 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
197
182
 
198
183
  private
199
184
 
185
+ def coords?(options)
186
+ options[:x] && options[:y]
187
+ end
188
+
200
189
  def boolean_attr(val)
201
- val and val != "false"
190
+ val && (val != "false")
202
191
  end
203
192
 
204
193
  # a reference to the select node if this is an option node
@@ -229,6 +218,11 @@ private
229
218
  def scroll_if_needed
230
219
  yield
231
220
  rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
221
+ scroll_to_center
222
+ yield
223
+ end
224
+
225
+ def scroll_to_center
232
226
  script = <<-'JS'
233
227
  try {
234
228
  arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
@@ -236,8 +230,11 @@ private
236
230
  arguments[0].scrollIntoView(true);
237
231
  }
238
232
  JS
239
- driver.execute_script(script, self)
240
- yield
233
+ begin
234
+ driver.execute_script(script, self)
235
+ rescue StandardError # rubocop:disable Lint/HandleExceptions
236
+ # Swallow error if scrollIntoView with options isn't supported
237
+ end
241
238
  end
242
239
 
243
240
  def set_date(value) # rubocop:disable Naming/AccessorMethodName
@@ -3,56 +3,11 @@
3
3
  require 'uri'
4
4
  require 'net/http'
5
5
  require 'rack'
6
+ require 'capybara/server/middleware'
7
+ require 'capybara/server/animation_disabler'
6
8
 
7
9
  module Capybara
8
10
  class Server
9
- class Middleware
10
- class Counter
11
- attr_reader :value
12
-
13
- def initialize
14
- @value = 0
15
- @mutex = Mutex.new
16
- end
17
-
18
- def increment
19
- @mutex.synchronize { @value += 1 }
20
- end
21
-
22
- def decrement
23
- @mutex.synchronize { @value -= 1 }
24
- end
25
- end
26
-
27
- attr_accessor :error
28
-
29
- def initialize(app, server_errors)
30
- @app = app
31
- @counter = Counter.new
32
- @server_errors = server_errors
33
- end
34
-
35
- def pending_requests?
36
- @counter.value > 0
37
- end
38
-
39
- def call(env)
40
- if env["PATH_INFO"] == "/__identify__"
41
- [200, {}, [@app.object_id.to_s]]
42
- else
43
- @counter.increment
44
- begin
45
- @app.call(env)
46
- rescue *@server_errors => e
47
- @error ||= e
48
- raise e
49
- ensure
50
- @counter.decrement
51
- end
52
- end
53
- end
54
- end
55
-
56
11
  class << self
57
12
  def ports
58
13
  @ports ||= {}
@@ -61,9 +16,10 @@ module Capybara
61
16
 
62
17
  attr_reader :app, :port, :host
63
18
 
64
- def initialize(app, *deprecated_options, port: Capybara.server_port, host: Capybara.server_host, reportable_errors: Capybara.server_errors)
19
+ def initialize(app, *deprecated_options, port: Capybara.server_port, host: Capybara.server_host, reportable_errors: Capybara.server_errors, extra_middleware: [])
65
20
  warn "Positional arguments, other than the application, to Server#new are deprecated, please use keyword arguments" unless deprecated_options.empty?
66
21
  @app = app
22
+ @extra_middleware = extra_middleware
67
23
  @server_thread = nil # suppress warnings
68
24
  @host = deprecated_options[1] || host
69
25
  @reportable_errors = deprecated_options[2] || reportable_errors
@@ -86,10 +42,10 @@ module Capybara
86
42
  end
87
43
 
88
44
  def responsive?
89
- return false if @server_thread && @server_thread.join(0)
45
+ return false if @server_thread&.join(0)
90
46
 
91
47
  begin
92
- res = if !@using_ssl
48
+ res = if !using_ssl?
93
49
  http_connect
94
50
  else
95
51
  https_connect
@@ -99,11 +55,11 @@ module Capybara
99
55
  @using_ssl = true
100
56
  end
101
57
 
102
- if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection)
58
+ if res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPRedirection)
103
59
  return res.body == app.object_id.to_s
104
60
  end
105
61
  rescue SystemCallError
106
- return false
62
+ false
107
63
  end
108
64
 
109
65
  def wait_for_pending_requests
@@ -147,7 +103,7 @@ module Capybara
147
103
  end
148
104
 
149
105
  def middleware
150
- @middleware ||= Middleware.new(app, @reportable_errors)
106
+ @middleware ||= Middleware.new(app, @reportable_errors, @extra_middleware)
151
107
  end
152
108
 
153
109
  def port_key
@@ -162,7 +118,7 @@ module Capybara
162
118
  server = TCPServer.new(host, 0)
163
119
  server.addr[1]
164
120
  ensure
165
- server.close if server
121
+ server&.close
166
122
  end
167
123
  end
168
124
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ class Server
5
+ class AnimationDisabler
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ @status, @headers, @body = @app.call(env)
12
+ return [@status, @headers, @body] unless html_content?
13
+ response = Rack::Response.new([], @status, @headers)
14
+
15
+ @body.each { |html| response.write insert_disable(html) }
16
+ @body.close if @body.respond_to?(:close)
17
+
18
+ response.finish
19
+ end
20
+
21
+ private
22
+
23
+ def html_content?
24
+ !!(@headers["Content-Type"] =~ /html/)
25
+ end
26
+
27
+ def insert_disable(html)
28
+ html.sub(%r{(</head>)}, DISABLE_MARKUP + '\\1')
29
+ end
30
+
31
+ DISABLE_MARKUP = <<~HTML
32
+ <script defer>(typeof jQuery !== 'undefined') && (jQuery.fx.off = true);</script>
33
+ <style>
34
+ * {
35
+ transition: none !important;
36
+ animation-duration: 0s !important;
37
+ animation-delay: 0s !important;
38
+ }
39
+ </style>
40
+ HTML
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ class Server
5
+ class Middleware
6
+ class Counter
7
+ attr_reader :value
8
+
9
+ def initialize
10
+ @value = 0
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def increment
15
+ @mutex.synchronize { @value += 1 }
16
+ end
17
+
18
+ def decrement
19
+ @mutex.synchronize { @value -= 1 }
20
+ end
21
+ end
22
+
23
+ attr_accessor :error
24
+
25
+ def initialize(app, server_errors, extra_middleware = [])
26
+ @app = app
27
+ @extended_app = extra_middleware.inject(@app) do |ex_app, klass|
28
+ klass.new(ex_app)
29
+ end
30
+ @counter = Counter.new
31
+ @server_errors = server_errors
32
+ end
33
+
34
+ def pending_requests?
35
+ @counter.value.positive?
36
+ end
37
+
38
+ def call(env)
39
+ if env["PATH_INFO"] == "/__identify__"
40
+ [200, {}, [@app.object_id.to_s]]
41
+ else
42
+ @counter.increment
43
+ begin
44
+ @extended_app.call(env)
45
+ rescue *@server_errors => e
46
+ @error ||= e
47
+ raise e
48
+ ensure
49
+ @counter.decrement
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end