capybara 3.20.2 → 3.21.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.
@@ -8,19 +8,14 @@ module Capybara
8
8
  # Find an {Capybara::Node::Element} based on the given arguments. +find+ will raise an error if the element
9
9
  # is not found.
10
10
  #
11
- # @!macro waiting_behavior
12
- # If the driver is capable of executing JavaScript, this method will wait for a set amount of time
13
- # and continuously retry finding the element until either the element is found or the time
14
- # expires. The length of time +find+ will wait is controlled through {Capybara.default_max_wait_time}
15
- # and defaults to 2 seconds.
16
- # @option options [false, true, Numeric] wait (Capybara.default_max_wait_time) Maximum time to wait for matching element to appear.
17
- #
18
11
  # page.find('#foo').find('.bar')
19
12
  # page.find(:xpath, './/div[contains(., "bar")]')
20
13
  # page.find('li', text: 'Quox').click_link('Delete')
21
14
  #
22
15
  # @param (see Capybara::Node::Finders#all)
23
16
  #
17
+ # @macro waiting_behavior
18
+ #
24
19
  # @!macro system_filters
25
20
  # @option options [String, Regexp] text Only find elements which contain this text or match this regexp
26
21
  # @option options [String, Boolean] exact_text (Capybara.exact_text) When String the elements contained text must match exactly, when Boolean controls whether the :text option must match exactly
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
4
- css { |css| css }
4
+ css do |css|
5
+ warn "DEPRECATED: Passing a symbol (#{css.inspect}) as the CSS locator is deprecated - please pass a string instead." if css.is_a? Symbol
6
+ css
7
+ end
5
8
  end
@@ -166,11 +166,11 @@ module Capybara
166
166
  end
167
167
 
168
168
  def min_repeat
169
- @exp.quantifier&.min || 1
169
+ @exp.repetitions.begin
170
170
  end
171
171
 
172
172
  def max_repeat
173
- @exp.quantifier&.max || 1
173
+ @exp.repetitions.end
174
174
  end
175
175
 
176
176
  def fixed_repeat?
@@ -22,6 +22,69 @@ class Capybara::Selenium::Node
22
22
  native.property('draggable')
23
23
  end
24
24
 
25
+ def html5_drop(*args)
26
+ if args[0].is_a? String
27
+ input = driver.evaluate_script ATTACH_FILE
28
+ input.set_file(args)
29
+ driver.execute_script DROP_FILE, self, input
30
+ else
31
+ items = args.each_with_object([]) do |arg, arr|
32
+ arg.each_with_object(arr) do |(type, data), arr_|
33
+ arr_ << { type: type, data: data }
34
+ end
35
+ end
36
+ driver.execute_script DROP_STRING, items, self
37
+ end
38
+ end
39
+
40
+ DROP_STRING = <<~JS
41
+ var strings = arguments[0],
42
+ el = arguments[1],
43
+ dt = new DataTransfer(),
44
+ opts = { cancelable: true, bubbles: true, dataTransfer: dt };
45
+ for (var i=0; i < strings.length; i++){
46
+ if (dt.items) {
47
+ dt.items.add(strings[i]['data'], strings[i]['type']);
48
+ } else {
49
+ dt.setData(strings[i]['type'], strings[i]['data']);
50
+ }
51
+ }
52
+ var dropEvent = new DragEvent('drop', opts);
53
+ el.dispatchEvent(dropEvent);
54
+ JS
55
+
56
+ DROP_FILE = <<~JS
57
+ var el = arguments[0],
58
+ input = arguments[1],
59
+ files = input.files,
60
+ dt = new DataTransfer(),
61
+ opts = { cancelable: true, bubbles: true, dataTransfer: dt };
62
+ input.parentElement.removeChild(input);
63
+ if (dt.items){
64
+ for (var i=0; i<files.length; i++){
65
+ dt.items.add(files[i]);
66
+ }
67
+ } else {
68
+ Object.defineProperty(dt, "files", {
69
+ value: files,
70
+ writable: false
71
+ });
72
+ }
73
+ var dropEvent = new DragEvent('drop', opts);
74
+ el.dispatchEvent(dropEvent);
75
+ JS
76
+
77
+ ATTACH_FILE = <<~JS
78
+ (function(){
79
+ var input = document.createElement('INPUT');
80
+ input.type = "file";
81
+ input.id = "_capybara_drop_file";
82
+ input.multiple = true;
83
+ document.body.appendChild(input);
84
+ return input;
85
+ })()
86
+ JS
87
+
25
88
  MOUSEDOWN_TRACKER = <<~JS
