capybara 2.5.0 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (205) hide show
  1. checksums.yaml +5 -5
  2. data/.yard/templates_custom/default/class/html/selectors.erb +38 -0
  3. data/.yard/templates_custom/default/class/html/setup.rb +17 -0
  4. data/.yard/yard_extensions.rb +78 -0
  5. data/.yardopts +1 -0
  6. data/History.md +413 -10
  7. data/License.txt +1 -1
  8. data/README.md +237 -130
  9. data/lib/capybara/config.rb +132 -0
  10. data/lib/capybara/cucumber.rb +3 -1
  11. data/lib/capybara/driver/base.rb +27 -6
  12. data/lib/capybara/driver/node.rb +14 -5
  13. data/lib/capybara/dsl.rb +2 -3
  14. data/lib/capybara/helpers.rb +13 -65
  15. data/lib/capybara/minitest/spec.rb +177 -0
  16. data/lib/capybara/minitest.rb +278 -0
  17. data/lib/capybara/node/actions.rb +180 -24
  18. data/lib/capybara/node/base.rb +17 -5
  19. data/lib/capybara/node/document.rb +5 -0
  20. data/lib/capybara/node/document_matchers.rb +15 -14
  21. data/lib/capybara/node/element.rb +55 -7
  22. data/lib/capybara/node/finders.rb +179 -67
  23. data/lib/capybara/node/matchers.rb +301 -105
  24. data/lib/capybara/node/simple.rb +15 -4
  25. data/lib/capybara/queries/ancestor_query.rb +25 -0
  26. data/lib/capybara/queries/base_query.rb +69 -3
  27. data/lib/capybara/queries/current_path_query.rb +17 -8
  28. data/lib/capybara/queries/match_query.rb +19 -0
  29. data/lib/capybara/queries/selector_query.rb +251 -0
  30. data/lib/capybara/queries/sibling_query.rb +25 -0
  31. data/lib/capybara/queries/text_query.rb +67 -16
  32. data/lib/capybara/queries/title_query.rb +4 -2
  33. data/lib/capybara/query.rb +3 -131
  34. data/lib/capybara/rack_test/browser.rb +14 -5
  35. data/lib/capybara/rack_test/css_handlers.rb +1 -0
  36. data/lib/capybara/rack_test/driver.rb +15 -8
  37. data/lib/capybara/rack_test/form.rb +34 -12
  38. data/lib/capybara/rack_test/node.rb +29 -12
  39. data/lib/capybara/rails.rb +3 -3
  40. data/lib/capybara/result.rb +104 -9
  41. data/lib/capybara/rspec/compound.rb +95 -0
  42. data/lib/capybara/rspec/features.rb +17 -6
  43. data/lib/capybara/rspec/matcher_proxies.rb +45 -0
  44. data/lib/capybara/rspec/matchers.rb +199 -80
  45. data/lib/capybara/rspec.rb +4 -2
  46. data/lib/capybara/selector/css.rb +30 -0
  47. data/lib/capybara/selector/filter.rb +20 -0
  48. data/lib/capybara/selector/filter_set.rb +74 -0
  49. data/lib/capybara/selector/filters/base.rb +33 -0
  50. data/lib/capybara/selector/filters/expression_filter.rb +40 -0
  51. data/lib/capybara/selector/filters/node_filter.rb +27 -0
  52. data/lib/capybara/selector/selector.rb +276 -0
  53. data/lib/capybara/selector.rb +452 -157
  54. data/lib/capybara/selenium/driver.rb +282 -81
  55. data/lib/capybara/selenium/node.rb +144 -46
  56. data/lib/capybara/server.rb +59 -16
  57. data/lib/capybara/session/config.rb +114 -0
  58. data/lib/capybara/session/matchers.rb +29 -19
  59. data/lib/capybara/session.rb +378 -143
  60. data/lib/capybara/spec/fixtures/no_extension +1 -0
  61. data/lib/capybara/spec/public/jquery-ui.js +13 -791
  62. data/lib/capybara/spec/public/jquery.js +4 -9045
  63. data/lib/capybara/spec/public/test.js +45 -11
  64. data/lib/capybara/spec/session/accept_alert_spec.rb +30 -7
  65. data/lib/capybara/spec/session/accept_confirm_spec.rb +14 -2
  66. data/lib/capybara/spec/session/accept_prompt_spec.rb +35 -6
  67. data/lib/capybara/spec/session/all_spec.rb +45 -32
  68. data/lib/capybara/spec/session/ancestor_spec.rb +85 -0
  69. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +110 -0
  70. data/lib/capybara/spec/session/assert_current_path.rb +15 -2
  71. data/lib/capybara/spec/session/assert_selector.rb +29 -28
  72. data/lib/capybara/spec/session/assert_text.rb +59 -20
  73. data/lib/capybara/spec/session/assert_title.rb +25 -11
  74. data/lib/capybara/spec/session/attach_file_spec.rb +42 -4
  75. data/lib/capybara/spec/session/body_spec.rb +1 -0
  76. data/lib/capybara/spec/session/check_spec.rb +90 -14
  77. data/lib/capybara/spec/session/choose_spec.rb +31 -5
  78. data/lib/capybara/spec/session/click_button_spec.rb +20 -9
  79. data/lib/capybara/spec/session/click_link_or_button_spec.rb +15 -9
  80. data/lib/capybara/spec/session/click_link_spec.rb +39 -15
  81. data/lib/capybara/spec/session/current_scope_spec.rb +2 -1
  82. data/lib/capybara/spec/session/current_url_spec.rb +12 -3
  83. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +6 -5
  84. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +4 -3
  85. data/lib/capybara/spec/session/element/assert_match_selector.rb +36 -0
  86. data/lib/capybara/spec/session/element/match_css_spec.rb +23 -0
  87. data/lib/capybara/spec/session/element/match_xpath_spec.rb +23 -0
  88. data/lib/capybara/spec/session/element/matches_selector_spec.rb +106 -0
  89. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +22 -0
  90. data/lib/capybara/spec/session/evaluate_script_spec.rb +23 -1
  91. data/lib/capybara/spec/session/execute_script_spec.rb +22 -3
  92. data/lib/capybara/spec/session/fill_in_spec.rb +50 -32
  93. data/lib/capybara/spec/session/find_button_spec.rb +43 -2
  94. data/lib/capybara/spec/session/find_by_id_spec.rb +3 -2
  95. data/lib/capybara/spec/session/find_field_spec.rb +42 -6
  96. data/lib/capybara/spec/session/find_link_spec.rb +22 -3
  97. data/lib/capybara/spec/session/find_spec.rb +103 -57
  98. data/lib/capybara/spec/session/first_spec.rb +34 -18
  99. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +103 -0
  100. data/lib/capybara/spec/session/{within_frame_spec.rb → frame/within_frame_spec.rb} +44 -2
  101. data/lib/capybara/spec/session/go_back_spec.rb +2 -1
  102. data/lib/capybara/spec/session/go_forward_spec.rb +2 -1
  103. data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
  104. data/lib/capybara/spec/session/has_button_spec.rb +17 -8
  105. data/lib/capybara/spec/session/has_css_spec.rb +85 -73
  106. data/lib/capybara/spec/session/has_current_path_spec.rb +91 -7
  107. data/lib/capybara/spec/session/has_field_spec.rb +93 -58
  108. data/lib/capybara/spec/session/has_link_spec.rb +9 -8
  109. data/lib/capybara/spec/session/has_none_selectors_spec.rb +76 -0
  110. data/lib/capybara/spec/session/has_select_spec.rb +159 -59
  111. data/lib/capybara/spec/session/has_selector_spec.rb +64 -28
  112. data/lib/capybara/spec/session/has_table_spec.rb +1 -0
  113. data/lib/capybara/spec/session/has_text_spec.rb +27 -12
  114. data/lib/capybara/spec/session/has_title_spec.rb +22 -4
  115. data/lib/capybara/spec/session/has_xpath_spec.rb +32 -29
  116. data/lib/capybara/spec/session/headers.rb +2 -1
  117. data/lib/capybara/spec/session/html_spec.rb +4 -3
  118. data/lib/capybara/spec/session/node_spec.rb +198 -38
  119. data/lib/capybara/spec/session/refresh_spec.rb +28 -0
  120. data/lib/capybara/spec/session/reset_session_spec.rb +46 -5
  121. data/lib/capybara/spec/session/response_code.rb +2 -1
  122. data/lib/capybara/spec/session/save_and_open_page_spec.rb +1 -0
  123. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +6 -5
  124. data/lib/capybara/spec/session/save_page_spec.rb +34 -2
  125. data/lib/capybara/spec/session/save_screenshot_spec.rb +31 -1
  126. data/lib/capybara/spec/session/screenshot_spec.rb +4 -2
  127. data/lib/capybara/spec/session/select_spec.rb +34 -32
  128. data/lib/capybara/spec/session/selectors_spec.rb +65 -0
  129. data/lib/capybara/spec/session/sibling_spec.rb +52 -0
  130. data/lib/capybara/spec/session/text_spec.rb +4 -4
  131. data/lib/capybara/spec/session/title_spec.rb +2 -1
  132. data/lib/capybara/spec/session/uncheck_spec.rb +42 -2
  133. data/lib/capybara/spec/session/unselect_spec.rb +17 -16
  134. data/lib/capybara/spec/session/visit_spec.rb +77 -2
  135. data/lib/capybara/spec/session/window/become_closed_spec.rb +12 -11
  136. data/lib/capybara/spec/session/window/current_window_spec.rb +1 -0
  137. data/lib/capybara/spec/session/window/open_new_window_spec.rb +1 -0
  138. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +16 -11
  139. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +7 -4
  140. data/lib/capybara/spec/session/window/window_spec.rb +36 -29
  141. data/lib/capybara/spec/session/window/windows_spec.rb +1 -0
  142. data/lib/capybara/spec/session/window/within_window_spec.rb +31 -7
  143. data/lib/capybara/spec/session/within_spec.rb +14 -6
  144. data/lib/capybara/spec/spec_helper.rb +37 -4
  145. data/lib/capybara/spec/test_app.rb +15 -3
  146. data/lib/capybara/spec/views/buttons.erb +1 -0
  147. data/lib/capybara/spec/views/fieldsets.erb +2 -1
  148. data/lib/capybara/spec/views/form.erb +169 -9
  149. data/lib/capybara/spec/views/frame_child.erb +10 -2
  150. data/lib/capybara/spec/views/frame_one.erb +2 -1
  151. data/lib/capybara/spec/views/frame_parent.erb +3 -2
  152. data/lib/capybara/spec/views/frame_two.erb +2 -1
  153. data/lib/capybara/spec/views/header_links.erb +1 -0
  154. data/lib/capybara/spec/views/host_links.erb +1 -0
  155. data/lib/capybara/spec/views/initial_alert.erb +10 -0
  156. data/lib/capybara/spec/views/path.erb +1 -0
  157. data/lib/capybara/spec/views/popup_one.erb +1 -0
  158. data/lib/capybara/spec/views/popup_two.erb +1 -0
  159. data/lib/capybara/spec/views/postback.erb +2 -1
  160. data/lib/capybara/spec/views/tables.erb +1 -0
  161. data/lib/capybara/spec/views/with_base_tag.erb +1 -0
  162. data/lib/capybara/spec/views/with_count.erb +2 -1
  163. data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
  164. data/lib/capybara/spec/views/with_hover.erb +7 -1
  165. data/lib/capybara/spec/views/with_html.erb +40 -2
  166. data/lib/capybara/spec/views/with_html_entities.erb +1 -0
  167. data/lib/capybara/spec/views/with_js.erb +32 -1
  168. data/lib/capybara/spec/views/with_scope.erb +1 -0
  169. data/lib/capybara/spec/views/with_simple_html.erb +2 -1
  170. data/lib/capybara/spec/views/with_slow_unload.erb +17 -0
  171. data/lib/capybara/spec/views/with_title.erb +2 -1
  172. data/lib/capybara/spec/views/with_unload_alert.erb +14 -0
  173. data/lib/capybara/spec/views/with_windows.erb +7 -0
  174. data/lib/capybara/spec/views/within_frames.erb +3 -2
  175. data/lib/capybara/version.rb +2 -1
  176. data/lib/capybara/window.rb +20 -3
  177. data/lib/capybara.rb +189 -93
  178. data/spec/basic_node_spec.rb +7 -6
  179. data/spec/capybara_spec.rb +90 -4
  180. data/spec/dsl_spec.rb +3 -1
  181. data/spec/filter_set_spec.rb +28 -0
  182. data/spec/fixtures/capybara.csv +1 -0
  183. data/spec/fixtures/selenium_driver_rspec_failure.rb +5 -1
  184. data/spec/fixtures/selenium_driver_rspec_success.rb +5 -1
  185. data/spec/minitest_spec.rb +130 -0
  186. data/spec/minitest_spec_spec.rb +135 -0
  187. data/spec/per_session_config_spec.rb +67 -0
  188. data/spec/rack_test_spec.rb +50 -7
  189. data/spec/result_spec.rb +76 -0
  190. data/spec/rspec/features_spec.rb +21 -8
  191. data/spec/rspec/scenarios_spec.rb +21 -0
  192. data/spec/rspec/{matchers_spec.rb → shared_spec_matchers.rb} +160 -54
  193. data/spec/rspec/views_spec.rb +5 -0
  194. data/spec/rspec_matchers_spec.rb +46 -0
  195. data/spec/rspec_spec.rb +79 -1
  196. data/spec/selector_spec.rb +199 -0
  197. data/spec/selenium_spec_chrome.rb +54 -9
  198. data/spec/selenium_spec_firefox.rb +68 -0
  199. data/spec/selenium_spec_marionette.rb +127 -0
  200. data/spec/server_spec.rb +102 -14
  201. data/spec/session_spec.rb +54 -0
  202. data/spec/shared_selenium_session.rb +215 -0
  203. data/spec/spec_helper.rb +7 -0
  204. metadata +140 -15
  205. data/spec/selenium_spec.rb +0 -128
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Capybara
2
3
  module Node
