capybara 3.24.0 → 3.25.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +16 -1
  3. data/README.md +1 -1
  4. data/lib/capybara.rb +2 -0
  5. data/lib/capybara/node/actions.rb +2 -2
  6. data/lib/capybara/node/element.rb +8 -5
  7. data/lib/capybara/node/matchers.rb +1 -1
  8. data/lib/capybara/rack_test/node.rb +3 -2
  9. data/lib/capybara/rspec/matchers/base.rb +5 -0
  10. data/lib/capybara/rspec/matchers/have_ancestor.rb +1 -4
  11. data/lib/capybara/rspec/matchers/have_selector.rb +1 -4
  12. data/lib/capybara/rspec/matchers/have_sibling.rb +1 -4
  13. data/lib/capybara/rspec/matchers/have_text.rb +1 -4
  14. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  15. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  16. data/lib/capybara/selenium/driver.rb +10 -9
  17. data/lib/capybara/selenium/extensions/find.rb +18 -17
  18. data/lib/capybara/selenium/extensions/html5_drag.rb +26 -4
  19. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  20. data/lib/capybara/selenium/node.rb +26 -16
  21. data/lib/capybara/selenium/nodes/chrome_node.rb +9 -0
  22. data/lib/capybara/selenium/nodes/firefox_node.rb +0 -23
  23. data/lib/capybara/selenium/nodes/safari_node.rb +1 -23
  24. data/lib/capybara/server/animation_disabler.rb +1 -1
  25. data/lib/capybara/session/config.rb +3 -1
  26. data/lib/capybara/spec/public/offset.js +6 -0
  27. data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -0
  28. data/lib/capybara/spec/session/node_spec.rb +119 -0
  29. data/lib/capybara/spec/session/selectors_spec.rb +1 -1
  30. data/lib/capybara/spec/spec_helper.rb +1 -0
  31. data/lib/capybara/spec/views/offset.erb +32 -0
  32. data/lib/capybara/spec/views/with_animation.erb +29 -1
  33. data/lib/capybara/spec/views/with_dragula.erb +22 -0
  34. data/lib/capybara/spec/views/with_html.erb +3 -0
  35. data/lib/capybara/spec/views/with_js.erb +1 -1
  36. data/lib/capybara/version.rb +1 -1
  37. data/spec/selenium_spec_edge.rb +6 -2
  38. data/spec/selenium_spec_firefox.rb +1 -1
  39. data/spec/shared_selenium_session.rb +7 -1
  40. metadata +10 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ea1da81bd27199aadc0d73d61e33b7ff0ea9911114acc375ea2e6f798a49eb4
4
- data.tar.gz: 30279a8a5ab221ae029bc95625c7c82ad11cdf68d91ef6be33e7a0b022268537
3
+ metadata.gz: 439a641ea3d9a06bcc674c159c3ef16612fb29874e71f11ef7bb2d0e73382892
4
+ data.tar.gz: a5e57dfb62e3682a87f05650a13945545a61183a84a15498390d07aa3ec6e4ed
5
5
  SHA512:
6
- metadata.gz: 0b8d7f760540297a204ae5eb0dac8b9450e8961652de87d543ae114edb9af6a48219db7c55a40e004d49847431ef545cc5628a6bbde858e58921209f984852d2
7
- data.tar.gz: 56d10157a97fd98f7e2e23c98fc21f0dc0f70f53338a271c9fa22b639648d919134fe1c7c803be43f435a9618d49e2517ccce5d207139635c49d9f6f18a66bd6
6
+ metadata.gz: d50ab813eecabef7465c9a42904174f65eefe6dc8e0f92fea59d523753582c1797a1a87880a1d9805edb8f74e33585314841d6fac7abcf2781a627ac9703191e
7
+ data.tar.gz: 0d4f2b360e528a3fae6357072592ac5295cb500d68251d23bc66a014fc62528473e960b4049d4e24a5339a8a41f02f15c70b7ae59b5f51ebfb1a7369afa47f27
data/History.md CHANGED
@@ -1,5 +1,20 @@
1
+ # Version 3.25.0
2
+ Release date: 2019-06-27
3
+
4
+ ### Added
5
+
6
+ * Animation disabler also disables before and after pseudoelements - Issue #2221 [Daniel Heath]
7
+ * `w3c_click_offset` configuration option to determine whether click offsets are calculated from element
8
+ center or top left corner
9
+
10
+ ### Fixed
11
+
12
+ * Woraround issue with chromedriver 76/77 in W3C mode losing mouse state during legacy drag. Only fixed if
13
+ both source and target are simultaenously inside the viewport - Issue #2223
14
+ * Negative ancestor expectations/predicates were incorrectly checking siblings rather than ancestors
15
+
1
16
  # Version 3.24.0
2
- Release date: 2079-06-13
17
+ Release date: 2019-06-13
3
18
 
4
19
  ### Added
5
20
 
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jnicklas/capybara?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
8
8
  [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=capybara&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=capybara&package-manager=bundler&version-scheme=semver)
9
9
 
10
- **Note** You are viewing the README for the 3.24.x version of Capybara.
10
+ **Note** You are viewing the README for the 3.25.x version of Capybara
11
11
 
12
12
  Capybara helps you test web applications by simulating how a real user would
13
13
  interact with your app. It is agnostic about the driver running your tests and
@@ -97,6 +97,7 @@ module Capybara
97
97
  # and {configure raise_server_errors} is `true`.
