capybara 3.8.2 → 3.9.0

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