26
89
  document.addEventListener('mousedown', ev => {
27
90
  window.capybara_mousedown_prevented = ev.defaultPrevented;
@@ -137,6 +137,10 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
137
137
  element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
138
138
  end
139
139
 
140
+ def drop(*_)
141
+ raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
142
+ end
143
+
140
144
  def tag_name
141
145
  @tag_name ||= native.tag_name.downcase
142
146
  end
@@ -34,6 +34,10 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
34
34
  html5_drag_to(element)
35
35
  end
36
36
 
37
+ def drop(*args)
38
+ html5_drop(*args)
39
+ end
40
+
37
41
  def click(*)
38
42
  super
39
43
  rescue ::Selenium::WebDriver::Error::WebDriverError => e
@@ -52,6 +52,10 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
52
52
  html5_drag_to(element)
53
53
  end
54
54
 
55
+ def drop(*args)
56
+ html5_drop(*args)
57
+ end
58
+
55
59
  def hover
56
60
  return super unless browser_version >= 65.0
57
61
 
@@ -117,19 +121,6 @@ private
117
121
  driver.browser.capabilities[:browser_version].to_f
118
122
  end
119
123
 
120
- DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
121
- x.parent(:fieldset)[
122
- x.attr(:disabled)
123
- ] + x.ancestor[
124
- ~x.self(:legend) |
125
- x.preceding_sibling(:legend)
126
- ][
127
- x.parent(:fieldset)[
128
- x.attr(:disabled)
129
- ]
130
- ]
131
- end.to_s.freeze
132
-
133
124
  class ModifierKeysStack
134
125
  def initialize
135
126
  @stack = []
@@ -4,8 +4,19 @@ require 'capybara/selenium/extensions/html5_drag'
4
4
 
5
5
  class Capybara::Selenium::IENode < Capybara::Selenium::Node
6
6
  def disabled?
7
- # TODO: Doesn't work for a bunch of cases - need to get IE running to see if it can be done like this
8
- # driver.evaluate_script("arguments[0].msMatchesSelector(':disabled, select:disabled *')", self)
9
- super
7
+ # super
8
+ # optimize to one script call
9
+ driver.evaluate_script <<~JS.delete("\n"), self
10
+ arguments[0].msMatchesSelector('
11
+ :disabled,
12
+ select:disabled *,
13
+ optgroup:disabled *,
14
+ fieldset[disabled],
15
+ fieldset[disabled] > *:not(legend),
16
+ fieldset[disabled] > *:not(legend) *,
17
+ fieldset[disabled] > legend:nth-of-type(n+2),
18
+ fieldset[disabled] > legend:nth-of-type(n+2) *
19
+ ')
20
+ JS
10
21
  end
11
22
  end
@@ -91,19 +91,6 @@ private
91
91
  driver.browser.send(:bridge)
92
92
  end
93
93
 
94
- DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
95
- x.parent(:fieldset)[
96
- x.attr(:disabled)
97
- ] + x.ancestor[
98
- ~x.self(:legend) |
99
- x.preceding_sibling(:legend)
100
- ][
101
- x.parent(:fieldset)[
102
- x.attr(:disabled)
103
- ]
104
- ]
105
- end.to_s.freeze
106
-
107
94
  def _send_keys(keys, actions = browser_action, down_keys = ModifierKeysStack.new)
108
95
  case keys
109
96
  when *MODIFIER_KEYS
@@ -18,7 +18,33 @@ $(function() {
18
18
  });
19
19
  $('#drop_html5, #drop_html5_scroll').on('drop', function(ev){
20
20
  ev.preventDefault();
21
- $(this).html('HTML5 Dropped ' + ev.originalEvent.dataTransfer.getData("text"));
21
+ var oev = ev.originalEvent;
22
+ if (oev.dataTransfer.items) {
23
+ for (var i = 0; i < oev.dataTransfer.items.length; i++){
24
+ var item = oev.dataTransfer.items[i];
25
+ if (item.kind === 'file'){
26
+ var file = item.getAsFile();
27
+ $(this).append('HTML5 Dropped file: ' + file.name);
28
+ } else {
29
+ var _this = this;
30
+ var callback = (function(type){
31
+ return function(s){
32
+ $(_this).append('HTML5 Dropped string: ' + type + ' ' + s)
33
+ }
34
+ })(item.type);
35
+ item.getAsString(callback);
36
+ }
37
+ }
38
+ } else {
39
+ $(this).html('HTML5 Dropped ' + oev.dataTransfer.getData("text"));
40
+ for (var i = 0; i < oev.dataTransfer.files.length; i++) {
41
+ $(this).append('HTML5 Dropped file: ' + oev.dataTransfer.files[i].name);
42
+ }
43
+ for (var i = 0; i < oev.dataTransfer.types.length; i++) {
44
+ var type = oev.dataTransfer.types[i];
45
+ $(this).append('HTML5 Dropped string: ' + type + ' ' + oev.dataTransfer.getData(type));
46
+ }
47
+ }
22
48
  });
23
49
  $('#clickable').click(function(e) {
24
50
  var link = $(this);
@@ -180,7 +206,7 @@ $(function() {
180
206
  sessionStorage.setItem('session', 'session_value');
181
207
  localStorage.setItem('local', 'local value');
182
208
  });
183
- $('#multiple-file').change(function(e){
184
- $('body').append($('<p class="file_change"input_event_triggered">File input changed</p>'));
209
+ $('#multiple-file, #hidden_file').change(function(e){
210
+ $('body').append($('<p class="file_change">File input changed</p>'));
185
211
  })
186
212
  });
@@ -180,6 +180,12 @@ Capybara::SpecHelper.spec '#attach_file' do
180
180
  @session.attach_file('hidden_file', with_os_path_separators(__FILE__), make_visible: true)
181
181
  expect(@session.evaluate_script('arguments[0].style.display', @session.find(:css, '#hidden_file', visible: :all))).to eq 'none'
182
182
  end
183
+
184
+ it 'should fire change' do
185
+ @session.visit('/with_js')
186
+ @session.attach_file('hidden_file', with_os_path_separators(__FILE__), make_visible: true)
187
+ expect(@session).to have_css('.file_change')
188
+ end
183
189
  end
184
190
 
185
191
  context 'with a block', requires: %i[js] do
@@ -198,11 +204,13 @@ Capybara::SpecHelper.spec '#attach_file' do
198
204
  @session.click_button('awesome')
199
205
  expect(extract_results(@session)['hidden_image']).to end_with(File.basename(__FILE__))
200
206
  end
201
- end
202
207
 
203
- private
204
-
205
- def with_os_path_separators(path)
206
- Gem.win_platform? ? path.to_s.tr('/', '\\') : path.to_s
208
+ it 'should fire change' do
209
+ @session.visit('/with_js')
210
+ @session.attach_file(with_os_path_separators(__FILE__)) do
211
+ @session.find(:label, 'Label for hidden file input').click
212
+ end
213
+ expect(@session).to have_css('.file_change')
214
+ end
207
215
  end
208
216
  end
@@ -10,10 +10,11 @@ Capybara::SpecHelper.spec '#has_css?' do
10
10
  expect(@session).to have_css('p a#foo')
11
11
  end
12
12
 
13
- it 'should take a symbol as the selector' do
13
+ it 'should warn when passed a symbol' do
14
14
  # This was never a specifically accepted format but it has worked for a
15
- # lot of versions. Probably should keep it until at least 4.0
16
- # TODO: consider not supporting symbol for the CSS selector.
15
+ # lot of versions.
16
+ # TODO: Remove in 4.0
17
+ expect_any_instance_of(Kernel).to receive(:warn) # rubocop:disable RSpec/AnyInstance
17
18
  expect(@session).to have_css(:p)
18
19
  end
19
20
 
@@ -242,6 +242,10 @@ Capybara::SpecHelper.spec 'node' do
242
242
  expect(@session.find('//div[@id="hidden_attr"]')).not_to be_visible
243
243
  expect(@session.find('//a[@id="hidden_attr_via_ancestor"]')).not_to be_visible
244
244
  expect(@session.find('//input[@id="hidden_input"]')).not_to be_visible
245
+ end
246
+
247
+ it 'template elements should not be visible' do
248
+ Capybara.ignore_hidden_elements = false
245
249
  expect(@session.find('//template')).not_to be_visible
246
250
  end
247
251
 
@@ -414,6 +418,87 @@ Capybara::SpecHelper.spec 'node' do
414
418
  link.drag_to target
415
419
  expect(@session).to have_xpath('//div[contains(., "Dropped!")]')
416
420
  end
421
+
422
+ context 'HTML5', requires: %i[js html5_drag] do
423
+ it 'should HTML5 drag and drop an object' do
424
+ @session.visit('/with_js')
425
+ element = @session.find('//div[@id="drag_html5"]')
426
+ target = @session.find('//div[@id="drop_html5"]')
427
+ element.drag_to(target)
428
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5")]')
429
+ end
430
+
431
+ it 'should set clientX/Y in dragover events' do
432
+ @session.visit('/with_js')
433
+ element = @session.find('//div[@id="drag_html5"]')
434
+ target = @session.find('//div[@id="drop_html5"]')
435
+ element.drag_to(target)
436
+ expect(@session).to have_css('div.log', text: /DragOver with client position: [1-9]\d*,[1-9]\d*/, count: 2)
437
+ end
438
+
439
+ it 'should not HTML5 drag and drop on a non HTML5 drop element' do
440
+ @session.visit('/with_js')
441
+ element = @session.find('//div[@id="drag_html5"]')
442
+ target = @session.find('//div[@id="drop_html5"]')
443
+ target.execute_script("$(this).removeClass('drop');")
444
+ element.drag_to(target)
445
+ sleep 1
446
+ expect(@session).not_to have_xpath('//div[contains(., "HTML5 Dropped")]')
447
+ end
448
+
449
+ it 'should HTML5 drag and drop when scrolling needed' do
450
+ @session.visit('/with_js')
451
+ element = @session.find('//div[@id="drag_html5_scroll"]')
452
+ target = @session.find('//div[@id="drop_html5_scroll"]')
453
+ element.drag_to(target)
454
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5_scroll")]')
455
+ end
456
+
457
+ it 'should drag HTML5 default draggable elements' do
458
+ @session.visit('/with_js')
459
+ link = @session.find_link('drag_link_html5')
460
+ target = @session.find(:id, 'drop_html5')
461
+ link.drag_to target
462
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped")]')
463
+ end
464
+ end
465
+ end
466
+
467
+ describe 'Element#drop', requires: %i[js html5_drag] do
468
+ it 'can drop a file' do
469
+ @session.visit('/with_js')
470
+ target = @session.find('//div[@id="drop_html5"]')
471
+ target.drop(
472
+ with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__)))
473
+ )
474
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]')
475
+ end
476
+
477
+ it 'can drop multiple files' do
478
+ @session.visit('/with_js')
479
+ target = @session.find('//div[@id="drop_html5"]')
480
+ target.drop(
481
+ with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__))),
482
+ with_os_path_separators(File.expand_path('../fixtures/test_file.txt', File.dirname(__FILE__)))
483
+ )
484
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]')
485
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: test_file.txt")]')
486
+ end
487
+
488
+ it 'can drop strings' do
489
+ @session.visit('/with_js')
490
+ target = @session.find('//div[@id="drop_html5"]')
491
+ target.drop('text/plain' => 'Some dropped text')
492
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain Some dropped text")]')
493
+ end
494
+
495
+ it 'can drop multiple strings' do
496
+ @session.visit('/with_js')
497
+ target = @session.find('//div[@id="drop_html5"]')
498
+ target.drop('text/plain' => 'Some dropped text', 'text/url' => 'http://www.google.com')
499
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain Some dropped text")]')
500
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/url http://www.google.com")]')
501
+ end
417
502
  end
