capybara 3.13.2 → 3.40.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 (260) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +587 -16
  4. data/README.md +240 -90
  5. data/lib/capybara/config.rb +24 -11
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +8 -0
  8. data/lib/capybara/driver/node.rb +20 -4
  9. data/lib/capybara/dsl.rb +5 -3
  10. data/lib/capybara/helpers.rb +25 -4
  11. data/lib/capybara/minitest/spec.rb +174 -90
  12. data/lib/capybara/minitest.rb +256 -142
  13. data/lib/capybara/node/actions.rb +123 -77
  14. data/lib/capybara/node/base.rb +20 -12
  15. data/lib/capybara/node/document.rb +2 -2
  16. data/lib/capybara/node/document_matchers.rb +3 -3
  17. data/lib/capybara/node/element.rb +223 -117
  18. data/lib/capybara/node/finders.rb +81 -71
  19. data/lib/capybara/node/matchers.rb +271 -134
  20. data/lib/capybara/node/simple.rb +18 -5
  21. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  22. data/lib/capybara/queries/active_element_query.rb +18 -0
  23. data/lib/capybara/queries/ancestor_query.rb +8 -9
  24. data/lib/capybara/queries/base_query.rb +3 -2
  25. data/lib/capybara/queries/current_path_query.rb +15 -5
  26. data/lib/capybara/queries/selector_query.rb +364 -54
  27. data/lib/capybara/queries/sibling_query.rb +8 -6
  28. data/lib/capybara/queries/style_query.rb +2 -2
  29. data/lib/capybara/queries/text_query.rb +13 -1
  30. data/lib/capybara/queries/title_query.rb +1 -1
  31. data/lib/capybara/rack_test/browser.rb +76 -11
  32. data/lib/capybara/rack_test/driver.rb +10 -5
  33. data/lib/capybara/rack_test/errors.rb +6 -0
  34. data/lib/capybara/rack_test/form.rb +31 -9
  35. data/lib/capybara/rack_test/node.rb +74 -23
  36. data/lib/capybara/registration_container.rb +41 -0
  37. data/lib/capybara/registrations/drivers.rb +42 -0
  38. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  39. data/lib/capybara/registrations/servers.rb +66 -0
  40. data/lib/capybara/result.rb +44 -20
  41. data/lib/capybara/rspec/matcher_proxies.rb +13 -11
  42. data/lib/capybara/rspec/matchers/base.rb +31 -16
  43. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  44. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  45. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  46. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  47. data/lib/capybara/rspec/matchers/have_selector.rb +21 -21
  48. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  49. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  50. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  51. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  52. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  53. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  54. data/lib/capybara/rspec/matchers.rb +111 -68
  55. data/lib/capybara/rspec.rb +2 -0
  56. data/lib/capybara/selector/builders/css_builder.rb +11 -7
  57. data/lib/capybara/selector/builders/xpath_builder.rb +5 -3
  58. data/lib/capybara/selector/css.rb +11 -9
  59. data/lib/capybara/selector/definition/button.rb +68 -0
  60. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  61. data/lib/capybara/selector/definition/css.rb +10 -0
  62. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  63. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  64. data/lib/capybara/selector/definition/element.rb +28 -0
  65. data/lib/capybara/selector/definition/field.rb +40 -0
  66. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  67. data/lib/capybara/selector/definition/file_field.rb +13 -0
  68. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  69. data/lib/capybara/selector/definition/frame.rb +17 -0
  70. data/lib/capybara/selector/definition/id.rb +6 -0
  71. data/lib/capybara/selector/definition/label.rb +62 -0
  72. data/lib/capybara/selector/definition/link.rb +55 -0
  73. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  74. data/lib/capybara/selector/definition/option.rb +27 -0
  75. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  76. data/lib/capybara/selector/definition/select.rb +81 -0
  77. data/lib/capybara/selector/definition/table.rb +109 -0
  78. data/lib/capybara/selector/definition/table_row.rb +21 -0
  79. data/lib/capybara/selector/definition/xpath.rb +5 -0
  80. data/lib/capybara/selector/definition.rb +280 -0
  81. data/lib/capybara/selector/filter_set.rb +19 -18
  82. data/lib/capybara/selector/filters/base.rb +11 -2
  83. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  84. data/lib/capybara/selector/regexp_disassembler.rb +11 -7
  85. data/lib/capybara/selector/selector.rb +50 -440
  86. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  87. data/lib/capybara/selector.rb +473 -482
  88. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  89. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  90. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  91. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  92. data/lib/capybara/selenium/driver.rb +174 -62
  93. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +74 -18
  94. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  95. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +37 -3
  96. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  97. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  98. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  99. data/lib/capybara/selenium/extensions/find.rb +68 -45
  100. data/lib/capybara/selenium/extensions/html5_drag.rb +192 -22
  101. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  102. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  103. data/lib/capybara/selenium/node.rb +268 -72
  104. data/lib/capybara/selenium/nodes/chrome_node.rb +105 -9
  105. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  106. data/lib/capybara/selenium/nodes/firefox_node.rb +51 -61
  107. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  108. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  109. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  110. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  111. data/lib/capybara/selenium/patches/logs.rb +45 -0
  112. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  113. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  114. data/lib/capybara/server/animation_disabler.rb +43 -21
  115. data/lib/capybara/server/checker.rb +6 -2
  116. data/lib/capybara/server/middleware.rb +25 -13
  117. data/lib/capybara/server.rb +20 -4
  118. data/lib/capybara/session/config.rb +15 -11
  119. data/lib/capybara/session/matchers.rb +11 -11
  120. data/lib/capybara/session.rb +162 -131
  121. data/lib/capybara/spec/public/offset.js +6 -0
  122. data/lib/capybara/spec/public/test.js +105 -6
  123. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  124. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  125. data/lib/capybara/spec/session/all_spec.rb +89 -15
  126. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  127. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  128. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  129. data/lib/capybara/spec/session/attach_file_spec.rb +64 -31
  130. data/lib/capybara/spec/session/check_spec.rb +26 -4
  131. data/lib/capybara/spec/session/choose_spec.rb +14 -2
  132. data/lib/capybara/spec/session/click_button_spec.rb +109 -61
  133. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  134. data/lib/capybara/spec/session/click_link_spec.rb +23 -1
  135. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  136. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  137. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  138. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  139. data/lib/capybara/spec/session/fill_in_spec.rb +46 -5
  140. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  141. data/lib/capybara/spec/session/find_spec.rb +80 -7
  142. data/lib/capybara/spec/session/first_spec.rb +2 -2
  143. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  144. data/lib/capybara/spec/session/frame/within_frame_spec.rb +14 -1
  145. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  146. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  147. data/lib/capybara/spec/session/has_any_selectors_spec.rb +6 -2
  148. data/lib/capybara/spec/session/has_button_spec.rb +81 -0
  149. data/lib/capybara/spec/session/has_css_spec.rb +45 -8
  150. data/lib/capybara/spec/session/has_current_path_spec.rb +22 -7
  151. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  152. data/lib/capybara/spec/session/has_field_spec.rb +59 -1
  153. data/lib/capybara/spec/session/has_link_spec.rb +40 -0
  154. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  155. data/lib/capybara/spec/session/has_select_spec.rb +42 -8
  156. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  157. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  158. data/lib/capybara/spec/session/has_table_spec.rb +177 -0
  159. data/lib/capybara/spec/session/has_text_spec.rb +31 -3
  160. data/lib/capybara/spec/session/html_spec.rb +1 -1
  161. data/lib/capybara/spec/session/matches_style_spec.rb +6 -4
  162. data/lib/capybara/spec/session/node_spec.rb +697 -23
  163. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  164. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  165. data/lib/capybara/spec/session/reset_session_spec.rb +21 -7
  166. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  167. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  168. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  169. data/lib/capybara/spec/session/scroll_spec.rb +9 -7
  170. data/lib/capybara/spec/session/select_spec.rb +5 -10
  171. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  172. data/lib/capybara/spec/session/uncheck_spec.rb +3 -3
  173. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  174. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  175. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  176. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  177. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  178. data/lib/capybara/spec/session/window/window_spec.rb +54 -57
  179. data/lib/capybara/spec/session/window/windows_spec.rb +2 -2
  180. data/lib/capybara/spec/session/within_spec.rb +36 -0
  181. data/lib/capybara/spec/spec_helper.rb +30 -19
  182. data/lib/capybara/spec/test_app.rb +122 -34
  183. data/lib/capybara/spec/views/animated.erb +49 -0
  184. data/lib/capybara/spec/views/form.erb +86 -8
  185. data/lib/capybara/spec/views/frame_child.erb +3 -2
  186. data/lib/capybara/spec/views/frame_one.erb +2 -1
  187. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  188. data/lib/capybara/spec/views/frame_two.erb +1 -1
  189. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  190. data/lib/capybara/spec/views/layout.erb +10 -0
  191. data/lib/capybara/spec/views/obscured.erb +10 -10
  192. data/lib/capybara/spec/views/offset.erb +33 -0
  193. data/lib/capybara/spec/views/path.erb +2 -2
  194. data/lib/capybara/spec/views/popup_one.erb +1 -1
  195. data/lib/capybara/spec/views/popup_two.erb +1 -1
  196. data/lib/capybara/spec/views/react.erb +45 -0
  197. data/lib/capybara/spec/views/scroll.erb +2 -1
  198. data/lib/capybara/spec/views/spatial.erb +31 -0
  199. data/lib/capybara/spec/views/tables.erb +67 -0
  200. data/lib/capybara/spec/views/with_animation.erb +39 -4
  201. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  202. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  203. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  204. data/lib/capybara/spec/views/with_hover.erb +3 -2
  205. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  206. data/lib/capybara/spec/views/with_html.erb +34 -6
  207. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  208. data/lib/capybara/spec/views/with_js.erb +7 -4
  209. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  210. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  211. data/lib/capybara/spec/views/with_scope.erb +2 -2
  212. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  213. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  214. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  215. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  216. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  217. data/lib/capybara/spec/views/with_windows.erb +1 -1
  218. data/lib/capybara/spec/views/within_frames.erb +1 -1
  219. data/lib/capybara/version.rb +1 -1
  220. data/lib/capybara/window.rb +14 -18
  221. data/lib/capybara.rb +91 -126
  222. data/spec/basic_node_spec.rb +30 -16
  223. data/spec/capybara_spec.rb +40 -28
  224. data/spec/counter_spec.rb +35 -0
  225. data/spec/css_builder_spec.rb +3 -1
  226. data/spec/css_splitter_spec.rb +1 -1
  227. data/spec/dsl_spec.rb +33 -22
  228. data/spec/filter_set_spec.rb +5 -5
  229. data/spec/fixtures/selenium_driver_rspec_failure.rb +3 -3
  230. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  231. data/spec/minitest_spec.rb +24 -2
  232. data/spec/minitest_spec_spec.rb +60 -45
  233. data/spec/per_session_config_spec.rb +1 -1
  234. data/spec/rack_test_spec.rb +131 -98
  235. data/spec/regexp_dissassembler_spec.rb +53 -39
  236. data/spec/result_spec.rb +68 -66
  237. data/spec/rspec/features_spec.rb +9 -4
  238. data/spec/rspec/scenarios_spec.rb +6 -2
  239. data/spec/rspec/shared_spec_matchers.rb +137 -98
  240. data/spec/rspec_matchers_spec.rb +25 -0
  241. data/spec/rspec_spec.rb +23 -21
  242. data/spec/sauce_spec_chrome.rb +43 -0
  243. data/spec/selector_spec.rb +77 -21
  244. data/spec/selenium_spec_chrome.rb +141 -39
  245. data/spec/selenium_spec_chrome_remote.rb +32 -17
  246. data/spec/selenium_spec_edge.rb +36 -8
  247. data/spec/selenium_spec_firefox.rb +110 -68
  248. data/spec/selenium_spec_firefox_remote.rb +22 -15
  249. data/spec/selenium_spec_ie.rb +29 -22
  250. data/spec/selenium_spec_safari.rb +162 -0
  251. data/spec/server_spec.rb +153 -81
  252. data/spec/session_spec.rb +11 -4
  253. data/spec/shared_selenium_node.rb +79 -0
  254. data/spec/shared_selenium_session.rb +179 -74
  255. data/spec/spec_helper.rb +80 -5
  256. data/spec/whitespace_normalizer_spec.rb +54 -0
  257. data/spec/xpath_builder_spec.rb +3 -1
  258. metadata +218 -30
  259. data/lib/capybara/spec/session/source_spec.rb +0 -0
  260. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Note: This file uses `sleep` to sync up parts of the tests. This is only implemented like this
