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 +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
|
[](https://gitter.im/jnicklas/capybara?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
8
8
|
[](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,
|