capybara 3.30.0 → 3.31.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +21 -0
  3. data/README.md +1 -1
  4. data/lib/capybara/dsl.rb +10 -2
  5. data/lib/capybara/minitest.rb +18 -4
  6. data/lib/capybara/node/element.rb +11 -8
  7. data/lib/capybara/node/finders.rb +5 -1
  8. data/lib/capybara/node/matchers.rb +24 -15
  9. data/lib/capybara/node/simple.rb +1 -1
  10. data/lib/capybara/queries/base_query.rb +2 -1
  11. data/lib/capybara/rack_test/node.rb +34 -9
  12. data/lib/capybara/result.rb +24 -4
  13. data/lib/capybara/rspec/matchers.rb +27 -27
  14. data/lib/capybara/rspec/matchers/base.rb +12 -6
  15. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  16. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  17. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  18. data/lib/capybara/rspec/matchers/have_selector.rb +15 -7
  19. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  20. data/lib/capybara/rspec/matchers/have_text.rb +2 -2
  21. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  22. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  23. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  24. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  25. data/lib/capybara/selector.rb +2 -0
  26. data/lib/capybara/selector/definition/label.rb +1 -1
  27. data/lib/capybara/selector/definition/select.rb +31 -12
  28. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +1 -1
  29. data/lib/capybara/selenium/extensions/html5_drag.rb +24 -8
  30. data/lib/capybara/selenium/node.rb +23 -6
  31. data/lib/capybara/selenium/nodes/chrome_node.rb +4 -2
  32. data/lib/capybara/selenium/nodes/edge_node.rb +1 -1
  33. data/lib/capybara/selenium/nodes/firefox_node.rb +1 -1
  34. data/lib/capybara/session.rb +30 -15
  35. data/lib/capybara/spec/public/test.js +40 -6
  36. data/lib/capybara/spec/session/all_spec.rb +45 -5
  37. data/lib/capybara/spec/session/assert_text_spec.rb +5 -5
  38. data/lib/capybara/spec/session/fill_in_spec.rb +20 -0
  39. data/lib/capybara/spec/session/has_css_spec.rb +3 -3
  40. data/lib/capybara/spec/session/has_select_spec.rb +28 -0
  41. data/lib/capybara/spec/session/has_text_spec.rb +5 -1
  42. data/lib/capybara/spec/session/node_spec.rb +92 -3
  43. data/lib/capybara/spec/views/form.erb +6 -1
  44. data/lib/capybara/version.rb +1 -1
  45. data/spec/rack_test_spec.rb +0 -1
  46. data/spec/result_spec.rb +4 -0
  47. data/spec/selenium_spec_chrome.rb +2 -1
  48. metadata +2 -2
@@ -96,7 +96,7 @@ private
96
96
 
97
97
  def execute_cdp(cmd, params = {})
98
98
  if browser.respond_to? :execute_cdp
99
- browser.execute_cdp(cmd, params)
99
+ browser.execute_cdp(cmd, **params)
100
100
  else
101
101
  args = { cmd: cmd, params: params }
102
102
  result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
@@ -4,25 +4,32 @@ class Capybara::Selenium::Node
4
4
  module Html5Drag
5
5
  # Implement methods to emulate HTML5 drag and drop
6
6
 
7
- def drag_to(element, html5: nil, delay: 0.05)
7
+ def drag_to(element, html5: nil, delay: 0.05, drop_modifiers: [])
8
+ drop_modifiers = Array(drop_modifiers)
9
+
8
10
  driver.execute_script MOUSEDOWN_TRACKER
9
11
  scroll_if_needed { browser_action.click_and_hold(native).perform }
10
12
  html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
11
13
  if html5
12
- perform_html5_drag(element, delay)
14
+ perform_html5_drag(element, delay, drop_modifiers)
13
15
  else
14
- perform_legacy_drag(element)
16
+ perform_legacy_drag(element, drop_modifiers)
15
17
  end
16
18
  end
17
19
 
18
20
  private
19
21
 
20
- def perform_legacy_drag(element)
21
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
22
+ def perform_legacy_drag(element, drop_modifiers)
23
+ element.scroll_if_needed do
24
+ # browser_action.move_to(element.native).release.perform
25
+ keys_down = modifiers_down(browser_action, drop_modifiers)
26
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
27
+ keys_up.perform
28
+ end
22
29
  end
23
30
 
