capybara 3.8.0 → 3.9.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 +4 -4
- data/History.md +25 -1
- data/lib/capybara/config.rb +1 -0
- data/lib/capybara/dsl.rb +2 -2
- data/lib/capybara/helpers.rb +1 -0
- data/lib/capybara/node/actions.rb +4 -0
- data/lib/capybara/node/base.rb +1 -0
- data/lib/capybara/node/element.rb +3 -0
- data/lib/capybara/node/finders.rb +2 -0
- data/lib/capybara/node/simple.rb +1 -0
- data/lib/capybara/queries/base_query.rb +1 -0
- data/lib/capybara/queries/match_query.rb +1 -0
- data/lib/capybara/queries/selector_query.rb +34 -37
- data/lib/capybara/queries/text_query.rb +2 -0
- data/lib/capybara/rack_test/browser.rb +1 -0
- data/lib/capybara/rack_test/driver.rb +5 -0
- data/lib/capybara/rack_test/node.rb +2 -0
- data/lib/capybara/result.rb +2 -0
- data/lib/capybara/rspec/compound.rb +2 -0
- data/lib/capybara/rspec/matchers.rb +1 -0
- data/lib/capybara/selector/builders/css_builder.rb +49 -0
- data/lib/capybara/selector/builders/xpath_builder.rb +56 -0
- data/lib/capybara/selector/filter_set.rb +1 -0
- data/lib/capybara/selector/filters/base.rb +2 -0
- data/lib/capybara/selector/regexp_disassembler.rb +66 -0
- data/lib/capybara/selector/selector.rb +25 -5
- data/lib/capybara/selector.rb +14 -27
- data/lib/capybara/selenium/driver.rb +8 -1
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +19 -1
- data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +1 -0
- data/lib/capybara/selenium/node.rb +11 -0
- data/lib/capybara/selenium/nodes/chrome_node.rb +2 -0
- data/lib/capybara/selenium/nodes/marionette_node.rb +37 -20
- data/lib/capybara/server/animation_disabler.rb +1 -0
- data/lib/capybara/server.rb +4 -0
- data/lib/capybara/session/config.rb +2 -0
- data/lib/capybara/session.rb +5 -0
- data/lib/capybara/spec/session/has_css_spec.rb +16 -0
- data/lib/capybara/spec/session/has_field_spec.rb +4 -0
- data/lib/capybara/spec/session/node_spec.rb +6 -0
- data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
- data/lib/capybara/spec/session/reset_session_spec.rb +15 -1
- data/lib/capybara/spec/session/selectors_spec.rb +12 -2
- data/lib/capybara/spec/views/form.erb +15 -0
- data/lib/capybara/version.rb +1 -1
- data/lib/capybara/xpath_patches.rb +27 -0
- data/lib/capybara.rb +32 -10
- data/spec/dsl_spec.rb +15 -1
- data/spec/rack_test_spec.rb +6 -1
- data/spec/regexp_dissassembler_spec.rb +154 -0
- data/spec/selector_spec.rb +53 -18
- data/spec/selenium_spec_chrome.rb +28 -0
- data/spec/selenium_spec_firefox_remote.rb +2 -0
- data/spec/selenium_spec_marionette.rb +11 -0
- data/spec/shared_selenium_session.rb +20 -0
- data/spec/spec_helper.rb +4 -0
- metadata +7 -2
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
require 'capybara/selector/filter_set'
|
|
6
6
|
require 'capybara/selector/css'
|
|
7
|
+
require 'capybara/selector/regexp_disassembler'
|
|
8
|
+
require 'capybara/selector/builders/xpath_builder'
|
|
9
|
+
require 'capybara/selector/builders/css_builder'
|
|
7
10
|
|
|
8
11
|
module Capybara
|
|
9
12
|
#
|
|
@@ -381,13 +384,29 @@ module Capybara
|
|
|
381
384
|
# * :all - finds visible and invisible elements.
|
|
382
385
|
# * :hidden - only finds invisible elements.
|
|
383
386
|
# * :visible - only finds visible elements.
|
|
384
|
-
def visible(default_visibility)
|
|
385
|
-
@default_visibility = default_visibility
|
|
387
|
+
def visible(default_visibility = nil, &block)
|
|
388
|
+
@default_visibility = block || default_visibility
|
|
386
389
|
end
|
|
387
390
|
|
|
388
|
-
def default_visibility(fallback = Capybara.ignore_hidden_elements)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
+
def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
|
|
392
|
+
vis = if @default_visibility&.respond_to?(:call)
|
|
393
|
+
@default_visibility.call(options)
|
|
394
|
+
else
|
|
395
|
+
@default_visibility
|
|
396
|
+
end
|
|
397
|
+
vis.nil? ? fallback : vis
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# @api private
|
|
401
|
+
def builder
|
|
402
|
+
case format
|
|
403
|
+
when :css
|
|
404
|
+
Capybara::Selector::CSSBuilder
|
|
405
|
+
when :xpath
|
|
406
|
+
Capybara::Selector::XPathBuilder
|
|
407
|
+
else
|
|
408
|
+
raise NotImplementedError, "No builder exists for selector of type #{format}"
|
|
409
|
+
end
|
|
391
410
|
end
|
|
392
411
|
|
|
393
412
|
private
|
|
@@ -402,6 +421,7 @@ module Capybara
|
|
|
402
421
|
|
|
403
422
|
def locate_field(xpath, locator, **_options)
|
|
404
423
|
return xpath if locator.nil?
|
|
424
|
+
|
|
405
425
|
locate_xpath = xpath # Need to save original xpath for the label wrap
|
|
406
426
|
locator = locator.to_s
|
|
407
427
|
attr_matchers = [XPath.attr(:id) == locator,
|
data/lib/capybara/selector.rb
CHANGED
|
@@ -38,8 +38,11 @@ Capybara.add_selector(:id) do
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
Capybara.add_selector(:field) do
|
|
41
|
+
visible { |options| :hidden if options[:type] == 'hidden' }
|
|
41
42
|
xpath do |locator, **options|
|
|
42
|
-
|
|
43
|
+
invalid_types = %w[submit image]
|
|
44
|
+
invalid_types << 'hidden' unless options[:type] == 'hidden'
|
|
45
|
+
xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
|
|
43
46
|
locate_field(xpath, locator, options)
|
|
44
47
|
end
|
|
45
48
|
|
|
@@ -87,18 +90,7 @@ end
|
|
|
87
90
|
Capybara.add_selector(:link) do
|
|
88
91
|
xpath(:title, :alt) do |locator, href: true, alt: nil, title: nil, **|
|
|
89
92
|
xpath = XPath.descendant(:a)
|
|
90
|
-
xpath = xpath[
|
|
91
|
-
case href
|
|
92
|
-
when nil, false
|
|
93
|
-
!XPath.attr(:href)
|
|
94
|
-
when true
|
|
95
|
-
XPath.attr(:href)
|
|
96
|
-
when Regexp
|
|
97
|
-
nil # needs to be handled in filter
|
|
98
|
-
else
|
|
99
|
-
XPath.attr(:href) == href.to_s
|
|
100
|
-
end
|
|
101
|
-
]
|
|
93
|
+
xpath = xpath[@href_conditions = builder.attribute_conditions(href: href)]
|
|
102
94
|
|
|
103
95
|
unless locator.nil?
|
|
104
96
|
locator = locator.to_s
|
|
@@ -132,13 +124,19 @@ Capybara.add_selector(:link) do
|
|
|
132
124
|
|
|
133
125
|
describe_expression_filters do |**options|
|
|
134
126
|
desc = +''
|
|
135
|
-
|
|
127
|
+
if (href = options[:href])
|
|
128
|
+
if !href.is_a?(Regexp)
|
|
129
|
+
desc << " with href #{href.inspect}"
|
|
130
|
+
elsif @href_conditions
|
|
131
|
+
desc << " with href matching #{href.inspect}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
136
134
|
desc << ' with no href attribute' if options.fetch(:href, true).nil?
|
|
137
135
|
desc
|
|
138
136
|
end
|
|
139
137
|
|
|
140
138
|
describe_node_filters do |href: nil, **|
|
|
141
|
-
" with href matching #{href.inspect}" if href.is_a?
|
|
139
|
+
" with href matching #{href.inspect}" if href.is_a?(Regexp) && @href_conditions.nil?
|
|
142
140
|
end
|
|
143
141
|
end
|
|
144
142
|
|
|
@@ -472,18 +470,7 @@ Capybara.add_selector(:element) do
|
|
|
472
470
|
end
|
|
473
471
|
|
|
474
472
|
expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
|
|
475
|
-
|
|
476
|
-
when Regexp
|
|
477
|
-
xpath
|
|
478
|
-
when true
|
|
479
|
-
xpath[XPath.attr(name)]
|
|
480
|
-
when false
|
|
481
|
-
xpath[!XPath.attr(name)]
|
|
482
|
-
when XPath::Expression
|
|
483
|
-
xpath[XPath.attr(name)[val]]
|
|
484
|
-
else
|
|
485
|
-
xpath[XPath.attr(name.to_sym) == val]
|
|
486
|
-
end
|
|
473
|
+
xpath[builder.attribute_conditions(name => val)]
|
|
487
474
|
end
|
|
488
475
|
|
|
489
476
|
node_filter(:attributes, matcher: /.+/) do |node, name, val|
|
|
@@ -17,6 +17,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
17
17
|
warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade." if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0')
|
|
18
18
|
rescue LoadError => err
|
|
19
19
|
raise err if err.message !~ /selenium-webdriver/
|
|
20
|
+
|
|
20
21
|
raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
|
|
21
22
|
end
|
|
22
23
|
|
|
@@ -117,6 +118,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
117
118
|
# Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
|
|
118
119
|
until find_xpath('/html/body/*').empty?
|
|
119
120
|
raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if timer.expired?
|
|
121
|
+
|
|
120
122
|
sleep 0.05
|
|
121
123
|
end
|
|
122
124
|
rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
|
@@ -184,6 +186,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
|
184
186
|
|
|
185
187
|
def close_window(handle)
|
|
186
188
|
raise ArgumentError, 'Not allowed to close the primary window' if handle == window_handles.first
|
|
189
|
+
|
|
187
190
|
within_given_window(handle) do
|
|
188
191
|
browser.close
|
|
189
192
|
end
|
|
@@ -260,7 +263,7 @@ private
|
|
|
260
263
|
end
|
|
261
264
|
|
|
262
265
|
def clear_browser_state
|
|
263
|
-
|
|
266
|
+
delete_all_cookies
|
|
264
267
|
clear_storage
|
|
265
268
|
rescue Selenium::WebDriver::Error::UnhandledError # rubocop:disable Lint/HandleExceptions
|
|
266
269
|
# delete_all_cookies fails when we've previously gone
|
|
@@ -268,6 +271,10 @@ private
|
|
|
268
271
|
# instead.
|
|
269
272
|
end
|
|
270
273
|
|
|
274
|
+
def delete_all_cookies
|
|
275
|
+
@browser.manage.delete_all_cookies
|
|
276
|
+
end
|
|
277
|
+
|
|
271
278
|
def clear_storage
|
|
272
279
|
clear_session_storage if options[:clear_session_storage]
|
|
273
280
|
clear_local_storage if options[:clear_local_storage]
|
|
@@ -9,7 +9,7 @@ module Capybara::Selenium::Driver::ChromeDriver
|
|
|
9
9
|
super
|
|
10
10
|
rescue NoMethodError => err
|
|
11
11
|
raise unless err.message =~ /full_screen_window/
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
|
|
14
14
|
result['value']
|
|
15
15
|
end
|
|
@@ -20,6 +20,7 @@ module Capybara::Selenium::Driver::ChromeDriver
|
|
|
20
20
|
super
|
|
21
21
|
rescue Selenium::WebDriver::Error::UnknownError => err
|
|
22
22
|
raise unless err.message =~ /failed to change window state/
|
|
23
|
+
|
|
23
24
|
# Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
|
|
24
25
|
# and raises unnecessary error. Wait a bit and try again.
|
|
25
26
|
sleep 0.5
|
|
@@ -37,7 +38,24 @@ module Capybara::Selenium::Driver::ChromeDriver
|
|
|
37
38
|
|
|
38
39
|
private
|
|
39
40
|
|
|
41
|
+
def delete_all_cookies
|
|
42
|
+
execute_cdp('Network.clearBrowserCookies')
|
|
43
|
+
rescue Selenium::WebDriver::Error::UnhandledError, Selenium::WebDriver::Error::WebDriverError
|
|
44
|
+
# If the CDP clear isn't supported do original limited clear
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def execute_cdp(cmd, params = {})
|
|
49
|
+
args = { cmd: cmd, params: params }
|
|
50
|
+
result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
|
|
51
|
+
result['value']
|
|
52
|
+
end
|
|
53
|
+
|
|
40
54
|
def build_node(native_node)
|
|
41
55
|
::Capybara::Selenium::ChromeNode.new(self, native_node)
|
|
42
56
|
end
|
|
57
|
+
|
|
58
|
+
def bridge
|
|
59
|
+
browser.send(:bridge)
|
|
60
|
+
end
|
|
43
61
|
end
|
|
@@ -34,6 +34,7 @@ module Capybara::Selenium::Driver::MarionetteDriver
|
|
|
34
34
|
|
|
35
35
|
def switch_to_frame(frame)
|
|
36
36
|
return super unless frame == :parent
|
|
37
|
+
|
|
37
38
|
# geckodriver/firefox has an issue if the current frame is removed from within it
|
|
38
39
|
# so we have to move to the default_content and iterate back through the frames
|
|
39
40
|
handles = @frame_handles[current_window_handle]
|
|
@@ -48,6 +48,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
|
48
48
|
# Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
|
|
49
49
|
def set(value, **options)
|
|
50
50
|
raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}" if value.is_a?(Array) && !multiple?
|
|
51
|
+
|
|
51
52
|
case tag_name
|
|
52
53
|
when 'input'
|
|
53
54
|
case self[:type]
|
|
@@ -79,12 +80,14 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
|
79
80
|
|
|
80
81
|
def unselect_option
|
|
81
82
|
raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
|
|
83
|
+
|
|
82
84
|
click if selected?
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
def click(keys = [], **options)
|
|
86
88
|
click_options = ClickOptions.new(keys, options)
|
|
87
89
|
return native.click if click_options.empty?
|
|
90
|
+
|
|
88
91
|
click_with_options(click_options)
|
|
89
92
|
rescue StandardError => err
|
|
90
93
|
if err.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
|
|
@@ -136,6 +139,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
|
136
139
|
|
|
137
140
|
def disabled?
|
|
138
141
|
return true unless native.enabled?
|
|
142
|
+
|
|
139
143
|
# WebDriver only defines `disabled?` for form controls but fieldset makes sense too
|
|
140
144
|
tag_name == 'fieldset' && find_xpath('ancestor-or-self::fieldset[@disabled]').any?
|
|
141
145
|
end
|
|
@@ -255,6 +259,7 @@ private
|
|
|
255
259
|
def set_date(value) # rubocop:disable Naming/AccessorMethodName
|
|
256
260
|
value = SettableValue.new(value)
|
|
257
261
|
return set_text(value) unless value.dateable?
|
|
262
|
+
|
|
258
263
|
# TODO: this would be better if locale can be detected and correct keystrokes sent
|
|
259
264
|
update_value_js(value.to_date_str)
|
|
260
265
|
end
|
|
@@ -262,6 +267,7 @@ private
|
|
|
262
267
|
def set_time(value) # rubocop:disable Naming/AccessorMethodName
|
|
263
268
|
value = SettableValue.new(value)
|
|
264
269
|
return set_text(value) unless value.timeable?
|
|
270
|
+
|
|
265
271
|
# TODO: this would be better if locale can be detected and correct keystrokes sent
|
|
266
272
|
update_value_js(value.to_time_str)
|
|
267
273
|
end
|
|
@@ -269,6 +275,7 @@ private
|
|
|
269
275
|
def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
|
|
270
276
|
value = SettableValue.new(value)
|
|
271
277
|
return set_text(value) unless value.timeable?
|
|
278
|
+
|
|
272
279
|
# TODO: this would be better if locale can be detected and correct keystrokes sent
|
|
273
280
|
update_value_js(value.to_datetime_str)
|
|
274
281
|
end
|
|
@@ -355,6 +362,10 @@ private
|
|
|
355
362
|
@value = value
|
|
356
363
|
end
|
|
357
364
|
|
|
365
|
+
def to_s
|
|
366
|
+
value.to_s
|
|
367
|
+
end
|
|
368
|
+
|
|
358
369
|
def dateable?
|
|
359
370
|
!value.is_a?(String) && value.respond_to?(:to_date)
|
|
360
371
|
end
|
|
@@ -11,11 +11,13 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
|
|
|
11
11
|
if err.message =~ /File not found : .+\n.+/m
|
|
12
12
|
raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload"
|
|
13
13
|
end
|
|
14
|
+
|
|
14
15
|
raise
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def drag_to(element)
|
|
18
19
|
return super unless html5_draggable?
|
|
20
|
+
|
|
19
21
|
html5_drag_to(element)
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -21,6 +21,7 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node
|
|
|
21
21
|
return super unless browser_version < 61.0
|
|
22
22
|
|
|
23
23
|
return true if super
|
|
24
|
+
|
|
24
25
|
# workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
|
|
25
26
|
if %w[option optgroup].include? tag_name
|
|
26
27
|
find_xpath('parent::*[self::optgroup or self::select]')[0].disabled?
|
|
@@ -52,13 +53,12 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node
|
|
|
52
53
|
return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
|
|
53
54
|
|
|
54
55
|
native.click
|
|
55
|
-
args.
|
|
56
|
-
_send_keys(keys, actions)
|
|
57
|
-
end.perform
|
|
56
|
+
_send_keys(args).perform
|
|
58
57
|
end
|
|
59
58
|
|
|
60
59
|
def drag_to(element)
|
|
61
60
|
return super unless (browser_version >= 62.0) && html5_draggable?
|
|
61
|
+
|
|
62
62
|
html5_drag_to(element)
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -71,35 +71,29 @@ private
|
|
|
71
71
|
super
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def _send_keys(keys, actions, down_keys =
|
|
74
|
+
def _send_keys(keys, actions = browser_action, down_keys = ModifierKeysStack.new)
|
|
75
75
|
case keys
|
|
76
|
-
when String
|
|
77
|
-
keys = keys.upcase if down_keys&.include?(:shift) # https://bugzilla.mozilla.org/show_bug.cgi?id=1405370
|
|
78
|
-
actions.send_keys(keys)
|
|
79
|
-
when :space
|
|
80
|
-
actions.send_keys(' ') # https://github.com/mozilla/geckodriver/issues/846
|
|
81
76
|
when :control, :left_control, :right_control,
|
|
82
77
|
:alt, :left_alt, :right_alt,
|
|
83
78
|
:shift, :left_shift, :right_shift,
|
|
84
79
|
:meta, :left_meta, :right_meta,
|
|
85
80
|
:command
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
81
|
+
down_keys.press(keys)
|
|
82
|
+
actions.key_down(keys)
|
|
83
|
+
when String
|
|
84
|
+
# https://bugzilla.mozilla.org/show_bug.cgi?id=1405370
|
|
85
|
+
keys = keys.upcase if (browser_version < 64.0) && down_keys&.include?(:shift)
|
|
86
|
+
actions.send_keys(keys)
|
|
92
87
|
when Symbol
|
|
93
88
|
actions.send_keys(keys)
|
|
94
89
|
when Array
|
|
95
|
-
|
|
96
|
-
keys.each
|
|
97
|
-
|
|
98
|
-
end
|
|
99
|
-
local_down_keys.each { |key| actions.key_up(key) }
|
|
90
|
+
down_keys.push
|
|
91
|
+
keys.each { |sub_keys| _send_keys(sub_keys, actions, down_keys) }
|
|
92
|
+
down_keys.pop.reverse_each { |key| actions.key_up(key) }
|
|
100
93
|
else
|
|
101
94
|
raise ArgumentError, 'Unknown keys type'
|
|
102
95
|
end
|
|
96
|
+
actions
|
|
103
97
|
end
|
|
104
98
|
|
|
105
99
|
def bridge
|
|
@@ -118,4 +112,27 @@ private
|
|
|
118
112
|
def browser_version
|
|
119
113
|
driver.browser.capabilities[:browser_version].to_f
|
|
120
114
|
end
|
|
115
|
+
|
|
116
|
+
class ModifierKeysStack
|
|
117
|
+
def initialize
|
|
118
|
+
@stack = []
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def include?(key)
|
|
122
|
+
@stack.flatten.include?(key)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def press(key)
|
|
126
|
+
@stack.last.push(key)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def push
|
|
130
|
+
@stack.push []
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def pop
|
|
134
|
+
@stack.pop
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
private_constant :ModifierKeysStack
|
|
121
138
|
end
|
data/lib/capybara/server.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'capybara/server/animation_disabler'
|
|
|
8
8
|
require 'capybara/server/checker'
|
|
9
9
|
|
|
10
10
|
module Capybara
|
|
11
|
+
# @api private
|
|
11
12
|
class Server
|
|
12
13
|
class << self
|
|
13
14
|
def ports
|
|
@@ -44,6 +45,7 @@ module Capybara
|
|
|
44
45
|
|
|
45
46
|
def responsive?
|
|
46
47
|
return false if @server_thread&.join(0)
|
|
48
|
+
|
|
47
49
|
res = @checker.request { |http| http.get('/__identify__') }
|
|
48
50
|
|
|
49
51
|
if res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPRedirection)
|
|
@@ -57,6 +59,7 @@ module Capybara
|
|
|
57
59
|
timer = Capybara::Helpers.timer(expire_in: 60)
|
|
58
60
|
while pending_requests?
|
|
59
61
|
raise 'Requests did not finish in 60 seconds' if timer.expired?
|
|
62
|
+
|
|
60
63
|
sleep 0.01
|
|
61
64
|
end
|
|
62
65
|
end
|
|
@@ -72,6 +75,7 @@ module Capybara
|
|
|
72
75
|
timer = Capybara::Helpers.timer(expire_in: 60)
|
|
73
76
|
until responsive?
|
|
74
77
|
raise 'Rack application timed out during boot' if timer.expired?
|
|
78
|
+
|
|
75
79
|
@server_thread.join(0.1)
|
|
76
80
|
end
|
|
77
81
|
end
|
|
@@ -78,12 +78,14 @@ module Capybara
|
|
|
78
78
|
remove_method :app_host=
|
|
79
79
|
def app_host=(url)
|
|
80
80
|
raise ArgumentError, "Capybara.app_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}." if url && url !~ URI::DEFAULT_PARSER.make_regexp
|
|
81
|
+
|
|
81
82
|
@app_host = url
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
remove_method :default_host=
|
|
85
86
|
def default_host=(url)
|
|
86
87
|
raise ArgumentError, "Capybara.default_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}." if url && url !~ URI::DEFAULT_PARSER.make_regexp
|
|
88
|
+
|
|
87
89
|
@default_host = url
|
|
88
90
|
end
|
|
89
91
|
|
data/lib/capybara/session.rb
CHANGED
|
@@ -76,11 +76,13 @@ module Capybara
|
|
|
76
76
|
|
|
77
77
|
def initialize(mode, app = nil)
|
|
78
78
|
raise TypeError, 'The second parameter to Session::new should be a rack app if passed.' if app && !app.respond_to?(:call)
|
|
79
|
+
|
|
79
80
|
@@instance_created = true
|
|
80
81
|
@mode = mode
|
|
81
82
|
@app = app
|
|
82
83
|
if block_given?
|
|
83
84
|
raise 'A configuration block is only accepted when Capybara.threadsafe == true' unless Capybara.threadsafe
|
|
85
|
+
|
|
84
86
|
yield config
|
|
85
87
|
end
|
|
86
88
|
@server = if config.run_server && @app && driver.needs_server?
|
|
@@ -138,6 +140,7 @@ module Capybara
|
|
|
138
140
|
#
|
|
139
141
|
def raise_server_error!
|
|
140
142
|
return unless @server&.error
|
|
143
|
+
|
|
141
144
|
# Force an explanation for the error being raised as the exception cause
|
|
142
145
|
begin
|
|
143
146
|
if config.raise_server_errors
|
|
@@ -468,6 +471,7 @@ module Capybara
|
|
|
468
471
|
def switch_to_window(window = nil, **options, &window_locator)
|
|
469
472
|
raise ArgumentError, '`switch_to_window` can take either a block or a window, not both' if window && block_given?
|
|
470
473
|
raise ArgumentError, '`switch_to_window`: either window or block should be provided' if !window && !block_given?
|
|
474
|
+
|
|
471
475
|
unless scopes.last.nil?
|
|
472
476
|
raise Capybara::ScopeError, '`switch_to_window` is not supposed to be invoked from '\
|
|
473
477
|
'`within` or `within_frame` blocks.'
|
|
@@ -769,6 +773,7 @@ module Capybara
|
|
|
769
773
|
#
|
|
770
774
|
def configure
|
|
771
775
|
raise 'Session configuration is only supported when Capybara.threadsafe == true' unless Capybara.threadsafe
|
|
776
|
+
|
|
772
777
|
yield config
|
|
773
778
|
end
|
|
774
779
|
|
|
@@ -23,6 +23,22 @@ Capybara::SpecHelper.spec '#has_css?' do
|
|
|
23
23
|
expect(@session).not_to have_css('p.nosuchclass')
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
it 'should support :id option' do
|
|
27
|
+
expect(@session).to have_css('h2', id: 'h2one')
|
|
28
|
+
expect(@session).to have_css('h2')
|
|
29
|
+
expect(@session).to have_css('h2', id: /h2o/)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'should support :class option' do
|
|
33
|
+
expect(@session).to have_css('li', class: 'guitarist')
|
|
34
|
+
expect(@session).to have_css('li', class: /guitar/)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'should support case insensitive :class and :id options' do
|
|
38
|
+
expect(@session).to have_css('li', class: /UiTaRI/i)
|
|
39
|
+
expect(@session).to have_css('h2', id: /2ON/i)
|
|
40
|
+
end
|
|
41
|
+
|
|
26
42
|
it 'should respect scopes' do
|
|
27
43
|
@session.within "//p[@id='first']" do
|
|
28
44
|
expect(@session).to have_css('a#foo')
|
|
@@ -61,6 +61,10 @@ Capybara::SpecHelper.spec '#has_field' do
|
|
|
61
61
|
expect(@session).not_to have_field('Description', type: 'email')
|
|
62
62
|
expect(@session).not_to have_field('Languages', type: 'textarea')
|
|
63
63
|
end
|
|
64
|
+
|
|
65
|
+
it 'it can find type="hidden" elements if explicity specified' do
|
|
66
|
+
expect(@session).to have_field('form[data]', with: 'TWTW', type: 'hidden')
|
|
67
|
+
end
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
context 'with multiple' do
|
|
@@ -478,6 +478,12 @@ Capybara::SpecHelper.spec 'node' do
|
|
|
478
478
|
expect(@session.find(:css, '#address1_city').value).to eq 'Oceanside'
|
|
479
479
|
end
|
|
480
480
|
|
|
481
|
+
it 'should hold modifers at top level' do
|
|
482
|
+
@session.visit('/form')
|
|
483
|
+
@session.find(:css, '#address1_city').send_keys('ocean', :shift, 'side')
|
|
484
|
+
expect(@session.find(:css, '#address1_city').value).to eq 'oceanSIDE'
|
|
485
|
+
end
|
|
486
|
+
|
|
481
487
|
it 'should generate key events', requires: %i[send_keys js] do
|
|
482
488
|
@session.visit('/with_js')
|
|
483
489
|
@session.find(:css, '#with-key-events').send_keys([:shift, 't'], [:shift, 'w'])
|
|
@@ -34,6 +34,6 @@ Capybara::SpecHelper.spec '#to_capybara_node' do
|
|
|
34
34
|
end.to raise_error(/^expected to find css "#second" within #<Capybara::Node::Element/)
|
|
35
35
|
expect do
|
|
36
36
|
expect(para).to have_link(href: %r{/without_simple_html})
|
|
37
|
-
end.to raise_error(%r{^expected to find
|
|
37
|
+
end.to raise_error(%r{^expected to find link nil with href matching /\\/without_simple_html/ within #<Capybara::Node::Element})
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Capybara::SpecHelper.spec '#reset_session!' do
|
|
4
|
-
it 'removes cookies' do
|
|
4
|
+
it 'removes cookies from current domain' do
|
|
5
5
|
@session.visit('/set_cookie')
|
|
6
6
|
@session.visit('/get_cookie')
|
|
7
7
|
expect(@session).to have_content('test_cookie')
|
|
@@ -11,6 +11,20 @@ Capybara::SpecHelper.spec '#reset_session!' do
|
|
|
11
11
|
expect(@session.body).not_to include('test_cookie')
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
it 'removes ALL cookies', requires: [:server] do
|
|
15
|
+
domains = ['localhost', '127.0.0.1']
|
|
16
|
+
domains.each do |domain|
|
|
17
|
+
@session.visit("http://#{domain}:#{@session.server.port}/set_cookie")
|
|
18
|
+
@session.visit("http://#{domain}:#{@session.server.port}/get_cookie")
|
|
19
|
+
expect(@session).to have_content('test_cookie')
|
|
20
|
+
end
|
|
21
|
+
@session.reset_session!
|
|
22
|
+
domains.each do |domain|
|
|
23
|
+
@session.visit("http://#{domain}:#{@session.server.port}/get_cookie")
|
|
24
|
+
expect(@session.body).not_to include('test_cookie')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
14
28
|
it 'resets current url, host, path' do
|
|
15
29
|
@session.visit '/foo'
|
|
16
30
|
expect(@session.current_url).not_to be_empty
|
|
@@ -40,8 +40,18 @@ Capybara::SpecHelper.spec Capybara::Selector do
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
describe 'field selectors' do
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
context 'with :id option' do
|
|
44
|
+
it 'can find specifically by id' do
|
|
45
|
+
expect(@session.find(:field, id: 'customer_email').value).to eq 'ben@ben.com'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'can find by regex' do
|
|
49
|
+
expect(@session.find(:field, id: /ustomer.emai/).value).to eq 'ben@ben.com'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'can find by case-insensitive id' do
|
|
53
|
+
expect(@session.find(:field, id: /StOmer.emAI/i).value).to eq 'ben@ben.com'
|
|
54
|
+
end
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
it 'can find specifically by name' do
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
</label>
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
|
+
<p>
|
|
29
|
+
<label for="customer_other_email">Customer Other Email
|
|
30
|
+
<input type="text" name="form[customer_other_email]" value="notben@notben.com" id="customer_other_email"/>
|
|
31
|
+
</label>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
28
34
|
<p>
|
|
29
35
|
<label for="form_other_title">Other title</label>
|
|
30
36
|
<select name="form[other_title]" id="form_other_title">
|
|
@@ -547,6 +553,11 @@ New line after and before textarea tag
|
|
|
547
553
|
<input type="file" name="form[multiple_documents][]" id="form_multiple_documents" multiple="multiple" />
|
|
548
554
|
</p>
|
|
549
555
|
|
|
556
|
+
<p>
|
|
557
|
+
<label for="form_directory_upload">Directory Upload</label>
|
|
558
|
+
<input type="file" name="form[multiple_documents][]" id="form_directory_upload" multiple="multiple" webkitdirectory="webkitdirectory" mozdirectory="mozdirectory" />
|
|
559
|
+
</p>
|
|
560
|
+
|
|
550
561
|
<p>
|
|
551
562
|
<input type="submit" value="Upload Multiple"/>
|
|
552
563
|
<p>
|
|
@@ -643,3 +654,7 @@ New line after and before textarea tag
|
|
|
643
654
|
<label for="asterisk_input">With Asterisk<abbr title="required">*</abbr></label>
|
|
644
655
|
<input id="asterisk_input" type="number"value="2016"/>
|
|
645
656
|
</p>
|
|
657
|
+
|
|
658
|
+
<p>
|
|
659
|
+
<input id="special" {custom}="abcdef" value="custom attribute"/>
|
|
660
|
+
</p>
|
data/lib/capybara/version.rb
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XPath
|
|
4
|
+
module DSL
|
|
5
|
+
def lowercase
|
|
6
|
+
method(:translate, 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ', 'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def uppercase
|
|
10
|
+
method(:translate, 'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ', 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Capybara
|
|
16
|
+
module XPathPatches
|
|
17
|
+
module Renderer
|
|
18
|
+
def attribute(current, name)
|
|
19
|
+
return super if name =~ /^[a-zA-Z_:][a-zA-Z0-9_:\.\-]*$/
|
|
20
|
+
|
|
21
|
+
"#{current}/attribute::*[local-name(.) = #{string_literal(name)}]"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
XPath::Renderer.prepend(Capybara::XPathPatches::Renderer)
|