capybara 3.28.0 → 3.29.0

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