capybara 3.28.0 → 3.29.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59f170c7004d4936ffb0d136a4dd0755a9c1ec5870f6bf1114d693b99a8fed51
4
- data.tar.gz: 955e163359d522952afdc7d776458667f97aa053e6eb704320733eae8340544a
3
+ metadata.gz: 5f11a887ca6aed51d064d9c8eb797e0db4605a3d013082a11f4f4b1013b27bef
4
+ data.tar.gz: 714e834fdb1c2f0e692f547a1d032113dd888d6905eddecc60f05503720bcd8e
5
5
  SHA512:
6
- metadata.gz: 13d33c4977455c5d2e9a5aa7532ffed98573795a1528685ded75574dfabd58512f5d3e71d3582d8155fe4325209ba3bbde3246a29b4436de573c5b492dffe1cc
7
- data.tar.gz: 52b33d646ec5c86b61a04b7f2c354a1d1e77d1c363b0ba8d19e7ad1b48679a53078766c8f018f1d7f223b730d9b4d951f005a689a3f143999fdeead4c522242c
6
+ metadata.gz: 82c37d1b9c8d1c7f9d858f8d7aebebfc2ecee4c5e35d35f9a3c949d978e4e97d48f78b9d5f93079e209fc779ab31e5707ecce8ac0d2a4ce8108fc3be2437e1bc
7
+ data.tar.gz: '06886b335dd0ef5f93f59a09026877bc460e6430b2d2f1902cd31a3179b1bd52b9418a1b43ab558dcf18e92b198506023c3549a065949bb33f881538ec9e018d'
data/History.md CHANGED
@@ -1,3 +1,21 @@
1
+ # Version 3.29.0
2
+ Release date: Unreleased
3
+
4
+ ### Added
5
+
6
+ * Allow clicking on file input when using the block version of `attach_file` with Chrome and Firefox
7
+ * Spatial filters (`left_of`, `right_of`, `above`, `below`, `near`)
8
+ * rack_test driver now supports clicking on details elements to open/close them
9
+
10
+ ### Fixed
11
+
12
+ * rack_test driver correctly determines visibility for open details elements descendants
13
+
14
+ ### Changed
15
+
16
+ * Results will now be lazily evaluated when using JRuby >= 9.2.8.0
17
+
18
+
1
19
  # Version 3.28.0
2
20
  Release date: 2019-08-03
3
21
 
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.28.x version of Capybara.
10
+ **Note** You are viewing the README for the 3.29.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
@@ -391,7 +391,7 @@ Capybara supports [Selenium 3.5+
391
391
  In order to use Selenium, you'll need to install the `selenium-webdriver` gem,
392
392
  and add it to your Gemfile if you're using bundler.
393
393
 
394
- Capybara pre-registers a number of named drives that use Selenium - they are:
394
+ Capybara pre-registers a number of named drivers that use Selenium - they are:
395
395
 
396
396
  * :selenium => Selenium driving Firefox
397
397
  * :selenium_headless => Selenium driving Firefox in a headless configuration
@@ -113,6 +113,10 @@ module Capybara
113
113
  !!self[:multiple]
114
114
  end
115
115
 
116
+ def rect
117
+ raise NotSupportedByDriverError, 'Capybara::Driver::Node#rect'
118
+ end
119
+
116
120
  def path
117
121
  raise NotSupportedByDriverError, 'Capybara::Driver::Node#path'
118
122
  end
@@ -288,6 +288,7 @@ module Capybara
288
288
  execute_script CAPTURE_FILE_ELEMENT_SCRIPT
289
289
  yield
290
290
  file_field = evaluate_script 'window._capybara_clicked_file_input'
291
+ raise ArgumentError, "Capybara was unable to determine the file input you're attaching to" unless file_field
291
292
  rescue ::Capybara::NotSupportedByDriverError
292
293
  warn 'Block mode of `#attach_file` is not supported by the current driver - ignoring.'
293
294
  end
@@ -371,6 +371,10 @@ module Capybara
371
371
  synchronize { base.path }
372
372
  end
373
373
 
374
+ def rect
375
+ synchronize { base.rect }
376
+ end
377
+
374
378
  ##
375
379
  #
376
380
  # Trigger any event on the current element, for example mouseover or focus
@@ -198,7 +198,7 @@ module Capybara
198
198
  x.attr(:style)[x.contains('display:none') | x.contains('display: none')] |
199
199
  x.attr(:hidden) |
200
200
  x.qname.one_of('script', 'head') |
201
- (~x.self(:summary) & XPath.parent(:details))
201
+ (~x.self(:summary) & XPath.parent(:details)[!XPath.attr(:open)])
202
202
  ].boolean