3
+ # NOTE: This file uses `sleep` to sync up parts of the tests. This is only implemented like this
4
4
  # because of the methods being tested. In tests using Capybara this type of behavior should be implemented
5
5
  # using Capybara provided assertions with builtin waiting behavior.
6
6
 
@@ -25,9 +25,9 @@ Capybara::SpecHelper.spec 'node' do
25
25
 
26
26
  describe '#query_scope' do
27
27
  it 'should have a reference to the element the query was evaluated on if there is one' do
28
- @node = @session.find(:css, '#first')
29
- expect(@node.query_scope).to eq(@node.session.document)
30
- expect(@node.find(:css, '#foo').query_scope).to eq(@node)
28
+ node = @session.find(:css, '#first')
29
+ expect(node.query_scope).to eq(node.session.document)
30
+ expect(node.find(:css, '#foo').query_scope).to eq(node)
31
31
  end
32
32
  end
33
33
 
@@ -109,12 +109,24 @@ Capybara::SpecHelper.spec 'node' do
109
109
  expect(@session.first('//input').value).to eq('')
110
110
  end
111
111
 
112
- it 'should raise if the text field is readonly' do
113
- expect { @session.first('//input[@readonly]').set('changed') }.to raise_error(Capybara::ReadOnlyElementError)
114
- end
112
+ if ENV['CAPYBARA_THOROUGH']
113
+ it 'should raise if the text field is readonly' do
114
+ expect { @session.first('//input[@readonly]').set('changed') }.to raise_error(Capybara::ReadOnlyElementError)
115
+ end
116
+
117
+ it 'should raise if the textarea is readonly' do
118
+ expect { @session.first('//textarea[@readonly]').set('changed') }.to raise_error(Capybara::ReadOnlyElementError)
119
+ end
120
+ else
121
+ it 'should not change if the text field is readonly' do
122
+ @session.first('//input[@readonly]').set('changed')
123
+ expect(@session.first('//input[@readonly]').value).to eq 'should not change'
124
+ end
115
125
 
