capybara 3.6.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +16 -0
  3. data/README.md +5 -1
  4. data/lib/capybara.rb +2 -0
  5. data/lib/capybara/minitest/spec.rb +1 -1
  6. data/lib/capybara/node/actions.rb +34 -25
  7. data/lib/capybara/node/base.rb +15 -17
  8. data/lib/capybara/node/document_matchers.rb +1 -3
  9. data/lib/capybara/node/element.rb +11 -12
  10. data/lib/capybara/node/finders.rb +2 -1
  11. data/lib/capybara/node/simple.rb +13 -6
  12. data/lib/capybara/queries/base_query.rb +4 -4
  13. data/lib/capybara/queries/selector_query.rb +119 -94
  14. data/lib/capybara/queries/text_query.rb +2 -1
  15. data/lib/capybara/rack_test/form.rb +4 -4
  16. data/lib/capybara/rack_test/node.rb +5 -5
  17. data/lib/capybara/result.rb +23 -32
  18. data/lib/capybara/rspec/compound.rb +1 -1
  19. data/lib/capybara/rspec/matchers.rb +63 -61
  20. data/lib/capybara/selector.rb +28 -10
  21. data/lib/capybara/selector/css.rb +17 -17
  22. data/lib/capybara/selector/filter_set.rb +9 -9
  23. data/lib/capybara/selector/selector.rb +3 -4
  24. data/lib/capybara/selenium/driver.rb +73 -95
  25. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +4 -4
  26. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +9 -0
  27. data/lib/capybara/selenium/node.rb +127 -67
  28. data/lib/capybara/selenium/nodes/chrome_node.rb +3 -3
  29. data/lib/capybara/selenium/nodes/marionette_node.rb +14 -8
  30. data/lib/capybara/server.rb +2 -2
  31. data/lib/capybara/server/animation_disabler.rb +17 -3
  32. data/lib/capybara/server/middleware.rb +8 -4
  33. data/lib/capybara/session.rb +43 -37
  34. data/lib/capybara/session/config.rb +8 -6
  35. data/lib/capybara/spec/session/assert_text_spec.rb +14 -0
  36. data/lib/capybara/spec/session/attach_file_spec.rb +7 -0
  37. data/lib/capybara/spec/session/check_spec.rb +21 -0
  38. data/lib/capybara/spec/session/choose_spec.rb +15 -1
  39. data/lib/capybara/spec/session/fill_in_spec.rb +7 -0
  40. data/lib/capybara/spec/session/find_spec.rb +2 -1
  41. data/lib/capybara/spec/session/has_selector_spec.rb +18 -0
  42. data/lib/capybara/spec/session/has_text_spec.rb +14 -0
  43. data/lib/capybara/spec/session/node_spec.rb +2 -1
  44. data/lib/capybara/spec/session/reset_session_spec.rb +4 -4
  45. data/lib/capybara/spec/session/text_spec.rb +2 -1
  46. data/lib/capybara/spec/session/title_spec.rb +2 -1
  47. data/lib/capybara/spec/session/uncheck_spec.rb +8 -0
  48. data/lib/capybara/spec/session/within_spec.rb +2 -1
  49. data/lib/capybara/spec/spec_helper.rb +1 -32
  50. data/lib/capybara/spec/views/with_js.erb +3 -4
  51. data/lib/capybara/version.rb +1 -1
  52. data/spec/minitest_spec.rb +4 -0
  53. data/spec/minitest_spec_spec.rb +4 -0
  54. data/spec/rack_test_spec.rb +4 -4
  55. data/spec/rspec/shared_spec_matchers.rb +4 -2
  56. data/spec/selector_spec.rb +15 -1
  57. data/spec/selenium_spec_chrome.rb +1 -6
  58. data/spec/selenium_spec_chrome_remote.rb +1 -1
  59. data/spec/selenium_spec_firefox_remote.rb +2 -5
  60. data/spec/selenium_spec_ie.rb +41 -4
  61. data/spec/selenium_spec_marionette.rb +1 -25
  62. data/spec/shared_selenium_session.rb +74 -16
  63. data/spec/spec_helper.rb +41 -0
  64. metadata +2 -2