98
98
  # - **test_id** (Symbol, String, `nil` = `nil`) - Optional attribute to match locator against with built-in selectors along with id.
99
99
  # - **threadsafe** (Boolean = `false`) - Whether sessions can be configured individually.
100
+ # - **w3c_click_offset** (Boolean = 'false') - Whether click offsets should be from element center (true) or top left (false)
100
101
  #
101
102
  # #### DSL Options
102
103
  #
@@ -506,4 +507,5 @@ Capybara.configure do |config|
506
507
  config.predicates_wait = true
507
508
  config.default_normalize_ws = false
508
509
  config.allow_gumbo = false
510
+ config.w3c_click_offset = false
509
511
  end
@@ -307,12 +307,12 @@ module Capybara
307
307
  synchronize(Capybara::Queries::BaseQuery.wait(options, session_options.default_max_wait_time)) do
308
308
  begin
309
309
  find(:select, from, options)
310
- rescue Capybara::ElementNotFound => select_error
310
+ rescue Capybara::ElementNotFound => select_error # rubocop:disable Naming/RescuedExceptionsVariableName
311
311
  raise if %i[selected with_selected multiple].any? { |option| options.key?(option) }
312
312
 
313
313
  begin
314
314
  find(:datalist_input, from, options)
315
- rescue Capybara::ElementNotFound => dlinput_error
315
+ rescue Capybara::ElementNotFound => dlinput_error # rubocop:disable Naming/RescuedExceptionsVariableName
316
316
  raise Capybara::ElementNotFound, "#{select_error.message} and #{dlinput_error.message}"
317
317
  end
318
318
  end
@@ -157,13 +157,16 @@ module Capybara
157
157
  # Both x: and y: must be specified if an offset is wanted, if not specified the click will occur at the middle of the element.
158
158
  # @overload $0(*modifier_keys, wait: nil, **offset)
159
159
  # @param *modifier_keys [:alt, :control, :meta, :shift] ([]) Keys to be held down when clicking
160
- # @option offset [Integer] x X coordinate to offset the click location from the top left corner of the element
161
- # @option offset [Integer] y Y coordinate to offset the click location from the top left corner of the element
160
+ # @option options [Integer] x X coordinate to offset the click location. If {Capybara.configure w3c_click_offset} is `true` the
161
+ # offset will be from the element center, otherwise it will be from the top left corner of the element
162
+ # @option options [Integer] y Y coordinate to offset the click location. If {Capybara.configure w3c_click_offset} is `true` the
163
+ # offset will be from the element center, otherwise it will be from the top left corner of the element
162
164
  # @return [Capybara::Node::Element] The element
163
- def click(*keys, wait: nil, **offset)
164
- raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ offset[:x] ^ offset[:y]
165
+ def click(*keys, wait: nil, **options)
166
+ raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ options[:x] ^ options[:y]
165
167
 
166
- synchronize(wait) { base.click(Array(keys), offset) }
168
+ options[:offset] = :center if session_options.w3c_click_offset
169
+ synchronize(wait) { base.click(Array(keys), options) }
167
170
  self
168
171
  end
169
172
 
@@ -743,7 +743,7 @@ module Capybara
743
743
  end
744
744
 
745
745
  def assert_no_ancestor(*args, &optional_filter_block)
746
- _verify_selector_result(args, optional_filter_block, Capybara::Queries::SiblingQuery) do |result, query|
746
+ _verify_selector_result(args, optional_filter_block, Capybara::Queries::AncestorQuery) do |result, query|
747
747
  if result.matches_count? && (!result.empty? || query.expects_none?)
748
748
  raise Capybara::ExpectationNotMet, result.negative_failure_message
749
749
  end
@@ -63,8 +63,9 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
63
63
  native.remove_attribute('selected')
64
64
  end
65
65
 
66
- def click(keys = [], **offset)
67
- raise ArgumentError, 'The RackTest driver does not support click options' unless keys.empty? && offset.empty?
66
+ def click(keys = [], **options)
67
+ options.delete(:offset)
68
+ raise ArgumentError, 'The RackTest driver does not support click options' unless keys.empty? && options.empty?
68
69
 
69
70
  if link?
70
71
  follow_link
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/rspec/matchers/compound'
4
+ require 'capybara/rspec/matchers/count_sugar'
4
5
 
5
6
  module Capybara
6
7
  module RSpecMatchers
@@ -65,6 +66,10 @@ module Capybara
65
66
  end
66
67
  end
67
68
 
69
+ class CountableWrappedElementMatcher < WrappedElementMatcher
70
+ include ::Capybara::RSpecMatchers::CountSugar
71
+ end
72
+
68
73
  class NegatedMatcher
69
74
  include ::Capybara::RSpecMatchers::Matchers::Compound if defined?(::Capybara::RSpecMatchers::Matchers::Compound)
70
75
 
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/rspec/matchers/base'
4
- require 'capybara/rspec/matchers/count_sugar'
5
4
 
6
5
  module Capybara
7
6
  module RSpecMatchers
8
7
  module Matchers
9
- class HaveAncestor < WrappedElementMatcher
10
- include CountSugar
11
-
8
+ class HaveAncestor < CountableWrappedElementMatcher
12
9
  def element_matches?(el)
13
10
  el.assert_ancestor(*@args, &@filter_block)
14
11
  end
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/rspec/matchers/base'
4
- require 'capybara/rspec/matchers/count_sugar'
5
4
 
6
5
  module Capybara
7
6
  module RSpecMatchers
8
7
  module Matchers