3
4
 
@@ -75,7 +76,7 @@ module Capybara
75
76
  #
76
77
  def value
77
78
  if tag_name == 'textarea'
78
- native.content
79
+ native['_capybara_raw_value']
79
80
  elsif tag_name == 'select'
80
81
  if native['multiple'] == 'multiple'
81
82
  native.xpath(".//option[@selected='selected']").map { |option| option[:value] || option.content }
@@ -99,6 +100,8 @@ module Capybara
99
100
  # @return [Boolean] Whether the element is visible
100
101
  #
101
102
  def visible?(check_ancestors = true)
103
+ return false if (tag_name == 'input') && (native[:type]=="hidden")
104
+
102
105
  if check_ancestors
103
106
  #check size because oldest supported nokogiri doesnt support xpath boolean() function
104
107
  native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none') or @hidden or name()='script' or name()='head']").size() == 0
@@ -115,7 +118,7 @@ module Capybara
115
118
  # @return [Boolean] Whether the element is checked
116
119
  #
117
120
  def checked?
118
- native[:checked]
121
+ native.has_attribute?('checked')
119
122
  end
120
123
 
121
124
  ##
@@ -124,7 +127,7 @@ module Capybara
124
127
  #
125
128
  # @return [Boolean] Whether the element is disabled