418
503
 
419
504
  describe '#hover', requires: [:hover] do
@@ -124,6 +124,10 @@ module Capybara
124
124
  def be_an_invalid_element_error(session)
125
125
  satisfy { |error| session.driver.invalid_element_errors.any? { |e| error.is_a? e } }
126
126
  end
127
+
128
+ def with_os_path_separators(path)
129
+ Gem.win_platform? ? path.to_s.tr('/', '\\') : path.to_s
130
+ end
127
131
  end
128
132
  end
129
133
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Capybara
4
- VERSION = '3.20.2'
4
+ VERSION = '3.21.0'
5
5
  end
@@ -64,6 +64,8 @@ Capybara::SpecHelper.run_specs TestSessions::SeleniumFirefox, 'selenium', capyba
64
64
  pending "Geckodriver doesn't provide a way to remove cookies outside the current domain"
65
65
  when 'Capybara::Session selenium #attach_file with a block can upload by clicking the file input'
66
66
  pending "Geckodriver doesn't allow clicking on file inputs"
67
+ when /drag_to.*HTML5/
68
+ pending "Firefox < 62 doesn't support a DataTransfer constuctor" if firefox_lt?(62.0, @session)
67
69
  end
68
70
  end
69
71
 
@@ -6,13 +6,13 @@ require 'shared_selenium_session'
6
6
  require 'shared_selenium_node'