9
- class HaveSelector < WrappedElementMatcher
10
- include CountSugar
11
-
8
+ class HaveSelector < CountableWrappedElementMatcher
12
9
  def element_matches?(el)
13
10
  el.assert_selector(*@args, &@filter_block)
14
11
  end
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/rspec/matchers/base'
4
- require 'capybara/rspec/matchers/count_sugar'
5
4
 
6
5
  module Capybara
7
6
  module RSpecMatchers
8
7
  module Matchers
9
- class HaveSibling < WrappedElementMatcher
10
- include CountSugar
11
-
8
+ class HaveSibling < CountableWrappedElementMatcher
12
9
  def element_matches?(el)
13
10
  el.assert_sibling(*@args, &@filter_block)
14
11
  end
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/rspec/matchers/base'
4
- require 'capybara/rspec/matchers/count_sugar'
5
4
 
6
5
  module Capybara
7
6
  module RSpecMatchers
8
7
  module Matchers
9
- class HaveText < WrappedElementMatcher
10
- include CountSugar
11
-
8
+ class HaveText < CountableWrappedElementMatcher
12
9
  def element_matches?(el)
13
10
  el.assert_text(*@args)
14
11
  end
@@ -17,11 +17,7 @@ module Capybara
17
17
  conditions = if name == :class
18
18
  class_conditions(value)
19
19
  elsif value.is_a? Regexp
20
- Selector::RegexpDisassembler.new(value).alternated_substrings.map do |strs|
21
- strs.map do |str|
22
- "[#{name}*='#{str}'#{' i' if value.casefold?}]"
23
- end.join
24
- end
20
+ regexp_conditions(name, value)
25
21
  else
26
22
  [attribute_conditions(name => value)]
27
23
  end
@@ -36,6 +32,14 @@ module Capybara
36
32
 
37
33
  private
38
34
 
35
+ def regexp_conditions(name, value)
36
+ Selector::RegexpDisassembler.new(value).alternated_substrings.map do |strs|
37
+ strs.map do |str|
38
+ "[#{name}*='#{str}'#{' i' if value.casefold?}]"
39
+ end.join
40
+ end
41
+ end
42
+
39
43
  def attribute_conditions(attributes)
40
44
  attributes.map do |attribute, value|
41
45
  case value
@@ -70,7 +74,7 @@ module Capybara
70
74
  end.join
71
75
  end
72
76
  else
73
- cls = Array(classes).group_by { |cl| cl.start_with?('!') && !cl.start_with?('!!!') }
77
+ cls = Array(classes).group_by { |cl| cl.match?(/^!(?!!!)/) }
74
78
  [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
75
79
  cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join]
76
80
  end
@@ -48,7 +48,7 @@ module Capybara
48
48
  attribute_conditions(class: classes)
49
49
  else
50
50
  Array(classes).map do |klass|
51
- if klass.start_with?('!') && !klass.start_with?('!!!')
51
+ if klass.match?(/^!(?!!!)/)
52
52
  !XPath.attr(:class).contains_word(klass.slice(1..-1))
53
53
  else
54
54
  XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
@@ -300,13 +300,10 @@ private
300
300
  end
301
301
 
302
302
  def unhandled_alert_errors
303
- @unhandled_alert_errors ||= [Selenium::WebDriver::Error::UnexpectedAlertOpenError].tap do |errors|
304
- unless selenium_4?
305
- ::Selenium::WebDriver.logger.suppress_deprecations do
306
- errors << Selenium::WebDriver::Error::UnhandledAlertError
307
- end
308
- end
309
- end
303
+ @unhandled_alert_errors ||= with_legacy_error(
304
+ [Selenium::WebDriver::Error::UnexpectedAlertOpenError],
305
+ 'UnhandledAlertError'
306
+ )
310
307
  end
311
308
 
312
309
  def delete_all_cookies
@@ -387,10 +384,14 @@ private
387
384
  end
388
385
 
389
386
  def find_modal_errors
390
- @find_modal_errors ||= [Selenium::WebDriver::Error::TimeoutError].tap do |errors|
387
+ @find_modal_errors ||= with_legacy_error([Selenium::WebDriver::Error::TimeoutError], 'TimeOutError')
388
+ end
389
+
390
+ def with_legacy_error(errors, legacy_error)
391
+ errors.tap do |errs|
391
392
  unless selenium_4?
392
393
  ::Selenium::WebDriver.logger.suppress_deprecations do
393
- errors << Selenium::WebDriver::Error::TimeOutError
394
+ errs << Selenium::WebDriver::Error.const_get(legacy_error)
394
395
  end
395
396
  end
396
397
  end
@@ -18,27 +18,28 @@ module Capybara
18
18
  hints = []
19
19
 
20
20
  if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
21
- begin
22
- els = filter_by_text(els, texts) unless texts.empty?
23
- hints_js, functions = build_hints_js(uses_visibility, styles)
24
-
25
- unless functions.empty?
26
- hints = es_context.execute_script(hints_js, els).map! do |results|
27
- hint = {}
28
- hint[:style] = results.pop if functions.include?(:style_func)
29
- hint[:visible] = results.pop if functions.include?(:vis_func)
30
- hint
31
- end
32
- end
33
- rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
34
- ::Capybara::NotSupportedByDriverError
35
- # warn 'Unexpected Stale Element Error - skipping optimization'
36
- hints = []
37
- end
21
+ els = filter_by_text(els, texts) unless texts.empty?
22
+ hints = gather_hints(els, uses_visibility: uses_visibility, styles: styles)
38
23
  end