126
129
  def disabled?
127
- native[:disabled]
130
+ native.has_attribute?('disabled')
128
131
  end
129
132
 
130
133
  ##
@@ -134,7 +137,7 @@ module Capybara
134
137
  # @return [Boolean] Whether the element is selected
135
138
  #
136
139
  def selected?
137
- native[:selected]
140
+ native.has_attribute?('selected')
138
141
  end
139
142
 
140
143
  def synchronize(seconds=nil)
@@ -145,6 +148,9 @@ module Capybara
145
148
  # no op
146
149
  end
147
150
 
151
+ ##
152
+ #
153
+ # @return [String] The title of the document
148
154
  def title
149
155
  if native.respond_to? :title
150
156
  native.title
@@ -167,6 +173,11 @@ module Capybara
167
173
  def find_xpath(xpath)
168
174
  native.xpath(xpath)
169
175
  end
176
+
177
+ # @api private
178
+ def session_options
179
+ Capybara.session_options
180
+ end
170
181
  end
171
182
  end
172
183
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module Capybara
3
+ module Queries
4
+ class AncestorQuery < MatchQuery
5
+ # @api private
6
+ def resolve_for(node, exact = nil)
7
+ @child_node = node
8
+ node.synchronize do
9
+ match_results = super(node.session.current_scope, exact)
10
+ node.all(:xpath, XPath.ancestor) do |el|
11
+ match_results.include?(el)
12
+ end
13
+ end
14
+ end
15
+
16
+ def description
17
+ desc = super
18
+ if @child_node && (child_query = @child_node.instance_variable_get(:@query))
19
+ desc += " that is an ancestor of #{child_query.description}"
20
+ end
21
+ desc
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Capybara
2
3
  # @api private