@@ -20,7 +20,7 @@ module Capybara
20
20
  end
21
21
  end
22
22
 
23
- attr_accessor :error
23
+ attr_reader :error
24
24
 
25
25
  def initialize(app, server_errors, extra_middleware = [])
26
26
  @app = app
@@ -35,6 +35,10 @@ module Capybara
35
35
  @counter.value.positive?
36
36
  end
37
37
 
38
+ def clear_error
39
+ @error = nil
40
+ end
41
+
38
42
  def call(env)
39
43
  if env['PATH_INFO'] == '/__identify__'
40
44
  [200, {}, [@app.object_id.to_s]]
@@ -42,9 +46,9 @@ module Capybara
42
46
  @counter.increment
43
47
  begin
44
48
  @extended_app.call(env)
45
- rescue *@server_errors => e
46
- @error ||= e
47
- raise e
49
+ rescue *@server_errors => err
50
+ @error ||= err
51
+ raise err
48
52
  ensure
49
53
  @counter.decrement
50
54
  end
@@ -137,7 +137,7 @@ module Capybara
137
137
  # Raise errors encountered in the server
138
138
  #
139
139
  def raise_server_error!
140
- return if @server.nil? || !@server.error
140
+ return unless @server&.error
141
141
  # Force an explanation for the error being raised as the exception cause
142
142
  begin
143
143
  if config.raise_server_errors
@@ -244,26 +244,19 @@ module Capybara
244
244
  @touched = true
245
245
 
246
246
  visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
247
+ base_uri = ::Addressable::URI.parse(config.app_host || server_url)
247
248
 
248
- base = config.app_host
249
- base ||= "http#{'s' if @server.using_ssl?}://#{@server.host}:#{@server.port}" if @server
250
-
251
- uri_base = ::Addressable::URI.parse(base)
252
-
253
- if uri_base && [nil, 'http', 'https'].include?(visit_uri.scheme)
249
+ if base_uri && [nil, 'http', 'https'].include?(visit_uri.scheme)
254
250
  if visit_uri.relative?
255
- uri_base.port ||= @server.port if @server && config.always_include_port
256
-
257
- visit_uri_parts = visit_uri.to_hash.delete_if { |_k, v| v.nil? }
251
+ visit_uri_parts = visit_uri.to_hash.delete_if { |_k, value| value.nil? }
258
252
 
259
253
  # Useful to people deploying to a subdirectory
260
254
  # and/or single page apps where only the url fragment changes
261
- visit_uri_parts[:path] = uri_base.path + visit_uri.path
255
+ visit_uri_parts[:path] = base_uri.path + visit_uri.path
262
256
 
263
- visit_uri = uri_base.merge(visit_uri_parts)
264
- elsif @server && config.always_include_port
265
- visit_uri.port ||= @server.port
257
+ visit_uri = base_uri.merge(visit_uri_parts)
266
258
  end
259
+ adjust_server_port(visit_uri)
267
260
  end
268
261
 
269
262
  driver.visit(visit_uri.to_s)
@@ -544,8 +537,7 @@ module Capybara
544
537
  old_handles = driver.window_handles
545
538
  yield
546
539
 
547
- wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
548
- document.synchronize(wait_time, errors: [Capybara::WindowError]) do
540
+ synchronize_windows(options) do
549
541
  opened_handles = (driver.window_handles - old_handles)
550
542
  if opened_handles.size != 1
551
543
  raise Capybara::WindowError, 'block passed to #window_opened_by '\
@@ -675,8 +667,8 @@ module Capybara
675
667
  # @return [String] the path to which the file was saved
676
668
  #
677
669
  def save_page(path = nil)
678
- prepare_path(path, 'html').tap do |p|
679
- File.write(p, Capybara::Helpers.inject_asset_host(body, host: config.asset_host), mode: 'wb')
670
+ prepare_path(path, 'html').tap do |p_path|
671
+ File.write(p_path, Capybara::Helpers.inject_asset_host(body, host: config.asset_host), mode: 'wb')
680
672
  end
681
673
  end
682
674
 
@@ -691,7 +683,7 @@ module Capybara
691
683
  # @param [String] path the path to where it should be saved
692
684
  #
693
685
  def save_and_open_page(path = nil)