39
24
  els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
40
25
  end
41
26
 
27
+ def gather_hints(elements, uses_visibility:, styles:)
28
+ hints_js, functions = build_hints_js(uses_visibility, styles)
29
+ return [] unless functions.any?
30
+
31
+ es_context.execute_script(hints_js, elements).map! do |results|
32
+ hint = {}
33
+ hint[:style] = results.pop if functions.include?(:style_func)
34
+ hint[:visible] = results.pop if functions.include?(:vis_func)
35
+ hint
36
+ end
37
+ rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
38
+ ::Capybara::NotSupportedByDriverError
39
+ # warn 'Unexpected Stale Element Error - skipping optimization'
40
+ []
41
+ end
42
+
42
43
  def filter_by_text(elements, texts)
43
44
  es_context.execute_script <<~JS, elements, texts
44
45
  var texts = arguments[1];
@@ -7,16 +7,24 @@ class Capybara::Selenium::Node
7
7
  def drag_to(element, delay: 0.05)
8
8
  driver.execute_script MOUSEDOWN_TRACKER
9
9
  scroll_if_needed { browser_action.click_and_hold(native).perform }
10
- if driver.evaluate_script('window.capybara_mousedown_prevented || !arguments[0].draggable', self)
11
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
10
+ if driver.evaluate_script(LEGACY_DRAG_CHECK, self)
11
+ perform_legacy_drag(element)
12
12
  else
13
- driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000
14
- browser_action.release.perform
13
+ perform_html5_drag(element, delay)
15
14
  end
16
15
  end
17
16
 
18
17
  private
19
18
 
19
+ def perform_legacy_drag(element)
20
+ element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
21
+ end
22
+
23
+ def perform_html5_drag(element, delay)
24
+ driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000
25
+ browser_action.release.perform
26
+ end
27
+
20
28
  def html5_drop(*args)
21
29
  if args[0].is_a? String
22
30
  input = driver.evaluate_script ATTACH_FILE
@@ -86,6 +94,16 @@ class Capybara::Selenium::Node
86
94
  }, { once: true, passive: true })
87
95
  JS
88
96
 
97
+ LEGACY_DRAG_CHECK = <<~JS
98
+ (function(el){
99
+ if (window.capybara_mousedown_prevented) return true;
100
+ do {
101
+ if (el.draggable) return false;
102
+ } while (el = el.parentElement );
103
+ return true;
104
+ })(arguments[0])
105
+ JS
106
+
89
107
  HTML5_DRAG_DROP_SCRIPT = <<~JS
