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,11 +1,13 @@
1
+ # frozen_string_literal: true
1
2
  class Capybara::Selenium::Node < Capybara::Driver::Node
3
+
2
4
  def visible_text
3
5
  # Selenium doesn't normalize Unicode whitespace.
4
6
  Capybara::Helpers.normalize_whitespace(native.text)
5
7
  end
6
8
 
7
9
  def all_text
8
- text = driver.browser.execute_script("return arguments[0].textContent", native)
10
+ text = driver.execute_script("return arguments[0].textContent", self)
9
11
  Capybara::Helpers.normalize_whitespace(text)
10
12
  end
11
13
 
@@ -16,77 +18,110 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
16
18
  end
17
19
 
18
20
  def value
19
- if tag_name == "select" and self[:multiple] and not self[:multiple] == "false"
20
- native.find_elements(:xpath, ".//option").select { |n| n.selected? }.map { |n| n[:value] || n.text }
21
+ if tag_name == "select" and multiple?
22
+ native.find_elements(:css, "option:checked").map { |n| n[:value] || n.text }
21
23
  else
22
24
  native[:value]
23
25
  end
24
26
  end
25
27
 
26
- def set(value, fill_options={})
28
+ ##
29
+ #
30
+ # Set the value of the form element to the given value.
31
+ #
32
+ # @param [String] value The new value
33
+ # @param [Hash{}] options Driver specific options for how to set the value
34
+ # @option options [Symbol,Array] :clear (nil) The method used to clear the previous value <br/>
35
+ # nil => clear via javascript <br/>
36
+ # :none => append the new value to the existing value <br/>
37
+ # :backspace => send backspace keystrokes to clear the field <br/>
38
+ # Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
39
+ def set(value, options={})
27
40
  tag_name = self.tag_name
28
41
  type = self[:type]
29
- if (Array === value) && !self[:multiple]
42
+
43
+ if (Array === value) && !multiple?
30
44
  raise ArgumentError.new "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
31
45
  end
32
- if tag_name == 'input' and type == 'radio'
33
- click
34
- elsif tag_name == 'input' and type == 'checkbox'
35
- click if value ^ native.attribute('checked').to_s.eql?("true")
36
- elsif tag_name == 'input' and type == 'file'
37
- path_names = value.to_s.empty? ? [] : value
38
- native.send_keys(*path_names)
39
- elsif tag_name == 'textarea' or tag_name == 'input'
40
- if self[:readonly]
41
- warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
42
- elsif value.to_s.empty?
43
- native.clear
46
+
47
+ case tag_name
48
+ when 'input'
49
+ case type
50
+ when 'radio'
51
+ click
52
+ when 'checkbox'
53
+ click if value ^ native.attribute('checked').to_s.eql?("true")
54
+ when 'file'
55
+ path_names = value.to_s.empty? ? [] : value
56
+ if driver.chrome?
57
+ native.send_keys(Array(path_names).join("\n"))
58
+ else
59
+ native.send_keys(*path_names)
60
+ end
44
61
  else
45
- if fill_options[:clear] == :backspace
46
- # Clear field by sending the correct number of backspace keys.
47
- backspaces = [:backspace] * self.value.to_s.length
48
- native.send_keys(*(backspaces + [value.to_s]))
62
+ set_text(value, options)
63
+ end
64
+ when 'textarea'
65
+ set_text(value, options)
66
+ else
67
+ if content_editable?
68
+ #ensure we are focused on the element
69
+ click
70
+
71
+ script = <<-JS
72
+ var range = document.createRange();
73
+ var sel = window.getSelection();
74
+ arguments[0].focus();
75
+ range.selectNodeContents(arguments[0]);
76
+ sel.removeAllRanges();
77
+ sel.addRange(range);
78
+ JS
79
+ driver.execute_script script, self
80
+
81
+ if driver.chrome? || driver.firefox?
82
+ # chromedriver raises a can't focus element for child elements if we use native.send_keys
83
+ # we've already focused it so just use action api
84
+ driver.browser.action.send_keys(value.to_s).perform
49
85
  else
50
- # Clear field by JavaScript assignment of the value property.
51
- # Script can change a readonly element which user input cannot, so
52
- # don't execute if readonly.
53
- driver.browser.execute_script "arguments[0].value = ''", native
86
+ # action api is really slow here just use native.send_keys
54
87
  native.send_keys(value.to_s)