694
- save_page(path).tap { |p| open_file(p) }
686
+ save_page(path).tap { |s_path| open_file(s_path) }
695
687
  end
696
688
 
697
689
  ##
@@ -706,7 +698,7 @@ module Capybara
706
698
  # @param [Hash] options a customizable set of options
707
699
  # @return [String] the path to which the file was saved
708
700
  def save_screenshot(path = nil, **options)
709
- prepare_path(path, 'png').tap { |p| driver.save_screenshot(p, options) }
701
+ prepare_path(path, 'png').tap { |p_path| driver.save_screenshot(p_path, options) }
710
702
  end
711
703
 
712
704
  ##
@@ -722,7 +714,7 @@ module Capybara
722
714
  #
723
715
  def save_and_open_screenshot(path = nil, **options)
724
716
  # rubocop:disable Lint/Debugger
725
- save_screenshot(path, options).tap { |p| open_file(p) }
717
+ save_screenshot(path, options).tap { |s_path| open_file(s_path) }
726
718
  # rubocop:enable Lint/Debugger
727
719
  end
728
720
 
@@ -822,7 +814,7 @@ module Capybara
822
814
  end
823
815
 
824
816
  def prepare_path(path, extension)
825
- File.expand_path(path || default_fn(extension), config.save_path).tap { |p| FileUtils.mkdir_p(File.dirname(p)) }
817
+ File.expand_path(path || default_fn(extension), config.save_path).tap { |p_path| FileUtils.mkdir_p(File.dirname(p_path)) }
826
818
  end
827
819
 
828
820
  def default_fn(extension)
@@ -837,9 +829,9 @@ module Capybara
837
829
  def element_script_result(arg)
838
830
  case arg
839
831
  when Array
840
- arg.map { |e| element_script_result(e) }
832
+ arg.map { |subarg| element_script_result(subarg) }
841
833
  when Hash
842
- arg.each { |k, v| arg[k] = element_script_result(v) }
834
+ arg.each { |key, value| arg[key] = element_script_result(value) }
843
835
  when Capybara::Driver::Node
844
836
  Capybara::Node::Element.new(self, arg, nil, nil)
845
837
  else
@@ -847,6 +839,14 @@ module Capybara
847
839
  end
848
840
  end
849
841
 
842
+ def server_url
843
+ "http#{'s' if @server.using_ssl?}://#{@server.host}:#{@server.port}" if @server
844
+ end
845
+
846
+ def adjust_server_port(uri)
847
+ uri.port ||= @server.port if @server && config.always_include_port
848
+ end
849
+
850
850
  def _find_frame(*args)
851
851
  return find(:frame) if args.length.zero?
852
852
 
@@ -865,31 +865,37 @@ module Capybara
865
865
  end
866
866
  end
867
867
 
868
- def _switch_to_window(window = nil, **options)
868
+ def _switch_to_window(window = nil, **options, &window_locator)
869
869
  raise Capybara::ScopeError, 'Window cannot be switched inside a `within_frame` block' if scopes.include?(:frame)
870
- raise Capybara::ScopeError, 'Window cannot be switch inside a `within` block' unless scopes.last.nil?
870
+ raise Capybara::ScopeError, 'Window cannot be switched inside a `within` block' unless scopes.last.nil?
871
871
 
872
872
  if window
873
873
  driver.switch_to_window(window.handle)
874
874
  window
875
875
  else
876
- wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
877
- document.synchronize(wait_time, errors: [Capybara::WindowError]) do
876
+ synchronize_windows(options) do
878
877
  original_window_handle = driver.current_window_handle
879
878
  begin
880
- driver.window_handles.each do |handle|
881
- driver.switch_to_window handle
882
- return Window.new(self, handle) if yield
883
- end
884
- rescue StandardError => e
879
+ _switch_to_window_by_locator(&window_locator)
880
+ rescue StandardError
885
881
  driver.switch_to_window(original_window_handle)
886
- raise e
887
- else
888
- driver.switch_to_window(original_window_handle)
889
- raise Capybara::WindowError, 'Could not find a window matching block/lambda'
882
+ raise
890
883
  end
891
884
  end
892
885
  end
893
886
  end