203
203
  end.to_s.freeze
204
204
  end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'matrix'
4
+
3
5
  module Capybara
4
6
  module Queries
5
7
  class SelectorQuery < Queries::BaseQuery
6
8
  attr_reader :expression, :selector, :locator, :options
7
- VALID_KEYS = COUNT_KEYS +
9
+ SPATIAL_KEYS = %i[above below left_of right_of near].freeze
10
+ VALID_KEYS = SPATIAL_KEYS + COUNT_KEYS +
8
11
  %i[text id class style visible obscured exact exact_text normalize_ws match wait filter_set]
9
12
  VALID_MATCH = %i[first smart prefer_exact one].freeze
10
13
 
@@ -18,6 +21,8 @@ module Capybara
18
21
  @resolved_node = nil
19
22
  @resolved_count = 0
20
23
  @options = options.dup
24
+ @filter_cache = Hash.new { |hsh, key| hsh[key] = {} }
25
+
21
26
  super(@options)
22
27
  self.session_options = session_options
23
28
 
@@ -50,13 +55,17 @@ module Capybara
50
55
  desc << 'visible ' if visible == :visible
51
56
  desc << 'non-visible ' if visible == :hidden
52
57
  end
58
+
53
59
  desc << "#{label} #{locator.inspect}"
60
+
54
61
  if show_for[:any]
55
62
  desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
56
63
  desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
57
64
  end
65
+
58
66
  desc << " with id #{options[:id]}" if options[:id]
59
67
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
68
+
60
69
  desc << case options[:style]
61
70
  when String
62
71
  " with style attribute #{options[:style].inspect}"
@@ -66,8 +75,15 @@ module Capybara
66
75
  " with styles #{options[:style].inspect}"
67
76
  else ''
68
77
  end
78
+
79
+ %i[above below left_of right_of near].each do |spatial_filter|
80
+ desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" if options[spatial_filter] && show_for[:spatial] # rubocop:disable Style/RescueModifier
81
+ end
82
+
69
83
  desc << selector.description(node_filters: show_for[:node], **options)
84
+
70
85
  desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
86
+
71
87
  desc << " within #{@resolved_node.inspect}" if describe_within?
72
88
  if locator.is_a?(String) && locator.start_with?('#', './/', '//')
73
89
  unless selector.raw_locator?
@@ -87,6 +103,7 @@ module Capybara
87
103
 
88
104
  matches_locator_filter?(node) &&
89
105
  matches_system_filters?(node) &&
106
+ matches_spatial_filters?(node) &&
90
107
  matches_node_filters?(node, node_filter_errors) &&
91
108
  matches_filter_block?(node)
92
109
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
@@ -125,8 +142,10 @@ module Capybara
125
142
  # @api private
126
143
  def resolve_for(node, exact = nil)
127
144
  applied_filters.clear
145
+ @filter_cache.clear
128
146
  @resolved_node = node
129
147
  @resolved_count += 1
148
+
130
149
  node.synchronize do
131
150
  children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
132
151
  Capybara::Result.new(children, self)
@@ -208,6 +227,7 @@ module Capybara
208
227
  hints[:uses_visibility] = true unless visible == :all
209
228
  hints[:texts] = text_fragments unless selector_format == :xpath
210
229
  hints[:styles] = options[:style] if use_default_style_filter?
230
+ hints[:position] = true if use_spatial_filter?
211
231
 
212
232
  if selector_format == :css
213
233
  if node.method(:find_css).arity != 1
@@ -333,6 +353,10 @@ module Capybara
333
353
  options.key?(:style) && !custom_keys.include?(:style)
334
354
  end
335
355
 
356
+ def use_spatial_filter?
357
+ options.values_at(*SPATIAL_KEYS).compact.any?
358
+ end
359
+
336
360
  def apply_expression_filters(expression)
337
361
  unapplied_options = options.keys - valid_keys
338
362
  expression_filters.inject(expression) do |expr, (name, ef)|
@@ -397,6 +421,42 @@ module Capybara
397
421
  matches_exact_text_filter?(node)
398
422
  end
399
423
 