24
- def perform_html5_drag(element, delay)
25
- driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000
31
+ def perform_html5_drag(element, delay, drop_modifiers)
32
+ driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000, normalize_keys(drop_modifiers)
26
33
  browser_action.release.perform
27
34
  end
28
35
 
@@ -153,6 +160,14 @@ class Capybara::Selenium::Node
153
160
  var targetRect = target.getBoundingClientRect();
154
161
  var sourceCenter = rectCenter(source.getBoundingClientRect());
155
162
 
163
+ for (var i = 0; i < drop_modifier_keys.length; i++) {
164
+ key = drop_modifier_keys[i];
165
+ if (key == "control"){
166
+ key = "ctrl"
167
+ }
168
+ opts[key + 'Key'] = true;
169
+ }
170
+
156
171
  // fire 2 dragover events to simulate dragging with a direction
157
172
  var entryPoint = pointOnRect(sourceCenter, targetRect)
158
173
  var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
@@ -185,7 +200,8 @@ class Capybara::Selenium::Node
185
200
  var source = arguments[0],
186
201
  target = arguments[1],
187
202
  step_delay = arguments[2],
188
- callback = arguments[3];
203
+ drop_modifier_keys = arguments[3],
204
+ callback = arguments[4];
189
205
 
190
206
  var dt = new DataTransfer();
191
207
  var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
@@ -78,6 +78,8 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
78
78
  set_datetime_local(value)
79
79
  when 'color'
80
80
  set_color(value)
81
+ when 'range'
82
+ set_range(value)
81
83
  else
82
84
  set_text(value, **options)
83
85
  end
@@ -134,11 +136,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
134
136
  scroll_if_needed { browser_action.move_to(native).perform }
135
137
  end
136
138
 
137
- def drag_to(element, **)
139
+ def drag_to(element, drop_modifiers: [], **)
140
+ drop_modifiers = Array(drop_modifiers)
138
141
  # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
139
142
  # which means Seleniums `drag_and_drop` is now broken - do it manually
140
143
  scroll_if_needed { browser_action.click_and_hold(native).perform }
141
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
144
+ # element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
145
+ element.scroll_if_needed do
146
+ keys_down = modifiers_down(browser_action, drop_modifiers)
147
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
148
+ keys_up.perform
149
+ end
142
150
  end
143
151
 
144
152
  def drop(*_)
@@ -290,6 +298,10 @@ private
290
298
  update_value_js(value)
291
299
  end
292
300
 
301
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
302
+ update_value_js(value)
303
+ end
304
+
293
305
  def update_value_js(value)
294
306
  driver.execute_script(<<-JS, self, value)
295
307
  if (arguments[0].readOnly) { return };
@@ -375,10 +387,12 @@ private
375
387
 
376
388
  def modifiers_down(actions, keys)
377
389
  each_key(keys) { |key| actions.key_down(key) }
390
+ actions
378
391
  end
379
392
 
380
393
  def modifiers_up(actions, keys)
381
394
  each_key(keys) { |key| actions.key_up(key) }
395
+ actions
382
396
  end
383
397
 
384
398
  def browser
@@ -393,18 +407,21 @@ private
393
407
  browser.action
394
408
  end
395
409
 
396
- def each_key(keys)
397
- keys.each do |key|
398
- key = case key
410
+ def normalize_keys(keys)
411
+ keys.map do |key|
412
+ case key
399
413
  when :ctrl then :control
400
414
  when :command, :cmd then :meta
401
415
  else
402
416
  key
403
417
  end
404
- yield key
405
418
  end
406
419
  end
407
420
 
421
+ def each_key(keys)
422
+ normalize_keys(keys).each { |key| yield(key) }
423
+ end
424
+
408
425
  def find_context
409
426
  native
410
427
  end
@@ -18,7 +18,7 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
18
18
  # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
19
19
  if browser_version >= 75.0
20
20
  driver.execute_script(<<~JS, self)
21
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
22
  arguments[0].value = null;
23
23
  }
24
24
  JS
@@ -75,9 +75,11 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
75
75
 
76
76
  private
77
77
 
78
- def perform_legacy_drag(element)
78
+ def perform_legacy_drag(element, drop_modifiers)
79
79
  return super if chromedriver_fixed_actions_key_state? || !w3c? || element.obscured?
80
80
 
81
+ raise ArgumentError, 'Modifier keys are not supported while dragging in this version of Chrome.' unless drop_modifiers.empty?
82
+
81
83
  # W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
82
84
  # https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
83
85
  browser_action.release.perform