116
- it 'should raise if the textarea is readonly' do
117
- expect { @session.first('//textarea[@readonly]').set('changed') }.to raise_error(Capybara::ReadOnlyElementError)
126
+ it 'should not change if the textarea is readonly' do
127
+ @session.first('//textarea[@readonly]').set('changed')
128
+ expect(@session.first('//textarea[@readonly]').value).to eq 'textarea should not change'
129
+ end
118
130
  end
119
131
 
120
132
  it 'should use global default options' do
@@ -145,6 +157,18 @@ Capybara::SpecHelper.spec 'node' do
145
157
  expect(@session.find(:css, '#existing_content_editable_child_parent').text).to eq("Some content\nWYSIWYG")
146
158
  end
147
159
  end
160
+
161
+ it 'should submit single text input forms if ended with \n' do
162
+ @session.visit('/form')
163
+ @session.find(:css, '#single_input').set("my entry\n")
164
+ expect(extract_results(@session)['single_input']).to eq('my entry')
165
+ end
166
+
167
+ it 'should not submit single text input forms if ended with \n and has multiple values' do
168
+ @session.visit('/form')
169
+ @session.find(:css, '#two_input_1').set("my entry\n")
170
+ expect(@session.find(:css, '#two_input_1').value).to eq("my entry\n").or(eq 'my entry')
171
+ end
148
172
  end
149
173
 
150
174
  describe '#tag_name' do
@@ -183,7 +207,7 @@ Capybara::SpecHelper.spec 'node' do
183
207
 
184
208
  it 'should see a disabled fieldset as disabled' do
185
209
  @session.visit('/form')
186
- expect(@session.find(:css, '#form_disabled_fieldset')).to be_disabled
210
+ expect(@session.find(:xpath, './/fieldset[@id="form_disabled_fieldset"]')).to be_disabled
187
211
  end
188
212
 
189
213
  context 'in a disabled fieldset' do
@@ -221,8 +245,9 @@ Capybara::SpecHelper.spec 'node' do
221
245
  end
222
246
 
223
247
  describe '#visible?' do
248
+ before { Capybara.ignore_hidden_elements = false }
249
+
224
250
  it 'should extract node visibility' do
225
- Capybara.ignore_hidden_elements = false
226
251
  expect(@session.first('//a')).to be_visible
227
252
 
228
253
  expect(@session.find('//div[@id="hidden"]')).not_to be_visible
@@ -232,11 +257,113 @@ Capybara::SpecHelper.spec 'node' do
232
257
  expect(@session.find('//input[@id="hidden_input"]')).not_to be_visible
233
258
  end
234
259
 
260
+ it 'template elements should not be visible' do
261
+ expect(@session.find('//template')).not_to be_visible
262
+ end
263
+
235
264
  it 'should be boolean' do
236
- Capybara.ignore_hidden_elements = false
237
265
  expect(@session.first('//a').visible?).to be true
238
266
  expect(@session.find('//div[@id="hidden"]').visible?).to be false
239
267
  end