887
+
888
+ def _switch_to_window_by_locator
889
+ driver.window_handles.each do |handle|
890
+ driver.switch_to_window handle
891
+ return Window.new(self, handle) if yield
892
+ end
893
+ raise Capybara::WindowError, 'Could not find a window matching block/lambda'
894
+ end
895
+
896
+ def synchronize_windows(options, &block)
897
+ wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
898
+ document.synchronize(wait_time, errors: [Capybara::WindowError], &block)
899
+ end
894
900
  end
895
901
  end
@@ -8,7 +8,7 @@ module Capybara
8
8
  automatic_reload match exact exact_text raise_server_errors visible_text_only
9
9
  automatic_label_click enable_aria_label save_path asset_host default_host app_host
10
10
  server_host server_port server_errors default_set_options disable_animation test_id
11
- predicates_wait].freeze
11
+ predicates_wait default_normalize_ws].freeze
12
12
 
13
13
  attr_accessor(*OPTIONS)
14
14
 
@@ -57,6 +57,8 @@ module Capybara
57
57
  # See {Capybara.configure}
58
58
  # @!method test_id
59
59
  # See {Capybara.configure}
60
+ # @!method default_normalize_ws
61
+ # See {Capybara.configure}
60
62
 
61
63
  remove_method :server_host
62
64
 
@@ -86,9 +88,9 @@ module Capybara
86
88
  end
87
89
 
88
90
  remove_method :disable_animation=
89
- def disable_animation=(bool)
90
- warn 'Capybara.disable_animation is a beta feature - it may change/disappear in a future point version' if bool
91
- @disable_animation = bool
91
+ def disable_animation=(bool_or_allowlist)
92
+ warn 'Capybara.disable_animation is a beta feature - it may change/disappear in a future point version' if bool_or_allowlist
93
+ @disable_animation = bool_or_allowlist
92
94
  end
93
95
 
94
96
  remove_method :test_id=
@@ -111,8 +113,8 @@ module Capybara
111
113
  end
112
114
 
113
115
  class ReadOnlySessionConfig < SimpleDelegator
114
- SessionConfig::OPTIONS.each do |m|
115
- define_method "#{m}=" do |_|
116
+ SessionConfig::OPTIONS.each do |option|
117
+ define_method "#{option}=" do |_|
116
118
  raise 'Per session settings are only supported when Capybara.threadsafe == true'
117
119
  end
118
120
  end
@@ -15,6 +15,20 @@ Capybara::SpecHelper.spec '#assert_text' do
15
15
  expect(@session.assert_text('text with whitespace', normalize_ws: true)).to eq(true)
16
16
  end
17
17
 
18
+ context 'with enabled default collapsing whitespace' do
19
+ before { Capybara.default_normalize_ws = true }
20
+
21
+ it 'should be true if the given unnormalized text is on the page' do
22
+ @session.visit('/with_html')
23
+ expect(@session.assert_text('text with whitespace', normalize_ws: false)).to eq(true)
24
+ end
25
+
26
+ it 'should support collapsing whitespace' do
27
+ @session.visit('/with_html')
28
+ expect(@session.assert_text('text with whitespace')).to eq(true)
29
+ end
30
+ end
31
+
18
32
  it 'should take scopes into account' do
19
33
  @session.visit('/with_html')
20
34
  @session.within("//a[@title='awesome title']") do
@@ -22,6 +22,13 @@ Capybara::SpecHelper.spec '#attach_file' do
22
22
  expect(extract_results(@session)['image']).to eq(File.basename(__FILE__))
23
23
  end
24
24
 
25
+ it 'should be able to set on element if no locator passed' do
26
+ ff = @session.find(:file_field, 'Image')
27
+ ff.attach_file(with_os_path_separators(__FILE__))
28
+ @session.click_button('awesome')
29
+ expect(extract_results(@session)['image']).to eq(File.basename(__FILE__))
30
+ end
31
+
25
32
  it 'casts to string' do
26
33
  @session.attach_file :form_image, with_os_path_separators(__FILE__)
27
34
  @session.click_button('awesome')
@@ -68,6 +68,13 @@ Capybara::SpecHelper.spec '#check' do
68
68
  expect(extract_results(@session)['pets']).to include('dog', 'cat', 'hamster')
