capybara 3.8.2 → 3.9.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +10 -0
  3. data/lib/capybara.rb +32 -10
  4. data/lib/capybara/config.rb +1 -0
  5. data/lib/capybara/dsl.rb +2 -2
  6. data/lib/capybara/helpers.rb +1 -0
  7. data/lib/capybara/node/actions.rb +4 -0
  8. data/lib/capybara/node/base.rb +1 -0
  9. data/lib/capybara/node/element.rb +3 -0
  10. data/lib/capybara/node/finders.rb +2 -0
  11. data/lib/capybara/node/simple.rb +1 -0
  12. data/lib/capybara/queries/base_query.rb +1 -0
  13. data/lib/capybara/queries/match_query.rb +1 -0
  14. data/lib/capybara/queries/selector_query.rb +34 -37
  15. data/lib/capybara/queries/text_query.rb +2 -0
  16. data/lib/capybara/rack_test/browser.rb +1 -0
  17. data/lib/capybara/rack_test/driver.rb +5 -0
  18. data/lib/capybara/rack_test/node.rb +2 -0
  19. data/lib/capybara/result.rb +2 -0
  20. data/lib/capybara/rspec/compound.rb +2 -0
  21. data/lib/capybara/rspec/matchers.rb +1 -0
  22. data/lib/capybara/selector.rb +14 -27
  23. data/lib/capybara/selector/builders/css_builder.rb +49 -0
  24. data/lib/capybara/selector/builders/xpath_builder.rb +56 -0
  25. data/lib/capybara/selector/filter_set.rb +1 -0
  26. data/lib/capybara/selector/filters/base.rb +2 -0
  27. data/lib/capybara/selector/regexp_disassembler.rb +66 -0
  28. data/lib/capybara/selector/selector.rb +25 -5
  29. data/lib/capybara/selenium/driver.rb +8 -1
  30. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +19 -1
  31. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +1 -0
  32. data/lib/capybara/selenium/node.rb +7 -0
  33. data/lib/capybara/selenium/nodes/chrome_node.rb +2 -0
  34. data/lib/capybara/selenium/nodes/marionette_node.rb +37 -20
  35. data/lib/capybara/server.rb +4 -0
  36. data/lib/capybara/server/animation_disabler.rb +1 -0
  37. data/lib/capybara/session.rb +5 -0
  38. data/lib/capybara/session/config.rb +2 -0
  39. data/lib/capybara/spec/session/has_css_spec.rb +16 -0
  40. data/lib/capybara/spec/session/has_field_spec.rb +4 -0
  41. data/lib/capybara/spec/session/node_spec.rb +6 -0
  42. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  43. data/lib/capybara/spec/session/reset_session_spec.rb +15 -1
  44. data/lib/capybara/spec/session/selectors_spec.rb +12 -2
  45. data/lib/capybara/spec/views/form.erb +15 -0
  46. data/lib/capybara/version.rb +1 -1
  47. data/lib/capybara/xpath_patches.rb +27 -0
  48. data/spec/dsl_spec.rb +15 -1
  49. data/spec/rack_test_spec.rb +6 -1
  50. data/spec/regexp_dissassembler_spec.rb +154 -0
  51. data/spec/selector_spec.rb +37 -2
  52. data/spec/selenium_spec_chrome.rb +2 -2
  53. data/spec/selenium_spec_firefox_remote.rb +2 -0
  54. data/spec/selenium_spec_marionette.rb +11 -0
  55. data/spec/shared_selenium_session.rb +20 -0
  56. data/spec/spec_helper.rb +4 -0
  57. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ac88f1b755e07883e3173cbdc66d44cede7aab5730fbcbf7d374cb3d9aa2a78
4
- data.tar.gz: 702d5533d8c404c67356748aa90f39ed0d606a00a611035b64608695a574d979
3
+ metadata.gz: 42a9311d8f3e7670a90f5851cc9927db169022d550e1c9b2a086900145b7ad18
4
+ data.tar.gz: 8159deadf9826514634c55f9744d2b8f18a910434d71f66870eb299f6c5980ff
5
5
  SHA512:
6
- metadata.gz: 99ef4c6e785412cd50ea04e89d46726cef9abba65d946a1c041abe71a4fadacf9d0772699959def2a48fe41b3a2f108c9f5a4bb44c88af13b5df4df6ff8db013
7
- data.tar.gz: d2eef690d02242a4ac16580d9fc0a5904844efe6b5ebaa3ce223e9e4fee39bf4140fdb1a28dbd9fb1ad9f1e0f9e1b0783fce4091a12c6e97d51ad203d34e4422
6
+ metadata.gz: 8d8256a0c4bc0ca9586d65aec40d4814c060226a7902f5122271d4c9e22b63eec1568c81315d327e02873d732062beef6a0c01d0a50df9d49e14fcd0189084c0
7
+ data.tar.gz: a86ce539c90c9fbaea0ac8165bbf6f1b66ee8fa4589c129c366d88730cb1881616a3260c3a710b546c0e833aa708de8791326cbaa9c5ffe5f8875ce249c779cd
data/History.md CHANGED
@@ -1,3 +1,13 @@
1
+ # Version 3.9.0
2
+ Release date: 2018-10-03
3
+
4
+ ### Added
5
+
6
+ * Selenium with Chrome removes all cookies at session reset instead of just cookies from current domain if possible
7
+ * Support for Regexp for system :id and :class filters where possible
8
+ * `using_session` now accepts a session object as well as the name of the session for users who manually manage sessions
9
+ * The `:field` selector will now find `type = "hidden"` fields if the `type: "hidden"` filter option is provided
10
+
1
11
  # Version 3.8.2
2
12
  Release date: 2018-09-26
3
13
 
@@ -299,7 +299,7 @@ module Capybara
299
299
  # @return [Capybara::Session] The currently used session
300
300
  #
301
301
  def current_session
302
- session_pool["#{current_driver}:#{session_name}:#{app.object_id}"] ||= Capybara::Session.new(current_driver, app)
302
+ specified_session || session_pool["#{current_driver}:#{session_name}:#{app.object_id}"]
303
303
  end
304
304
 
305
305
  ##
@@ -337,22 +337,25 @@ module Capybara
337
337
 
338
338
  ##
339
339
  #
340
- # Yield a block using a specific session name.
340
+ # Yield a block using a specific session name or Capybara::Session instance.
341
341
  #
342
- def using_session(name)
342
+ def using_session(name_or_session)
343
343
  previous_session_info = {
344
+ specified_session: specified_session,
344
345
  session_name: session_name,
345
346
  current_driver: current_driver,
346
347
  app: app
347
348
  }
348
- self.session_name = name
349
+ self.specified_session = self.session_name = nil
350
+ if name_or_session.is_a? Capybara::Session
351
+ self.specified_session = name_or_session
352
+ else
353
+ self.session_name = name_or_session
354
+ end
349
355
  yield
350
356
  ensure
351
- self.session_name = previous_session_info[:session_name]
352
- if threadsafe
353
- self.current_driver = previous_session_info[:current_driver]
354
- self.app = previous_session_info[:app]
355
- end
357
+ self.session_name, self.specified_session = previous_session_info.values_at(:session_name, :specified_session)
358
+ self.current_driver, self.app = previous_session_info.values_at(:current_driver, :app) if threadsafe
356
359
  end
357
360
 
358
361
  ##
@@ -381,7 +384,25 @@ module Capybara
381
384
  end
382
385
 
383
386
  def session_pool
384
- @session_pool ||= {}
387
+ @session_pool ||= Hash.new do |hash, name|
388
+ hash[name] = Capybara::Session.new(current_driver, app)
389
+ end
390
+ end
391
+
392
+ def specified_session
393
+ if threadsafe
394
+ Thread.current['capybara_specified_session']
395
+ else
396
+ @specified_session
397
+ end
398
+ end
399
+
400
+ def specified_session=(session)
401
+ if threadsafe
402
+ Thread.current['capybara_specified_session'] = session
403
+ else
404
+ @specified_session = session
405
+ end
385
406
  end
386
407
  end
387
408
 
@@ -393,6 +414,7 @@ module Capybara
393
414
  module RackTest; end
394
415
  module Selenium; end
395
416
 
417
+ require 'capybara/xpath_patches'
396
418
  require 'capybara/helpers'
397
419
  require 'capybara/session'
398
420
  require 'capybara/window'
@@ -27,6 +27,7 @@ module Capybara
27
27
 
28
28
  def threadsafe=(bool)
29
29
  raise 'Threadsafe setting cannot be changed once a session is created' if (bool != threadsafe) && Session.instance_created?
30
+
30
31
  @threadsafe = bool
31
32
  end
32
33
 
@@ -18,8 +18,8 @@ module Capybara
18
18
  #
19
19
  # Shortcut to working in a different session.
20
20
  #
21
- def using_session(name, &block)
22
- Capybara.using_session(name, &block)
21
+ def using_session(name_or_session, &block)
22
+ Capybara.using_session(name_or_session, &block)
23
23
  end