424
+ def matches_spatial_filters?(node)
425
+ applied_filters << :spatial
426
+ return true unless use_spatial_filter?
427
+
428
+ node_rect = Rectangle.new(node.initial_cache[:position] || node.rect)
429
+
430
+ if options[:above]
431
+ el_rect = rect_cache(options[:above])
432
+ return false unless node_rect.above? el_rect
433
+ end
434
+
435
+ if options[:below]
436
+ el_rect = rect_cache(options[:below])
437
+ return false unless node_rect.below? el_rect
438
+ end
439
+
440
+ if options[:left_of]
441
+ el_rect = rect_cache(options[:left_of])
442
+ return false unless node_rect.left_of? el_rect
443
+ end
444
+
445
+ if options[:right_of]
446
+ el_rect = rect_cache(options[:right_of])
447
+ return false unless node_rect.right_of? el_rect
448
+ end
449
+
450
+ if options[:near]
451
+ return false if node == options[:near]
452
+
453
+ el_rect = rect_cache(options[:near])
454
+ return false unless node_rect.near? el_rect
455
+ end
456
+
457
+ true
458
+ end
459
+
400
460
  def matches_id_filter?(node)
401
461
  return true unless use_default_id_filter? && options[:id].is_a?(Regexp)
402
462
 
@@ -491,6 +551,147 @@ module Capybara
491
551
  def builder(expr)
492
552
  selector.builder(expr)
493
553
  end
554
+
555
+ def position_cache(key)
556
+ @filter_cache[key][:position] ||= key.rect
557
+ end
558
+
559
+ def rect_cache(key)
560
+ @filter_cache[key][:rect] ||= Rectangle.new(position_cache(key))
561
+ end
562
+
563
+ class Rectangle
564
+ attr_reader :top, :bottom, :left, :right
565
+ def initialize(position)
566
+ # rubocop:disable Style/RescueModifier
567
+ @top = position['top'] rescue position['y']
568
+ @bottom = position['bottom'] rescue (@top + position['height'])
569
+ @left = position['left'] rescue position['x']
570
+ @right = position['right'] rescue (@left + position['width'])
571
+ # rubocop:enable Style/RescueModifier
572
+ end
573
+
574
+ def distance(other)
575
+ distance = Float::INFINITY
576
+
577
+ line_segments.each do |ls1|
578
+ other.line_segments.each do |ls2|
579
+ distance = [
580
+ distance,
581
+ distance_segment_segment(*ls1, *ls2)
582
+ ].min
583
+ end
584
+ end
585
+
586
+ distance
587
+ end
588
+
589
+ def above?(other)
590
+ bottom <= other.top
591
+ end
592
+
593
+ def below?(other)
594
+ top >= other.bottom
595
+ end
596
+
597
+ def left_of?(other)
598
+ right <= other.left
599
+ end
600
+
601
+ def right_of?(other)
602
+ left >= other.right
603
+ end
604
+
605
+ def near?(other)
606
+ distance(other) <= 50
607
+ end
608
+
609
+ protected
610
+
611
+ def line_segments
612
+ [
613
+ [Vector[top, left], Vector[top, right]],
614
+ [Vector[top, right], Vector[bottom, left]],
615
+ [Vector[bottom, left], Vector[bottom, right]],
616
+ [Vector[bottom, right], Vector[top, left]]
617
+ ]
618
+ end
619
+
620
+ private
621
+
622
+ def distance_segment_segment(l1p1, l1p2, l2p1, l2p2)
623
+ # See http://geomalgorithms.com/a07-_distance.html
624
+ # rubocop:disable Naming/VariableName
625
+ u = l1p2 - l1p1
626
+ v = l2p2 - l2p1
627
+ w = l1p1 - l2p1
628
+
629
+ a = u.dot u
630
+ b = u.dot v
631
+ c = v.dot v
632
+
633
+ d = u.dot w
634
+ e = v.dot w
635
+ cap_d = (a * c) - (b * b)
636
+ sD = tD = cap_d
637
+
638
+ # compute the line parameters of the two closest points
639
+ if cap_d < Float::EPSILON # the lines are almost parallel
640
+ sN = 0.0 # force using point P0 on segment S1
641
+ sD = 1.0 # to prevent possible division by 0.0 later
642
+ tN = e
643
+ tD = c
644
+ else # get the closest points on the infinite lines
645
+ sN = (b * e) - (c * d)
646
+ tN = (a * e) - (b * d)
647
+ if sN.negative? # sc < 0 => the s=0 edge is visible
648
+ sN = 0
649
+ tN = e
650
+ tD = c
651
+ elsif sN > sD # sc > 1 => the s=1 edge is visible
652
+ sN = sD
653
+ tN = e + b
654
+ tD = c
655
+ end
656
+ end
657
+
658
+ if tN.negative? # tc < 0 => the t=0 edge is visible
659
+ tN = 0
660
+ # recompute sc for this edge
661
+ if (-d).negative?
662
+ sN = 0.0
663
+ elsif -d > a
664
+ sN = sD
665
+ else
666
+ sN = -d
667
+ sD = a
668
+ end
669
+ elsif tN > tD # tc > 1 => the t=1 edge is visible
670
+ tN = tD
671
+ # recompute sc for this edge
672
+ if (-d + b).negative?
673
+ sN = 0.0
674
+ elsif (-d + b) > a
675
+ sN = sD
676
+ else
677
+ sN = (-d + b)
678
+ sD = a
679
+ end
680
+ end
681
+
682
+ # finally do the division to get sc and tc
683
+ sc = sN.abs < Float::EPSILON ? 0.0 : sN / sD
684
+ tc = tN.abs < Float::EPSILON ? 0.0 : tN / tD
685
+
686
+ # difference of the two closest points
687
+ dP = w + (u * sc) - (v * tc)
688
+
689
+ Math.sqrt(dP.dot(dP))
690
+ # rubocop:enable Naming/VariableName
691
+ end
692
+ end
693
+
694
+ private_constant :Rectangle
494
695
  end