69
69
  end
70
70
 
71
+ it 'should be able to check itself if no locator specified' do
72
+ cb = @session.find(:id, 'form_pets_cat')
73
+ cb.check
74
+ @session.click_button('awesome')
75
+ expect(extract_results(@session)['pets']).to include('dog', 'cat', 'hamster')
76
+ end
77
+
71
78
  it 'casts to string' do
72
79
  @session.check(:form_pets_cat)
73
80
  @session.click_button('awesome')
@@ -142,6 +149,20 @@ Capybara::SpecHelper.spec '#check' do
142
149
  expect(extract_results(@session)['cars']).to include('mclaren')
143
150
  end
144
151
 
152
+ it 'should check via clicking the label with :for attribute if locator nil' do
153
+ cb = @session.find(:checkbox, 'form_cars_tesla', unchecked: true, visible: :hidden)
154
+ cb.check
155
+ @session.click_button('awesome')
156
+ expect(extract_results(@session)['cars']).to include('tesla')
157
+ end
158
+
159
+ it 'should check self via clicking the wrapping label if locator nil' do
160
+ cb = @session.find(:checkbox, 'form_cars_mclaren', unchecked: true, visible: :hidden)
161
+ cb.check
162
+ @session.click_button('awesome')
163
+ expect(extract_results(@session)['cars']).to include('mclaren')
164
+ end
165
+
145
166
  it 'should not click the label if unneeded' do
146
167
  expect(@session.find(:checkbox, 'form_cars_jaguar', checked: true, visible: :hidden)).to be_truthy
147
168
  @session.check('form_cars_jaguar')
@@ -23,6 +23,13 @@ Capybara::SpecHelper.spec '#choose' do
23
23
  expect(extract_results(@session)['gender']).to eq('male')
24
24
  end
25
25
 
26
+ it 'should be able to choose self when no locator string specified' do
27
+ rb = @session.find(:id, 'gender_male')
28
+ rb.choose
29
+ @session.click_button('awesome')
30
+ expect(extract_results(@session)['gender']).to eq('male')
31
+ end
32
+
26
33
  it 'casts to string' do
27
34
  @session.choose('Both')
28
35
  @session.click_button(:awesome)
@@ -82,12 +89,19 @@ Capybara::SpecHelper.spec '#choose' do
82
89
  Capybara.automatic_label_click = old_click_label
83
90
  end
84
91
 
85
- it 'should select by clicking the link if available' do
92
+ it 'should select by clicking the label if available' do
86
93
  @session.choose('party_democrat')
87
94
  @session.click_button('awesome')
88
95
  expect(extract_results(@session)['party']).to eq('democrat')
89
96
  end
90
97
 
98
+ it 'should select self by clicking the label if no locator specified' do
99
+ cb = @session.find(:id, 'party_democrat', visible: :hidden)
100
+ cb.choose
101
+ @session.click_button('awesome')
102
+ expect(extract_results(@session)['party']).to eq('democrat')
103
+ end
104
+
91
105
  it 'should raise error if not allowed to click label' do
92
106
  expect { @session.choose('party_democrat', allow_label_click: false) }.to raise_error(Capybara::ElementNotFound, 'Unable to find visible radio button "party_democrat"')
93
107
  end
@@ -137,6 +137,13 @@ Capybara::SpecHelper.spec '#fill_in' do
137
137
  expect(extract_results(@session)['schmooo']).to eq('Schmooo for all')
138
138
  end
139
139
 
140
+ it 'should be able to fill in element called on when no locator passed' do
141
+ field = @session.find(:fillable_field, 'form[password]')
142
+ field.fill_in(with: 'supasikrit')
143
+ @session.click_button('awesome')
144
+ expect(extract_results(@session)['password']).to eq('supasikrit')
145
+ end
146
+
140
147
  it "should throw an exception if a hash containing 'with' is not provided" do
141
148
  expect { @session.fill_in 'Name' }.to raise_error(ArgumentError, /with/)
142
149
  end
@@ -208,11 +208,12 @@ Capybara::SpecHelper.spec '#find' do
208
208
  context 'with css as default selector' do