55
88
  end
56
89
  end
57
- elsif native.attribute('isContentEditable')
58
- #ensure we are focused on the element
59
- script = <<-JS
60
- var range = document.createRange();
61
- range.selectNodeContents(arguments[0]);
62
- window.getSelection().addRange(range);
63
- JS
64
- driver.browser.execute_script script, native
65
- native.send_keys(value.to_s)
66
90
  end
67
91
  end
68
92
 
69
93
  def select_option
70
- native.click unless selected?
94
+ native.click unless selected? || disabled?
71
95
  end
72
96
 
73
97
  def unselect_option
74
- if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true'
75
- raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
76
- end
98
+ raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." if !select_node.multiple?
77
99
  native.click if selected?
78
100
  end
79
101
 
80
102
  def click
81
103
  native.click
104
+ rescue => e
105
+ if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
106
+ e.message =~ /Other element would receive the click/
107
+ begin
108
+ driver.execute_script("arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'})", self)
109
+ rescue
110
+ end
111
+ end
112
+ raise e
82
113
  end
83
114
 
84
115
  def right_click
85
- driver.browser.action.context_click(native).perform
116
+ scroll_if_needed do
117
+ driver.browser.action.context_click(native).perform
118
+ end
86
119
  end
87
120
 
88
121
  def double_click
89
- driver.browser.action.double_click(native).perform
122
+ scroll_if_needed do
123
+ driver.browser.action.double_click(native).perform
124
+ end
90
125
  end
91
126
 
92
127
  def send_keys(*args)
@@ -94,11 +129,15 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
94
129
  end
95
130
 
96
131
  def hover
97
- driver.browser.action.move_to(native).perform
132
+ scroll_if_needed do
133
+ driver.browser.action.move_to(native).perform
134
+ end
98
135
  end
99
136
 
100
137
  def drag_to(element)
101
- driver.browser.action.drag_and_drop(native, element.native).perform
138
+ scroll_if_needed do
139
+ driver.browser.action.drag_and_drop(native, element.native).perform
140
+ end
102
141
  end
103
142
 
104
143
  def tag_name
@@ -114,12 +153,34 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
114
153
  selected = native.selected?
115
154
  selected and selected != "false"
116
155
  end
156
+ alias :checked? :selected?
117
157
 
118
158
  def disabled?
119
- !native.enabled?
159
+ # workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
160
+ if driver.marionette?
161
+ if %w(option optgroup).include? tag_name
162
+ !native.enabled? || find_xpath("parent::*[self::optgroup or self::select]")[0].disabled?
163
+ else
164
+ !native.enabled? || !find_xpath("parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]").empty?
165
+ end
166
+ else
167
+ !native.enabled?
168
+ end
120
169
  end
121
170
 
122
- alias :checked? :selected?
171
+ def readonly?
172
+ readonly = self[:readonly]
173
+ readonly and readonly != "false"
174
+ end
175
+
176
+ def multiple?
177
+ multiple = self[:multiple]
178
+ multiple and multiple != "false"
179
+ end
180
+
181
+ def content_editable?
182
+ native.attribute('isContentEditable')
183
+ end
123
184
 
124
185
  def find_xpath(locator)
125
186
  native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
@@ -158,9 +219,46 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
158
219
  end
159
220
 
160
221
  private
161
-
162
222
  # a reference to the select node if this is an option node
163
223
  def select_node
164
- find_xpath('./ancestor::select').first
224
+ find_xpath('./ancestor::select[1]').first
225
+ end
226
+
227
+ def set_text(value, options)
228
+ if readonly?
229
+ warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
230
+ elsif value.to_s.empty? && options[:clear].nil?
231
+ native.clear
232
+ else
233
+ if options[:clear] == :backspace
234
+ # Clear field by sending the correct number of backspace keys.
235
+ backspaces = [:backspace] * self.value.to_s.length
236
+ native.send_keys(*(backspaces + [value.to_s]))
237
+ elsif options[:clear] == :none
238
+ native.send_keys(value.to_s)
239
+ elsif options[:clear].is_a? Array
240
+ native.send_keys(*options[:clear], value.to_s)
241
+ else
242
+ # Clear field by JavaScript assignment of the value property.
243
+ # Script can change a readonly element which user input cannot, so
244
+ # don't execute if readonly.
245
+ driver.execute_script "arguments[0].value = ''", self
246
+ native.send_keys(value.to_s)
247
+ end
248
+ end
249
+ end
250
+
251
+ def scroll_if_needed(&block)
252
+ block.call
253
+ rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
254
+ script = <<-JS
255
+ try {
256
+ arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
257
+ } catch(e) {
258
+ arguments[0].scrollIntoView(true);
259
+ }
260
+ JS
261
+ driver.execute_script(script, self)
262
+ block.call
165
263
  end
