capybara 2.13.0 → 2.18.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 +5 -5
- data/History.md +218 -18
- data/README.md +54 -23
- data/lib/capybara/config.rb +132 -0
- data/lib/capybara/cucumber.rb +1 -0
- data/lib/capybara/driver/base.rb +14 -0
- data/lib/capybara/dsl.rb +1 -3
- data/lib/capybara/helpers.rb +3 -3
- data/lib/capybara/minitest/spec.rb +14 -37
- data/lib/capybara/minitest.rb +95 -114
- data/lib/capybara/node/actions.rb +10 -10
- data/lib/capybara/node/base.rb +7 -2
- data/lib/capybara/node/element.rb +9 -3
- data/lib/capybara/node/finders.rb +92 -18
- data/lib/capybara/node/matchers.rb +21 -9
- data/lib/capybara/node/simple.rb +5 -0
- data/lib/capybara/queries/ancestor_query.rb +25 -0
- data/lib/capybara/queries/base_query.rb +12 -3
- data/lib/capybara/queries/current_path_query.rb +13 -9
- data/lib/capybara/queries/selector_query.rb +62 -23
- data/lib/capybara/queries/sibling_query.rb +25 -0
- data/lib/capybara/queries/text_query.rb +10 -5
- data/lib/capybara/queries/title_query.rb +1 -0
- data/lib/capybara/rack_test/browser.rb +13 -5
- data/lib/capybara/rack_test/driver.rb +6 -1
- data/lib/capybara/rack_test/form.rb +4 -3
- data/lib/capybara/rack_test/node.rb +1 -1
- data/lib/capybara/rspec/compound.rb +95 -0
- data/lib/capybara/rspec/matcher_proxies.rb +45 -0
- data/lib/capybara/rspec/matchers.rb +108 -7
- data/lib/capybara/rspec.rb +3 -1
- data/lib/capybara/selector/filter.rb +13 -41
- data/lib/capybara/selector/filter_set.rb +30 -4
- data/lib/capybara/selector/filters/base.rb +33 -0
- data/lib/capybara/selector/filters/expression_filter.rb +40 -0
- data/lib/capybara/selector/filters/node_filter.rb +27 -0
- data/lib/capybara/selector/selector.rb +36 -15
- data/lib/capybara/selector.rb +63 -42
- data/lib/capybara/selenium/driver.rb +177 -33
- data/lib/capybara/selenium/node.rb +106 -55
- data/lib/capybara/server.rb +6 -5
- data/lib/capybara/session/config.rb +114 -0
- data/lib/capybara/session/matchers.rb +15 -4
- data/lib/capybara/session.rb +178 -65
- data/lib/capybara/spec/fixtures/no_extension +1 -0
- data/lib/capybara/spec/public/test.js +18 -3
- data/lib/capybara/spec/session/accept_alert_spec.rb +9 -1
- data/lib/capybara/spec/session/accept_prompt_spec.rb +29 -1
- data/lib/capybara/spec/session/all_spec.rb +13 -1
- data/lib/capybara/spec/session/ancestor_spec.rb +85 -0
- data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +24 -8
- data/lib/capybara/spec/session/assert_selector.rb +1 -1
- data/lib/capybara/spec/session/assert_text.rb +8 -0
- data/lib/capybara/spec/session/assert_title.rb +22 -9
- data/lib/capybara/spec/session/attach_file_spec.rb +8 -1
- data/lib/capybara/spec/session/check_spec.rb +4 -4
- data/lib/capybara/spec/session/choose_spec.rb +2 -2
- data/lib/capybara/spec/session/click_button_spec.rb +1 -1
- data/lib/capybara/spec/session/click_link_or_button_spec.rb +3 -3
- data/lib/capybara/spec/session/click_link_spec.rb +1 -1
- data/lib/capybara/spec/session/current_url_spec.rb +3 -3
- data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
- data/lib/capybara/spec/session/dismiss_prompt_spec.rb +1 -1
- data/lib/capybara/spec/session/evaluate_async_script_spec.rb +22 -0
- data/lib/capybara/spec/session/evaluate_script_spec.rb +1 -1
- data/lib/capybara/spec/session/fill_in_spec.rb +8 -2
- data/lib/capybara/spec/session/find_field_spec.rb +1 -0
- data/lib/capybara/spec/session/find_spec.rb +8 -6
- data/lib/capybara/spec/session/first_spec.rb +10 -5
- data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
- data/lib/capybara/spec/session/has_css_spec.rb +11 -0
- data/lib/capybara/spec/session/has_current_path_spec.rb +52 -7
- data/lib/capybara/spec/session/has_link_spec.rb +4 -4
- data/lib/capybara/spec/session/has_none_selectors_spec.rb +76 -0
- data/lib/capybara/spec/session/has_select_spec.rb +64 -6
- data/lib/capybara/spec/session/has_selector_spec.rb +1 -3
- data/lib/capybara/spec/session/has_text_spec.rb +5 -3
- data/lib/capybara/spec/session/has_title_spec.rb +4 -2
- data/lib/capybara/spec/session/has_xpath_spec.rb +5 -3
- data/lib/capybara/spec/session/node_spec.rb +50 -26
- data/lib/capybara/spec/session/refresh_spec.rb +28 -0
- data/lib/capybara/spec/session/reset_session_spec.rb +3 -3
- data/lib/capybara/spec/session/select_spec.rb +3 -2
- data/lib/capybara/spec/session/sibling_spec.rb +52 -0
- data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
- data/lib/capybara/spec/session/unselect_spec.rb +2 -2
- data/lib/capybara/spec/session/visit_spec.rb +56 -1
- data/lib/capybara/spec/session/window/become_closed_spec.rb +11 -11
- data/lib/capybara/spec/session/window/switch_to_window_spec.rb +11 -9
- data/lib/capybara/spec/session/window/window_opened_by_spec.rb +4 -4
- data/lib/capybara/spec/session/window/within_window_spec.rb +27 -2
- data/lib/capybara/spec/spec_helper.rb +28 -4
- data/lib/capybara/spec/test_app.rb +3 -1
- data/lib/capybara/spec/views/form.erb +27 -1
- data/lib/capybara/spec/views/initial_alert.erb +10 -0
- data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
- data/lib/capybara/spec/views/with_hover.erb +5 -0
- data/lib/capybara/spec/views/with_html.erb +33 -2
- data/lib/capybara/spec/views/with_js.erb +12 -0
- data/lib/capybara/spec/views/with_windows.erb +4 -0
- data/lib/capybara/version.rb +1 -1
- data/lib/capybara/window.rb +1 -1
- data/lib/capybara.rb +102 -124
- data/spec/capybara_spec.rb +43 -21
- data/spec/dsl_spec.rb +1 -0
- data/spec/filter_set_spec.rb +28 -0
- data/spec/minitest_spec.rb +9 -1
- data/spec/minitest_spec_spec.rb +19 -5
- data/spec/per_session_config_spec.rb +67 -0
- data/spec/result_spec.rb +20 -0
- data/spec/rspec/shared_spec_matchers.rb +148 -44
- data/spec/rspec/views_spec.rb +4 -0
- data/spec/rspec_matchers_spec.rb +46 -0
- data/spec/rspec_spec.rb +77 -0
- data/spec/selector_spec.rb +2 -1
- data/spec/selenium_spec_chrome.rb +25 -17
- data/spec/selenium_spec_firefox.rb +2 -1
- data/spec/selenium_spec_marionette.rb +18 -5
- data/spec/session_spec.rb +44 -0
- data/spec/shared_selenium_session.rb +72 -8
- data/spec/spec_helper.rb +4 -0
- metadata +55 -8
@@ -14,13 +14,16 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
14
14
|
|
15
15
|
def browser
|
16
16
|
unless @browser
|
17
|
-
if
|
18
|
-
options[:desired_capabilities] ||=
|
17
|
+
if firefox?
|
18
|
+
options[:desired_capabilities] ||= {}
|
19
19
|
options[:desired_capabilities].merge!({ unexpectedAlertBehaviour: "ignore" })
|
20
20
|
end
|
21
21
|
|
22
|
-
@
|
22
|
+
@processed_options = options.reject { |key,_val| SPECIAL_OPTIONS.include?(key) }
|
23
|
+
@browser = Selenium::WebDriver.for(options[:browser], @processed_options)
|
23
24
|
|
25
|
+
@w3c = ((defined?(Selenium::WebDriver::Remote::W3CCapabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)) ||
|
26
|
+
(defined?(Selenium::WebDriver::Remote::W3C::Capabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3C::Capabilities)))
|
24
27
|
main = Process.pid
|
25
28
|
at_exit do
|
26
29
|
# Store the exit status of the test run since it goes away after calling the at_exit proc...
|
@@ -33,17 +36,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
33
36
|
end
|
34
37
|
|
35
38
|
def initialize(app, options={})
|
36
|
-
|
37
|
-
|
38
|
-
rescue LoadError => e
|
39
|
-
if e.message =~ /selenium-webdriver/
|
40
|
-
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."
|
41
|
-
else
|
42
|
-
raise e
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
|
39
|
+
load_selenium
|
40
|
+
@session = nil
|
47
41
|
@app = app
|
48
42
|
@browser = nil
|
49
43
|
@exit_status = nil
|
@@ -55,6 +49,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
55
49
|
browser.navigate.to(path)
|
56
50
|
end
|
57
51
|
|
52
|
+
def refresh
|
53
|
+
accept_modal(nil, wait: 0.1) do
|
54
|
+
browser.navigate.refresh
|
55
|
+
end
|
56
|
+
rescue Capybara::ModalNotFound
|
57
|
+
end
|
58
|
+
|
58
59
|
def go_back
|
59
60
|
browser.navigate.back
|
60
61
|
end
|
@@ -95,6 +96,12 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
95
96
|
unwrap_script_result(result)
|
96
97
|
end
|
97
98
|
|
99
|
+
def evaluate_async_script(script, *args)
|
100
|
+
browser.manage.timeouts.script_timeout = Capybara.default_max_wait_time
|
101
|
+
result = browser.execute_async_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg} )
|
102
|
+
unwrap_script_result(result)
|
103
|
+
end
|
104
|
+
|
98
105
|
def save_screenshot(path, _options={})
|
99
106
|
browser.save_screenshot(path)
|
100
107
|
end
|
@@ -138,14 +145,14 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
138
145
|
raise Capybara::ExpectationNotMet.new('Timed out waiting for Selenium session reset') if (Capybara::Helpers.monotonic_time - start_time) >= 10
|
139
146
|
sleep 0.05
|
140
147
|
end
|
141
|
-
rescue Selenium::WebDriver::Error::UnhandledAlertError
|
148
|
+
rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
142
149
|
# This error is thrown if an unhandled alert is on the page
|
143
150
|
# Firefox appears to automatically dismiss this alert, chrome does not
|
144
151
|
# We'll try to accept it
|
145
152
|
begin
|
146
153
|
@browser.switch_to.alert.accept
|
147
154
|
sleep 0.25 # allow time for the modal to be handled
|
148
|
-
rescue
|
155
|
+
rescue modal_error
|
149
156
|
# The alert is now gone - nothing to do
|
150
157
|
end
|
151
158
|
# try cleaning up the browser again
|
@@ -185,7 +192,12 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
185
192
|
|
186
193
|
def resize_window_to(handle, width, height)
|
187
194
|
within_given_window(handle) do
|
188
|
-
|
195
|
+
# Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
|
196
|
+
if marionette? && (window_size(handle) == [width, height])
|
197
|
+
{}
|
198
|
+
else
|
199
|
+
browser.manage.window.resize_to(width, height)
|
200
|
+
end
|
189
201
|
end
|
190
202
|
end
|
191
203
|
|
@@ -222,7 +234,9 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
222
234
|
def accept_modal(_type, options={})
|
223
235
|
yield if block_given?
|
224
236
|
modal = find_modal(options)
|
237
|
+
|
225
238
|
modal.send_keys options[:with] if options[:with]
|
239
|
+
|
226
240
|
message = modal.text
|
227
241
|
modal.accept
|
228
242
|
message
|
@@ -238,7 +252,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
238
252
|
|
239
253
|
def quit
|
240
254
|
@browser.quit if @browser
|
241
|
-
rescue Errno::ECONNREFUSED
|
255
|
+
rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
|
242
256
|
# Browser must have already gone
|
243
257
|
rescue Selenium::WebDriver::Error::UnknownError => e
|
244
258
|
unless silenced_unknown_error_message?(e.message) # Most likely already gone
|
@@ -250,10 +264,15 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
250
264
|
end
|
251
265
|
|
252
266
|
def invalid_element_errors
|
253
|
-
[Selenium::WebDriver::Error::StaleElementReferenceError,
|
254
|
-
Selenium::WebDriver::Error::UnhandledError,
|
255
|
-
Selenium::WebDriver::Error::ElementNotVisibleError,
|
256
|
-
Selenium::WebDriver::Error::InvalidSelectorError
|
267
|
+
[::Selenium::WebDriver::Error::StaleElementReferenceError,
|
268
|
+
::Selenium::WebDriver::Error::UnhandledError,
|
269
|
+
::Selenium::WebDriver::Error::ElementNotVisibleError,
|
270
|
+
::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
|
271
|
+
::Selenium::WebDriver::Error::ElementNotInteractableError,
|
272
|
+
::Selenium::WebDriver::Error::ElementClickInterceptedError,
|
273
|
+
::Selenium::WebDriver::Error::InvalidElementStateError,
|
274
|
+
::Selenium::WebDriver::Error::ElementNotSelectableError,
|
275
|
+
]
|
257
276
|
end
|
258
277
|
|
259
278
|
def no_such_window_error
|
@@ -261,6 +280,40 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
261
280
|
end
|
262
281
|
|
263
282
|
# @api private
|
283
|
+
def marionette?
|
284
|
+
firefox? && browser && @w3c
|
285
|
+
end
|
286
|
+
|
287
|
+
# @api private
|
288
|
+
def firefox?
|
289
|
+
browser_name == "firefox"
|
290
|
+
end
|
291
|
+
|
292
|
+
# @api private
|
293
|
+
def chrome?
|
294
|
+
browser_name == "chrome"
|
295
|
+
end
|
296
|
+
|
297
|
+
# @deprecated This method is being removed
|
298
|
+
def browser_initialized?
|
299
|
+
super && !@browser.nil?
|
300
|
+
end
|
301
|
+
|
302
|
+
private
|
303
|
+
|
304
|
+
# @api private
|
305
|
+
def browser_name
|
306
|
+
options[:browser].to_s
|
307
|
+
end
|
308
|
+
|
309
|
+
def modal_error
|
310
|
+
if defined?(Selenium::WebDriver::Error::NoSuchAlertError)
|
311
|
+
Selenium::WebDriver::Error::NoSuchAlertError
|
312
|
+
else
|
313
|
+
Selenium::WebDriver::Error::NoAlertPresentError
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
264
317
|
def find_window(locator)
|
265
318
|
handles = browser.window_handles
|
266
319
|
return locator if handles.include? locator
|
@@ -278,18 +331,60 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
278
331
|
raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}"
|
279
332
|
end
|
280
333
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
334
|
+
def insert_modal_handlers(accept, response_text)
|
335
|
+
prompt_response = if accept
|
336
|
+
if response_text.nil?
|
337
|
+
"default_text"
|
338
|
+
else
|
339
|
+
"'#{response_text.gsub("\\", "\\\\\\").gsub("'", "\\\\'")}'"
|
340
|
+
end
|
341
|
+
else
|
342
|
+
'null'
|
343
|
+
end
|
285
344
|
|
286
|
-
|
287
|
-
|
288
|
-
|
345
|
+
script = <<-JS
|
346
|
+
if (typeof window.capybara === 'undefined') {
|
347
|
+
window.capybara = {
|
348
|
+
modal_handlers: [],
|
349
|
+
current_modal_status: function() {
|
350
|
+
return [this.modal_handlers[0].called, this.modal_handlers[0].modal_text];
|
351
|
+
},
|
352
|
+
add_handler: function(handler) {
|
353
|
+
this.modal_handlers.unshift(handler);
|
354
|
+
},
|
355
|
+
remove_handler: function(handler) {
|
356
|
+
window.alert = handler.alert;
|
357
|
+
window.confirm = handler.confirm;
|
358
|
+
window.prompt = handler.prompt;
|
359
|
+
},
|
360
|
+
handler_called: function(handler, str) {
|
361
|
+
handler.called = true;
|
362
|
+
handler.modal_text = str;
|
363
|
+
this.remove_handler(handler);
|
364
|
+
}
|
365
|
+
};
|
366
|
+
};
|
367
|
+
|
368
|
+
var modal_handler = {
|
369
|
+
prompt: window.prompt,
|
370
|
+
confirm: window.confirm,
|
371
|
+
alert: window.alert,
|
372
|
+
called: false
|
373
|
+
}
|
374
|
+
window.capybara.add_handler(modal_handler);
|
375
|
+
|
376
|
+
window.alert = window.confirm = function(str = "") {
|
377
|
+
window.capybara.handler_called(modal_handler, str.toString());
|
378
|
+
return #{accept ? 'true' : 'false'};
|
379
|
+
}
|
380
|
+
window.prompt = function(str = "", default_text = "") {
|
381
|
+
window.capybara.handler_called(modal_handler, str.toString());
|
382
|
+
return #{prompt_response};
|
383
|
+
}
|
384
|
+
JS
|
385
|
+
execute_script script
|
289
386
|
end
|
290
387
|
|
291
|
-
private
|
292
|
-
|
293
388
|
def within_given_window(handle)
|
294
389
|
original_handle = self.current_window_handle
|
295
390
|
if handle == original_handle
|
@@ -306,8 +401,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
306
401
|
# Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
|
307
402
|
# Actual wait time may be longer than specified
|
308
403
|
wait = Selenium::WebDriver::Wait.new(
|
309
|
-
timeout: (
|
310
|
-
ignore:
|
404
|
+
timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
|
405
|
+
ignore: modal_error)
|
311
406
|
begin
|
312
407
|
wait.until do
|
313
408
|
alert = @browser.switch_to.alert
|
@@ -319,6 +414,36 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
319
414
|
end
|
320
415
|
end
|
321
416
|
|
417
|
+
def find_headless_modal(options={})
|
418
|
+
# Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
|
419
|
+
# Actual wait time may be longer than specified
|
420
|
+
wait = Selenium::WebDriver::Wait.new(
|
421
|
+
timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
|
422
|
+
ignore: modal_error)
|
423
|
+
begin
|
424
|
+
wait.until do
|
425
|
+
called, alert_text = evaluate_script('window.capybara && window.capybara.current_modal_status()')
|
426
|
+
if called
|
427
|
+
execute_script('window.capybara && window.capybara.modal_handlers.shift()')
|
428
|
+
regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
|
429
|
+
if alert_text.match(regexp)
|
430
|
+
alert_text
|
431
|
+
else
|
432
|
+
raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
|
433
|
+
end
|
434
|
+
elsif called.nil?
|
435
|
+
# page changed so modal_handler data has gone away
|
436
|
+
warn "Can't verify modal text when page change occurs - ignoring" if options[:text]
|
437
|
+
""
|
438
|
+
else
|
439
|
+
nil
|
440
|
+
end
|
441
|
+
end
|
442
|
+
rescue Selenium::WebDriver::Error::TimeOutError
|
443
|
+
raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
322
447
|
def silenced_unknown_error_message?(msg)
|
323
448
|
silenced_unknown_error_messages.any? { |r| msg =~ r }
|
324
449
|
end
|
@@ -339,4 +464,23 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
339
464
|
arg
|
340
465
|
end
|
341
466
|
end
|
467
|
+
|
468
|
+
def load_selenium
|
469
|
+
begin
|
470
|
+
require 'selenium-webdriver'
|
471
|
+
# Fix for selenium-webdriver 3.4.0 which misnamed these
|
472
|
+
if !defined?(::Selenium::WebDriver::Error::ElementNotInteractableError)
|
473
|
+
::Selenium::WebDriver::Error.const_set('ElementNotInteractableError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
|
474
|
+
end
|
475
|
+
if !defined?(::Selenium::WebDriver::Error::ElementClickInterceptedError)
|
476
|
+
::Selenium::WebDriver::Error.const_set('ElementClickInterceptedError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
|
477
|
+
end
|
478
|
+
rescue LoadError => e
|
479
|
+
if e.message =~ /selenium-webdriver/
|
480
|
+
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."
|
481
|
+
else
|
482
|
+
raise e
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
342
486
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
class Capybara::Selenium::Node < Capybara::Driver::Node
|
3
|
+
|
3
4
|
def visible_text
|
4
5
|
# Selenium doesn't normalize Unicode whitespace.
|
5
6
|
Capybara::Helpers.normalize_whitespace(native.text)
|
@@ -18,7 +19,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
18
19
|
|
19
20
|
def value
|
20
21
|
if tag_name == "select" and multiple?
|
21
|
-
native.find_elements(:
|
22
|
+
native.find_elements(:css, "option:checked").map { |n| n[:value] || n.text }
|
22
23
|
else
|
23
24
|
native[:value]
|
24
25
|
end
|
@@ -38,61 +39,54 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
38
39
|
def set(value, options={})
|
39
40
|
tag_name = self.tag_name
|
40
41
|
type = self[:type]
|
42
|
+
|
41
43
|
if (Array === value) && !multiple?
|
42
44
|
raise ArgumentError.new "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
|
43
45
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
native.
|
46
|
+
|
47
|
+
case tag_name
|
48
|
+
when 'input'
|
49
|
+
case type
|
50
|
+
when 'radio'
|
51
|
+
click
|
52
|
+
when 'checkbox'
|
53
|
+
click if value ^ native.attribute('checked').to_s.eql?("true")
|
54
|
+
when 'file'
|
55
|
+
path_names = value.to_s.empty? ? [] : value
|
56
|
+
if driver.chrome?
|
57
|
+
native.send_keys(Array(path_names).join("\n"))
|
58
|
+
else
|
59
|
+
native.send_keys(*path_names)
|
60
|
+
end
|
52
61
|
else
|
53
|
-
|
62
|
+
set_text(value, options)
|
54
63
|
end
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
64
|
+
when 'textarea'
|
65
|
+
set_text(value, options)
|
66
|
+
else
|
67
|
+
if content_editable?
|
68
|
+
#ensure we are focused on the element
|
69
|
+
click
|
70
|
+
|
71
|
+
script = <<-JS
|
72
|
+
var range = document.createRange();
|
73
|
+
var sel = window.getSelection();
|
74
|
+
arguments[0].focus();
|
75
|
+
range.selectNodeContents(arguments[0]);
|
76
|
+
sel.removeAllRanges();
|
77
|
+
sel.addRange(range);
|
78
|
+
JS
|
79
|
+
driver.execute_script script, self
|
80
|
+
|
81
|
+
if driver.chrome? || driver.firefox?
|
82
|
+
# chromedriver raises a can't focus element for child elements if we use native.send_keys
|
83
|
+
# we've already focused it so just use action api
|
84
|
+
driver.browser.action.send_keys(value.to_s).perform
|
69
85
|
else
|
70
|
-
#
|
71
|
-
# Script can change a readonly element which user input cannot, so
|
72
|
-
# don't execute if readonly.
|
73
|
-
driver.execute_script "arguments[0].value = ''", self
|
86
|
+
# action api is really slow here just use native.send_keys
|
74
87
|
native.send_keys(value.to_s)
|
75
88
|
end
|
76
89
|
end
|
77
|
-
elsif native.attribute('isContentEditable')
|
78
|
-
#ensure we are focused on the element
|
79
|
-
native.click
|
80
|
-
script = <<-JS
|
81
|
-
var range = document.createRange();
|
82
|
-
arguments[0].focus();
|
83
|
-
range.selectNodeContents(arguments[0]);
|
84
|
-
window.getSelection().addRange(range);
|
85
|
-
JS
|
86
|
-
driver.execute_script script, self
|
87
|
-
if (driver.options[:browser].to_s == "chrome") ||
|
88
|
-
(driver.options[:browser].to_s == "firefox" && !driver.marionette?)
|
89
|
-
# chromedriver raises a can't focus element if we use native.send_keys
|
90
|
-
# we've already focused it so just use action api
|
91
|
-
driver.browser.action.send_keys(value.to_s).perform
|
92
|
-
else
|
93
|
-
# action api is really slow here just use native.send_keys
|
94
|
-
native.send_keys(value.to_s)
|
95
|
-
end
|
96
90
|
end
|
97
91
|
end
|
98
92
|
|
@@ -101,22 +95,33 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
101
95
|
end
|
102
96
|
|
103
97
|
def unselect_option
|
104
|
-
|
105
|
-
raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
|
106
|
-
end
|
98
|
+
raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." if !select_node.multiple?
|
107
99
|
native.click if selected?
|
108
100
|
end
|
109
101
|
|
110
102
|
def click
|
111
103
|
native.click
|
104
|
+
rescue => e
|
105
|
+
if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
|
106
|
+
e.message =~ /Other element would receive the click/
|
107
|
+
begin
|
108
|
+
driver.execute_script("arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'})", self)
|
109
|
+
rescue
|
110
|
+
end
|
111
|
+
end
|
112
|
+
raise e
|
112
113
|
end
|
113
114
|
|
114
115
|
def right_click
|
115
|
-
|
116
|
+
scroll_if_needed do
|
117
|
+
driver.browser.action.context_click(native).perform
|
118
|
+
end
|
116
119
|
end
|
117
120
|
|
118
121
|
def double_click
|
119
|
-
|
122
|
+
scroll_if_needed do
|
123
|
+
driver.browser.action.double_click(native).perform
|
124
|
+
end
|
120
125
|
end
|
121
126
|
|
122
127
|
def send_keys(*args)
|
@@ -124,11 +129,15 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
124
129
|
end
|
125
130
|
|
126
131
|
def hover
|
127
|
-
|
132
|
+
scroll_if_needed do
|
133
|
+
driver.browser.action.move_to(native).perform
|
134
|
+
end
|
128
135
|
end
|
129
136
|
|
130
137
|
def drag_to(element)
|
131
|
-
|
138
|
+
scroll_if_needed do
|
139
|
+
driver.browser.action.drag_and_drop(native, element.native).perform
|
140
|
+
end
|
132
141
|
end
|
133
142
|
|
134
143
|
def tag_name
|
@@ -169,6 +178,10 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
169
178
|
multiple and multiple != "false"
|
170
179
|
end
|
171
180
|
|
181
|
+
def content_editable?
|
182
|
+
native.attribute('isContentEditable')
|
183
|
+
end
|
184
|
+
|
172
185
|
def find_xpath(locator)
|
173
186
|
native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
|
174
187
|
end
|
@@ -208,6 +221,44 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
208
221
|
private
|
209
222
|
# a reference to the select node if this is an option node
|
210
223
|
def select_node
|
211
|
-
find_xpath('./ancestor::select').first
|
224
|
+
find_xpath('./ancestor::select[1]').first
|
225
|
+
end
|
226
|
+
|
227
|
+
def set_text(value, options)
|
228
|
+
if readonly?
|
229
|
+
warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
|
230
|
+
elsif value.to_s.empty? && options[:clear].nil?
|
231
|
+
native.clear
|
232
|
+
else
|
233
|
+
if options[:clear] == :backspace
|
234
|
+
# Clear field by sending the correct number of backspace keys.
|
235
|
+
backspaces = [:backspace] * self.value.to_s.length
|
236
|
+
native.send_keys(*(backspaces + [value.to_s]))
|
237
|
+
elsif options[:clear] == :none
|
238
|
+
native.send_keys(value.to_s)
|
239
|
+
elsif options[:clear].is_a? Array
|
240
|
+
native.send_keys(*options[:clear], value.to_s)
|
241
|
+
else
|
242
|
+
# Clear field by JavaScript assignment of the value property.
|
243
|
+
# Script can change a readonly element which user input cannot, so
|
244
|
+
# don't execute if readonly.
|
245
|
+
driver.execute_script "arguments[0].value = ''", self
|
246
|
+
native.send_keys(value.to_s)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def scroll_if_needed(&block)
|
252
|
+
block.call
|
253
|
+
rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
|
254
|
+
script = <<-JS
|
255
|
+
try {
|
256
|
+
arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
257
|
+
} catch(e) {
|
258
|
+
arguments[0].scrollIntoView(true);
|
259
|
+
}
|
260
|
+
JS
|
261
|
+
driver.execute_script(script, self)
|
262
|
+
block.call
|
212
263
|
end
|
213
264
|
end
|
data/lib/capybara/server.rb
CHANGED
@@ -25,9 +25,10 @@ module Capybara
|
|
25
25
|
|
26
26
|
attr_accessor :error
|
27
27
|
|
28
|
-
def initialize(app)
|
28
|
+
def initialize(app, server_errors)
|
29
29
|
@app = app
|
30
30
|
@counter = Counter.new
|
31
|
+
@server_errors = server_errors
|
31
32
|
end
|
32
33
|
|
33
34
|
def pending_requests?
|
@@ -41,7 +42,7 @@ module Capybara
|
|
41
42
|
@counter.increment
|
42
43
|
begin
|
43
44
|
@app.call(env)
|
44
|
-
rescue
|
45
|
+
rescue *@server_errors => e
|
45
46
|
@error = e unless @error
|
46
47
|
raise e
|
47
48
|
ensure
|
@@ -59,10 +60,10 @@ module Capybara
|
|
59
60
|
|
60
61
|
attr_reader :app, :port, :host
|
61
62
|
|
62
|
-
def initialize(app, port=Capybara.server_port, host=Capybara.server_host)
|
63
|
+
def initialize(app, port=Capybara.server_port, host=Capybara.server_host, server_errors=Capybara.server_errors)
|
63
64
|
@app = app
|
64
65
|
@server_thread = nil # suppress warnings
|
65
|
-
@host, @port = host, port
|
66
|
+
@host, @port, @server_errors = host, port, server_errors
|
66
67
|
@port ||= Capybara::Server.ports[port_key]
|
67
68
|
@port ||= find_available_port(host)
|
68
69
|
end
|
@@ -112,7 +113,7 @@ module Capybara
|
|
112
113
|
private
|
113
114
|
|
114
115
|
def middleware
|
115
|
-
@middleware ||= Middleware.new(app)
|
116
|
+
@middleware ||= Middleware.new(app, @server_errors)
|
116
117
|
end
|
117
118
|
|
118
119
|
def port_key
|