495
696
  end
496
697
  end
@@ -76,6 +76,8 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
76
76
  set(!checked?)
77
77
  elsif tag_name == 'label'
78
78
  click_label
79
+ elsif tag_name == 'details'
80
+ toggle_details
79
81
  end
80
82
  end
81
83
 
@@ -236,6 +238,14 @@ private
236
238
  labelled_control.set(!labelled_control.checked?) if checkbox_or_radio?(labelled_control)
237
239
  end
238
240
 
241
+ def toggle_details
242
+ if native.has_attribute?('open')
243
+ native.remove_attribute('open')
244
+ else
245
+ native.set_attribute('open', 'open')
246
+ end
247
+ end
248
+
239
249
  def link?
240
250
  tag_name == 'a' && !self[:href].nil?
241
251
  end
@@ -150,15 +150,15 @@ module Capybara
150
150
  @rest ||= @elements - full_results
151
151
  end
152
152
 
153
- def lazy_select_elements(&block)
153
+ if (RUBY_PLATFORM == 'java') && (Gem::Version.new(JRUBY_VERSION) < Gem::Version.new('9.2.8.0'))
154
154
  # JRuby < 9.2.8.0 has an issue with lazy enumerators which
155
155
  # causes a concurrency issue with network requests here
156
156
  # https://github.com/jruby/jruby/issues/4212
157
- if (RUBY_PLATFORM == 'java') && (Gem::Version.new(JRUBY_VERSION) < Gem::Version.new('9.2.8.0'))
158
- # :nocov:
157
+ def lazy_select_elements(&block)
159
158
  @elements.select(&block).to_enum # non-lazy evaluation
160
- # :nocov:
161
- else
159
+ end
160
+ else
161
+ def lazy_select_elements(&block)
162
162
  @elements.lazy.select(&block)
163
163
  end
164
164
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'capybara/rspec/matchers/compound'
4
4
  require 'capybara/rspec/matchers/count_sugar'
5
+ require 'capybara/rspec/matchers/spatial_sugar'
5
6
 
6
7
  module Capybara
7
8
  module RSpecMatchers
@@ -68,6 +69,7 @@ module Capybara
68
69
 
69
70
  class CountableWrappedElementMatcher < WrappedElementMatcher
70
71
  include ::Capybara::RSpecMatchers::CountSugar
72
+ include ::Capybara::RSpecMatchers::SpatialSugar
71
73
  end
72
74
 
73
75
  class NegatedMatcher
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module RSpecMatchers
5
+ module SpatialSugar
6
+ def above(el)
7
+ options[:above] = el
8
+ self
9
+ end
10
+
11
+ def below(el)
12
+ options[:below] = el
13
+ self
14
+ end
15
+
16
+ def left_of(el)
17
+ options[:left_of] = el
18
+ self
19
+ end
20
+
21
+ def right_of(el)
22
+ options[:right_of] = el
23
+ self
24
+ end
25
+
26
+ def near(el)
27
+ options[:near] = el
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ def options
34
+ (@args.last.is_a?(Hash) ? @args : @args.push({})).last
35
+ end
36
+ end
37
+ end
38
+ end
@@ -9,6 +9,11 @@ require 'capybara/selector/definition'
9
9
  # * :id (String, Regexp, XPath::Expression) - Matches the id attribute
