capybara 3.20.2 → 3.21.0

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