90
108
  function rectCenter(rect){
91
109
  return new DOMPoint(
@@ -166,6 +184,10 @@ class Capybara::Selenium::Node
166
184
  var dt = new DataTransfer();
167
185
  var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
168
186
 
187
+ while (source && !source.draggable) {
188
+ source = source.parentElement;
189
+ }
190
+
169
191
  if (source.tagName == 'A'){
170
192
  dt.setData('text/uri-list', source.href);
171
193
  dt.setData('text', source.href);
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Capybara::Selenium::Node
4
+ #
5
+ # @api private
6
+ #
7
+ class ModifierKeysStack
8
+ def initialize
9
+ @stack = []
10
+ end
11
+
12
+ def include?(key)
13
+ @stack.flatten.include?(key)
14
+ end
15
+
16
+ def press(key)
17
+ @stack.last.push(key)
18
+ end
19
+
20
+ def push
21
+ @stack.push []
22
+ end
23
+
24
+ def pop
25
+ @stack.pop
26
+ end
27
+ end
28
+ end
@@ -188,6 +188,21 @@ protected
188
188
  yield
189
189
  end
190
190
 
191
+ def scroll_to_center
192
+ script = <<-'JS'
193
+ try {
194
+ arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
195
+ } catch(e) {
196
+ arguments[0].scrollIntoView(true);
197
+ }
198
+ JS
199
+ begin
200
+ driver.execute_script(script, self)
201
+ rescue StandardError # rubocop:disable Lint/HandleExceptions
202
+ # Swallow error if scrollIntoView with options isn't supported
203
+ end
204
+ end
205
+
191
206
  private
192
207
 
193
208
  def sibling_index(parent, node, selector)
@@ -241,21 +256,6 @@ private
241
256
  end
242
257
  end
243
258
 
244
- def scroll_to_center
245
- script = <<-'JS'
246
- try {
247
- arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
248
- } catch(e) {
249
- arguments[0].scrollIntoView(true);
250
- }
251
- JS
252
- begin
253
- driver.execute_script(script, self)
254
- rescue StandardError # rubocop:disable Lint/HandleExceptions
255
- # Swallow error if scrollIntoView with options isn't supported
256
- end
257
- end
258
-
259
259
  def set_date(value) # rubocop:disable Naming/AccessorMethodName
260
260
  value = SettableValue.new(value)
261
261
  return set_text(value) unless value.dateable?
@@ -328,7 +328,13 @@ private
328
328
  end
329
329
 
330
330
  def action_with_modifiers(click_options)
331
- actions = browser_action.move_to(native, *click_options.coords)
331
+ actions = browser_action.tap do |acts|
332
+ if click_options.center_offset? && click_options.coords?
333
+ acts.move_to(native).move_by(*click_options.coords)
334
+ else
335
+ acts.move_to(native, *click_options.coords)
336
+ end
337
+ end
332
338
  modifiers_down(actions, click_options.keys)
333
339
  yield actions
334
340
  modifiers_up(actions, click_options.keys)
@@ -483,6 +489,10 @@ private
483
489
  [options[:x], options[:y]]
484
490
  end
485
491
 
492
+ def center_offset?
493
+ options[:offset] == :center
494
+ end
495
+
486
496
  def empty?
487
497
  keys.empty? && !coords?
488
498
  end
@@ -57,6 +57,15 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
57
57
 
58
58
  private
59
59
 
60
+ def perform_legacy_drag(element)
61
+ return super unless (browser_version < 77.0) && w3c? && !element.obscured?
62
+
63
+ # W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
64
+ # https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
65
+ browser_action.release.perform
66
+ browser_action.click_and_hold(native).move_to(element.native).release.perform
67
+ end
68
+
60
69
  def file_errors
61
70
  @file_errors = ::Selenium::WebDriver.logger.suppress_deprecations do
62
71
  [::Selenium::WebDriver::Error::ExpectedError]
@@ -114,27 +114,4 @@ private
114
114
  def browser_version
115
115
  driver.browser.capabilities[:browser_version].to_f
116
116
  end
117
-
118
- class ModifierKeysStack
119
- def initialize
120
- @stack = []
121
- end
122
-
123
- def include?(key)
124
- @stack.flatten.include?(key)
125
- end
126
-
127
- def press(key)
128
- @stack.last.push(key)
129
- end
130
-
131
- def push
132
- @stack.push []
133
- end
134
-
135
- def pop
136
- @stack.pop
137
- end
138
- end
139
- private_constant :ModifierKeysStack
140
117
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/modifier_keys_stack'
4
5
 
5
6
  class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
6
7
  # include Html5Drag
@@ -118,27 +119,4 @@ private
118
119
  shift left_shift right_shift
119
120
  meta left_meta right_meta
120
121
  command].freeze
121
-
122
- class ModifierKeysStack
123
- def initialize
124
- @stack = []
125
- end
126
-
127
- def include?(key)
128
- @stack.flatten.include?(key)
129
- end
130
-
131
- def press(key)
132
- @stack.last.push(key)
133
- end
134
-
135
- def push
136
- @stack.push []
137
- end
138
-
139
- def pop
140
- @stack.pop
141
- end
142
- end
143
- private_constant :ModifierKeysStack
144
122
  end
@@ -46,7 +46,7 @@ module Capybara
46
46
  DISABLE_MARKUP_TEMPLATE = <<~HTML
47
47
  <script defer>(typeof jQuery !== 'undefined') && (jQuery.fx.off = true);</script>
48
48
  <style>
49
- %<selector>s {
49
+ %<selector>s, %<selector>s::before, %<selector>s::after {
50
50
  transition: none !important;
51
51
  animation-duration: 0s !important;
52
52
  animation-delay: 0s !important;
@@ -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 default_normalize_ws].freeze
11
+ predicates_wait default_normalize_ws w3c_click_offset].freeze
12
12
 
13
13
  attr_accessor(*OPTIONS)
14
14
 
@@ -59,6 +59,8 @@ module Capybara
59
59
  # See {Capybara.configure}
60
60
  # @!method default_normalize_ws
61
61
  # See {Capybara.configure}
62
+ # @!method w3c_click_offset
63
+ # See {Capybara.configure}
62
64
 
63
65
  remove_method :server_host
64
66
 
@@ -0,0 +1,6 @@
1
+ $(function() {
2
+ $(document).on('click dblclick contextmenu', function(e){
3
+ e.preventDefault();
4
+ $(document.body).append('<div id="has-been-clicked">Has been clicked at ' + e.clientX + ',' + e.clientY + '</div>');
5
+ })
6
+ })
@@ -39,6 +39,8 @@ Capybara::SpecHelper.spec '#have_no_ancestor' do
39
39
  it 'should assert no matching ancestor' do
40
40
  el = @session.find(:css, '#ancestor1')
41
41
  expect(el).to have_no_ancestor(:css, '#child')
42
+ expect(el).to have_no_ancestor(:css, '#ancestor1_sibiling')
42
43
  expect(el).not_to have_ancestor(:css, '#child')
44
+ expect(el).not_to have_ancestor(:css, '#ancestor1_sibiling')
43
45
  end
44
46
  end
@@ -419,6 +419,16 @@ Capybara::SpecHelper.spec 'node' do
419
419
  expect(@session).to have_xpath('//div[contains(., "Dropped!")]')
420
420
  end
421
421
 
422
+ it 'should work with Dragula' do
423
+ @session.visit('/with_dragula')
424
+ @session.within(:css, '#sortable') do
425
+ src = @session.find('div', text: 'Item 1')
426
+ target = @session.find('div', text: 'Item 3')
427
+ src.drag_to target
428
+ expect(@session).to have_content(/Item 2.*Item 1/, normalize_ws: true)
429
+ end
430
+ end
431
+
422
432
  context 'HTML5', requires: %i[js html5_drag] do
423
433
  it 'should HTML5 drag and drop an object' do
424
434
  @session.visit('/with_js')
@@ -428,6 +438,14 @@ Capybara::SpecHelper.spec 'node' do
428
438
  expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5")]')
429
439
  end
430
440
 
441
+ it 'should HTML5 drag and drop an object child' do
442
+ @session.visit('/with_js')
443
+ element = @session.find('//div[@id="drag_html5"]/p')
444
+ target = @session.find('//div[@id="drop_html5"]')
445
+ element.drag_to(target)
446
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5")]')
447
+ end
448
+
431
449
  it 'should set clientX/Y in dragover events' do
432
450
  @session.visit('/with_js')
433
451
  element = @session.find('//div[@id="drag_html5"]')
@@ -471,6 +489,14 @@ Capybara::SpecHelper.spec 'node' do
471
489
  expect(@session).to have_content(/Item 3.*Item 1/, normalize_ws: true)
472
490
  end
473
491
  end
492
+
493
+ it 'should drag HTML5 default draggable element child' do
494
+ @session.visit('/with_js')
495
+ source = @session.find_link('drag_link_html5').find(:css, 'p')
496
+ target = @session.find(:id, 'drop_html5')
497
+ source.drag_to target
498
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped")]')
499
+ end
474
500
  end
475
501
  end
476
502
 
@@ -635,6 +661,55 @@ Capybara::SpecHelper.spec 'node' do
635
661
  JS
636
662
  expect { obscured.click(wait: 0) }.to(raise_error { |e| expect(e).to be_an_invalid_element_error(@session) })
637
663
  end
664
+
665
+ context 'offset', requires: [:js] do
666
+ before do
667
+ @session.visit('/offset')
668
+ @clicker = @session.find(:id, 'clicker')
669
+ end
670
+
671
+ context 'when w3c_click_offset is false' do
672
+ before do
673
+ Capybara.w3c_click_offset = false
674
+ end
675
+
676
+ it 'should offset from top left of element' do
677
+ @clicker.click(x: 10, y: 5)
678
+ expect(@session).to have_text(/clicked at 110,105/)
679
+ end
680
+
681
+ it 'should offset outside the element' do
682
+ @clicker.click(x: -15, y: -10)
683
+ expect(@session).to have_text(/clicked at 85,90/)
684
+ end
685
+
686
+ it 'should default to click the middle' do
687
+ @clicker.click
688
+ expect(@session).to have_text(/clicked at 150,150/)
689
+ end
690
+ end
691
+
692
+ context 'when w3c_click_offset is true' do
693
+ before do
694
+ Capybara.w3c_click_offset = true
695
+ end
696
+
697
+ it 'should offset from center of element' do
698
+ @clicker.click(x: 10, y: 5)
699
+ expect(@session).to have_text(/clicked at 160,155/)
700
+ end
701
+
702
+ it 'should offset outside from center of element' do
703
+ @clicker.click(x: -65, y: -60)
704
+ expect(@session).to have_text(/clicked at 85,90/)
705
+ end
706
+
707
+ it 'should default to click the middle' do
708
+ @clicker.click
709
+ expect(@session).to have_text(/clicked at 150,150/)
710
+ end
711
+ end
712
+ end
638
713
  end
639
714
 
640
715
  describe '#double_click', requires: [:js] do
@@ -669,6 +744,28 @@ Capybara::SpecHelper.spec 'node' do
669
744
  JS
670
745
  expect { obscured.double_click }.not_to raise_error
671
746
  end
747
+
748
+ context 'offset', requires: [:js] do
749
+ before do
750
+ @session.visit('/offset')
751
+ @clicker = @session.find(:id, 'clicker')
752
+ end
753
+
754
+ it 'should offset from top left of element' do
755
+ @clicker.click(x: 10, y: 5)
756
+ expect(@session).to have_text(/clicked at 110,105/)
757
+ end
758
+
759
+ it 'should offset outside the element' do
760
+ @clicker.click(x: -15, y: -10)
761
+ expect(@session).to have_text(/clicked at 85,90/)
762
+ end
763
+
764
+ it 'should default to click the middle' do
765
+ @clicker.click
766
+ expect(@session).to have_text(/clicked at 150,150/)
767
+ end
768
+ end
672
769
  end
673
770
 
674
771
  describe '#right_click', requires: [:js] do
@@ -703,6 +800,28 @@ Capybara::SpecHelper.spec 'node' do
703
800
  JS
704
801
  expect { obscured.right_click }.not_to raise_error
705
802
  end
803
+
804
+ context 'offset', requires: [:js] do
805
+ before do
806
+ @session.visit('/offset')
807
+ @clicker = @session.find(:id, 'clicker')
808
+ end
809
+
810
+ it 'should offset from top left of element' do
811
+ @clicker.click(x: 10, y: 5)
812
+ expect(@session).to have_text(/clicked at 110,105/)
813
+ end
814
+
815
+ it 'should offset outside the element' do
816
+ @clicker.click(x: -15, y: -10)
817
+ expect(@session).to have_text(/clicked at 85,90/)
818
+ end
819
+
820
+ it 'should default to click the middle' do
821
+ @clicker.click
822
+ expect(@session).to have_text(/clicked at 150,150/)
823
+ end
824
+ end
706
825
  end
707
826
 
708
827
  describe '#send_keys', requires: [:send_keys] do
@@ -79,7 +79,7 @@ Capybara::SpecHelper.spec Capybara::Selector do
79
79
  it 'can find by class' do
80
80
  expect(@session.find(:field, class: 'confusion-checkbox')['id']).to eq 'confusion_checkbox'
81
81
  expect(@session).to have_selector(:field, class: 'confusion', count: 3)
82
- expect(@session.find(:field, class: ['confusion', 'confusion-textarea'])['id']).to eq 'confusion_textarea'
82
+ expect(@session.find(:field, class: %w[confusion confusion-textarea])['id']).to eq 'confusion_textarea'
83
83
  end
84
84
  end
85
85
  end
@@ -36,6 +36,7 @@ module Capybara
36
36
  Capybara.predicates_wait = true
37
37
  Capybara.default_normalize_ws = false
38
38
  Capybara.allow_gumbo = true
39
+ Capybara.w3c_click_offset = false
39
40
  reset_threadsafe
40
41
  end
41
42
 
@@ -0,0 +1,32 @@
1
+ <html>
2
+ <head>
3
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
4
+ <title>Offset</title>
5
+ <style>
6
+ body {
7
+ margin: 0px;
8
+ }
9
+ #wrapper {
10
+ width: 300px;
11
+ height: 300px;
12
+ margin: 0px;
13
+ }
14
+ #clicker {
15
+ position: relative;
16
+ width: 100px;
17
+ height: 100px;
18
+ top: 100px;
19
+ left: 100px;
20
+ background-color: red;
21
+ margin: 0px;
22
+ }
23
+ </style>
24
+ <script src="/jquery.js" type="text/javascript" charset="utf-8"></script>
25
+ <script src="/offset.js" type="text/javascript" charset="utf-8"></script>
26
+ </head>
27
+ <body>
28
+ <div id="wrapper">
29
+ <div id="clicker"></div>
30
+ </div>
31
+ </body>
32
+ </html>
@@ -6,6 +6,17 @@
6
6
  <script src="/jquery.js" type="text/javascript" charset="utf-8"></script>