7
7
  require 'rspec/shared_spec_matchers'
8
8
 
9
- if ENV['CI']
10
- if ::Selenium::WebDriver::Service.respond_to? :driver_path=
11
- ::Selenium::WebDriver::IE::Service
12
- else
13
- ::Selenium::WebDriver::IE
14
- end.driver_path = 'C:\Tools\WebDriver\IEDriverServer.exe'
15
- end
9
+ # if ENV['CI']
10
+ # if ::Selenium::WebDriver::Service.respond_to? :driver_path=
11
+ # ::Selenium::WebDriver::IE::Service
12
+ # else
13
+ # ::Selenium::WebDriver::IE
14
+ # end.driver_path = 'C:\Tools\WebDriver\IEDriverServer.exe'
15
+ # end
16
16
 
17
17
  def selenium_host
18
18
  ENV.fetch('SELENIUM_HOST', '192.168.56.102')
@@ -110,6 +110,12 @@ Capybara::SpecHelper.run_specs TestSessions::SeleniumIE, 'selenium', capybara_sk
110
110
  pending 'IE treats blank href as a parent request (against HTML spec)'
111
111
  when /#attach_file with a block/
112
112
  skip 'Hangs IE testing for unknown reason'
113
+ when /drag_to.*HTML5/
114
+ pending "IE doesn't support a DataTransfer constuctor"
115
+ when /template elements should not be visible/
116
+ skip "IE doesn't support template elements"
117
+ when /Element#drop/
118
+ pending "IE doesn't support DataTransfer constructor"
113
119
  end
114
120
  end
115
121