10
10
  # * :class (String, Array<String>, Regexp, XPath::Expression) - Matches the class(es) provided
11
11
  # * :style (String, Regexp, Hash<String, String>) - Match on elements style
12
+ # * :above (Element) - Match elements above the passed element on the page
13
+ # * :below (Element) - Match elements below the passed element on the page
14
+ # * :left_of (Element) - Match elements left of the passed element on the page
15
+ # * :right_of (Element) - Match elements right of the passed element on the page
16
+ # * :near (Element) - Match elements near (within 50px) the passed element on the page
12
17
  #
13
18
  # ### Built-in Selectors
14
19
  #
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Capybara::Selenium::Node
4
+ module FileInputClickEmulation
5
+ def click(keys = [], **options)
6
+ super
7
+ rescue Selenium::WebDriver::Error::InvalidArgumentError
8
+ return emulate_click if attaching_file? && visible_file_field?
9
+
10
+ raise
11
+ end
12
+
13
+ private
14
+
15
+ def visible_file_field?
16
+ (attrs(:tagName, :type).map { |val| val&.downcase } == %w[input file]) && visible?
17
+ end
18
+
19
+ def attaching_file?
20
+ caller_locations.any? { |cl| cl.base_label == 'attach_file' }
21
+ end
22
+
23
+ def emulate_click
24
+ driver.execute_script(<<~JS, self)
25
+ arguments[0].dispatchEvent(
26
+ new MouseEvent('click', {
27
+ view: window,
28
+ bubbles: true,
29
+ cancelable: true
30
+ }));
31
+ JS
32
+ end
33
+ end
34
+ end
@@ -3,34 +3,35 @@
3
3
  module Capybara
4
4
  module Selenium
5
5
  module Find
6
- def find_xpath(selector, uses_visibility: false, styles: nil, **_options)
7
- find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles)
6
+ def find_xpath(selector, uses_visibility: false, styles: nil, position: false, **_options)
7
+ find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles, position: position)
8
8
  end
9
9
 
10
- def find_css(selector, uses_visibility: false, texts: [], styles: nil, **_options)
11
- find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles)
10
+ def find_css(selector, uses_visibility: false, texts: [], styles: nil, position: false, **_options)
11
+ find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles, position: position)
12
12
  end
13
13
 
14
14
  private
15
15
 
16
- def find_by(format, selector, uses_visibility:, texts:, styles:)
16
+ def find_by(format, selector, uses_visibility:, texts:, styles:, position:)
17
17
  els = find_context.find_elements(format, selector)
18
18
  hints = []
19
19
 
20
20
  if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
21
21
  els = filter_by_text(els, texts) unless texts.empty?
22
- hints = gather_hints(els, uses_visibility: uses_visibility, styles: styles)
22
+ hints = gather_hints(els, uses_visibility: uses_visibility, styles: styles, position: position)
23
23
  end
24
24
  els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
25
25
  end
26
26
 
27
- def gather_hints(elements, uses_visibility:, styles:)
28
- hints_js, functions = build_hints_js(uses_visibility, styles)
27
+ def gather_hints(elements, uses_visibility:, styles:, position:)
28
+ hints_js, functions = build_hints_js(uses_visibility, styles, position)
29
29
  return [] unless functions.any?
30
30
 
31
31
  es_context.execute_script(hints_js, elements).map! do |results|
32
32
  hint = {}
33
33
  hint[:style] = results.pop if functions.include?(:style_func)
34
+ hint[:position] = results.pop if functions.include?(:position_func)
34
35
  hint[:visible] = results.pop if functions.include?(:vis_func)
35
36
  hint
36
37
  end
@@ -50,7 +51,7 @@ module Capybara
50
51
  JS
51
52
  end
52
53
 
53
- def build_hints_js(uses_visibility, styles)
54
+ def build_hints_js(uses_visibility, styles, position)
54
55
  functions = []
55
56
  hints_js = +''
56
57
 
@@ -61,6 +62,15 @@ module Capybara
61
62
  functions << :vis_func
62
63
  end
63
64
 
65
+ if position
66
+ hints_js << <<~POSITION_JS
67
+ var position_func = function(el){
68
+ return el.getBoundingClientRect();
69
+ };
70
+ POSITION_JS
71
+ functions << :position_func
72
+ end
73
+
64
74
  if styles.is_a? Hash
65
75
  hints_js << <<~STYLE_JS
