capybara 3.11.1 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +18 -1
  3. data/README.md +4 -4
  4. data/lib/capybara.rb +7 -0
  5. data/lib/capybara/node/element.rb +7 -1
  6. data/lib/capybara/node/matchers.rb +0 -18
  7. data/lib/capybara/queries/base_query.rb +1 -1
  8. data/lib/capybara/queries/selector_query.rb +24 -39
  9. data/lib/capybara/result.rb +4 -4
  10. data/lib/capybara/selector.rb +7 -15
  11. data/lib/capybara/selector/builders/css_builder.rb +59 -27
  12. data/lib/capybara/selector/builders/xpath_builder.rb +50 -35
  13. data/lib/capybara/selector/css.rb +7 -7
  14. data/lib/capybara/selector/filter.rb +1 -0
  15. data/lib/capybara/selector/filter_set.rb +17 -15
  16. data/lib/capybara/selector/filters/locator_filter.rb +19 -0
  17. data/lib/capybara/selector/regexp_disassembler.rb +104 -61
  18. data/lib/capybara/selector/selector.rb +14 -5
  19. data/lib/capybara/selenium/driver.rb +14 -9
  20. data/lib/capybara/selenium/driver_specializations/{marionette_driver.rb → firefox_driver.rb} +3 -3
  21. data/lib/capybara/selenium/nodes/{marionette_node.rb → firefox_node.rb} +1 -1
  22. data/lib/capybara/spec/session/all_spec.rb +8 -1
  23. data/lib/capybara/spec/session/assert_selector_spec.rb +0 -10
  24. data/lib/capybara/spec/session/click_button_spec.rb +5 -3
  25. data/lib/capybara/spec/session/click_link_spec.rb +5 -5
  26. data/lib/capybara/spec/session/find_spec.rb +1 -1
  27. data/lib/capybara/spec/session/first_spec.rb +1 -1
  28. data/lib/capybara/spec/session/has_css_spec.rb +7 -0
  29. data/lib/capybara/spec/session/has_xpath_spec.rb +17 -0
  30. data/lib/capybara/spec/session/node_spec.rb +10 -3
  31. data/lib/capybara/spec/session/window/window_spec.rb +2 -2
  32. data/lib/capybara/spec/spec_helper.rb +1 -2
  33. data/lib/capybara/spec/views/obscured.erb +3 -0
  34. data/lib/capybara/version.rb +1 -1
  35. data/spec/css_builder_spec.rb +99 -0
  36. data/spec/result_spec.rb +6 -0
  37. data/spec/selector_spec.rb +26 -1
  38. data/spec/selenium_spec_chrome.rb +18 -16
  39. data/spec/selenium_spec_chrome_remote.rb +0 -2
  40. data/spec/{selenium_spec_marionette.rb → selenium_spec_firefox.rb} +31 -25
  41. data/spec/selenium_spec_firefox_remote.rb +4 -6
  42. data/spec/shared_selenium_session.rb +2 -2
  43. data/spec/spec_helper.rb +5 -5
  44. data/spec/xpath_builder_spec.rb +91 -0
  45. metadata +10 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54ee49a77f4b1cec2ed59869334766af110a64a50dae8cdc02a95b5ba750fb9b
4
- data.tar.gz: db2319b75d0626c01babc298eccd8ce76d3da65a8a0a994bb25de5c742e1721f
3
+ metadata.gz: 8b3b4d3a612269d9ee165e7ed94e1e62f91c6fd85bcc5c5ae614c25d0c22bd32
4
+ data.tar.gz: a2bdfa0c2398789bd15d791e3b2d0d03d9c3371033e4f4e4d21e302db845c93f
5
5
  SHA512:
6
- metadata.gz: d9c1965b9ba0d38813d3060aaefe797ef8d6610416c954bf4cebdae70de5ca23bc4b07303033ecbf2f5e62bc681319c6b20dc0567f49e89ccc02df38dd10d44d
7
- data.tar.gz: bf971abb444f58438091422e656108d29bb7efa0d2f2dfb1728a818626cea38e7fa700b3701904ca10344a4f272e14fd99482c23b62b9cfe3b6139d868fa5b30
6
+ metadata.gz: f4815632f5bc14c932e9b5181d68584ac3088f11241e8ee3c19f3ee0a27190f8ad1544b88a532f901eaf79f9ae3c2f721dfca1d2d6d20d098a40438042b9f64a
7
+ data.tar.gz: eb2765462cdbdda76a4ca5d100b93688029d1ca727c00579c5e9e5d77f26fc4a0fe64bbe3616bb3f89b34e730c337e242b319b2ea606b5287a780f8219ee5b08
data/History.md CHANGED
@@ -1,7 +1,24 @@
1
+ # Version 3.12.0
2
+ Release date: 2018-11-28
3
+
4
+ ### Added
5
+
6
+ * Support Ruby 2.6 endless range in Result#[] and query `:between` option
7
+ * Pre-registered headless firefox driver :selenium_headless [Andrew Havens]
8
+ * Selenium driver now defaults to clearing `sessionStorage` and `localStorage`. To disable pass `clear_local_storage: false` and/or `clear_session_storage: false` when creating Capybara::Selenium::Driver instance in your driver registration
9
+
10
+ ### Fixed
11
+
12
+ * Raise error if only :x or :y are passed as an offset to click methods
13
+
14
+ ### Removed
15
+
16
+ * Support for RSpec < 3.5
17
+
1
18
  # Version 3.11.1
2
19
  Release date: 2018-11-16
3
20
 
4
- ###Fixed
21
+ ### Fixed
5
22
 
6
23
  * Fixed :link_or_button XPath generation when it has had an expression filter added
7
24
 