268
+
269
+ it 'closed details > summary elements and descendants should be visible' do
270
+ expect(@session.find(:css, '#closed_details summary')).to be_visible
271
+ expect(@session.find(:css, '#closed_details summary h6')).to be_visible
272
+ end
273
+
274
+ it 'details non-summary descendants should be non-visible when closed' do
275
+ descendants = @session.all(:css, '#closed_details > *:not(summary), #closed_details > *:not(summary) *', minimum: 2)
276
+ expect(descendants).not_to include(be_visible)
277
+ end
278
+
279
+ it 'deatils descendants should be visible when open' do
280
+ descendants = @session.all(:css, '#open_details *')
281
+ expect(descendants).to all(be_visible)
282
+ end
283
+
284
+ it 'works when details is toggled open and closed' do
285
+ @session.find(:css, '#closed_details > summary').click
286
+ expect(@session).to have_css('#closed_details *', visible: :visible, count: 5)
287
+ .and(have_no_css('#closed_details *', visible: :hidden))
288
+
289
+ @session.find(:css, '#closed_details > summary').click
290
+ descendants_css = '#closed_details > *:not(summary), #closed_details > *:not(summary) *'
291
+ expect(@session).to have_no_css(descendants_css, visible: :visible)
292
+ .and(have_css(descendants_css, visible: :hidden, count: 3))
293
+ end
294
+ end
295
+
296
+ describe '#obscured?', requires: [:css] do
297
+ it 'should see non visible elements as obscured' do
298
+ Capybara.ignore_hidden_elements = false
299
+ expect(@session.find('//div[@id="hidden"]')).to be_obscured
300
+ expect(@session.find('//div[@id="hidden_via_ancestor"]')).to be_obscured
301
+ expect(@session.find('//div[@id="hidden_attr"]')).to be_obscured
302
+ expect(@session.find('//a[@id="hidden_attr_via_ancestor"]')).to be_obscured
303
+ expect(@session.find('//input[@id="hidden_input"]')).to be_obscured
304
+ end
305
+
306
+ it 'should see non-overlapped elements as not obscured' do
307
+ @session.visit('/obscured')
308
+ expect(@session.find(:css, '#cover')).not_to be_obscured
309
+ end
310
+
311
+ it 'should see elements only overlapped by descendants as not obscured' do
312
+ expect(@session.first(:css, 'p:not(.para)')).not_to be_obscured
313
+ end
314
+
315
+ it 'should see elements outside the viewport as obscured' do
316
+ @session.visit('/obscured')
317
+ off = @session.find(:css, '#offscreen')
318
+ off_wrapper = @session.find(:css, '#offscreen_wrapper')
319
+ expect(off).to be_obscured
320
+ expect(off_wrapper).to be_obscured
321
+ @session.scroll_to(off_wrapper)
322
+ expect(off_wrapper).not_to be_obscured
323
+ expect(off).to be_obscured
324
+ off_wrapper.scroll_to(off)
325
+ expect(off).not_to be_obscured
326
+ expect(off_wrapper).not_to be_obscured
327
+ end
328
+
329
+ it 'should see overlapped elements as obscured' do
330
+ @session.visit('/obscured')
331
+ expect(@session.find(:css, '#obscured')).to be_obscured
332
+ end
333
+
334
+ it 'should be boolean' do
335
+ Capybara.ignore_hidden_elements = false
336
+ expect(@session.first('//a').obscured?).to be false
337
+ expect(@session.find('//div[@id="hidden"]').obscured?).to be true
338
+ end
339
+
340
+ it 'should work in frames' do
341
+ @session.visit('/obscured')
342
+ frame = @session.find(:css, '#frameOne')
343
+ @session.within_frame(frame) do
344
+ div = @session.find(:css, '#divInFrameOne')
345
+ expect(div).to be_obscured
346
+ @session.scroll_to div
347
+ expect(div).not_to be_obscured
348
+ end
349
+ end
350
+
351
+ it 'should work in nested iframes' do
352
+ @session.visit('/obscured')
353
+ frame = @session.find(:css, '#nestedFrames')
354
+ @session.within_frame(frame) do
355
+ @session.within_frame(:css, '#childFrame') do
356
+ gcframe = @session.find(:css, '#grandchildFrame2')
357
+ @session.within_frame(gcframe) do
358
+ expect(@session.find(:css, '#divInFrameTwo')).to be_obscured
359
+ end
360
+ @session.scroll_to(gcframe)
361
+ @session.within_frame(gcframe) do
362
+ expect(@session.find(:css, '#divInFrameTwo')).not_to be_obscured
363
+ end
364
+ end
365
+ end
366
+ end
240
367
  end
241
368
 
242
369
  describe '#checked?' do
@@ -273,8 +400,8 @@ Capybara::SpecHelper.spec 'node' do
273
400
 
274
401
  describe '#==' do
275
402
  it 'preserve object identity' do
276
- expect(@session.find('//h1') == @session.find('//h1')).to be true # rubocop:disable Lint/UselessComparison
277
- expect(@session.find('//h1') === @session.find('//h1')).to be true # rubocop:disable Style/CaseEquality, Lint/UselessComparison
403
+ expect(@session.find('//h1') == @session.find('//h1')).to be true # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
404
+ expect(@session.find('//h1') === @session.find('//h1')).to be true # rubocop:disable Style/CaseEquality, Lint/BinaryOperatorWithIdenticalOperands
278
405
  expect(@session.find('//h1').eql?(@session.find('//h1'))).to be false
279
406
  end
280
407
 
@@ -294,6 +421,17 @@ Capybara::SpecHelper.spec 'node' do
294
421
  element = @session.find(:link, 'Second Link')
295
422
  expect(@session.find(:xpath, element.path)).to eq(element)
296
423
  end
424
+
425
+ it 'reports when element in shadow dom', requires: [:shadow_dom] do
426
+ @session.visit('/with_js')
427
+ shadow = @session.find(:css, '#shadow')
428
+ element = @session.evaluate_script(<<~JS, shadow)
429
+ (function(root){
430
+ return root.shadowRoot.querySelector('span');
431
+ })(arguments[0])
432
+ JS
433
+ expect(element.path).to eq '(: Shadow DOM element - no XPath :)'
434
+ end
297
435
  end
298
436
 
299
437
  describe '#trigger', requires: %i[js trigger] do
@@ -328,6 +466,260 @@ Capybara::SpecHelper.spec 'node' do
328
466
  link.drag_to target
329
467
  expect(@session).to have_xpath('//div[contains(., "Dropped!")]')
330
468
  end
