capybara 3.8.2 → 3.9.0

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