166
264
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'uri'
2
3
  require 'net/http'
3
4
  require 'rack'
@@ -5,21 +6,47 @@ require 'rack'
5
6
  module Capybara
6
7
  class Server
7
8
  class Middleware
9
+ class Counter
10
+ attr_reader :value
11
+
12
+ def initialize
13
+ @value = 0
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ def increment
18
+ @mutex.synchronize { @value += 1 }
19
+ end
20
+
21
+ def decrement
22
+ @mutex.synchronize { @value -= 1 }
23
+ end
24
+ end
25
+
8
26
  attr_accessor :error
9
27
 
10
- def initialize(app)
28
+ def initialize(app, server_errors)
11
29
  @app = app
30
+ @counter = Counter.new
31
+ @server_errors = server_errors
32
+ end
33
+
34
+ def pending_requests?
35
+ @counter.value > 0
12
36
  end
13
37
 
14
38
  def call(env)
15
39
  if env["PATH_INFO"] == "/__identify__"
16
40
  [200, {}, [@app.object_id.to_s]]
17
41
  else
42
+ @counter.increment
18
43
  begin
19
44
  @app.call(env)
20
- rescue *Capybara.server_errors => e
45
+ rescue *@server_errors => e
21
46
  @error = e unless @error
22
47
  raise e
48
+ ensure
49
+ @counter.decrement
23
50
  end
24
51
  end
25
52
  end
@@ -33,41 +60,46 @@ module Capybara
33
60
 
34
61
  attr_reader :app, :port, :host
35
62
 
36
- def initialize(app, port=Capybara.server_port, host=Capybara.server_host)
63
+ def initialize(app, port=Capybara.server_port, host=Capybara.server_host, server_errors=Capybara.server_errors)
37
64
  @app = app
38
- @middleware = Middleware.new(@app)
39
65
  @server_thread = nil # suppress warnings
40
- @host, @port = host, port
41
- @port ||= Capybara::Server.ports[@app.object_id]
42
- @port ||= find_available_port
66
+ @host, @port, @server_errors = host, port, server_errors
67
+ @port ||= Capybara::Server.ports[port_key]
68
+ @port ||= find_available_port(host)
43
69
  end
44
70
 
45
71
  def reset_error!
46
- @middleware.error = nil
72
+ middleware.error = nil
47
73
  end
48
74
 
49
75
  def error
50
- @middleware.error
76
+ middleware.error
51
77
  end
52
78
 
53
79
  def responsive?
54
80
  return false if @server_thread && @server_thread.join(0)
55
81
 
56
- res = Net::HTTP.start(host, @port) { |http| http.get('/__identify__') }
82
+ res = Net::HTTP.start(host, port) { |http| http.get('/__identify__') }
57
83
 
58
84
  if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection)
59
- return res.body == @app.object_id.to_s
85
+ return res.body == app.object_id.to_s
60
86
  end
61
87
  rescue SystemCallError
62
88
  return false
63
89
  end
64
90
 
91
+ def wait_for_pending_requests
92
+ Timeout.timeout(60) { sleep(0.01) while pending_requests? }
93
+ rescue Timeout::Error
94
+ raise "Requests did not finish in 60 seconds"
95
+ end
96
+
65
97
  def boot
66
98
  unless responsive?
67
- Capybara::Server.ports[@app.object_id] = @port
99
+ Capybara::Server.ports[port_key] = port
68
100
 
69
101
  @server_thread = Thread.new do
70
- Capybara.server.call(@middleware, @port)
102
+ Capybara.server.call(middleware, port, host)
71
103
  end
72
104
 
73
105
  Timeout.timeout(60) { @server_thread.join(0.1) until responsive? }
@@ -80,12 +112,23 @@ module Capybara
80
112
 
81
113
  private
82
114
 
