capybara 3.1.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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