7
7
  <script src="/jquery-ui.js" type="text/javascript" charset="utf-8"></script>
8
8
  <script src="/test.js" type="text/javascript" charset="utf-8"></script>
9
+ <script type="text/javascript">
10
+ $(document).on('transitionend', function(){
11
+ $(document.body).append('<div>Transition Ended</div>')
12
+ });
13
+ $(document).on('animationend', function(){
14
+ $(document.body).append('<div>Animation Ended</div>')
15
+ });
16
+ $(document).on('contextmenu', function(e){
17
+ e.preventDefault();
18
+ });
19
+ </script>
9
20
  <style>
10
21
  .transition.away {
11
22
  width: 0%;
@@ -17,6 +28,13 @@
17
28
  overflow: hidden;
18
29
  }
19
30
 
31
+ a::after {
32
+ content: "";
33
+ width: 0px;
34
+ height: 0px;
35
+ background-color: blue;
36
+ }
37
+
20
38
  a:not(.away) {
21
39
  height: 20px;
22
40
  }
@@ -35,12 +53,22 @@
35
53
  animation-duration: 3s;
36
54
  animation-fill-mode: forwards;
37
55
  }
56
+
57
+ @keyframes pseudo_grow {
58
+ 100% { height: 100px, width: 100px };
59
+ }
60
+
61
+ a.animation.pseudo::after {
62
+ animation: pseudo_grow 3s forwards;
63
+ }
38
64
  </style>