66
76
  var style_func = function(el){
@@ -179,6 +179,10 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
179
179
  driver.frame_obscured_at?(x: res['x'], y: res['y'])
180
180
  end
181
181
 
182
+ def rect
183
+ native.rect
184
+ end
185
+
182
186
  protected
183
187
 
184
188
  def scroll_if_needed
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/file_input_click_emulation'
4
5
 
5
6
  class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
6
7
  include Html5Drag
8
+ include FileInputClickEmulation
7
9
 
8
10
  def set_text(value, clear: nil, **_unused)
9
11
  super.tap do
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/file_input_click_emulation'
4
5
 
5
6
  class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
6
7
  include Html5Drag
8
+ include FileInputClickEmulation
7
9
 
8
10
  def click(keys = [], **options)
9
11
  super
@@ -428,6 +428,38 @@ Capybara::SpecHelper.spec '#find' do
428
428
  expect(@session.find(:css, 'input', &:disabled?)[:name]).to eq('disabled_text')
429
429
  end
430
430
 
431
+ context 'with spatial filters', requires: [:spatial] do
432
+ before do
433
+ @session.visit('/spatial')
434
+ @center = @session.find(:css, 'div.center')
435
+ end
436
+
437
+ it 'should find an element above another element' do
438
+ expect(@session.find(:css, 'div:not(.corner)', above: @center).text).to eq('2')
439
+ end
440
+
441
+ it 'should find an element below another element' do
442
+ expect(@session.find(:css, 'div:not(.corner):not(.footer)', below: @center).text).to eq('8')
443
+ end
444
+
445
+ it 'should find an element left of another element' do
446
+ expect(@session.find(:css, 'div:not(.corner)', left_of: @center).text).to eq('4')
447
+ end
448
+
449
+ it 'should find an element right of another element' do
450
+ expect(@session.find(:css, 'div:not(.corner)', right_of: @center).text).to eq('6')
451
+ end
452
+
453
+ it 'should combine spatial filters' do
454
+ expect(@session.find(:css, 'div', left_of: @center, above: @center).text).to eq('1')
455
+ expect(@session.find(:css, 'div', right_of: @center, below: @center).text).to eq('9')
456
+ end
457
+
458
+ it 'should find an element "near" another element' do
459
+ expect(@session.find(:css, 'div.distance', near: @center).text).to eq('2')
460
+ end
461
+ end
462
+
431
463
  context 'within a scope' do
432
464
  before do
433
465
  @session.visit('/with_scope')
@@ -231,6 +231,24 @@ Capybara::SpecHelper.spec '#has_css?' do
231
231
  end
232
232
  end
233
233
 
234
+ context 'with spatial requirements', requires: [:spatial] do
235
+ before do
236
+ @session.visit('/spatial')
237
+ @center = @session.find(:css, '.center')
238
+ end
239
+
240
+ it 'accepts spatial options' do
241
+ expect(@session).to have_css('div', above: @center).thrice
242
+ expect(@session).to have_css('div', above: @center, right_of: @center).once
243
+ end
244
+
245
+ it 'supports spatial sugar' do
246
+ expect(@session).to have_css('div').left_of(@center).thrice
247
+ expect(@session).to have_css('div').below(@center).right_of(@center).once
248
+ expect(@session).to have_css('div').near(@center).exactly(8).times
249
+ end
250
+ end
251
+
234
252
  it 'should allow escapes in the CSS selector' do
235
253
  expect(@session).to have_css('p[data-random="abc\\\\def"]')
236
254
  expect(@session).to have_css("p[data-random='#{Capybara::Selector::CSS.escape('abc\def')}']")
@@ -254,20 +254,30 @@ Capybara::SpecHelper.spec 'node' do
254
254
  expect(@session.find('//div[@id="hidden"]').visible?).to be false
255
255
  end
256
256
 
257
- it 'details > summary elements and descendants should be visible' do
258
- expect(@session.find(:css, 'details summary')).to be_visible
259
- expect(@session.find(:css, 'details summary h6')).to be_visible
257
+ it 'closed details > summary elements and descendants should be visible' do
258
+ expect(@session.find(:css, '#closed_details summary')).to be_visible
259
+ expect(@session.find(:css, '#closed_details summary h6')).to be_visible
260
260
  end
261
261
 
262
- it 'details non-summary descendants should be non-visible' do
263
- @session.first(:css, 'details li').visible?
264
- descendants = @session.all(:css, 'details > *:not(summary), details > *:not(summary) *', minimum: 2)
262
+ it 'details non-summary descendants should be non-visible when closed' do
263
+ descendants = @session.all(:css, '#closed_details > *:not(summary), #closed_details > *:not(summary) *', minimum: 2)
265
264
  expect(descendants).not_to include(be_visible)
266
265
  end
267
266
 
268
- it 'sees open details as visible', requires: [:js] do
269
- @session.find(:css, 'details').click
270
- expect(@session.all(:css, 'details *')).to all(be_visible)
267
+ it 'deatils descendants should be visible when open' do
268
+ descendants = @session.all(:css, '#open_details *')
269
+ expect(descendants).to all(be_visible)
270
+ end
271
+
272
+ it 'works when details is toggled open and closed' do
273
+ @session.find(:css, '#closed_details').click
274
+ expect(@session).to have_css('#closed_details *', visible: true, count: 5)
275
+ .and(have_no_css('#closed_details *', visible: :hidden))
276
+
277
+ @session.find(:css, '#closed_details').click
278
+ descendants_css = '#closed_details > *:not(summary), #closed_details > *:not(summary) *'
279
+ expect(@session).to have_no_css(descendants_css, visible: true)
280
+ .and(have_css(descendants_css, visible: :hidden, count: 3))
271
281
  end
272
282
  end
273
283
 
@@ -0,0 +1,31 @@
1
+ <html>
2
+ <head>
3
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
4
+ <title>spatial</title>
5
+ <style>
6
+ #spatial {
7
+ display: grid;
8
+ grid-template-columns: repeat(3, 1fr);
9
+ grid-gap: 10px;
10
+ grid-auto-rows: minmax(100px, auto);
11
+ }
12
+ .footer {
13
+ grid-column: 1 / 4;
14
+ }
15
+ </style>
16
+ </head>
17
+
18
+ <body id="spatial">
19
+ <div class="corner">1</div>
20
+ <div class="distance">2</div>
21
+ <div class="corner">3</div>
22
+ <div>4</div>
23
+ <div class="center">5</div>
24
+ <div>6</div>
25
+ <div class="corner">7</div>
26
+ <div>8</div>
27
+ <div class="corner">9</div>
28
+ <div class="footer distance">10</div>
29
+ </body>
30
+ </html>
31
+
@@ -181,7 +181,7 @@ banana
181
181
  &#x20;&#x1680;&#x2000;&#x2001;&#x2002; &#x2003;&#x2004;&nbsp;&#x2005; &#x2006;&#x2007;&#x2008;&#x2009;&#x200A;&#x202F;&#x205F;&#x3000;