209
209
  before { Capybara.default_selector = :css }
210
210
 
211
+ after { Capybara.default_selector = :xpath }
212
+
211
213
  it 'should find the first element using the given locator' do
212
214
  expect(@session.find('h1').text).to eq('This is a test')
213
215
  expect(@session.find("input[id='test_field']").value).to eq('monkey')
214
216
  end
215
- after { Capybara.default_selector = :xpath }
216
217
  end
217
218
 
218
219
  it 'should raise ElementNotFound with a useful default message if nothing was found' do
@@ -78,6 +78,24 @@ Capybara::SpecHelper.spec '#has_selector?' do
78
78
  expect(@session).to have_selector(:css, 'p a#foo', 'extra')
79
79
  end.to raise_error ArgumentError, /extra/
80
80
  end
81
+
82
+ context 'with whitespace normalization' do
83
+ context 'Capybara.default_normalize_ws = false' do
84
+ it 'should support normalize_ws option' do
85
+ Capybara.default_normalize_ws = false
86
+ expect(@session).not_to have_selector(:id, 'second', text: 'text with whitespace')
87
+ expect(@session).to have_selector(:id, 'second', text: 'text with whitespace', normalize_ws: true)
88
+ end
89
+ end
90
+
91
+ context 'Capybara.default_normalize_ws = true' do
92
+ it 'should support normalize_ws option' do
93
+ Capybara.default_normalize_ws = true
94
+ expect(@session).to have_selector(:id, 'second', text: 'text with whitespace')
95
+ expect(@session).not_to have_selector(:id, 'second', text: 'text with whitespace', normalize_ws: false)
96
+ end
97
+ end
98
+ end
81
99
  end
82
100
 
83
101
  context 'with exact_text' do
@@ -39,6 +39,20 @@ Capybara::SpecHelper.spec '#has_text?' do
39
39
  expect(@session).to have_text('text with whitespace', normalize_ws: true)
40
40
  end
41
41
 
42
+ context 'with enabled default collapsing whitespace' do
43
+ before { Capybara.default_normalize_ws = true }
44
+
45
+ it 'should search unnormalized text' do
46
+ @session.visit('/with_html')
47
+ expect(@session).to have_text('text with whitespace', normalize_ws: false)
48
+ end
49
+
50
+ it 'should search whitespace collapsed text' do
51
+ @session.visit('/with_html')
52
+ expect(@session).to have_text('text with whitespace')
53
+ end
54
+ end
55
+
42
56
  it 'should be false if the given text is not on the page' do
43
57
  @session.visit('/with_html')
44
58
  expect(@session).not_to have_text('xxxxyzzz')
@@ -545,6 +545,8 @@ Capybara::SpecHelper.spec 'node' do
545
545
  context 'without automatic reload' do
546
546
  before { Capybara.automatic_reload = false }
547
547
 
548
+ after { Capybara.automatic_reload = true }
549
+
548
550
  it 'should reload the current context of the node' do
549
551
  @session.visit('/with_js')
550
552
  node = @session.find(:css, '#reload-me')
@@ -574,7 +576,6 @@ Capybara::SpecHelper.spec 'node' do
574
576
  expect(error).to be_an_invalid_element_error(@session)
575
577
  end)
576
578
  end
577
- after { Capybara.automatic_reload = true }
578
579
  end
579
580
 
580
581
  context 'with automatic reload' do
@@ -87,6 +87,10 @@ Capybara::SpecHelper.spec '#reset_session!' do
87
87
  Capybara.reuse_server = false
88
88
  end
89
89
 
90
+ after do
91
+ Capybara.reuse_server = @reuse_server
92
+ end
93
+
90
94
  it 'raises any standard errors caught inside the server during a second session', requires: [:server] do
91
95
  Capybara.using_driver(@session.mode) do
92
96
  Capybara.using_session(:another_session) do
@@ -100,10 +104,6 @@ Capybara::SpecHelper.spec '#reset_session!' do
100
104
  end
101
105
  end
102
106
  end
103
-
104
- after do
105
- Capybara.reuse_server = @reuse_server
106
- end
107
107
  end
108
108
 
109
109
  it 'raises configured errors caught inside the server', requires: [:server] do