3
4
  module Queries
@@ -5,17 +6,82 @@ module Capybara
5
6
  COUNT_KEYS = [:count, :minimum, :maximum, :between]
6
7
 
7
8
  attr_reader :options
9
+ attr_writer :session_options
10
+
11
+ def initialize(options)
12
+ @session_options = options.delete(:session_options)
13
+ end
14
+
15
+ def session_options
16
+ @session_options || Capybara.session_options
17
+ end
8
18
 
9
19
  def wait
10
- if @options.has_key?(:wait)
11
- @options[:wait] || 0
20
+ self.class.wait(options, session_options.default_max_wait_time)
21
+ end
22
+
23
+ def self.wait(options, default=Capybara.default_max_wait_time)
24
+ options.fetch(:wait, default) || 0
25
+ end
26
+
27
+ ##
28
+ #
29
+ # Checks if a count of 0 is valid for the query
30
+ # Returns false if query does not have any count options specified.
31
+ #
32
+ def expects_none?
33
+ if COUNT_KEYS.any? { |k| options.has_key? k }
34
+ matches_count?(0)
12
35
  else
13
- Capybara.default_max_wait_time
36
+ false
14
37
  end
15
38
  end
16
39
 
40
+ ##
41
+ #
42
+ # Checks if the given count matches the query count options.
43
+ # Defaults to true if no count options are specified. If multiple
44
+ # count options exist, it tests that all conditions are met;
45
+ # however, if :count is specified, all other options are ignored.
46
+ #
47
+ # @param [Integer] count The actual number. Should be coercible via Integer()
48
+ #
49
+ def matches_count?(count)
50
+ return (Integer(options[:count]) == count) if options[:count]
51
+ return false if options[:maximum] && (Integer(options[:maximum]) < count)
52
+ return false if options[:minimum] && (Integer(options[:minimum]) > count)
53
+ return false if options[:between] && !(options[:between] === count)
54
+ return true
55
+ end
56
+
57
+ ##
58
+ #
59
+ # Generates a failure message from the query description and count options.
60
+ #
61
+ def failure_message
62
+ String.new("expected to find #{description}") << count_message
63
+ end
64
+
65
+ def negative_failure_message
66
+ String.new("expected not to find #{description}") << count_message
67
+ end
68
+
17
69
  private