24
24
 
25
25
  # Shortcut to using a different wait time.
@@ -87,6 +87,7 @@ module Capybara
87
87
 
88
88
  def expired?
89
89
  raise Capybara::FrozenInTime, 'Time appears to be frozen. Capybara does not work with libraries which freeze time, consider using time travelling instead' if stalled?
90
+
90
91
  current - @start >= @expire_in
91
92
  end
92
93
 
@@ -274,6 +274,7 @@ module Capybara
274
274
  find(:select, from, options)
275
275
  rescue Capybara::ElementNotFound => select_error
276
276
  raise if %i[selected with_selected multiple].any? { |option| options.key?(option) }
277
+
277
278
  begin
278
279
  find(:datalist_input, from, options)
279
280
  rescue Capybara::ElementNotFound => dlinput_error
@@ -287,6 +288,7 @@ module Capybara
287
288
  datalist_options = input.evaluate_script(DATALIST_OPTIONS_SCRIPT)
288
289
  option = datalist_options.find { |opt| opt.values_at('value', 'label').include?(value) }
289
290
  raise ::Capybara::ElementNotFound, %(Unable to find datalist option "#{value}") unless option
291
+
290
292
  input.set(option['value'])
291
293
  rescue ::Capybara::NotSupportedByDriverError
292
294
  # Implement for drivers that don't support JS
@@ -299,6 +301,7 @@ module Capybara
299
301
  visible_css = { opacity: 1, display: 'block', visibility: 'visible' } if visible_css == true
300
302
  _update_style(element, visible_css)
301
303
  raise ExpectationNotMet, 'The style changes in :make_visible did not make the file input visible' unless element.visible?
304
+
302
305
  begin
303
306
  yield element
304
307
  ensure
@@ -326,6 +329,7 @@ module Capybara
326
329
  el.set(checked)
327
330
  rescue StandardError => err
328
331
  raise unless allow_label_click && catch_error?(err)
332
+
329
333
  begin
330
334
  el ||= find(selector, locator, options.merge(visible: :all))
331
335
  el.session.find(:label, for: el, visible: true).click unless el.checked? == checked
@@ -84,6 +84,7 @@ module Capybara
84
84
  session.raise_server_error!
85
85
  raise err unless driver.wait? && catch_error?(err, errors)
86
86
  raise err if timer.expired?
87
+
87
88
  sleep(0.05)
88
89
  reload if session_options.automatic_reload
89
90
  retry
@@ -84,6 +84,7 @@ module Capybara
84
84
  def style(*styles)
85
85
  styles = styles.flatten.map(&:to_s)
86
86
  raise ArgumentError, 'You must specify at least one CSS style' if styles.empty?
87
+
87
88
  begin
88
89
  synchronize { base.style(styles) }
89
90
  rescue NotImplementedError => err
@@ -113,6 +114,7 @@ module Capybara
113
114
  # @return [Capybara::Node::Element] The element
114
115
  def set(value, **options)
115
116
  raise Capybara::ReadOnlyElementError, "Attempt to set readonly element with value: #{value}" if readonly?
117
+
116
118
  options = session_options.default_set_options.to_h.merge(options)
117
119
  synchronize { base.set(value, options) }
118
120
  self