182
182
  </div>
183
183
 
184
- <details>
184
+ <details id="closed_details">
185
185
  <summary>
186
186
  <h6>Something</h6>
187
187
  </summary>
@@ -191,6 +191,15 @@ banana
191
191
  </ul>
192
192
  </details>
193
193
 
194
+ <details id="open_details" open>
195
+ <summary>
196
+ <h6>Summary</h6>
197
+ </summary>
198
+ <div>
199
+ Contents
200
+ </div>
201
+ </details>
202
+
194
203
  <template id="template">
195
204
  <input />
196
205
  </template>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Capybara
4
- VERSION = '3.28.0'
4
+ VERSION = '3.29.0'
5
5
  end
@@ -8,7 +8,7 @@ class TestClass
8
8
  end
9
9
 
10
10
  Capybara::SpecHelper.run_specs TestClass.new, 'DSL', capybara_skip: %i[
11
- js modals screenshot frames windows send_keys server hover about_scheme psc download css driver scroll
11
+ js modals screenshot frames windows send_keys server hover about_scheme psc download css driver scroll spatial
12
12
  ] do |example|
13
13
  case example.metadata[:full_description]
14
14
  when /has_css\? should support case insensitive :class and :id options/
@@ -25,6 +25,7 @@ skipped_tests = %i[
25
25
  download
26
26
  css
27
27
  scroll
28
+ spatial
28
29
  ]
29
30
  Capybara::SpecHelper.run_specs TestSessions::RackTest, 'RackTest', capybara_skip: skipped_tests do |example|
30
31
  case example.metadata[:full_description]
@@ -166,7 +166,7 @@ RSpec.describe Capybara::Result do
166
166
  context 'lazy select' do
167
167
  it 'is compatible' do
168
168
  # This test will let us know when JRuby fixes lazy select so we can re-enable it in Result
169
- pending 'JRuby has an issue with lazy enumberator evaluation' if RUBY_PLATFORM == 'java'
169
+ pending 'JRuby < 9.2.8.0 has an issue with lazy enumberator evaluation' if jruby_lazy_enumerator_workaround?
170
170
  eval_count = 0