18
70
 
71
+ def count_message
72
+ message = String.new()
73
+ if options[:count]
74
+ message << " #{options[:count]} #{Capybara::Helpers.declension('time', 'times', options[:count])}"
75
+ elsif options[:between]
76
+ message << " between #{options[:between].first} and #{options[:between].last} times"
77
+ elsif options[:maximum]
78
+ message << " at most #{options[:maximum]} #{Capybara::Helpers.declension('time', 'times', options[:maximum])}"
79
+ elsif options[:minimum]
80
+ message << " at least #{options[:minimum]} #{Capybara::Helpers.declension('time', 'times', options[:minimum])}"
81
+ end
82
+ message
83
+ end
84
+
19
85
  def assert_valid_keys
20
86
  invalid_keys = @options.keys - valid_keys
21
87
  unless invalid_keys.empty?
@@ -1,30 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require 'addressable/uri'
3
+
1
4
  module Capybara
2
5
  # @api private
3
6
  module Queries
4
7
  class CurrentPathQuery < BaseQuery
5
8
  def initialize(expected_path, options = {})
9
+ super(options)
6
10
  @expected_path = expected_path
11
+ warn "DEPRECATED: The :only_path option is deprecated in favor of the :ignore_query option" if options.has_key?(:only_path)
12
+
7
13
  @options = {
8
- url: false,
9
- only_path: false }.merge(options)
14
+ url: !@expected_path.is_a?(Regexp) && !::Addressable::URI.parse(@expected_path || "").hostname.nil?,
15
+ only_path: false,
16
+ ignore_query: false }.merge(options)
10
17
  assert_valid_keys
11
18
  end
12
19
 
13
20
  def resolves_for?(session)
21
+ uri = ::Addressable::URI.parse(session.current_url)
22
+ uri.query = nil if uri && options[:ignore_query]
14
23
  @actual_path = if options[:url]
15
- session.current_url
24
+ uri.to_s
16
25
  else
17
26
  if options[:only_path]
18
- URI.parse(session.current_url).path
27
+ uri && uri.path
19
28
  else
20
- URI.parse(session.current_url).request_uri
29
+ uri && uri.request_uri
21
30
  end
22
31
  end
23
32
 
24
33
  if @expected_path.is_a? Regexp
25
- @actual_path.match(@expected_path)
34
+ @actual_path.to_s.match(@expected_path)
26
35
  else
27
- @expected_path == @actual_path
36
+ ::Addressable::URI.parse(@expected_path) == ::Addressable::URI.parse(@actual_path)
28
37
  end
29
38
  end
30
39
 
@@ -44,7 +53,7 @@ module Capybara
44
53
  end
45
54
 
46
55
  def valid_keys
47
- [:wait, :url, :only_path]
56
+ [:wait, :url, :only_path, :ignore_query]
48
57
  end
49
58
 
50
59
  def assert_valid_keys