@@ -442,6 +444,7 @@ module Capybara
442
444
  %(#<Capybara::Node::Element tag="#{base.tag_name}">)
443
445
  rescue StandardError => err
444
446
  raise unless session.driver.invalid_element_errors.any? { |et| err.is_a?(et) }
447
+
445
448
  %(Obsolete #<Capybara::Node::Element>)
446
449
  end
447
450
 
@@ -252,10 +252,12 @@ module Capybara
252
252
  synchronize(query.wait) do
253
253
  result = query.resolve_for(self)
254
254
  raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count?
255
+
255
256
  result
256
257
  end
257
258
  rescue Capybara::ExpectationNotMet
258
259
  raise if minimum_specified || (result.compare_count == 1)
260
+
259
261
  Result.new([], nil)
260
262
  end
261
263
  end
@@ -182,6 +182,7 @@ module Capybara
182
182
 
183
183
  def option_value(option)
184
184
  return nil if option.nil?
185
+
185
186
  option[:value] || option.content
186
187
  end
187
188
  end
@@ -51,6 +51,7 @@ module Capybara
51
51
  return false if options[:maximum] && (Integer(options[:maximum]) < count)
52
52
  return false if options[:minimum] && (Integer(options[:minimum]) > count)
53
53
  return false if options[:between] && !options[:between].include?(count)
54
+
54
55
  true
55
56
  end
56
57
 
@@ -14,6 +14,7 @@ module Capybara
14
14
  unless invalid_options.empty?
15
15
  raise ArgumentError, "Match queries don't support quantity options. Invalid keys - #{invalid_options.join(', ')}"
16
16
  end
17
+
17
18
  super
18
19
  end
19
20
 
@@ -59,8 +59,11 @@ module Capybara
59
59
 
60
60
  def matches_filters?(node)
61
61
  return true if (@resolved_node&.== node) && options[:allow_self]
62
+
62
63
  @applied_filters ||= :system
64
+ return false unless matches_id_filter?(node) && matches_class_filter?(node)
63
65
  return false unless matches_text_filter?(node) && matches_exact_text_filter?(node) && matches_visible_filter?(node)
66
+
64
67
  @applied_filters = :node
65
68
  matches_node_filters?(node) && matches_filter_block?(node)
66
69
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
@@ -68,7 +71,7 @@ module Capybara
68
71
  end
69
72
 
70
73
  def visible
71
- case (vis = options.fetch(:visible) { @selector.default_visibility(session_options.ignore_hidden_elements) })
74
+ case (vis = options.fetch(:visible) { @selector.default_visibility(session_options.ignore_hidden_elements, options) })
72
75
  when true then :visible
73
76
  when false then :all
74
77
  else vis
@@ -174,6 +177,7 @@ module Capybara
174
177
 
175
178
  def matches_filter_block?(node)
176
179
  return true unless @filter_block
180
+
177
181
  if node.respond_to?(:session)
178
182
  node.session.using_wait_time(0) { @filter_block.call(node) }
179
183
  else
@@ -203,6 +207,7 @@ module Capybara
203
207
  unless VALID_MATCH.include?(match)
204
208
  raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
205
209
  end
210
+
206
211
  unhandled_options = @options.keys.reject do |option_name|
207
212
  valid_keys.include?(option_name) ||
208
213
  expression_filters.any? { |_name, ef| ef.handles_option? option_name } ||
@@ -210,28 +215,22 @@ module Capybara
210
215
  end
211
216
 
212
217
  return if unhandled_options.empty?
218
+
213
219
  invalid_names = unhandled_options.map(&:inspect).join(', ')
214
220
  valid_names = (valid_keys - [:allow_self]).map(&:inspect).join(', ')
215
221
  raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
216
222
  end
217
223
 
218
224
  def filtered_xpath(expr)
219
- if use_default_id_filter?
220
- id_xpath = if options[:id].is_a? XPath::Expression
221
- XPath.attr(:id)[options[:id]]
222
- else
223
- XPath.attr(:id) == options[:id]
224
- end
225
- expr = "(#{expr})[#{id_xpath}]"
226
- end
227
- expr = "(#{expr})[#{xpath_from_classes}]" if use_default_class_filter?
225
+ expr = "(#{expr})[#{conditions_from_id}]" if use_default_id_filter?
226
+ expr = "(#{expr})[#{conditions_from_classes}]" if use_default_class_filter?
228
227
  expr
229
228
  end
230
229
 
231
230
  def filtered_css(expr)
232
231
  ::Capybara::Selector::CSS.split(expr).map do |sel|
233
- sel += css_from_id if use_default_id_filter?
234
- sel += css_from_classes if use_default_class_filter?
232
+ sel += conditions_from_id if use_default_id_filter?
233
+ sel += conditions_from_classes if use_default_class_filter?
235
234
  sel
236
235
  end.join(', ')
237
236
  end
@@ -244,33 +243,12 @@ module Capybara
244
243
  options.key?(:class) && !custom_keys.include?(:class)
245
244
  end
246
245
 
247
- def css_from_classes
248
- if options[:class].is_a?(XPath::Expression)
249
- raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
250
- end
251
-
252
- classes = Array(options[:class]).group_by { |cl| cl.start_with? '!' }
253
- (classes[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl)}" } +
254
- classes[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join
246
+ def conditions_from_classes
247
+ builder.class_conditions(options[:class])
255
248
  end
256
249
 
257
- def css_from_id
258
- if options[:id].is_a?(XPath::Expression)
259
- raise ArgumentError, 'XPath expressions are not supported for the :id filter with CSS based selectors'
260
- end
261
- "##{::Capybara::Selector::CSS.escape(options[:id])}"
262
- end
263
-
264
- def xpath_from_classes
265
- return XPath.attr(:class)[options[:class]] if options[:class].is_a?(XPath::Expression)
266
-
267
- Array(options[:class]).map do |klass|
268
- if klass.start_with?('!')
269
- !XPath.attr(:class).contains_word(klass.slice(1..-1))
270
- else
271
- XPath.attr(:class).contains_word(klass)
272
- end
273
- end.reduce(:&)
250
+ def conditions_from_id
251
+ builder.attribute_conditions(id: options[:id])
274
252
  end
275
253
 
276
254
  def apply_expression_filters(expression)
@@ -294,6 +272,7 @@ module Capybara
294
272
 
295
273
  def warn_exact_usage
296
274
  return unless options.key?(:exact) && !supports_exact?
275
+
297
276
  warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression}\" has no effect."
298
277
  end
299
278
 
@@ -313,16 +292,30 @@ module Capybara
313
292
  node.is_a?(::Capybara::Node::Simple) && node.path == '/'
314
293
  end
315
294
 
295
+ def matches_id_filter?(node)
296
+ return true unless use_default_id_filter? && options[:id].is_a?(Regexp)
297
+
298
+ node[:id] =~ options[:id]
299
+ end
300
+
301
+ def matches_class_filter?(node)
302
+ return true unless use_default_class_filter? && options[:class].is_a?(Regexp)
303
+
304
+ node[:class] =~ options[:class]
305
+ end
306
+
316
307
  def matches_text_filter?(node)
317
308
  value = options[:text]
318
309
  return true unless value
319
310
  return matches_text_exactly?(node, value) if exact_text == true
311
+
320
312
  regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
321
313
  matches_text_regexp?(node, regexp)
322
314
  end
323
315
 
324
316
  def matches_exact_text_filter?(node)
325
317
  return true unless exact_text.is_a?(String)
318
+
326
319
  matches_text_exactly?(node, exact_text)
327
320
  end
328
321
 
@@ -348,6 +341,10 @@ module Capybara
348
341
  text_visible = :all if text_visible == :hidden
349
342
  !!node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
350
343
  end
344
+
345
+ def builder
346
+ selector.builder
347
+ end
351
348
  end
352
349
  end
353
350
  end
@@ -64,6 +64,7 @@ module Capybara
64
64
  insensitive_regexp = Capybara::Helpers.to_regexp(@expected_text, options: Regexp::IGNORECASE)
65
65
  insensitive_count = @actual_text.scan(insensitive_regexp).size
66
66
  return if insensitive_count == @count
67
+
67
68
  "it was found #{insensitive_count} #{Capybara::Helpers.declension('time', 'times', insensitive_count)} using a case insensitive search"
68
69
  end
69
70
 
@@ -71,6 +72,7 @@ module Capybara
71
72
  invisible_text = text(query_type: :all)
72
73
  invisible_count = invisible_text.scan(@search_regexp).size
73
74
  return if invisible_count == @count
75
+
74
76
  "it was found #{invisible_count} #{Capybara::Helpers.declension('time', 'times', invisible_count)} including non-visible text"
75
77
  rescue StandardError
76
78
  # An error getting the non-visible text (if element goes out of scope) should not affect the response
@@ -35,6 +35,7 @@ class Capybara::RackTest::Browser
35
35
 
36
36
  def follow(method, path, **attributes)
37
37
  return if fragment_or_script?(path)
38
+
38
39
  process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
39
40
  end
40
41
 
@@ -16,6 +16,7 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
16
16
 
17
17
  def initialize(app, **options)
18
18
  raise ArgumentError, 'rack-test requires a rack application, but none was given' unless app
19
+
19
20
  @app = app
20
21
  @options = DEFAULT_OPTIONS.merge(options)
21
22
  end
@@ -74,6 +75,10 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
74
75
 
75
76
  def find_css(selector)
76
77
  browser.find(:css, selector)
78
+ rescue Nokogiri::CSS::SyntaxError
79
+ raise unless selector.include?(' i]')
80
+
81
+ raise ArgumentError, "This driver doesn't support case insensitive attribute matching when using CSS base selectors"
77
82
  end
78
83
 
79
84
  def html
@@ -50,12 +50,14 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
50
50
 
51
51
  def select_option
52
52
  return if disabled?
53
+
53
54
  deselect_options unless select_node.multiple?
54
55
  native['selected'] = 'selected'
55
56
  end
56
57
 
57
58
  def unselect_option
58
59
  raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
60
+
59
61
  native.remove_attribute('selected')
60
62
  end
61
63