469
+
470
+ it 'should work with Dragula' do
471
+ @session.visit('/with_dragula')
472
+ @session.within(:css, '#sortable.ready') do
473
+ src = @session.find('div', text: 'Item 1')
474
+ target = @session.find('div', text: 'Item 3')
475
+ src.drag_to target
476
+ expect(@session).to have_content(/Item 2.*Item 1/, normalize_ws: true)
477
+ end
478
+ end
479
+
480
+ it 'should work with jsTree' do
481
+ @session.visit('/with_jstree')
482
+ @session.within(:css, '#container') do
483
+ @session.assert_text(/A.*B.*C/m)
484
+ source = @session.find(:css, '#j1_1_anchor')
485
+ target = @session.find(:css, '#j1_2_anchor')
486
+
487
+ source.drag_to(target)
488
+
489
+ @session.assert_no_text(/A.*B.*C/m)
490
+ @session.assert_text(/B.*C/m)
491
+ end
492
+ end
493
+
494
+ it 'should simulate a single held down modifier key' do
495
+ %I[
496
+ alt
497
+ ctrl
498
+ meta
499
+ shift
500
+ ].each do |modifier_key|
501
+ @session.visit('/with_js')
502
+
503
+ element = @session.find('//div[@id="drag"]')
504
+ target = @session.find('//div[@id="drop"]')
505
+
506
+ element.drag_to(target, drop_modifiers: modifier_key)
507
+ expect(@session).to have_css('div.drag_start', exact_text: 'Dragged!')
508
+ expect(@session).to have_xpath("//div[contains(., 'Dropped!-#{modifier_key}')]")
509
+ end
510
+ end
511
+
512
+ it 'should simulate multiple held down modifier keys' do
513
+ @session.visit('/with_js')
514
+
515
+ element = @session.find('//div[@id="drag"]')
516
+ target = @session.find('//div[@id="drop"]')
517
+
518
+ modifier_keys = %I[alt ctrl meta shift]
519
+
520
+ element.drag_to(target, drop_modifiers: modifier_keys)
521
+ expect(@session).to have_xpath("//div[contains(., 'Dropped!-#{modifier_keys.join('-')}')]")
522
+ end
523
+
524
+ it 'should support key aliases' do
525
+ { control: :ctrl,
526
+ command: :meta,
527
+ cmd: :meta }.each do |(key_alias, key)|
528
+ @session.visit('/with_js')
529
+
530
+ element = @session.find('//div[@id="drag"]')
531
+ target = @session.find('//div[@id="drop"]')
532
+
533
+ element.drag_to(target, drop_modifiers: [key_alias])
534
+ expect(target).to have_text("Dropped!-#{key}", exact: true)
535
+ end
536
+ end
537
+
538
+ context 'HTML5', requires: %i[js html5_drag] do
539
+ it 'should HTML5 drag and drop an object' do
540
+ @session.visit('/with_js')
541
+ element = @session.find('//div[@id="drag_html5"]')
542
+ target = @session.find('//div[@id="drop_html5"]')
543
+ element.drag_to(target)
544
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5")]')
545
+ end
546
+
547
+ it 'should HTML5 drag and drop an object child' do
548
+ @session.visit('/with_js')
549
+ element = @session.find('//div[@id="drag_html5"]/p')
550
+ target = @session.find('//div[@id="drop_html5"]')
551
+ element.drag_to(target)
552
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5")]')
553
+ end
554
+
555
+ it 'should set clientX/Y in dragover events' do
556
+ @session.visit('/with_js')
557
+ element = @session.find('//div[@id="drag_html5"]')
558
+ target = @session.find('//div[@id="drop_html5"]')
559
+ element.drag_to(target)
560
+ expect(@session).to have_css('div.log', text: /DragOver with client position: [1-9]\d*,[1-9]\d*/, count: 2)
561
+ end
562
+
563
+ it 'should preserve clientX/Y from last dragover event' do
564
+ @session.visit('/with_js')
565
+ element = @session.find('//div[@id="drag_html5"]')
566
+ target = @session.find('//div[@id="drop_html5"]')
567
+ element.drag_to(target)
568
+
569
+ conditions = %w[DragLeave Drop DragEnd].map do |text|
570
+ have_css('div.log', text: text)
571
+ end
572
+ expect(@session).to(conditions.reduce { |memo, cond| memo.and(cond) })
573
+
574
+ # The first "DragOver" div is inserted by the last dragover event dispatched
575
+ drag_over_div = @session.first('//div[@class="log" and starts-with(text(), "DragOver")]')
576
+ position = drag_over_div.text.sub('DragOver ', '')
577
+
578
+ expect(@session).to have_css('div.log', text: /DragLeave #{position}/, count: 1)
579
+ expect(@session).to have_css('div.log', text: /Drop #{position}/, count: 1)
580
+ expect(@session).to have_css('div.log', text: /DragEnd #{position}/, count: 1)
581
+ end
582
+
583
+ it 'should not HTML5 drag and drop on a non HTML5 drop element' do
584
+ @session.visit('/with_js')
585
+ element = @session.find('//div[@id="drag_html5"]')
586
+ target = @session.find('//div[@id="drop_html5"]')
587
+ target.execute_script("$(this).removeClass('drop');")
588
+ element.drag_to(target)
589
+ sleep 1
590
+ expect(@session).not_to have_xpath('//div[contains(., "HTML5 Dropped")]')
591
+ end
592
+
593
+ it 'should HTML5 drag and drop when scrolling needed' do
594
+ @session.visit('/with_js')
595
+ element = @session.find('//div[@id="drag_html5_scroll"]')
596
+ target = @session.find('//div[@id="drop_html5_scroll"]')
597
+ element.drag_to(target)
598
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5_scroll")]')
599
+ end
600
+
601
+ it 'should drag HTML5 default draggable elements' do
602
+ @session.visit('/with_js')
603
+ link = @session.find_link('drag_link_html5')
604
+ target = @session.find(:id, 'drop_html5')
605
+ link.drag_to target
606
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped")]')
607
+ end
608
+
609
+ it 'should work with SortableJS' do
610
+ @session.visit('/with_sortable_js')
611
+ @session.within(:css, '#sortable') do
612
+ src = @session.find('div', text: 'Item 1')
613
+ target = @session.find('div', text: 'Item 3')
614
+ src.drag_to target
615
+ expect(@session).to have_content(/Item 3.*Item 1/, normalize_ws: true)
616
+ end
617
+ end
618
+
619
+ it 'should drag HTML5 default draggable element child' do
620
+ @session.visit('/with_js')
621
+ source = @session.find_link('drag_link_html5').find(:css, 'p')
622
+ target = @session.find(:id, 'drop_html5')
623
+ source.drag_to target
624
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped")]')
625
+ end
626
+
627
+ it 'should simulate a single held down modifier key' do
628
+ %I[alt ctrl meta shift].each do |modifier_key|
629
+ @session.visit('/with_js')
630
+
631
+ element = @session.find('//div[@id="drag_html5"]')
632
+ target = @session.find('//div[@id="drop_html5"]')
633
+
634
+ element.drag_to(target, drop_modifiers: modifier_key)
635
+
636
+ expect(@session).to have_css('div.drag_start', exact_text: 'HTML5 Dragged!')
637
+ expect(@session).to have_xpath("//div[contains(., 'HTML5 Dropped string: text/plain drag_html5-#{modifier_key}')]")
638
+ end
639
+ end
640
+
641
+ it 'should simulate multiple held down modifier keys' do
642
+ @session.visit('/with_js')
643
+
644
+ element = @session.find('//div[@id="drag_html5"]')
645
+ target = @session.find('//div[@id="drop_html5"]')
646
+
647
+ modifier_keys = %I[alt ctrl meta shift]
648
+
649
+ element.drag_to(target, drop_modifiers: modifier_keys)
650
+ expect(@session).to have_xpath("//div[contains(., 'HTML5 Dropped string: text/plain drag_html5-#{modifier_keys.join('-')}')]")
651
+ end
652
+
653
+ it 'should support key aliases' do
654
+ { control: :ctrl,
655
+ command: :meta,
656
+ cmd: :meta }.each do |(key_alias, key)|
657
+ @session.visit('/with_js')
658
+
659
+ element = @session.find('//div[@id="drag_html5"]')
660
+ target = @session.find('//div[@id="drop_html5"]')
661
+
662
+ element.drag_to(target, drop_modifiers: [key_alias])
663
+ expect(target).to have_text(%r{^HTML5 Dropped string: text/plain drag_html5-#{key}$}m, exact: true)
664
+ end
665
+ end
666
+
667
+ it 'should trigger a dragenter event, before the first dragover event' do
668
+ @session.visit('/with_js')
669
+ element = @session.find('//div[@id="drag_html5"]')
670
+ target = @session.find('//div[@id="drop_html5"]')
671
+ element.drag_to(target)
672
+
673
+ # Events are listed in reverse chronological order
674
+ expect(@session).to have_text(/DragOver.*DragEnter/m)
675
+ end
676
+ end
677
+ end
678
+
679
+ describe 'Element#drop', requires: %i[js html5_drag] do
680
+ it 'can drop a file' do
681
+ @session.visit('/with_js')
682
+ target = @session.find('//div[@id="drop_html5"]')
683
+ target.drop(
684
+ with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__)))
685
+ )
686
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]')
687
+ end
688
+
689
+ it 'can drop multiple files' do
690
+ @session.visit('/with_js')
691
+ target = @session.find('//div[@id="drop_html5"]')
692
+ target.drop(
693
+ with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__))),
694
+ with_os_path_separators(File.expand_path('../fixtures/test_file.txt', File.dirname(__FILE__)))
695
+ )
696
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]')
697
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: test_file.txt")]')
698
+ end
699
+
700
+ it 'can drop strings' do
701
+ @session.visit('/with_js')
702
+ target = @session.find('//div[@id="drop_html5"]')
703
+ target.drop('text/plain' => 'Some dropped text')
704
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain Some dropped text")]')
705
+ end
706
+
707
+ it 'can drop a pathname' do
708
+ @session.visit('/with_js')
709
+ target = @session.find('//div[@id="drop_html5"]')
710
+ target.drop(
711
+ Pathname.new(with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__))))
712
+ )
713
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]')
714
+ end
715
+
716
+ it 'can drop multiple strings' do
717
+ @session.visit('/with_js')
718
+ target = @session.find('//div[@id="drop_html5"]')
719
+ target.drop('text/plain' => 'Some dropped text', 'text/url' => 'http://www.google.com')
720
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain Some dropped text")]')
721
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/url http://www.google.com")]')
722
+ end
331
723
  end