39
65
  </head>
40
66
 
41
67
  <body id="with_animation">
42
68
  <a href='#' class='transition' onclick='this.classList.add("away")'>transition me away</a>
43
- <a href='#' class='animation' onclick='this.classList.add("away")'>animate me away</a>
69
+ <a href='#' class='animation' onclick='this.classList.add("away")' oncontextmenu='this.classList.add("pseudo")'>
70
+ animate me away
71
+ </a>
44
72
  </body>
45
73
  </html>
46
74
 
@@ -0,0 +1,22 @@
1
+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
2
+ <head>
3
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
4
+ <title>with_dragula</title>
5
+
6
+ </head>
7
+
8
+ <body id="with_dragula">
9
+ <div id="sortable">
10
+ <div class="item1">Item 1</div>
11
+ <div class="item2">Item 2</div>
12
+ <div class="item3">Item 3</div>
13
+ <div class="item4">Item 4</div>
14
+ <div class="item5">Item 5</div>
15
+ </div>
16
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.js" type="text/javascript"></script>
17
+ <script>
18
+ dragula([document.getElementById("sortable")]);
19
+ </script>
20
+ </body>
21
+ </html>
22
+
@@ -138,6 +138,9 @@ banana</textarea>
138
138
  Ancestor
139
139
  <div id="child">Child</div>
140
140
  </div>
141
+ <div id="ancestor1_sibiling">
142
+ ASibling
143
+ </div>
141
144
  </div>
142
145
  <button id="ancestor_button" type="submit" disabled>
143
146
  <img id="button_img" width="20" height="20" alt="button img"/>
@@ -28,7 +28,7 @@
28
28
  <div id="drag_html5" draggable="true">
29
29
  <p>This is an HTML5 draggable element.</p>
30
30
  </div>
31
- <a id="drag_link_html5" href="#">This is an HTML5 draggable link</a>
31
+ <a id="drag_link_html5" href="#"><p>This is an HTML5 draggable link</p></a>
32
32
  <div id="drop_html5" class="drop">
33
33
  <p>It should be dropped here.</p>
34
34
  </div>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Capybara
4
- VERSION = '3.24.0'
4
+ VERSION = '3.25.0'
5
5
  end
@@ -6,8 +6,12 @@ require 'shared_selenium_session'
6
6
  require 'shared_selenium_node'
7
7
  require 'rspec/shared_spec_matchers'
8
8
 
