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 +4 -4
- data/History.md +18 -0
- data/README.md +2 -2
- data/lib/capybara/driver/node.rb +4 -0
- data/lib/capybara/node/actions.rb +1 -0
- data/lib/capybara/node/element.rb +4 -0
- data/lib/capybara/node/simple.rb +1 -1
- data/lib/capybara/queries/selector_query.rb +202 -1
- data/lib/capybara/rack_test/node.rb +10 -0
- data/lib/capybara/result.rb +5 -5
- data/lib/capybara/rspec/matchers/base.rb +2 -0
- data/lib/capybara/rspec/matchers/spatial_sugar.rb +38 -0
- data/lib/capybara/selector.rb +5 -0
- data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
- data/lib/capybara/selenium/extensions/find.rb +19 -9
- data/lib/capybara/selenium/node.rb +4 -0
- data/lib/capybara/selenium/nodes/chrome_node.rb +2 -0
- data/lib/capybara/selenium/nodes/firefox_node.rb +2 -0
- data/lib/capybara/spec/session/find_spec.rb +32 -0
- data/lib/capybara/spec/session/has_css_spec.rb +18 -0
- data/lib/capybara/spec/session/node_spec.rb +19 -9
- data/lib/capybara/spec/views/spatial.erb +31 -0
- data/lib/capybara/spec/views/with_html.erb +10 -1
- data/lib/capybara/version.rb +1 -1
- data/spec/dsl_spec.rb +1 -1
- data/spec/rack_test_spec.rb +1 -0
- data/spec/result_spec.rb +1 -1
- data/spec/selenium_spec_chrome.rb +4 -0
- data/spec/selenium_spec_firefox.rb +0 -2
- data/spec/selenium_spec_firefox_remote.rb +0 -2
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f11a887ca6aed51d064d9c8eb797e0db4605a3d013082a11f4f4b1013b27bef
|
4
|
+
data.tar.gz: 714e834fdb1c2f0e692f547a1d032113dd888d6905eddecc60f05503720bcd8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
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
|
data/lib/capybara/driver/node.rb
CHANGED
@@ -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
|
data/lib/capybara/node/simple.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/capybara/result.rb
CHANGED
@@ -150,15 +150,15 @@ module Capybara
|
|
150
150
|
@rest ||= @elements - full_results
|
151
151
|
end
|
152
152
|
|
153
|
-
|
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
|
-
|
158
|
-
# :nocov:
|
157
|
+
def lazy_select_elements(&block)
|
159
158
|
@elements.select(&block).to_enum # non-lazy evaluation
|
160
|
-
|
161
|
-
|
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
|
data/lib/capybara/selector.rb
CHANGED
@@ -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){
|
@@ -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, '
|
259
|
-
expect(@session.find(:css, '
|
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.
|
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 '
|
269
|
-
@session.
|
270
|
-
expect(
|
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
|
                   
|
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>
|
data/lib/capybara/version.rb
CHANGED
data/spec/dsl_spec.rb
CHANGED
@@ -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/
|
data/spec/rack_test_spec.rb
CHANGED
data/spec/result_spec.rb
CHANGED
@@ -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
|
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.
|
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-
|
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.
|
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,
|