332
724
 
333
725
  describe '#hover', requires: [:hover] do
@@ -344,6 +736,15 @@ Capybara::SpecHelper.spec 'node' do
344
736
  @session.find(:css, '.wrapper.scroll_needed').hover
345
737
  expect(@session.find(:css, '.wrapper.scroll_needed .hidden_until_hover', visible: false)).to be_visible
346
738
  end
739
+
740
+ it 'should hover again after following a link and back' do
741
+ @session.visit('/with_hover')
742
+ @session.find(:css, '.wrapper:not(.scroll_needed)').hover
743
+ @session.click_link('Other hover page')
744
+ @session.click_link('Go back')
745
+ @session.find(:css, '.wrapper:not(.scroll_needed)').hover
746
+ expect(@session.find(:css, '.wrapper:not(.scroll_needed) .hidden_until_hover', visible: false)).to be_visible
747
+ end
347
748
  end
348
749
 
349
750
  describe '#click' do
@@ -396,10 +797,11 @@ Capybara::SpecHelper.spec 'node' do
396
797
  end
397
798
 
398
799
  it 'should allow to adjust the click offset', requires: [:js] do
800
+ Capybara.w3c_click_offset = false
399
801
  @session.visit('with_js')
400
802
  @session.find(:css, '#click-test').click(x: 5, y: 5)
401
803
  link = @session.find(:link, 'has-been-clicked')