@@ -0,0 +1,19 @@
1
+ module Capybara
2
+ module Queries
3
+ class MatchQuery < Capybara::Queries::SelectorQuery
4
+ def visible
5
+ if options.has_key?(:visible)
6
+ super
7
+ else
8
+ :all
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def valid_keys
15
+ super - COUNT_KEYS
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+ module Capybara
3
+ module Queries
4
+ class SelectorQuery < Queries::BaseQuery
5
+ attr_accessor :selector, :locator, :options, :expression, :find, :negative
6
+
7
+ VALID_KEYS = COUNT_KEYS + [:text, :id, :class, :visible, :exact, :exact_text, :match, :wait, :filter_set]
8
+ VALID_MATCH = [:first, :smart, :prefer_exact, :one]
9
+
10
+ def initialize(*args, &filter_block)
11
+ @resolved_node = nil
12
+ @options = if args.last.is_a?(Hash) then args.pop.dup else {} end
13
+ super(@options)
14
+
15
+ @filter_block = filter_block
16
+
17
+ if args[0].is_a?(Symbol)
18
+ @selector = Selector.all.fetch(args.shift) do |selector_type|
19
+ raise ArgumentError, "Unknown selector type (:#{selector_type})"
20
+ end
21
+ @locator = args.shift
22
+ else
23
+ @selector = Selector.all.values.find { |s| s.match?(args[0]) }
24
+ @locator = args.shift
25
+ end
26
+ @selector ||= Selector.all[session_options.default_selector]
27
+
28
+ warn "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
29
+
30
+ # for compatibility with Capybara 2.0
31
+ if session_options.exact_options and @selector == Selector.all[:option]
32
+ @options[:exact] = true
33
+ end
34
+
35
+ @expression = @selector.call(@locator, @options.merge(enable_aria_label: session_options.enable_aria_label))
36
+
37
+ warn_exact_usage
38
+
39
+ assert_valid_keys
40
+ end
41
+
42
+ def name; selector.name; end
43
+ def label; selector.label or selector.name; end
44
+
45
+ def description
46
+ @description = String.new()
47
+ @description << "visible " if visible == :visible
48
+ @description << "non-visible " if visible == :hidden
49
+ @description << "#{label} #{locator.inspect}"
50
+ @description << " with#{" exact" if exact_text == true} text #{options[:text].inspect}" if options[:text]
51
+ @description << " with exact text #{options[:exact_text]}" if options[:exact_text].is_a?(String)
52
+ @description << " with id #{options[:id]}" if options[:id]
53
+ @description << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
54
+ @description << selector.description(options)
55
+ @description << " that also matches the custom filter block" if @filter_block
56
+ @description << " within #{@resolved_node.inspect}" if describe_within?
57
+ @description
58
+ end
59
+
60
+ def matches_filters?(node)
61
+ if options[:text]
62
+ regexp = if options[:text].is_a?(Regexp)
63
+ options[:text]
64
+ else
65
+ if exact_text == true
66
+ /\A#{Regexp.escape(options[:text].to_s)}\z/
67
+ else
68
+ Regexp.escape(options[:text].to_s)
69
+ end
70
+ end
71
+ text_visible = visible
72
+ text_visible = :all if text_visible == :hidden
73
+ return false if not node.text(text_visible).match(regexp)
74
+ end
75
+
76
+ if exact_text.is_a?(String)
77
+ regexp = /\A#{Regexp.escape(options[:exact_text])}\z/
78
+ text_visible = visible
79
+ text_visible = :all if text_visible == :hidden
80
+ return false if not node.text(text_visible).match(regexp)
81
+ end
82
+
83
+ case visible
84
+ when :visible then return false unless node.visible?
85
+ when :hidden then return false if node.visible?
86
+ end
87
+
88
+ res = node_filters.all? do |name, filter|
89
+ if options.has_key?(name)
90
+ filter.matches?(node, options[name])
91
+ elsif filter.default?
92
+ filter.matches?(node, filter.default)
93
+ else
94
+ true
95
+ end
96
+ end
97
+
98
+ res &&= if node.respond_to?(:session)
99
+ node.session.using_wait_time(0){ @filter_block.call(node) }
100
+ else
101
+ @filter_block.call(node)
102
+ end unless @filter_block.nil?
103
+
104
+ res
105
+
106
+ rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
107
+ return false
108
+ end
109
+
110
+ def visible
111
+ case (vis = options.fetch(:visible){ @selector.default_visibility(session_options.ignore_hidden_elements) })
112
+ when true then :visible
113
+ when false then :all
114
+ else vis
115
+ end
116
+ end
117
+
118
+ def exact?
119
+ return false if !supports_exact?
120
+ options.fetch(:exact, session_options.exact)
121
+ end
122
+
123
+ def match
124
+ options.fetch(:match, session_options.match)
125
+ end
126
+
127
+ def xpath(exact=nil)
128
+ exact = self.exact? if exact.nil?
129
+ expr = apply_expression_filters(@expression)
130
+ expr = if expr.respond_to?(:to_xpath) and exact
131
+ expr.to_xpath(:exact)
132
+ else
133
+ expr.to_s
134
+ end
135
+ filtered_xpath(expr)
136
+ end
137
+
138
+ def css
139
+ filtered_css(apply_expression_filters(@expression))
140
+ end
141
+
142
+ # @api private
143
+ def resolve_for(node, exact = nil)
144
+ @resolved_node = node
145
+ node.synchronize do
146
+ children = if selector.format == :css
147
+ node.find_css(self.css)
148
+ else
149
+ node.find_xpath(self.xpath(exact))
150
+ end.map do |child|
151
+ if node.is_a?(Capybara::Node::Base)
152
+ Capybara::Node::Element.new(node.session, child, node, self)
153
+ else
154
+ Capybara::Node::Simple.new(child)
155
+ end
156
+ end
157
+ Capybara::Result.new(children, self)
158
+ end
159
+ end
160
+
161
+ # @api private
162
+ def supports_exact?
163
+ @expression.respond_to? :to_xpath
164
+ end
165
+
166
+ private
167
+
168
+ def valid_keys
169
+ VALID_KEYS + custom_keys
170
+ end
171
+
172
+ def node_filters
173
+ if options.has_key?(:filter_set)
174
+ ::Capybara::Selector::FilterSet.all[options[:filter_set]].node_filters
175
+ else
176
+ @selector.node_filters
177
+ end
178
+ end
179
+
180
+ def expression_filters
181
+ filters = @selector.expression_filters
182
+ filters.merge ::Capybara::Selector::FilterSet.all[options[:filter_set]].expression_filters if options.has_key?(:filter_set)
183
+ filters
184
+ end
185
+
186
+ def custom_keys
187
+ @custom_keys ||= node_filters.keys + expression_filters.keys
188
+ end
189
+
190
+ def assert_valid_keys
191
+ super
192
+ unless VALID_MATCH.include?(match)
193
+ raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(", ")}"
194
+ end
195
+ end
196
+
197
+ def filtered_xpath(expr)
198
+ if options.has_key?(:id) || options.has_key?(:class)
199
+ expr = "(#{expr})"
200
+ expr = "#{expr}[#{XPath.attr(:id) == options[:id]}]" if options.has_key?(:id) && !custom_keys.include?(:id)
201
+ if options.has_key?(:class) && !custom_keys.include?(:class)
202
+ class_xpath = Array(options[:class]).map do |klass|
203
+ "contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
204
+ end.join(" and ")
205
+ expr = "#{expr}[#{class_xpath}]"
206
+ end
207
+ end
208
+ expr
209
+ end
210
+
211
+ def filtered_css(expr)
212
+ if options.has_key?(:id) || options.has_key?(:class)
213
+ css_selectors = expr.split(',').map(&:rstrip)
214
+ expr = css_selectors.map do |sel|
215
+ sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if options.has_key?(:id) && !custom_keys.include?(:id)
216
+ sel += Array(options[:class]).map { |k| ".#{Capybara::Selector::CSS.escape(k)}"}.join if options.has_key?(:class) && !custom_keys.include?(:class)
217
+ sel
218
+ end.join(", ")
219
+ end
220
+ expr
221
+ end
222
+
223
+ def apply_expression_filters(expr)
224
+ expression_filters.inject(expr) do |memo, (name, ef)|
225
+ if options.has_key?(name)
226
+ ef.apply_filter(memo, options[name])
227
+ elsif ef.default?
228
+ ef.apply_filter(memo, ef.default)
229
+ else
230
+ memo
231
+ end
232
+ end
233
+ end
234
+
235
+ def warn_exact_usage
236
+ if options.has_key?(:exact) && !supports_exact?
237
+ warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression}\" has no effect."
238
+ end
239
+ end
240
+
241
+ def exact_text
242
+ options.fetch(:exact_text, session_options.exact_text)
243
+ end
244
+
245
+ def describe_within?
246
+ @resolved_node && !(@resolved_node.is_a?(::Capybara::Node::Document) ||
247
+ (@resolved_node.is_a?(::Capybara::Node::Simple) && @resolved_node.path == '/'))
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module Capybara
3
+ module Queries
4
+ class SiblingQuery < MatchQuery
5
+ # @api private
6
+ def resolve_for(node, exact = nil)
7
+ @sibling_node = node
8
+ node.synchronize do
9
+ match_results = super(node.session.current_scope, exact)
10
+ node.all(:xpath, XPath.preceding_sibling.union(XPath.following_sibling)) do |el|
11
+ match_results.include?(el)
12
+ end
13
+ end
14
+ end
15
+
16
+ def description
17
+ desc = super
18
+ if @sibling_node && (sibling_query = @sibling_node.instance_variable_get(:@query))
19
+ desc += " that is a sibling of #{sibling_query.description}"
20
+ end
21
+ desc
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,46 +1,97 @@
1
+ # frozen_string_literal: true
1
2
  module Capybara
