capybara 3.24.0 → 3.25.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 (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