402
- locations = link.text.match(/^Has been clicked at (?<x>[\d\.-]+),(?<y>[\d\.-]+)$/)
804
+ locations = link.text.match(/^Has been clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
403
805
  # Resulting click location should be very close to 0, 0 relative to top left corner of the element, but may not be exact due to
404
806
  # integer/float conversions and rounding.
405
807
  expect(locations[:x].to_f).to be_within(1).of(5)
@@ -420,7 +822,7 @@ Capybara::SpecHelper.spec 'node' do
420
822
  end
421
823
 
422
824
  it 'should retry clicking', requires: [:js] do
423
- @session.visit('/obscured')
825
+ @session.visit('/animated')
424
826
  obscured = @session.find(:css, '#obscured')
425
827
  @session.execute_script <<~JS
426
828
  setTimeout(function(){ $('#cover').hide(); }, 700)
@@ -429,7 +831,7 @@ Capybara::SpecHelper.spec 'node' do
429
831
  end
430
832
 
431
833
  it 'should allow to retry longer', requires: [:js] do
432
- @session.visit('/obscured')
834
+ @session.visit('/animated')
433
835
  obscured = @session.find(:css, '#obscured')
434
836
  @session.execute_script <<~JS
435
837
  setTimeout(function(){ $('#cover').hide(); }, 3000)
@@ -438,13 +840,74 @@ Capybara::SpecHelper.spec 'node' do
438
840
  end
439
841
 
440
842
  it 'should not retry clicking when wait is disabled', requires: [:js] do
441
- @session.visit('/obscured')
843
+ @session.visit('/animated')
442
844
  obscured = @session.find(:css, '#obscured')
443
845
  @session.execute_script <<~JS
444
846
  setTimeout(function(){ $('#cover').hide(); }, 2000)
445
847
  JS
446
848
  expect { obscured.click(wait: 0) }.to(raise_error { |e| expect(e).to be_an_invalid_element_error(@session) })
447
849
  end
850
+
851
+ context 'offset', requires: [:js] do
852
+ before do
853
+ @session.visit('/offset')
854
+ end
855
+
856
+ let :clicker do
857
+ @session.find(:id, 'clicker')
858
+ end
859
+
860
+ context 'when w3c_click_offset is false' do
861
+ before do
862
+ Capybara.w3c_click_offset = false
863
+ end
864
+
865
+ it 'should offset from top left of element' do
866
+ clicker.click(x: 10, y: 5)
867
+ expect(@session).to have_text(/clicked at 110,105/)
868
+ end
869
+
870
+ it 'should offset outside the element' do
871
+ clicker.click(x: -15, y: -10)
872
+ expect(@session).to have_text(/clicked at 85,90/)
873
+ end
874
+
875
+ it 'should default to click the middle' do
876
+ clicker.click
877
+ expect(@session).to have_text(/clicked at 150,150/)
878
+ end
879
+ end
880
+
881
+ context 'when w3c_click_offset is true' do
882
+ before do
883
+ Capybara.w3c_click_offset = true
884
+ end
885
+
886
+ it 'should offset from center of element' do
887
+ clicker.click(x: 10, y: 5)
888
+ expect(@session).to have_text(/clicked at 160,155/)
889
+ end
890
+
891
+ it 'should offset outside from center of element' do
892
+ clicker.click(x: -65, y: -60)
893
+ expect(@session).to have_text(/clicked at 85,90/)
894
+ end
895
+
896
+ it 'should default to click the middle' do
897
+ clicker.click
898
+ expect(@session).to have_text(/clicked at 150,150/)
899
+ end
900
+ end
901
+ end
902
+
903
+ context 'delay', requires: [:js] do
904
+ it 'should delay the mouse up' do
905
+ @session.visit('with_js')
906
+ @session.find(:css, '#click-test').click(delay: 2)
907
+ delay = @session.evaluate_script('window.click_delay')
908
+ expect(delay).to be >= 2
909
+ end
910
+ end
448
911
  end
449
912
 
450
913
  describe '#double_click', requires: [:js] do
@@ -461,10 +924,11 @@ Capybara::SpecHelper.spec 'node' do
461
924
  end
462
925
 
463
926
  it 'should allow to adjust the offset', requires: [:js] do
927
+ Capybara.w3c_click_offset = false
464
928
  @session.visit('with_js')
465
929
  @session.find(:css, '#click-test').double_click(x: 10, y: 5)
466
930
  link = @session.find(:link, 'has-been-double-clicked')
467
- locations = link.text.match(/^Has been double clicked at (?<x>[\d\.-]+),(?<y>[\d\.-]+)$/)
931
+ locations = link.text.match(/^Has been double clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
468
932
  # Resulting click location should be very close to 10, 5 relative to top left corner of the element, but may not be exact due
469
933
  # to integer/float conversions and rounding.
470
934
  expect(locations[:x].to_f).to be_within(1).of(10)
@@ -472,13 +936,65 @@ Capybara::SpecHelper.spec 'node' do
472
936
  end
473
937
 
474
938
  it 'should retry clicking', requires: [:js] do
475
- @session.visit('/obscured')
939
+ @session.visit('/animated')
476
940
  obscured = @session.find(:css, '#obscured')
477
941
  @session.execute_script <<~JS
478
942
  setTimeout(function(){ $('#cover').hide(); }, 700)
479
943
  JS
480
944
  expect { obscured.double_click }.not_to raise_error
481
945
  end
946
+
947
+ context 'offset', requires: [:js] do
948
+ before do
949
+ @session.visit('/offset')
950
+ end
951
+
952
+ let :clicker do
953
+ @session.find(:id, 'clicker')
954
+ end
955
+
956
+ context 'when w3c_click_offset is false' do
957
+ before do
958
+ Capybara.w3c_click_offset = false
959
+ end
960
+
961
+ it 'should offset from top left of element' do
962
+ clicker.double_click(x: 10, y: 5)
963
+ expect(@session).to have_text(/clicked at 110,105/)
964
+ end
965
+
966
+ it 'should offset outside the element' do
967
+ clicker.double_click(x: -15, y: -10)
968
+ expect(@session).to have_text(/clicked at 85,90/)
969
+ end
970
+
971
+ it 'should default to click the middle' do
972
+ clicker.double_click
973
+ expect(@session).to have_text(/clicked at 150,150/)
974
+ end
975
+ end
976
+
977
+ context 'when w3c_click_offset is true' do
978
+ before do
979
+ Capybara.w3c_click_offset = true
980
+ end
981
+
982
+ it 'should offset from center of element' do
983
+ clicker.double_click(x: 10, y: 5)
984
+ expect(@session).to have_text(/clicked at 160,155/)
985
+ end
986
+
987
+ it 'should offset outside from center of element' do
988
+ clicker.double_click(x: -65, y: -60)
989
+ expect(@session).to have_text(/clicked at 85,90/)
990
+ end
991
+
992
+ it 'should default to click the middle' do
993
+ clicker.double_click
994
+ expect(@session).to have_text(/clicked at 150,150/)
995
+ end
996
+ end
997
+ end
482
998
  end
483
999
 
484
1000
  describe '#right_click', requires: [:js] do
@@ -495,10 +1011,11 @@ Capybara::SpecHelper.spec 'node' do
495
1011
  end
496
1012
 
497
1013
  it 'should allow to adjust the offset', requires: [:js] do
1014
+ Capybara.w3c_click_offset = false
498
1015
  @session.visit('with_js')
499
1016
  @session.find(:css, '#click-test').right_click(x: 10, y: 10)
500
1017
  link = @session.find(:link, 'has-been-right-clicked')
501
- locations = link.text.match(/^Has been right clicked at (?<x>[\d\.-]+),(?<y>[\d\.-]+)$/)
1018
+ locations = link.text.match(/^Has been right clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
502
1019
  # Resulting click location should be very close to 10, 10 relative to top left corner of the element, but may not be exact due
503
1020
  # to integer/float conversions and rounding
504
1021
  expect(locations[:x].to_f).to be_within(1).of(10)
@@ -506,13 +1023,74 @@ Capybara::SpecHelper.spec 'node' do
506
1023
  end
507
1024
 
508
1025
  it 'should retry clicking', requires: [:js] do
509
- @session.visit('/obscured')
1026
+ @session.visit('/animated')
510
1027
  obscured = @session.find(:css, '#obscured')
511
1028
  @session.execute_script <<~JS
512
1029
  setTimeout(function(){ $('#cover').hide(); }, 700)
513
1030
  JS
514
1031
  expect { obscured.right_click }.not_to raise_error
515
1032
  end
1033
+
1034
+ context 'offset', requires: [:js] do
1035
+ before do
1036
+ @session.visit('/offset')
1037
+ end
1038
+
1039
+ let :clicker do
1040
+ @session.find(:id, 'clicker')
1041
+ end
1042
+
1043
+ context 'when w3c_click_offset is false' do
1044
+ before do
1045
+ Capybara.w3c_click_offset = false
1046
+ end
1047
+
1048
+ it 'should offset from top left of element' do
1049
+ clicker.right_click(x: 10, y: 5)
1050
+ expect(@session).to have_text(/clicked at 110,105/)
1051
+ end
1052
+
1053
+ it 'should offset outside the element' do
1054
+ clicker.right_click(x: -15, y: -10)
1055
+ expect(@session).to have_text(/clicked at 85,90/)
1056
+ end
1057
+
1058
+ it 'should default to click the middle' do
1059
+ clicker.right_click
1060
+ expect(@session).to have_text(/clicked at 150,150/)
1061
+ end
1062
+ end
1063
+
1064
+ context 'when w3c_click_offset is true' do
1065
+ before do
1066
+ Capybara.w3c_click_offset = true
1067
+ end
1068
+
1069
+ it 'should offset from center of element' do
1070
+ clicker.right_click(x: 10, y: 5)
1071
+ expect(@session).to have_text(/clicked at 160,155/)
1072
+ end
1073
+
1074
+ it 'should offset outside from center of element' do
1075
+ clicker.right_click(x: -65, y: -60)
1076
+ expect(@session).to have_text(/clicked at 85,90/)
1077
+ end
1078
+
1079
+ it 'should default to click the middle' do
1080
+ clicker.right_click
1081
+ expect(@session).to have_text(/clicked at 150,150/)
1082
+ end
1083
+ end
1084
+ end
1085
+
1086
+ context 'delay', requires: [:js] do
1087
+ it 'should delay the mouse up' do
1088
+ @session.visit('with_js')
1089
+ @session.find(:css, '#click-test').right_click(delay: 2)
1090
+ delay = @session.evaluate_script('window.right_click_delay')
1091
+ expect(delay).to be >= 2
1092
+ end
1093
+ end
516
1094
  end
517
1095
 
518
1096
  describe '#send_keys', requires: [:send_keys] do
@@ -562,7 +1140,7 @@ Capybara::SpecHelper.spec 'node' do
562
1140
  end
563
1141
 
564
1142
  describe '#evaluate_script', requires: %i[js es_args] do
565
- it 'should evaluate the given script in the context of the element and return whatever it produces' do
1143
+ it 'should evaluate the given script in the context of the element and return whatever it produces' do
566
1144
  @session.visit('/with_js')
567
1145
  el = @session.find(:css, '#with_change_event')
568
1146
  expect(el.evaluate_script('this.value')).to eq('default value')
@@ -594,6 +1172,18 @@ Capybara::SpecHelper.spec 'node' do
594
1172
  expect(el).to be_instance_of(Capybara::Node::Element)
595
1173
  expect(el).to eq(change)
596
1174
  end
1175
+
1176
+ it 'should support multiple statements via IIFE' do
1177
+ @session.visit('/with_js')
1178
+ change = @session.find(:css, '#change') # ensure page has loaded and element is available
1179
+ res = change.evaluate_script(<<~JS, 3)
1180
+ (function(n){
1181
+ var el = this;
1182
+ return [el, n];
1183
+ }).apply(this, arguments)
1184
+ JS
1185
+ expect(res).to eq [change, 3]
1186
+ end
597
1187
  end
598
1188
 
599
1189
  describe '#evaluate_async_script', requires: %i[js es_args] do
@@ -611,7 +1201,91 @@ Capybara::SpecHelper.spec 'node' do
611
1201
  end
612
1202
  end
613
1203
 
1204
+ describe '#shadow_root', requires: %i[js] do
1205
+ it 'should get the shadow root' do
1206
+ @session.visit('/with_shadow')
1207
+ expect do
1208
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1209
+ expect(shadow_root).to be_a(Capybara::Node::Element)
1210
+ end.not_to raise_error
1211
+ end
1212
+
1213
+ it 'should find elements inside the shadow dom using CSS' do
1214
+ @session.visit('/with_shadow')
1215
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1216
+ expect(shadow_root).to have_css('#shadow_content', text: 'some text')
1217
+ end
1218
+
1219
+ it 'should find nested shadow roots' do
1220
+ @session.visit('/with_shadow')
1221
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1222
+ nested_shadow_root = shadow_root.find(:css, '#nested_shadow_host').shadow_root
1223
+ expect(nested_shadow_root).to have_css('#nested_shadow_content', text: 'nested text')
1224
+ end
1225
+
1226
+ it 'should click on elements' do
1227
+ @session.visit('/with_shadow')
1228
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1229
+ checkbox = shadow_root.find(:css, 'input[type="checkbox"]')
1230
+ expect(checkbox).not_to be_checked
1231
+ checkbox.click
1232
+ expect(checkbox).to be_checked
1233
+ end
1234
+
1235
+ it 'should use convenience methods once moved to a descendant of the shadow root' do
1236
+ @session.visit('/with_shadow')
1237
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1238
+ descendant = shadow_root.find(:css, '#controls_wrapper')
1239
+ expect do
1240
+ descendant.check('shadow_checkbox')
1241
+ end.not_to raise_error
1242
+ expect(descendant).to have_checked_field('shadow_checkbox')
1243
+ end
1244
+
1245
+ it 'should produce error messages when failing' do
1246
+ @session.visit('/with_shadow')
1247
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1248
+ expect do
1249
+ expect(shadow_root).to have_css('#shadow_content', text: 'Not in the document')
1250
+ end.to raise_error(/tag="ShadowRoot"/)
1251
+ end
1252
+
1253
+ it 'should get visible text' do
1254
+ @session.visit('/with_shadow')
1255
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1256
+ expect(shadow_root).to have_text('some text scroll.html')
1257
+ end
1258
+
1259
+ it 'should get all text' do
1260
+ @session.visit('/with_shadow')
1261
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1262
+ expect(shadow_root).to have_text(:all, 'some text scroll.html')
1263
+ end
1264
+ end
1265
+
614
1266
  describe '#reload', requires: [:js] do
1267
+ it 'should reload elements found via ancestor with CSS' do
1268
+ @session.visit('/with_js')
1269
+ node = @session.find(:css, '#reload-me em').ancestor(:css, 'div')
1270
+ node.reload
1271
+ expect(node[:id]).to eq 'reload-me'
1272
+ end
1273
+
1274
+ it 'should reload elements found via ancestor with XPath' do
1275
+ @session.visit('/with_js')
1276
+ node = @session.find(:css, '#reload-me em').ancestor(:xpath, './/div')
1277
+ node.reload
1278
+ expect(node[:id]).to eq 'reload-me'
1279
+ end
1280
+
1281
+ it 'should reload elements found via sibling' do
1282
+ @session.visit('/with_js')
1283
+ node = @session.find(:css, '#the-list li', text: 'Item 1').sibling(:css, 'li')
1284
+ expect(node.text).to eq 'Item 2'
1285
+ node.reload
1286
+ expect(node.text).to eq 'Item 2'
1287
+ end
1288
+
615
1289
  context 'without automatic reload' do
616
1290
  before { Capybara.automatic_reload = false }
617
1291