@@ -18,7 +18,7 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
18
18
  # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
19
19
  if chrome_edge?
20
20
  driver.execute_script(<<~JS, self)
21
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
22
  arguments[0].value = null;
23
23
  }
24
24
  JS
@@ -26,7 +26,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
26
26
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
27
27
  # By default files are appended so we have to clear here if its multiple and already set
28
28
  driver.execute_script(<<~JS, self)
29
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
29
+ if (arguments[0].multiple && arguments[0].files.length){
30
30
  arguments[0].value = null;
31
31
  }
32
32
  JS
@@ -338,8 +338,8 @@ module Capybara
338
338
  #
339
339
  # @raise [Capybara::ElementNotFound] If the scope can't be found before time expires
340
340
  #
341
- def within(*args)
342
- new_scope = args.first.respond_to?(:to_capybara_node) ? args.first.to_capybara_node : find(*args)
341
+ def within(*args, **kw_args)
342
+ new_scope = args.first.respond_to?(:to_capybara_node) ? args.first.to_capybara_node : find(*args, **kw_args)
343
343
  begin
344
344
  scopes.push(new_scope)
345
345
  yield if block_given?
@@ -423,8 +423,8 @@ module Capybara
423
423
  # @param [String] locator The locator for the given selector kind. For :frame this is the name/id of a frame/iframe element
424
424
  # @overload within_frame(index)
425
425
  # @param [Integer] index index of a frame (0 based)
426
- def within_frame(*args)
427
- switch_to_frame(_find_frame(*args))
426
+ def within_frame(*args, **kw_args)
427
+ switch_to_frame(_find_frame(*args, **kw_args))
428
428
  begin
429
429
  yield if block_given?
430
430
  ensure
@@ -746,15 +746,32 @@ module Capybara
746
746
  end
747
747
 
748
748
  NODE_METHODS.each do |method|
749
- define_method method do |*args, &block|
750
- @touched = true
751
- current_scope.send(method, *args, &block)
749
+ if RUBY_VERSION >= '2.7'
750
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
751
+ def #{method}(...)
752
+ @touched = true
753
+ current_scope.#{method}(...)
754
+ end
755
+ METHOD
756
+ else
757
+ define_method method do |*args, &block|
758
+ @touched = true
759
+ current_scope.send(method, *args, &block)
760
+ end
752
761
  end
753
762
  end
754
763
 
755
764
  DOCUMENT_METHODS.each do |method|
756
- define_method method do |*args, &block|
757
- document.send(method, *args, &block)
765
+ if RUBY_VERSION >= '2.7'
766
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
767
+ def #{method}(...)
768
+ document.#{method}(...)
769
+ end
770
+ METHOD
771
+ else
772
+ define_method method do |*args, &block|
773
+ document.send(method, *args, &block)
774
+ end
758
775
  end
759
776
  end
760
777
 
@@ -873,16 +890,14 @@ module Capybara
873
890
  uri.port ||= @server.port if @server && config.always_include_port
874
891
  end
875
892
 
876
- def _find_frame(*args)
877
- return find(:frame) if args.length.zero?
878
-
893
+ def _find_frame(*args, **kw_args)
879
894
  case args[0]
880
895
  when Capybara::Node::Element
881
896
  args[0]
882
- when String, Hash
883
- find(:frame, *args)
897
+ when String, nil
898
+ find(:frame, *args, **kw_args)
884
899
  when Symbol
885
- find(*args)
900
+ find(*args, **kw_args)
886
901
  when Integer
887
902
  idx = args[0]
888
903
  all(:frame, minimum: idx + 1)[idx]
@@ -1,15 +1,40 @@
1
1
  var activeRequests = 0;