2
3
  # @api private
3
4
  module Queries
4
5
  class TextQuery < BaseQuery
5
6
  def initialize(*args)
6
7
  @type = (args.first.is_a?(Symbol) || args.first.nil?) ? args.shift : nil
7
- @expected_text, @options = args
8
+ # @type = (Capybara.ignore_hidden_elements or Capybara.visible_text_only) ? :visible : :all if @type.nil?
9
+ @options = if args.last.is_a?(Hash) then args.pop.dup else {} end
10
+ super(@options)
11
+
12
+ @type = (session_options.ignore_hidden_elements or session_options.visible_text_only) ? :visible : :all if @type.nil?
13
+
14
+ @expected_text = args.shift
8
15
  unless @expected_text.is_a?(Regexp)
9
16
  @expected_text = Capybara::Helpers.normalize_whitespace(@expected_text)
10
17
  end
11
- @search_regexp = Capybara::Helpers.to_regexp(@expected_text)
12
- @options ||= {}
18
+ @search_regexp = Capybara::Helpers.to_regexp(@expected_text, nil, exact?)
19
+ warn "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
13
20
  assert_valid_keys
14
21
  end
15
22
 
16
23
  def resolve_for(node)
17
- @actual_text = Capybara::Helpers.normalize_whitespace(node.text(@type))
24
+ @node = node
25
+ @actual_text = text(node, @type)
18
26
  @count = @actual_text.scan(@search_regexp).size
