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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +25 -1
  3. data/lib/capybara/config.rb +1 -0
  4. data/lib/capybara/dsl.rb +2 -2
  5. data/lib/capybara/helpers.rb +1 -0
  6. data/lib/capybara/node/actions.rb +4 -0
  7. data/lib/capybara/node/base.rb +1 -0
  8. data/lib/capybara/node/element.rb +3 -0
  9. data/lib/capybara/node/finders.rb +2 -0
  10. data/lib/capybara/node/simple.rb +1 -0
  11. data/lib/capybara/queries/base_query.rb +1 -0
  12. data/lib/capybara/queries/match_query.rb +1 -0
  13. data/lib/capybara/queries/selector_query.rb +34 -37
  14. data/lib/capybara/queries/text_query.rb +2 -0
  15. data/lib/capybara/rack_test/browser.rb +1 -0
  16. data/lib/capybara/rack_test/driver.rb +5 -0
  17. data/lib/capybara/rack_test/node.rb +2 -0
  18. data/lib/capybara/result.rb +2 -0
  19. data/lib/capybara/rspec/compound.rb +2 -0
  20. data/lib/capybara/rspec/matchers.rb +1 -0
  21. data/lib/capybara/selector/builders/css_builder.rb +49 -0
  22. data/lib/capybara/selector/builders/xpath_builder.rb +56 -0
  23. data/lib/capybara/selector/filter_set.rb +1 -0
  24. data/lib/capybara/selector/filters/base.rb +2 -0
  25. data/lib/capybara/selector/regexp_disassembler.rb +66 -0
  26. data/lib/capybara/selector/selector.rb +25 -5
  27. data/lib/capybara/selector.rb +14 -27
  28. data/lib/capybara/selenium/driver.rb +8 -1
  29. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +19 -1
  30. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +1 -0
  31. data/lib/capybara/selenium/node.rb +11 -0
  32. data/lib/capybara/selenium/nodes/chrome_node.rb +2 -0
  33. data/lib/capybara/selenium/nodes/marionette_node.rb +37 -20
  34. data/lib/capybara/server/animation_disabler.rb +1 -0
  35. data/lib/capybara/server.rb +4 -0
  36. data/lib/capybara/session/config.rb +2 -0
  37. data/lib/capybara/session.rb +5 -0
  38. data/lib/capybara/spec/session/has_css_spec.rb +16 -0
  39. data/lib/capybara/spec/session/has_field_spec.rb +4 -0
  40. data/lib/capybara/spec/session/node_spec.rb +6 -0
  41. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  42. data/lib/capybara/spec/session/reset_session_spec.rb +15 -1
  43. data/lib/capybara/spec/session/selectors_spec.rb +12 -2
  44. data/lib/capybara/spec/views/form.erb +15 -0
  45. data/lib/capybara/version.rb +1 -1
  46. data/lib/capybara/xpath_patches.rb +27 -0
  47. data/lib/capybara.rb +32 -10
  48. data/spec/dsl_spec.rb +15 -1
  49. data/spec/rack_test_spec.rb +6 -1
  50. data/spec/regexp_dissassembler_spec.rb +154 -0
  51. data/spec/selector_spec.rb +53 -18
  52. data/spec/selenium_spec_chrome.rb +28 -0
  53. data/spec/selenium_spec_firefox_remote.rb +2 -0
  54. data/spec/selenium_spec_marionette.rb +11 -0
  55. data/spec/shared_selenium_session.rb +20 -0
  56. data/spec/spec_helper.rb +4 -0
  57. 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
- return @default_visibility unless @default_visibility.nil?
390
- fallback
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,
@@ -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
- xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of('submit', 'image', 'hidden')]
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
- desc << " with href #{options[:href].inspect}" if options[:href] && !options[:href].is_a?(Regexp)
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? Regexp
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
- case val
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
- @browser.manage.delete_all_cookies
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
- bridge = browser.send(:bridge)
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.each_with_object(browser_action) do |keys, actions|
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 = nil)
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
- if down_keys.nil?
87
- actions.send_keys(keys)
88
- else
89
- down_keys << keys
90
- actions.key_down(keys)
91
- end
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
- local_down_keys = []
96
- keys.each do |sub_keys|
97
- _send_keys(sub_keys, actions, local_down_keys)
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
@@ -22,6 +22,7 @@ module Capybara
22
22
  def call(env)
23
23
  @status, @headers, @body = @app.call(env)
24
24
  return [@status, @headers, @body] unless html_content?
25
+
25
26
  response = Rack::Response.new([], @status, @headers)
26
27
 
27
28
  @body.each { |html| response.write insert_disable(html) }
@@ -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
 
@@ -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 visible link nil with href matching /\\/without_simple_html/ within #<Capybara::Node::Element})
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
- it 'can find specifically by id' do
44
- expect(@session.find(:field, id: 'customer_email').value).to eq 'ben@ben.com'
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>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Capybara
4
- VERSION = '3.8.0'
4
+ VERSION = '3.9.0'
5
5
  end
@@ -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)