2
2
  $(function() {
3
3
  $('#change').text('I changed it');
4
- $('#drag, #drag_scroll, #drag_link').draggable();
4
+ $('#drag, #drag_scroll, #drag_link').draggable({
5
+ start: function(event, ui){
6
+ $(document.body).append(
7
+ "<div class='drag_start'>Dragged!" +
8
+ (event.altKey ? "-alt" : "") +
9
+ (event.ctrlKey ? "-ctrl" : "") +
10
+ (event.metaKey ? "-meta" : "") +
11
+ (event.shiftKey ? "-shift" : "") +
12
+ "</div>"
13
+ );
14
+ }
15
+ });
5
16
  $('#drop, #drop_scroll').droppable({
6
17
  tolerance: 'touch',
7
18
  drop: function(event, ui) {
8
19
  ui.draggable.remove();
9
- $(this).html('Dropped!');
20
+ $(this).html(
21
+ "Dropped!" +
22
+ (event.altKey ? "-alt" : "") +
23
+ (event.ctrlKey ? "-ctrl" : "") +
24
+ (event.metaKey ? "-meta" : "") +
25
+ (event.shiftKey ? "-shift" : "")
26
+ );
10
27
  }
11
28
  });
12
29
  $('#drag_html5, #drag_html5_scroll').on('dragstart', function(ev){
30
+ $(document.body).append(
31
+ "<div class='drag_start'>HTML5 Dragged!" +
32
+ (event.altKey ? "-alt" : "") +
33
+ (event.ctrlKey ? "-ctrl" : "") +
34
+ (event.metaKey ? "-meta" : "") +
35
+ (event.shiftKey ? "-shift" : "") +
36
+ "</div>"
37
+ );
13
38
  ev.originalEvent.dataTransfer.setData("text", ev.target.id);
14
39
  });
15
40
  $('#drag_html5, #drag_html5_scroll').on('dragend', function(ev){
@@ -38,10 +63,19 @@ $(function() {
38
63
  $(this).append('HTML5 Dropped file: ' + file.name);
39
64
  } else {
40
65
  var _this = this;
41
- var callback = (function(type){
42
- return function(s){
43
- $(_this).append('HTML5 Dropped string: ' + type + ' ' + s)
44
- }
66
+ var callback = (function(type) {
67
+ return function(s) {
68
+ $(_this).append(
69
+ "HTML5 Dropped string: " +
70
+ type +
71
+ " " +
72
+ s +
73
+ (ev.altKey ? "-alt" : "") +
74
+ (ev.ctrlKey ? "-ctrl" : "") +
75
+ (ev.metaKey ? "-meta" : "") +
76
+ (ev.shiftKey ? "-shift" : "")
77
+ );
78
+ };
45
79
  })(item.type);
46
80
  item.getAsString(callback);
47
81
  }
@@ -47,6 +47,39 @@ Capybara::SpecHelper.spec '#all' do
47
47
  expect { @session.all('//p', schmoo: 'foo') }.to raise_error(ArgumentError)
48
48
  end
49
49
 
50
+ it 'should not reload by default', requires: [:driver] do
51
+ paras = @session.all(:css, 'p', minimum: 3)
52
+ expect { paras[0].text }.not_to raise_error
53
+ @session.refresh
54
+ expect { paras[0].text }.to raise_error do |err|
55
+ expect(err).to be_an_invalid_element_error(@session)
56
+ end
57
+ end
58
+
59
+ context 'with allow_reload' do
60
+ it 'should reload if true' do
61
+ paras = @session.all(:css, 'p', allow_reload: true, minimum: 3)
62
+ expect { paras[0].text }.not_to raise_error
63
+ @session.refresh
64
+ sleep 1 # Ensure page has started to reload
65
+ expect(paras[0]).to have_text('Lorem ipsum dolor')
66
+ expect(paras[1]).to have_text('Duis aute irure dolor')
67
+ end
68
+
69
+ it 'should not reload if false', requires: [:driver] do
70
+ paras = @session.all(:css, 'p', allow_reload: false, minimum: 3)
71
+ expect { paras[0].text }.not_to raise_error
72
+ @session.refresh
73
+ sleep 1 # Ensure page has started to reload
74
+ expect { paras[0].text }.to raise_error do |err|
75
+ expect(err).to be_an_invalid_element_error(@session)
76
+ end
77
+ expect { paras[2].text }.to raise_error do |err|
78
+ expect(err).to be_an_invalid_element_error(@session)
79
+ end
80
+ end
81
+ end
82
+
50
83
  context 'with css selectors' do
51
84
  it 'should find all elements using the given selector' do
52
85
  expect(@session.all(:css, 'h1').first.text).to eq('This is a test')
@@ -176,6 +209,13 @@ Capybara::SpecHelper.spec '#all' do
176
209
  expect { @session.all(:css, 'h1, p', between: 5..) }.to raise_error(Capybara::ExpectationNotMet)
177
210
  end
178
211
  TEST
212
+
213
+ eval <<~TEST, binding, __FILE__, __LINE__ + 1 if RUBY_VERSION.to_f > 2.6
214
+ it'treats a beginless range as maximum' do
215
+ expect { @session.all(:css, 'h1, p', between: ..7) }.not_to raise_error
216
+ expect { @session.all(:css, 'h1, p', between: ..3) }.to raise_error(Capybara::ExpectationNotMet)
217
+ end
218
+ TEST
179
219
  end
180
220
 
181
221
  context 'with multiple count filters' do
@@ -184,7 +224,7 @@ Capybara::SpecHelper.spec '#all' do
184
224
  minimum: 5,
185
225
  maximum: 0,
186
226
  between: 0..3 }
187
- expect { @session.all(:css, 'h1, p', o) }.not_to raise_error
227
+ expect { @session.all(:css, 'h1, p', **o) }.not_to raise_error
188
228
  end
189
229
 
190
230
  context 'with no :count expectation' do
@@ -192,28 +232,28 @@ Capybara::SpecHelper.spec '#all' do
192
232
  o = { minimum: 5,
193
233
  maximum: 4,
194
234
  between: 2..7 }
195
- expect { @session.all(:css, 'h1, p', o) }.to raise_error(Capybara::ExpectationNotMet)
235
+ expect { @session.all(:css, 'h1, p', **o) }.to raise_error(Capybara::ExpectationNotMet)
196
236
  end
197
237
 
198
238
  it 'fails if :maximum is not met' do
199
239
  o = { minimum: 0,
200
240
  maximum: 0,
201
241
  between: 2..7 }
202
- expect { @session.all(:css, 'h1, p', o) }.to raise_error(Capybara::ExpectationNotMet)
242
+ expect { @session.all(:css, 'h1, p', **o) }.to raise_error(Capybara::ExpectationNotMet)
203
243
  end
204
244
 
205
245
  it 'fails if :between is not met' do
206
246
  o = { minimum: 0,
207
247
  maximum: 4,
208
248
  between: 0..3 }
209
- expect { @session.all(:css, 'h1, p', o) }.to raise_error(Capybara::ExpectationNotMet)
249
+ expect { @session.all(:css, 'h1, p', **o) }.to raise_error(Capybara::ExpectationNotMet)
210
250
  end
211
251
 
212
252
  it 'succeeds if all combineable expectations are met' do
213
253
  o = { minimum: 0,
214
254
  maximum: 4,
215
255
  between: 2..7 }
216
- expect { @session.all(:css, 'h1, p', o) }.not_to raise_error
256
+ expect { @session.all(:css, 'h1, p', **o) }.not_to raise_error
217
257
  end
218
258
  end
219
259
  end
@@ -157,7 +157,7 @@ Capybara::SpecHelper.spec '#assert_text' do
157
157
  minimum: 6,
158
158
  maximum: 0,
159
159
  between: 0..4 }
160
- expect { @session.assert_text('Header', o) }.not_to raise_error
160
+ expect { @session.assert_text('Header', **o) }.not_to raise_error
161
161
  end
162
162
 
163
163
  context 'with no :count expectation' do
@@ -165,28 +165,28 @@ Capybara::SpecHelper.spec '#assert_text' do
165
165
  o = { minimum: 6,
166
166
  maximum: 5,
167
167
  between: 2..7 }
168
- expect { @session.assert_text('Header', o) }.to raise_error(Capybara::ExpectationNotMet)
168
+ expect { @session.assert_text('Header', **o) }.to raise_error(Capybara::ExpectationNotMet)
169
169
  end
170
170
 
171
171
  it 'fails if :maximum is not met' do
172
172
  o = { minimum: 0,
173
173
  maximum: 0,
174
174
  between: 2..7 }
175
- expect { @session.assert_text('Header', o) }.to raise_error(Capybara::ExpectationNotMet)
175
+ expect { @session.assert_text('Header', **o) }.to raise_error(Capybara::ExpectationNotMet)
176
176
  end
177
177
 
178
178
  it 'fails if :between is not met' do
179
179
  o = { minimum: 0,
180
180
  maximum: 5,
181
181
  between: 0..4 }
182
- expect { @session.assert_text('Header', o) }.to raise_error(Capybara::ExpectationNotMet)
182
+ expect { @session.assert_text('Header', **o) }.to raise_error(Capybara::ExpectationNotMet)
183
183
  end
184
184
 
185
185
  it 'succeeds if all combineable expectations are met' do
186
186
  o = { minimum: 0,
187
187
  maximum: 5,
188
188
  between: 2..7 }
189
- expect { @session.assert_text('Header', o) }.not_to raise_error
189
+ expect { @session.assert_text('Header', **o) }.not_to raise_error
190
190
  end
191
191
  end
192
192
  end