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