19
27
  end
20
28
 
21
29
  def failure_message
22
- description =
23
- if @expected_text.is_a?(Regexp)
24
- "text matching #{@expected_text.inspect}"
25
- else
26
- "text #{@expected_text.inspect}"
27
- end
30
+ super << build_message(true)
31
+ end
32
+
33
+ def negative_failure_message
34
+ super << build_message(false)
35
+ end
36
+
37
+ def description
38
+ if @expected_text.is_a?(Regexp)
39
+ "text matching #{@expected_text.inspect}"
40
+ else
41
+ "#{"exact " if exact?}text #{@expected_text.inspect}"
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def exact?
48
+ options.fetch(:exact, session_options.exact_text)
49
+ end
28
50
 
29
- message = Capybara::Helpers.failure_message(description, @options)
51
+ def build_message(report_on_invisible)
52
+ message = String.new()
30
53
  unless (COUNT_KEYS & @options.keys).empty?
31
54
  message << " but found #{@count} #{Capybara::Helpers.declension('time', 'times', @count)}"
32
55
  end
33
56
  message << " in #{@actual_text.inspect}"
57
+
58
+ details_message = []
59
+
60
+ if @node and !@expected_text.is_a? Regexp
61
+ insensitive_regexp = Capybara::Helpers.to_regexp(@expected_text, Regexp::IGNORECASE)
62
+ insensitive_count = @actual_text.scan(insensitive_regexp).size
63
+ if insensitive_count != @count
64
+ details_message << "it was found #{insensitive_count} #{Capybara::Helpers.declension("time", "times", insensitive_count)} using a case insensitive search"
65
+ end
66
+ end
67
+
68
+ if @node and check_visible_text? and report_on_invisible
69
+ begin
70
+ invisible_text = text(@node, :all)
71
+ invisible_count = invisible_text.scan(@search_regexp).size
72
+ if invisible_count != @count
73
+ details_message << "it was found #{invisible_count} #{Capybara::Helpers.declension("time", "times", invisible_count)} including non-visible text"
74
+ end
75
+ rescue
76
+ # An error getting the non-visible text (if element goes out of scope) should not affect the response
77
+ end
78
+ end
79
+
80
+ message << ". (However, #{details_message.join(' and ')}.)" unless details_message.empty?
81
+
82
+ message
34
83
  end
35
84
 
36
- def negative_failure_message
37
- failure_message.sub(/(to find)/, 'not \1')
85
+ def valid_keys
86
+ COUNT_KEYS + [:wait, :exact]
38
87
  end
39
88
 
40
- private
89
+ def check_visible_text?
90
+ @type == :visible
91
+ end
41
92
 
42
- def valid_keys
43
- COUNT_KEYS + [:wait]
93
+ def text(node, query_type)
94
+ Capybara::Helpers.normalize_whitespace(node.text(query_type))
44
95
  end
45
96
  end
46
97
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Capybara
2
3
  # @api private
3
4
  module Queries
@@ -5,10 +6,11 @@ module Capybara
5
6
  def initialize(expected_title, options = {})
6
7
  @expected_title = expected_title
7
8
  @options = options
9
+ super(@options)
8
10
  unless @expected_title.is_a?(Regexp)
9
11
  @expected_title = Capybara::Helpers.normalize_whitespace(@expected_title)
10
12
  end
11
- @search_regexp = Capybara::Helpers.to_regexp(@expected_title)
13
+ @search_regexp = Capybara::Helpers.to_regexp(@expected_title, nil, options.fetch(:exact, false))
12
14
  assert_valid_keys
13
15
  end
14
16
 
@@ -33,7 +35,7 @@ module Capybara
33
35
  end
34
36
 
35
37
  def valid_keys
36
- [:wait]
38
+ [:wait, :exact]
37
39
  end
38
40
  end
39
41
  end