83
- def find_available_port
84
- server = TCPServer.new('127.0.0.1', 0)
115
+ def middleware
116
+ @middleware ||= Middleware.new(app, @server_errors)
117
+ end
118
+
119
+ def port_key
120
+ Capybara.reuse_server ? app.object_id : middleware.object_id
121
+ end
122
+
123
+ def pending_requests?
124
+ middleware.pending_requests?
125
+ end
126
+
127
+ def find_available_port(host)
128
+ server = TCPServer.new(host, 0)
85
129
  server.addr[1]
86
130
  ensure
87
131
  server.close if server
88
132
  end
89
-
90
133
  end
91
134
  end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require 'delegate'
3
+
4
+ module Capybara
5
+ class SessionConfig
6
+ OPTIONS = [:always_include_port, :run_server, :default_selector, :default_max_wait_time, :ignore_hidden_elements,
7
+ :automatic_reload, :match, :exact, :exact_text, :raise_server_errors, :visible_text_only, :wait_on_first_by_default,
8
+ :automatic_label_click, :enable_aria_label, :save_path, :exact_options, :asset_host, :default_host, :app_host,
9
+ :save_and_open_page_path, :server_host, :server_port, :server_errors]
10
+
11
+ attr_accessor(*OPTIONS)
12
+
13
+ ##
14
+ #@!method always_include_port
15
+ # See {Capybara.configure}
16
+ #@!method run_server
17
+ # See {Capybara.configure}
18
+ #@!method default_selector
19
+ # See {Capybara.configure}
20
+ #@!method default_max_wait_time
21
+ # See {Capybara.configure}
22
+ #@!method ignore_hidden_elements
23
+ # See {Capybara.configure}
24
+ #@!method automatic_reload
25
+ # See {Capybara.configure}
26
+ #@!method match
27
+ # See {Capybara.configure}
28
+ #@!method exact
29
+ # See {Capybara.configure}
30
+ #@!method raise_server_errors
31
+ # See {Capybara.configure}
32
+ #@!method visible_text_only
33
+ # See {Capybara.configure}
34
+ #@!method wait_on_first_by_default
35
+ # See {Capybara.configure}
36
+ #@!method automatic_label_click
37
+ # See {Capybara.configure}
38
+ #@!method enable_aria_label
39
+ # See {Capybara.configure}
40
+ #@!method save_path
41
+ # See {Capybara.configure}
42
+ #@deprecated
43
+ #@!method exact_options
44
+ # See {Capybara.configure}
45
+ #@!method asset_host
46
+ # See {Capybara.configure}
47
+ #@!method default_host
48
+ # See {Capybara.configure}
49
+ #@!method app_host
50
+ # See {Capybara.configure}
51
+ #@!method save_and_open_page_path
52
+ # See {Capybara.configure}
53
+ #@!method server_host
54
+ # See {Capybara.configure}
55
+ #@!method server_port
56
+ # See {Capybara.configure}
57
+ #@!method server_errors
58
+ # See {Capybara.configure}
59
+
60
+ remove_method :server_host
61
+
62
+ ##
63
+ #
64
+ # @return [String] The IP address bound by default server
65
+ #
66
+ def server_host
67
+ @server_host || '127.0.0.1'
68
+ end
69
+
70
+ remove_method :server_errors=
71
+ def server_errors=(errors)
72
+ (@server_errors ||= []).replace(errors.dup)
73
+ end
74
+
75
+ remove_method :app_host=
76
+ def app_host=(url)
77
+ raise ArgumentError.new("Capybara.app_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
78
+ @app_host = url
79
+ end
80
+
81
+ remove_method :default_host=
82
+ def default_host=(url)
83
+ raise ArgumentError.new("Capybara.default_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
84
+ @default_host = url
85
+ end
86
+
87
+ remove_method :save_and_open_page_path=
88
+ def save_and_open_page_path=(path)
89
+ warn "DEPRECATED: #save_and_open_page_path is deprecated, please use #save_path instead. \n"\
90
+ "Note: Behavior is slightly different with relative paths - see documentation" unless path.nil?
91
+ @save_and_open_page_path = path
92
+ end
93
+
94
+ remove_method :exact_options=
95
+ def exact_options=(opt)
96
+ @exact_options = opt
97
+ warn "DEPRECATED: #exact_options is deprecated, please scope your findes/actions and use the `:exact` "\
98
+ "option if similar functionality is needed."
99
+ end
100
+
101
+ def initialize_copy(other)
102
+ super
103
+ @server_errors = @server_errors.dup
104
+ end
105
+ end
106
+
107
+ class ReadOnlySessionConfig < SimpleDelegator
108
+ SessionConfig::OPTIONS.each do |m|
109
+ define_method "#{m}=" do |val|
110
+ raise "Per session settings are only supported when Capybara.threadsafe == true"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,49 +1,46 @@
1
+ # frozen_string_literal: true
1
2
  module Capybara
2
3
  module SessionMatchers
3
4
  ##
4
5
  # Asserts that the page has the given path.
5
- # By default this will compare against the path+query portion of the full url
6
+ # By default, if passed a full url this will compare against the full url,
7
+ # if passed a path only the path+query portion will be compared, if passed a regexp
8
+ # the comparison will depend on the :url option
6
9
  #
7
10
  # @!macro current_path_query_params
8
11
  # @overload $0(string, options = {})
9
12
  # @param string [String] The string that the current 'path' should equal
10
13
  # @overload $0(regexp, options = {})
11
14
  # @param regexp [Regexp] The regexp that the current 'path' should match to
12
- # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for the current path to eq/match given string/regexp argument
13
- # @option options [Boolean] :url (false) Whether the compare should be done against the full url
14
- # @option options [Boolean] :only_path (false) Whether the compare should be done against just the path protion of the url
15
+ # @option options [Boolean] :url (true if `string` ia a full url, otherwise false) Whether the compare should be done against the full current url or just the path
16
+ # @option options [Boolean] :ignore_query (false) Whether the query portion of the current url/path should be ignored
17
+ # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for the current url/path to eq/match given string/regexp argument
15
18
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
16
19
  # @return [true]
17
20
  #
18
21
  def assert_current_path(path, options={})
19
- query = Capybara::Queries::CurrentPathQuery.new(path, options)
20
- document.synchronize(query.wait) do
21
- unless query.resolves_for?(self)
22
- raise Capybara::ExpectationNotMet, query.failure_message
23
- end
24
- end
25
- return true
22
+ _verify_current_path(path,options) { |query| raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self) }
26
23
  end
27
24
 
28
25
  ##
29
26
  # Asserts that the page doesn't have the given path.
27
+ # By default, if passed a full url this will compare against the full url,
28
+ # if passed a path only the path+query portion will be compared, if passed a regexp
29
+ # the comparison will depend on the :url option
30
30
  #
31
31
  # @macro current_path_query_params
32
32
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
33
33
  # @return [true]
34
34
  #
35
35
  def assert_no_current_path(path, options={})
36
- query = Capybara::Queries::CurrentPathQuery.new(path, options)
37
- document.synchronize(query.wait) do
38
- if query.resolves_for?(self)
39
- raise Capybara::ExpectationNotMet, query.negative_failure_message
40
- end
41
- end
42
- return true
36
+ _verify_current_path(path,options) { |query| raise Capybara::ExpectationNotMet, query.negative_failure_message if query.resolves_for?(self) }
43
37
  end
44
38
 
45
39
  ##
46
40
  # Checks if the page has the given path.
41
+ # By default, if passed a full url this will compare against the full url,
42
+ # if passed a path only the path+query portion will be compared, if passed a regexp
43
+ # the comparison will depend on the :url option
47
44
  #
48
45
  # @macro current_path_query_params
49
46
  # @return [Boolean]
@@ -56,6 +53,9 @@ module Capybara
56
53
 
57
54
  ##
58
55
  # Checks if the page doesn't have the given path.
56
+ # By default, if passed a full url this will compare against the full url,
57
+ # if passed a path only the path+query portion will be compared, if passed a regexp
58
+ # the comparison will depend on the :url option
59
59
  #
60
60
  # @macro current_path_query_params
61
61
  # @return [Boolean]
@@ -65,5 +65,15 @@ module Capybara
65
65
  rescue Capybara::ExpectationNotMet
66
66
  return false
67
67
  end
68
+
69
+ private
70
+
71
+ def _verify_current_path(path, options)
72
+ query = Capybara::Queries::CurrentPathQuery.new(path, options)
73
+ document.synchronize(query.wait) do
74
+ yield(query)
75
+ end
76
+ return true
77
+ end
68
78
  end
69
- end
79
+ end