capybara 3.8.2 → 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 +10 -0
  3. data/lib/capybara.rb +32 -10
  4. data/lib/capybara/config.rb +1 -0
  5. data/lib/capybara/dsl.rb +2 -2
  6. data/lib/capybara/helpers.rb +1 -0
  7. data/lib/capybara/node/actions.rb +4 -0
  8. data/lib/capybara/node/base.rb +1 -0
  9. data/lib/capybara/node/element.rb +3 -0
  10. data/lib/capybara/node/finders.rb +2 -0
  11. data/lib/capybara/node/simple.rb +1 -0
  12. data/lib/capybara/queries/base_query.rb +1 -0
  13. data/lib/capybara/queries/match_query.rb +1 -0
  14. data/lib/capybara/queries/selector_query.rb +34 -37
  15. data/lib/capybara/queries/text_query.rb +2 -0
  16. data/lib/capybara/rack_test/browser.rb +1 -0
  17. data/lib/capybara/rack_test/driver.rb +5 -0
  18. data/lib/capybara/rack_test/node.rb +2 -0
  19. data/lib/capybara/result.rb +2 -0
  20. data/lib/capybara/rspec/compound.rb +2 -0
  21. data/lib/capybara/rspec/matchers.rb +1 -0
  22. data/lib/capybara/selector.rb +14 -27
  23. data/lib/capybara/selector/builders/css_builder.rb +49 -0
  24. data/lib/capybara/selector/builders/xpath_builder.rb +56 -0
  25. data/lib/capybara/selector/filter_set.rb +1 -0
  26. data/lib/capybara/selector/filters/base.rb +2 -0
  27. data/lib/capybara/selector/regexp_disassembler.rb +66 -0
  28. data/lib/capybara/selector/selector.rb +25 -5
  29. data/lib/capybara/selenium/driver.rb +8 -1
  30. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +19 -1
  31. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +1 -0
  32. data/lib/capybara/selenium/node.rb +7 -0
  33. data/lib/capybara/selenium/nodes/chrome_node.rb +2 -0
  34. data/lib/capybara/selenium/nodes/marionette_node.rb +37 -20
  35. data/lib/capybara/server.rb +4 -0
  36. data/lib/capybara/server/animation_disabler.rb +1 -0
  37. data/lib/capybara/session.rb +5 -0
  38. data/lib/capybara/session/config.rb +2 -0
  39. data/lib/capybara/spec/session/has_css_spec.rb +16 -0
  40. data/lib/capybara/spec/session/has_field_spec.rb +4 -0
  41. data/lib/capybara/spec/session/node_spec.rb +6 -0
  42. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  43. data/lib/capybara/spec/session/reset_session_spec.rb +15 -1
  44. data/lib/capybara/spec/session/selectors_spec.rb +12 -2
  45. data/lib/capybara/spec/views/form.erb +15 -0
  46. data/lib/capybara/version.rb +1 -1
  47. data/lib/capybara/xpath_patches.rb +27 -0
  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 +37 -2
  52. data/spec/selenium_spec_chrome.rb +2 -2
  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
@@ -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
@@ -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
@@ -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
@@ -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) }
@@ -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
 
@@ -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
 
@@ -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.2'
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)
@@ -9,7 +9,12 @@ end
9
9
 
10
10
  Capybara::SpecHelper.run_specs TestClass.new, 'DSL', capybara_skip: %i[
11
11
  js modals screenshot frames windows send_keys server hover about_scheme psc download css driver
12
- ]
12
+ ] do |example|
13
+ case example.metadata[:full_description]
14
+ when /has_css\? should support case insensitive :class and :id options/
15
+ pending "Nokogiri doesn't support case insensitive CSS attribute matchers"
16
+ end
17
+ end
13
18
 
14
19
  RSpec.describe Capybara::DSL do
15
20
  after do
@@ -224,6 +229,15 @@ RSpec.describe Capybara::DSL do
224
229
  end
225
230
  expect(Capybara.session_name).to eq(:default)
226
231
  end
232
+
233
+ it 'should allow a session object' do
234
+ original_session = Capybara.current_session
235
+ new_session = Capybara::Session.new(:rack_test, proc {})
236
+ Capybara.using_session(new_session) do
237
+ expect(Capybara.current_session).to eq(new_session)
238
+ end
239
+ expect(Capybara.current_session).to eq(original_session)
240
+ end
227
241
  end
228
242
 
229
243
  describe '#session_name' do