9
- Selenium::WebDriver::Edge::Service.driver_path = '/usr/local/bin/msedgedriver'
10
- Selenium::WebDriver::EdgeChrome.path = '/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary'
9
+ unless ENV['CI']
10
+ Selenium::WebDriver::Edge::Service.driver_path = '/usr/local/bin/msedgedriver'
11
+ Selenium::WebDriver::EdgeChrome.path = '/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary'
12
+ end
13
+
14
+ Webdrivers::Edgedriver.required_version = '76.0.168' if ENV['CI']
11
15
 
12
16
  Capybara.register_driver :selenium_edge do |app|
13
17
  # ::Selenium::WebDriver.logger.level = "debug"
@@ -57,7 +57,7 @@ Capybara::SpecHelper.run_specs TestSessions::SeleniumFirefox, 'selenium', capyba
57
57
  when 'Capybara::Session selenium #attach_file with multipart form should fire change once when uploading multiple files from empty'
58
58
  pending "FF < 62 doesn't support setting all files at once" if firefox_lt?(62, @session)
59
59
  when 'Capybara::Session selenium #accept_confirm should work with nested modals'
60
- skip 'Broken in FF 63 - https://bugzilla.mozilla.org/show_bug.cgi?id=1487358' if firefox_gte?(63, @session)
60
+ skip 'Broken in 63 <= FF < 69 - https://bugzilla.mozilla.org/show_bug.cgi?id=1487358' if firefox_gte?(63, @session) && firefox_lt?(69, @session)
61
61
  when 'Capybara::Session selenium #click_link can download a file'
62
62
  skip 'Need to figure out testing of file downloading on windows platform' if Gem.win_platform?
63
63
  when 'Capybara::Session selenium #reset_session! removes ALL cookies'
@@ -355,11 +355,17 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
355
355
  expect(@animation_session).to have_no_link('transition me away', wait: 0.5)
356
356
  end
357
357
 
358
- it 'should disable CSS animations' do
358
+ it 'should disable CSS animations (set to 0s)' do
359
359
  @animation_session.visit('with_animation')
360
360
  @animation_session.click_link('animate me away')
361
361
  expect(@animation_session).to have_no_link('animate me away', wait: 0.5)
362
362
  end
363
+
364
+ it 'should disable CSS animations on pseudo elements (set to 0s)' do
365
+ @animation_session.visit('with_animation')
366
+ @animation_session.find_link('animate me away').right_click
367
+ expect(@animation_session).to have_content('Animation Ended', wait: 0.1)
368
+ end
363
369
  end
364
370
 
365
371
  context 'if we pass in css that matches elements' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.24.0
4
+ version: 3.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Walpole
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain:
12
12
  - gem-public_cert.pem
13
- date: 2019-06-14 00:00:00.000000000 Z
13
+ date: 2019-06-28 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: addressable
@@ -268,16 +268,16 @@ dependencies:
268
268
  name: rubocop
269
269
  requirement: !ruby/object:Gem::Requirement
270
270
  requirements:
271
- - - ">="
271
+ - - "~>"
272
272
  - !ruby/object:Gem::Version
273
- version: '0'
273
+ version: '0.72'
274
274
  type: :development
275
275
  prerelease: false
276
276
  version_requirements: !ruby/object:Gem::Requirement
277
277
  requirements:
278
- - - ">="
278
+ - - "~>"
279
279
  - !ruby/object:Gem::Version
280
- version: '0'
280
+ version: '0.72'
281
281
  - !ruby/object:Gem::Dependency
282
282
  name: rubocop-performance
283
283
  requirement: !ruby/object:Gem::Requirement
@@ -516,6 +516,7 @@ files:
516
516
  - lib/capybara/selenium/driver_specializations/safari_driver.rb
517
517
  - lib/capybara/selenium/extensions/find.rb
518
518
  - lib/capybara/selenium/extensions/html5_drag.rb
519
+ - lib/capybara/selenium/extensions/modifier_keys_stack.rb
519
520
  - lib/capybara/selenium/extensions/scroll.rb
520
521
  - lib/capybara/selenium/logger_suppressor.rb
521
522
  - lib/capybara/selenium/node.rb
@@ -541,6 +542,7 @@ files:
541
542
  - lib/capybara/spec/fixtures/test_file.txt
542
543
  - lib/capybara/spec/public/jquery-ui.js
543
544
  - lib/capybara/spec/public/jquery.js
545
+ - lib/capybara/spec/public/offset.js
544
546
  - lib/capybara/spec/public/test.js
545
547
  - lib/capybara/spec/session/accept_alert_spec.rb
546
548
  - lib/capybara/spec/session/accept_confirm_spec.rb
@@ -646,6 +648,7 @@ files:
646
648
  - lib/capybara/spec/views/host_links.erb
647
649
  - lib/capybara/spec/views/initial_alert.erb
648
650
  - lib/capybara/spec/views/obscured.erb
651
+ - lib/capybara/spec/views/offset.erb
649
652
  - lib/capybara/spec/views/path.erb
650
653
  - lib/capybara/spec/views/popup_one.erb
651
654
  - lib/capybara/spec/views/popup_two.erb
@@ -656,6 +659,7 @@ files:
656
659
  - lib/capybara/spec/views/with_animation.erb
657
660
  - lib/capybara/spec/views/with_base_tag.erb
658
661
  - lib/capybara/spec/views/with_count.erb
662
+ - lib/capybara/spec/views/with_dragula.erb
659
663
  - lib/capybara/spec/views/with_fixed_header_footer.erb
660
664
  - lib/capybara/spec/views/with_hover.erb
661
665
  - lib/capybara/spec/views/with_hover1.erb