171
171
  enum = %w[Text1 Text2 Text3].lazy.select do
172
172
  eval_count += 1
@@ -13,9 +13,13 @@ Selenium::WebDriver::Chrome.path = '/usr/bin/google-chrome-beta' if ENV['CI'] &&
13
13
  browser_options = ::Selenium::WebDriver::Chrome::Options.new
14
14
  browser_options.headless! if ENV['HEADLESS']
15
15
  browser_options.add_option(:w3c, ENV['W3C'] != 'false')
16
+ # Chromedriver 77 requires setting this for headless mode on linux
17
+ # browser_options.add_preference('download.default_directory', Capybara.save_path)
18
+ browser_options.add_preference(:download, default_directory: Capybara.save_path)
16
19
 
17
20
  Capybara.register_driver :selenium_chrome do |app|
18
21
  Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options, timeout: 30).tap do |driver|
22
+ # Set download dir for Chrome < 77
19
23
  driver.browser.download_path = Capybara.save_path
20
24
  end
21
25
  end
@@ -62,8 +62,6 @@ Capybara::SpecHelper.run_specs TestSessions::SeleniumFirefox, 'selenium', capyba
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'
64
64
  pending "Geckodriver doesn't provide a way to remove cookies outside the current domain"
65
- when 'Capybara::Session selenium #attach_file with a block can upload by clicking the file input'
66
- pending "Geckodriver doesn't allow clicking on file inputs"
67
65
  when /drag_to.*HTML5/
68
66
  pending "Firefox < 62 doesn't support a DataTransfer constuctor" if firefox_lt?(62.0, @session)
69
67
  end
@@ -65,8 +65,6 @@ Capybara::SpecHelper.run_specs TestSessions::RemoteFirefox, FIREFOX_REMOTE_DRIVE
65
65
  when /#accept_confirm should work with nested modals$/
66
66
  # skip because this is timing based and hence flaky when set to pending
67
67
  skip 'Broken in FF 63 - https://bugzilla.mozilla.org/show_bug.cgi?id=1487358' if firefox_gte?(63, @session)
68
- when 'Capybara::Session selenium_firefox_remote #attach_file with a block can upload by clicking the file input'
69
- pending "Geckodriver doesn't allow clicking on file inputs"
70
68
  end
71
69
  end
72
70
 
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.28.0
4
+ version: 3.29.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-08-03 00:00:00.000000000 Z
13
+ date: 2019-09-02 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: addressable
@@ -470,6 +470,7 @@ files:
470
470
  - lib/capybara/rspec/matchers/have_title.rb
471
471
  - lib/capybara/rspec/matchers/match_selector.rb
472
472
  - lib/capybara/rspec/matchers/match_style.rb
473
+ - lib/capybara/rspec/matchers/spatial_sugar.rb
473
474
  - lib/capybara/selector.rb
474
475
  - lib/capybara/selector/builders/css_builder.rb
475
476
  - lib/capybara/selector/builders/xpath_builder.rb
@@ -515,6 +516,7 @@ files:
515
516
  - lib/capybara/selenium/driver_specializations/firefox_driver.rb
516
517
  - lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb
517
518
  - lib/capybara/selenium/driver_specializations/safari_driver.rb
519
+ - lib/capybara/selenium/extensions/file_input_click_emulation.rb
518
520
  - lib/capybara/selenium/extensions/find.rb
519
521
  - lib/capybara/selenium/extensions/html5_drag.rb
520
522
  - lib/capybara/selenium/extensions/modifier_keys_stack.rb
@@ -657,6 +659,7 @@ files:
657
659
  - lib/capybara/spec/views/postback.erb
658
660
  - lib/capybara/spec/views/react.erb
659
661
  - lib/capybara/spec/views/scroll.erb
662
+ - lib/capybara/spec/views/spatial.erb
660
663
  - lib/capybara/spec/views/tables.erb
661
664
  - lib/capybara/spec/views/with_animation.erb
662
665
  - lib/capybara/spec/views/with_base_tag.erb
@@ -741,7 +744,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
741
744
  - !ruby/object:Gem::Version
742
745
  version: '0'
743
746
  requirements: []
744
- rubygems_version: 3.0.3
747
+ rubygems_version: 3.0.6
745
748
  signing_key:
746
749
  specification_version: 4
747
750
  summary: Capybara aims to simplify the process of integration testing Rack applications,