data/README.md CHANGED
@@ -6,8 +6,7 @@
6
6
  [![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)
7
7
  [![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)
8
8
 
9
- **Note** You are viewing the README for the 3.11.0 version of Capybara.
10
-
9
+ **Note** You are viewing the README for the 3.12.x version of Capybara.
11
10
 
12
11
  Capybara helps you test web applications by simulating how a real user would
13
12
  interact with your app. It is agnostic about the driver running your tests and
@@ -139,7 +138,7 @@ There are also explicit tags for each registered driver set up for you (`@seleni
139
138
 
140
139
  ## <a name="using-capybara-with-rspec"></a>Using Capybara with RSpec
141
140
 
142
- Load RSpec 3.x support by adding the following line (typically to your
141
+ Load RSpec 3.5+ support by adding the following line (typically to your
143
142
  `spec_helper.rb` file):
144
143
 
145
144
  ```ruby
@@ -394,6 +393,7 @@ and add it to your Gemfile if you're using bundler.
394
393
  Capybara pre-registers a number of named drives that use Selenium - they are:
395
394
 
396
395
  * :selenium => Selenium driving Firefox
396
+ * :selenium_headless => Selenium driving Firefox in a headless configuration
397
397
  * :selenium_chrome => Selenium driving Chrome
398
398
  * :selenium_chrome_headless => Selenium driving Chrome in a headless configuration
399
399
 
@@ -404,7 +404,7 @@ to the browsers. See the section on adding and configuring drivers.
404
404
 
405
405
  **Note**: drivers which run the server in a different thread may not share the
406
406
  same transaction as your tests, causing data not to be shared between your test
407
- and test server, see "Transactions and database setup" below.
407
+ and test server, see [Transactions and database setup](#transactions-and-database-setup) below.
408
408
 
409
409
  ### <a name="capybara-webkit"></a>Capybara-webkit
410
410
 
@@ -532,6 +532,13 @@ Capybara.register_driver :selenium do |app|
532
532
  Capybara::Selenium::Driver.new(app)
533
533
  end
534
534
 
535
+ Capybara.register_driver :selenium_headless do |app|
536
+ Capybara::Selenium::Driver.load_selenium
537
+ browser_options = ::Selenium::WebDriver::Firefox::Options.new
538
+ browser_options.args << '-headless'
539
+ Capybara::Selenium::Driver.new(app, browser: :firefox, options: browser_options)
540
+ end
541
+
535
542
  Capybara.register_driver :selenium_chrome do |app|
536
543
  Capybara::Selenium::Driver.new(app, browser: :chrome)
537
544
  end
@@ -153,7 +153,9 @@ module Capybara
153
153
  # @option offset [Integer] y Y coordinate to offset the click location from the top left corner of the element
154
154
  # @return [Capybara::Node::Element] The element
155
155
  def click(*keys, wait: nil, **offset)
156
- synchronize(wait) { base.click(keys, offset) }
156
+ raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ offset[:x] ^ offset[:y]
157
+
158
+ synchronize(wait) { base.click(Array(keys), offset) }
157
159
  self
158
160
  end
159
161
 
@@ -164,6 +166,8 @@ module Capybara
164
166
  # @macro click_modifiers
165
167
  # @return [Capybara::Node::Element] The element
166
168
  def right_click(*keys, wait: nil, **offset)
169
+ raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ offset[:x] ^ offset[:y]
170
+
167
171
  synchronize(wait) { base.right_click(keys, offset) }
168
172
  self
169
173
  end
@@ -175,6 +179,8 @@ module Capybara
175
179
  # @macro click_modifiers
176
180
  # @return [Capybara::Node::Element] The element
177
181
  def double_click(*keys, wait: nil, **offset)
182
+ raise ArgumentError, 'You must specify both x: and y: for a click offset' if nil ^ offset[:x] ^ offset[:y]
183
+
178
184
  synchronize(wait) { base.double_click(keys, offset) }
179
185
  self
180
186
  end
@@ -546,24 +546,6 @@ module Capybara
546
546
  end
547
547
  end
548
548
 
549
- # Deprecated
550
- # TODO: remove
551
- def refute_selector(*args, &optional_filter_block)
552
- warn '`refute_selector` was never meant to be in this scope unless ' \
553
- 'using minitest. Either replace with `assert_no_selector` ' \
554
- "or require 'capybara/minitest'."
555
- assert_no_selector(*args, &optional_filter_block)
556
- end
557
-
558
- # Deprecated
559
- # TODO: remove
560
- def refute_matches_selector(*args, &optional_filter_block)
561
- warn '`refute_matches_selector` was never meant to be in this scope unless ' \
562
- 'using minitest. Either replace with `assert_not_matches_selector` ' \
563
- "or require 'capybara/minitest'."
564
- assert_not_matches_selector(*args, &optional_filter_block)
565
- end
566
-
567
549
  ##
568
550
  #
569
551
  # Checks if the current node matches given selector
@@ -79,7 +79,7 @@ module Capybara
79
79
  if count
80
80
  message << " #{occurrences count}"
81
81
  elsif between
82
- message << " between #{between.first} and #{between.last} times"
82
+ message << " between #{between.first} and #{between.end ? between.last : 'infinite'} times"
83
83
  elsif maximum
84
84
  message << " at most #{occurrences maximum}"
85
85
  elsif minimum
@@ -62,13 +62,11 @@ module Capybara
62
62
 
63
63
  def matches_filters?(node, node_filter_errors = [])
64
64
  return true if (@resolved_node&.== node) && options[:allow_self]
65
- return false unless matches_locator_filter?(node)
66
65
 
67
- applied_filters << :system
68
- return false unless matches_system_filters?(node)
69
-
70
- applied_filters << :node
71
- matches_node_filters?(node, node_filter_errors) && matches_filter_block?(node)
66
+ matches_locator_filter?(node) &&
67
+ matches_system_filters?(node) &&
68
+ matches_node_filters?(node, node_filter_errors) &&
69
+ matches_filter_block?(node)
72
70
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
73
71
  false
74
72
  end
@@ -93,11 +91,11 @@ module Capybara
93
91
  exact = exact? if exact.nil?
94
92
  expr = apply_expression_filters(@expression)
95
93
  expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
96
- filtered_xpath(expr)
94
+ filtered_expression(expr)
97
95
  end
98
96
 
99
97
  def css
100
- filtered_css(apply_expression_filters(@expression))
98
+ filtered_expression(apply_expression_filters(@expression))
101
99
  end
102
100
 
103
101
  # @api private
@@ -136,12 +134,10 @@ module Capybara
136
134
  end
137
135
 
138
136
  def find_selector(locator)
139
- selector = if locator.is_a?(Symbol)
140
- Selector.all.fetch(locator) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
141
- else
142
- Selector.all.values.find { |sel| sel.match?(locator) }
143
- end
144
- selector || Selector.all[session_options.default_selector]
137
+ case locator
138
+ when Symbol then Selector[locator]
139
+ else Selector.for(locator)
140
+ end || Selector[session_options.default_selector]
145
141
  end
146
142
 
147
143
  def find_nodes_by_selector_format(node, exact)
@@ -165,6 +161,8 @@ module Capybara
165
161
  end
166
162
 
167
163
  def matches_node_filters?(node, errors)
164
+ applied_filters << :node
165
+
168
166
  unapplied_options = options.keys - valid_keys
169
167
  @selector.with_filter_errors(errors) do
170
168
  node_filters.all? do |filter_name, filter|
@@ -196,7 +194,7 @@ module Capybara
196
194
  end
197
195
 
198
196
  def filter_set(name)
199
- ::Capybara::Selector::FilterSet.all[name]
197
+ ::Capybara::Selector::FilterSet[name]
200
198
  end
201
199
 
202
200
  def node_filters
@@ -235,18 +233,11 @@ module Capybara
235
233
  raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
236
234
  end
237
235
 
238
- def filtered_xpath(expr)
239
- expr = "(#{expr})[#{conditions_from_id}]" if use_default_id_filter?
240
- expr = "(#{expr})[#{conditions_from_classes}]" if use_default_class_filter?
241
- expr
242
- end
243
-
244
- def filtered_css(expr)
245
- ::Capybara::Selector::CSS.split(expr).map do |sel|
246
- sel += conditions_from_id if use_default_id_filter?
247
- sel += conditions_from_classes if use_default_class_filter?
248
- sel
249
- end.join(', ')
236
+ def filtered_expression(expr)
237
+ conditions = {}
238
+ conditions[:id] = options[:id] if use_default_id_filter?
239
+ conditions[:class] = options[:class] if use_default_class_filter?
240
+ builder(expr).add_attribute_conditions(conditions)
250
241
  end
251
242
 
252
243
  def use_default_id_filter?
@@ -257,14 +248,6 @@ module Capybara
257
248
  options.key?(:class) && !custom_keys.include?(:class)
258
249
  end
259
250
 
260
- def conditions_from_classes
261
- builder.class_conditions(options[:class])
262
- end
263
-
264
- def conditions_from_id
265
- builder.attribute_conditions(id: options[:id])
266
- end
267
-
268
251
  def apply_expression_filters(expression)
269
252
  unapplied_options = options.keys - valid_keys
270
253
  expression_filters.inject(expression) do |expr, (name, ef)|
@@ -307,12 +290,14 @@ module Capybara
307
290
  end
308
291
 
309
292
  def matches_locator_filter?(node)
310
- return true if @selector.locator_filter.nil?
293
+ return true unless @selector.locator_filter
311
294
 
312
- @selector.locator_filter.call(node, @locator)
295
+ @selector.locator_filter.matches?(node, @locator, @selector)
313
296
  end
314
297
 
315
298
  def matches_system_filters?(node)
299
+ applied_filters << :system
300
+
316
301
  matches_id_filter?(node) &&
317
302
  matches_class_filter?(node) &&
318
303
  matches_text_filter?(node) &&
@@ -374,8 +359,8 @@ module Capybara
374
359
  @selector.default_visibility(session_options.ignore_hidden_elements, options)
375
360
  end
376
361
 
377
- def builder
378
- selector.builder
362
+ def builder(expr)
363
+ selector.builder(expr)
379
364
  end
380
365
  end
381
366
  end
@@ -59,7 +59,7 @@ module Capybara
59
59
  nil
60
60
  end
61
61
  when Range
62
- idx.max
62
+ idx.end && idx.max # endless range will have end == nil
63
63
  end
64
64
 
65
65
  if max_idx.nil?
@@ -92,9 +92,9 @@ module Capybara
92
92
  end
93
93
 
94
94
  if between
95
- min, max = between.minmax
96
- size = load_up_to(max + 1)
97
- return between.include?(size) ? 0 : size <=> min
95
+ min, max = between.min, (between.end && between.max)
96
+ size = load_up_to(max ? max + 1 : min)
97
+ return size <=> min unless between.include?(size)
98
98
  end
99
99
 
100
100
  0
@@ -34,7 +34,7 @@ Capybara.add_selector(:css) do
34
34
  end
35
35
 
36
36
  Capybara.add_selector(:id) do
37
- xpath { |id| XPath.descendant[builder.attribute_conditions(id: id)] }
37
+ xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
38
38
  locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true }
39
39
  end
40
40
 
@@ -92,8 +92,7 @@ end
92
92
 
93
93
  Capybara.add_selector(:link) do
94
94
  xpath do |locator, href: true, alt: nil, title: nil, **|
95
- xpath = XPath.descendant(:a)
96
- xpath = xpath[@href_conditions = builder.attribute_conditions(href: href)]
95
+ xpath = builder(XPath.descendant(:a)).add_attribute_conditions(href: href)
97
96
 
98
97
  unless locator.nil?
99
98
  locator = locator.to_s
@@ -119,25 +118,18 @@ Capybara.add_selector(:link) do
119
118
  end
120
119
 
121
120
  expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
122
- expr[builder.attribute_conditions(download: download)]
121
+ builder(expr).add_attribute_conditions(download: download)
123
122
  end
124
123
 
125
124
  describe_expression_filters do |**options|
126
125
  desc = +''
127
126
  if (href = options[:href])
128
- if !href.is_a?(Regexp)
129
- desc << " with href #{href.inspect}"
130
- elsif @href_conditions
131
- desc << " with href matching #{href.inspect}"
132
- end
127
+ desc << " with href #{'matching ' if href.is_a? Regexp}#{href.inspect}"
128
+ elsif options.key?(:href) # is nil/false specified?
129
+ desc << ' with no href attribute'
133
130
  end
134
- desc << ' with no href attribute' if options.fetch(:href, true).nil?
135
131
  desc
136
132
  end
137
-
138
- describe_node_filters do |href: nil, **|
139
- " with href matching #{href.inspect}" if href.is_a?(Regexp) && @href_conditions.nil?
140
- end
141
133
  end
142
134
 
143
135
  Capybara.add_selector(:button) do
@@ -489,7 +481,7 @@ Capybara.add_selector(:element) do
489
481
  end
490
482
 
491
483
  expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
492
- xpath[builder.attribute_conditions(name => val)]
484
+ builder(xpath).add_attribute_conditions(name => val)
493
485
  end
494
486
 
495
487
  node_filter(:attributes, matcher: /.+/) do |node, name, val|
@@ -6,41 +6,73 @@ module Capybara
6
6
  class Selector
7
7
  # @api private
8
8
  class CSSBuilder
9
- class << self
10
- def attribute_conditions(attributes)
11
- attributes.map do |attribute, value|
12
- case value
13
- when XPath::Expression
14
- raise ArgumentError, "XPath expressions are not supported for the :#{attribute} filter with CSS based selectors"
15
- when Regexp
16
- Selector::RegexpDisassembler.new(value).substrings.map do |str|
17
- "[#{attribute}*='#{str}'#{' i' if value.casefold?}]"
9
+ def initialize(expression)
10
+ @expression = expression || ''
11
+ end
12
+
13
+ attr_reader :expression
14
+
15
+ def add_attribute_conditions(**attributes)
16
+ @expression = attributes.inject(expression) do |css, (name, value)|
17
+ conditions = if name == :class
18
+ class_conditions(value)
19
+ elsif value.is_a? Regexp
20
+ Selector::RegexpDisassembler.new(value).alternated_substrings.map do |strs|
21
+ strs.map do |str|
22
+ "[#{name}*='#{str}'#{' i' if value.casefold?}]"
18
23
  end.join
19
- when true
20
- "[#{attribute}]"
21
- when false
22
- ':not([attribute])'
23
- else
24
- if attribute == :id
25
- "##{::Capybara::Selector::CSS.escape(value)}"
26
- else
27
- "[#{attribute}='#{value}']"
28
- end
29
24
  end
30
- end.join
25
+ else
26
+ [attribute_conditions(name => value)]
27
+ end
28
+
29
+ ::Capybara::Selector::CSS.split(css).map do |sel|
30
+ next sel if conditions.empty?
31
+
32
+ conditions.map { |cond| sel + cond }.join(', ')
33
+ end.join(', ')
31
34
  end
35
+ end
36
+
37
+ private
32
38
 
33
- def class_conditions(classes)
34
- case classes
39
+ def attribute_conditions(attributes)
40
+ attributes.map do |attribute, value|
41
+ case value
35
42
  when XPath::Expression
36
- raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
43
+ raise ArgumentError, "XPath expressions are not supported for the :#{attribute} filter with CSS based selectors"
37
44
  when Regexp
38
- attribute_conditions(class: classes)
45
+ Selector::RegexpDisassembler.new(value).substrings.map do |str|
46
+ "[#{attribute}*='#{str}'#{' i' if value.casefold?}]"
47
+ end.join
48
+ when true
49
+ "[#{attribute}]"
50
+ when false
51
+ ':not([attribute])'
39
52
  else
40
- cls = Array(classes).group_by { |cl| cl.start_with?('!') && !cl.start_with?('!!!') }
41
- (cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
42
- cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join
53
+ if attribute == :id
54
+ "##{::Capybara::Selector::CSS.escape(value)}"
55
+ else
56
+ "[#{attribute}='#{value}']"
57
+ end
58
+ end
59
+ end.join
60
+ end
61
+
62
+ def class_conditions(classes)
63
+ case classes
64
+ when XPath::Expression
65
+ raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
66
+ when Regexp
67
+ Selector::RegexpDisassembler.new(classes).alternated_substrings.map do |strs|
68
+ strs.map do |str|
69
+ "[class*='#{str}'#{' i' if classes.casefold?}]"
70
+ end.join
43
71
  end
72
+ else
73
+ cls = Array(classes).group_by { |cl| cl.start_with?('!') && !cl.start_with?('!!!') }
74
+ [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
75
+ cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join]
44
76
  end
45
77
  end
46
78
  end