capybara 3.29.0 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +91 -1
  3. data/README.md +10 -3
  4. data/lib/capybara.rb +17 -7
  5. data/lib/capybara/config.rb +7 -3
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/dsl.rb +10 -2
  8. data/lib/capybara/helpers.rb +3 -1
  9. data/lib/capybara/minitest.rb +232 -144
  10. data/lib/capybara/minitest/spec.rb +153 -97
  11. data/lib/capybara/node/actions.rb +35 -35
  12. data/lib/capybara/node/document.rb +2 -2
  13. data/lib/capybara/node/document_matchers.rb +3 -3
  14. data/lib/capybara/node/element.rb +23 -16
  15. data/lib/capybara/node/finders.rb +17 -11
  16. data/lib/capybara/node/matchers.rb +64 -51
  17. data/lib/capybara/node/simple.rb +4 -2
  18. data/lib/capybara/queries/ancestor_query.rb +1 -1
  19. data/lib/capybara/queries/base_query.rb +2 -1
  20. data/lib/capybara/queries/selector_query.rb +25 -5
  21. data/lib/capybara/queries/sibling_query.rb +1 -1
  22. data/lib/capybara/queries/style_query.rb +1 -1
  23. data/lib/capybara/queries/text_query.rb +6 -0
  24. data/lib/capybara/rack_test/browser.rb +7 -2
  25. data/lib/capybara/rack_test/driver.rb +1 -1
  26. data/lib/capybara/rack_test/form.rb +1 -1
  27. data/lib/capybara/rack_test/node.rb +34 -9
  28. data/lib/capybara/registration_container.rb +44 -0
  29. data/lib/capybara/registrations/servers.rb +1 -1
  30. data/lib/capybara/result.rb +29 -5
  31. data/lib/capybara/rspec/matcher_proxies.rb +4 -4
  32. data/lib/capybara/rspec/matchers.rb +27 -27
  33. data/lib/capybara/rspec/matchers/base.rb +12 -6
  34. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  35. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  36. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  37. data/lib/capybara/rspec/matchers/have_selector.rb +15 -7
  38. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  39. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  40. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  41. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  42. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  43. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  44. data/lib/capybara/selector.rb +34 -17
  45. data/lib/capybara/selector/css.rb +1 -1
  46. data/lib/capybara/selector/definition.rb +7 -6
  47. data/lib/capybara/selector/definition/button.rb +8 -2
  48. data/lib/capybara/selector/definition/checkbox.rb +2 -2
  49. data/lib/capybara/selector/definition/css.rb +3 -1
  50. data/lib/capybara/selector/definition/datalist_input.rb +1 -1
  51. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  52. data/lib/capybara/selector/definition/element.rb +1 -1
  53. data/lib/capybara/selector/definition/field.rb +1 -1
  54. data/lib/capybara/selector/definition/file_field.rb +1 -1
  55. data/lib/capybara/selector/definition/fillable_field.rb +2 -2
  56. data/lib/capybara/selector/definition/label.rb +4 -2
  57. data/lib/capybara/selector/definition/link.rb +8 -0
  58. data/lib/capybara/selector/definition/radio_button.rb +2 -2
  59. data/lib/capybara/selector/definition/select.rb +32 -13
  60. data/lib/capybara/selector/definition/table.rb +6 -3
  61. data/lib/capybara/selector/filter_set.rb +11 -9
  62. data/lib/capybara/selector/filters/base.rb +6 -1
  63. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  64. data/lib/capybara/selector/selector.rb +8 -2
  65. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  66. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  67. data/lib/capybara/selenium/driver.rb +22 -11
  68. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +8 -10
  69. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +7 -9
  70. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
  71. data/lib/capybara/selenium/extensions/html5_drag.rb +30 -13
  72. data/lib/capybara/selenium/node.rb +97 -18
  73. data/lib/capybara/selenium/nodes/chrome_node.rb +11 -14
  74. data/lib/capybara/selenium/nodes/edge_node.rb +4 -2
  75. data/lib/capybara/selenium/nodes/firefox_node.rb +4 -4
  76. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  77. data/lib/capybara/selenium/patches/logs.rb +3 -5
  78. data/lib/capybara/server.rb +15 -3
  79. data/lib/capybara/server/checker.rb +1 -1
  80. data/lib/capybara/server/middleware.rb +20 -10
  81. data/lib/capybara/session.rb +43 -26
  82. data/lib/capybara/session/config.rb +9 -3
  83. data/lib/capybara/session/matchers.rb +6 -6
  84. data/lib/capybara/spec/public/test.js +69 -6
  85. data/lib/capybara/spec/session/all_spec.rb +60 -5
  86. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  87. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  88. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  89. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  90. data/lib/capybara/spec/session/find_spec.rb +31 -8
  91. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  92. data/lib/capybara/spec/session/has_css_spec.rb +12 -9
  93. data/lib/capybara/spec/session/has_current_path_spec.rb +2 -2
  94. data/lib/capybara/spec/session/has_field_spec.rb +16 -0
  95. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  96. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  97. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  98. data/lib/capybara/spec/session/has_text_spec.rb +35 -0
  99. data/lib/capybara/spec/session/node_spec.rb +160 -29
  100. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  101. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  102. data/lib/capybara/spec/session/selectors_spec.rb +15 -2
  103. data/lib/capybara/spec/session/window/window_spec.rb +7 -7
  104. data/lib/capybara/spec/spec_helper.rb +2 -2
  105. data/lib/capybara/spec/test_app.rb +14 -18
  106. data/lib/capybara/spec/views/form.erb +18 -2
  107. data/lib/capybara/spec/views/with_dragula.erb +3 -1
  108. data/lib/capybara/spec/views/with_html.erb +2 -2
  109. data/lib/capybara/spec/views/with_js.erb +1 -0
  110. data/lib/capybara/version.rb +1 -1
  111. data/spec/capybara_spec.rb +1 -1
  112. data/spec/dsl_spec.rb +16 -3
  113. data/spec/minitest_spec.rb +1 -1
  114. data/spec/minitest_spec_spec.rb +46 -46
  115. data/spec/rack_test_spec.rb +13 -1
  116. data/spec/regexp_dissassembler_spec.rb +40 -36
  117. data/spec/result_spec.rb +43 -32
  118. data/spec/rspec/features_spec.rb +1 -0
  119. data/spec/rspec/shared_spec_matchers.rb +68 -56
  120. data/spec/rspec_spec.rb +4 -4
  121. data/spec/selector_spec.rb +1 -1
  122. data/spec/selenium_spec_chrome.rb +9 -6
  123. data/spec/selenium_spec_chrome_remote.rb +2 -0
  124. data/spec/selenium_spec_firefox.rb +7 -2
  125. data/spec/server_spec.rb +65 -31
  126. data/spec/session_spec.rb +1 -1
  127. data/spec/shared_selenium_node.rb +21 -3
  128. data/spec/shared_selenium_session.rb +33 -14
  129. data/spec/spec_helper.rb +1 -1
  130. metadata +6 -4
@@ -39,6 +39,14 @@ Capybara::SpecHelper.spec '#has_button?' do
39
39
  expect(@session).to have_button('awe123', type: 'submit')
40
40
  expect(@session).not_to have_button('awe123', type: 'reset')
41
41
  end
42
+
43
+ it 'should be true for role=button when enable_aria_role: true' do
44
+ expect(@session).to have_button('ARIA button', enable_aria_role: true)
45
+ end
46
+
47
+ it 'should be false for role=button when enable_aria_role: false' do
48
+ expect(@session).not_to have_button('ARIA button', enable_aria_role: false)
49
+ end
42
50
  end
43
51
 
44
52
  Capybara::SpecHelper.spec '#has_no_button?' do
@@ -66,4 +74,12 @@ Capybara::SpecHelper.spec '#has_no_button?' do
66
74
  it 'should be false for disabled buttons if disabled: false' do
67
75
  expect(@session).to have_no_button('Disabled button', disabled: false)
68
76
  end
77
+
78
+ it 'should be true for role=button when enable_aria_role: false' do
79
+ expect(@session).to have_no_button('ARIA button', enable_aria_role: false)
80
+ end
81
+
82
+ it 'should be false for role=button when enable_aria_role: true' do
83
+ expect(@session).not_to have_no_button('ARIA button', enable_aria_role: true)
84
+ end
69
85
  end
@@ -185,10 +185,10 @@ Capybara::SpecHelper.spec '#has_css?' do
185
185
  end
186
186
 
187
187
  it 'should be false when content occurs more times than given' do
188
- expect(@session).not_to have_css('h2.head', maximum: 4) # edge case
189
- expect(@session).not_to have_css('h2', maximum: 3)
188
+ # expect(@session).not_to have_css('h2.head', maximum: 4) # edge case
189
+ # expect(@session).not_to have_css('h2', maximum: 3)
190
190
  expect(@session).not_to have_css('h2').at_most(3).times
191
- expect(@session).not_to have_css('p', maximum: 1)
191
+ # expect(@session).not_to have_css('p', maximum: 1)
192
192
  end
193
193
 
194
194
  it 'should coerce maximum to an integer' do
@@ -234,18 +234,21 @@ Capybara::SpecHelper.spec '#has_css?' do
234
234
  context 'with spatial requirements', requires: [:spatial] do
235
235
  before do
236
236
  @session.visit('/spatial')
237
- @center = @session.find(:css, '.center')
237
+ end
238
+
239
+ let :center do
240
+ @session.find(:css, '.center')
238
241
  end
239
242
 
240
243
  it 'accepts spatial options' do
241
- expect(@session).to have_css('div', above: @center).thrice
242
- expect(@session).to have_css('div', above: @center, right_of: @center).once
244
+ expect(@session).to have_css('div', above: center).thrice
245
+ expect(@session).to have_css('div', above: center, right_of: center).once
243
246
  end
244
247
 
245
248
  it 'supports spatial sugar' do
246
- expect(@session).to have_css('div').left_of(@center).thrice
247
- expect(@session).to have_css('div').below(@center).right_of(@center).once
248
- expect(@session).to have_css('div').near(@center).exactly(8).times
249
+ expect(@session).to have_css('div').left_of(center).thrice
250
+ expect(@session).to have_css('div').below(center).right_of(center).once
251
+ expect(@session).to have_css('div').near(center).exactly(8).times
249
252
  end
250
253
  end
251
254
 
@@ -128,11 +128,11 @@ Capybara::SpecHelper.spec '#has_no_current_path?' do
128
128
  # Without ignore_query option
129
129
  expect do
130
130
  expect(@session).not_to have_current_path('/with_js')
131
- end. not_to raise_exception
131
+ end.not_to raise_exception
132
132
 
133
133
  # With ignore_query option
134
134
  expect do
135
135
  expect(@session).not_to have_current_path('/with_js', ignore_query: true)
136
- end. not_to raise_exception
136
+ end.not_to raise_exception
137
137
  end
138
138
  end
@@ -60,6 +60,22 @@ Capybara::SpecHelper.spec '#has_field' do
60
60
  end
61
61
  end
62
62
 
63
+ context 'with validation message', requires: [:html_validation] do
64
+ it 'should accept a regexp' do
65
+ @session.fill_in('form_zipcode', with: '1234')
66
+ expect(@session).to have_field('form_zipcode', validation_message: /match the requested format/)
67
+ expect(@session).not_to have_field('form_zipcode', validation_message: /random/)
68
+ end
69
+
70
+ it 'should accept a string' do
71
+ @session.fill_in('form_zipcode', with: '1234')
72
+ expect(@session).to have_field('form_zipcode', validation_message: 'Please match the requested format.')
73
+ expect(@session).not_to have_field('form_zipcode', validation_message: 'match the requested format.')
74
+ @session.fill_in('form_zipcode', with: '12345')
75
+ expect(@session).to have_field('form_zipcode', validation_message: '')
76
+ end
77
+ end
78
+
63
79
  context 'with type' do
64
80
  it 'should be true if a field with the given type is on the page' do
65
81
  expect(@session).to have_field('First Name', type: 'text')
@@ -63,7 +63,7 @@ Capybara::SpecHelper.spec '#has_select?' do
63
63
  end
64
64
 
65
65
  it "should be true even when the selected option invisible, regardless of the select's visibility" do
66
- expect(@session).to have_select('Icecream', visible: false, selected: 'Chocolate')
66
+ expect(@session).to have_select('Icecream', visible: :hidden, selected: 'Chocolate')
67
67
  expect(@session).to have_select('Sorbet', selected: 'Vanilla')
68
68
  end
69
69
  end
@@ -88,7 +88,7 @@ Capybara::SpecHelper.spec '#has_select?' do
88
88
  end
89
89
 
90
90
  it "should be true even when the selected values are invisible, regardless of the select's visibility" do
91
- expect(@session).to have_select('Dessert', visible: false, with_options: %w[Pudding Tiramisu])
91
+ expect(@session).to have_select('Dessert', visible: :hidden, with_options: %w[Pudding Tiramisu])
92
92
  expect(@session).to have_select('Cake', with_selected: ['Chocolate Cake', 'Sponge Cake'])
93
93
  end
94
94
 
@@ -113,7 +113,35 @@ Capybara::SpecHelper.spec '#has_select?' do
113
113
  end
114
114
 
115
115
  it 'should be true even when the options are invisible, if the select itself is invisible' do
116
- expect(@session).to have_select('Icecream', visible: false, options: %w[Chocolate Vanilla Strawberry])
116
+ expect(@session).to have_select('Icecream', visible: :hidden, options: %w[Chocolate Vanilla Strawberry])
117
+ end
118
+ end
119
+
120
+ context 'with enabled options' do
121
+ it 'should be true if the listed options exist and are enabled' do
122
+ expect(@session).to have_select('form_title', enabled_options: %w[Mr Mrs Miss])
123
+ end
124
+
125
+ it 'should be false if the listed options do not exist' do
126
+ expect(@session).not_to have_select('form_title', enabled_options: ['Not there'])
127
+ end
128
+
129
+ it 'should be false if the listed option exists but is not enabled' do
130
+ expect(@session).not_to have_select('form_title', enabled_options: %w[Mr Mrs Miss Other])
131
+ end
132
+ end
133
+
134
+ context 'with disabled options' do
135
+ it 'should be true if the listed options exist and are disabled' do
136
+ expect(@session).to have_select('form_title', disabled_options: ['Other'])
137
+ end
138
+
139
+ it 'should be false if the listed options do not exist' do
140
+ expect(@session).not_to have_select('form_title', disabled_options: ['Not there'])
141
+ end
142
+
143
+ it 'should be false if the listed option exists but is not disabled' do
144
+ expect(@session).not_to have_select('form_title', disabled_options: %w[Other Mrs])
117
145
  end
118
146
  end
119
147
 
@@ -130,7 +158,7 @@ Capybara::SpecHelper.spec '#has_select?' do
130
158
  end
131
159
 
132
160
  it 'should be true even when the options are invisible, if the select itself is invisible' do
133
- expect(@session).to have_select('Icecream', visible: false, with_options: %w[Vanilla Strawberry])
161
+ expect(@session).to have_select('Icecream', visible: :hidden, with_options: %w[Vanilla Strawberry])
134
162
  end
135
163
  end
136
164
 
@@ -61,12 +61,12 @@ Capybara::SpecHelper.spec '#has_selector?' do
61
61
  end
62
62
 
63
63
  it 'should respect visibility setting' do
64
- expect(@session).to have_selector(:id, 'hidden-text', text: 'Some of this text is hidden!', visible: false)
65
- expect(@session).not_to have_selector(:id, 'hidden-text', text: 'Some of this text is hidden!', visible: true)
64
+ expect(@session).to have_selector(:id, 'hidden-text', text: 'Some of this text is hidden!', visible: :all)
65
+ expect(@session).not_to have_selector(:id, 'hidden-text', text: 'Some of this text is hidden!', visible: :visible)
66
66
  Capybara.ignore_hidden_elements = false
67
- expect(@session).to have_selector(:id, 'hidden-text', text: 'Some of this text is hidden!', visible: false)
67
+ expect(@session).to have_selector(:id, 'hidden-text', text: 'Some of this text is hidden!', visible: :all)
68
68
  Capybara.visible_text_only = true
69
- expect(@session).not_to have_selector(:id, 'hidden-text', text: 'Some of this text is hidden!', visible: true)
69
+ expect(@session).not_to have_selector(:id, 'hidden-text', text: 'Some of this text is hidden!', visible: :visible)
70
70
  end
71
71
 
72
72
  it 'should discard all matches where the given regexp is not matched' do
@@ -143,10 +143,56 @@ Capybara::SpecHelper.spec '#has_no_table?' do
143
143
  ])
144
144
  end
145
145
 
146
- it 'should consider columns' do
147
- expect(@session).to have_no_table('Vertical Headers', with_cols:
148
- [
149
- { 'First Name' => 'Joe' }
150
- ])
146
+ context 'using :with_cols' do
147
+ it 'should consider a single column' do
148
+ expect(@session).to have_no_table('Vertical Headers', with_cols:
149
+ [
150
+ { 'First Name' => 'Joe' }
151
+ ])
152
+ end
153
+
154
+ it 'should be true even if the last column does exist' do
155
+ expect(@session).to have_no_table('Vertical Headers', with_cols:
156
+ [
157
+ {
158
+ 'First Name' => 'What?',
159
+ 'What?' => 'Walpole',
160
+ 'City' => 'Oceanside' # This line makes the example fail
161
+ }
162
+ ])
163
+ end
164
+
165
+ it 'should be true if none of the columns exist' do
166
+ expect(@session).to have_no_table('Vertical Headers', with_cols:
167
+ [
168
+ {
169
+ 'First Name' => 'What?',
170
+ 'What?' => 'Walpole',
171
+ 'City' => 'What?'
172
+ }
173
+ ])
174
+ end
175
+
176
+ it 'should be true if the first column does match' do
177
+ expect(@session).to have_no_table('Vertical Headers', with_cols:
178
+ [
179
+ {
180
+ 'First Name' => 'Thomas',
181
+ 'Last Name' => 'What',
182
+ 'City' => 'What'
183
+ }
184
+ ])
185
+ end
186
+
187
+ it 'should be true if none of the columns match' do
188
+ expect(@session).to have_no_table('Vertical Headers', with_cols:
189
+ [
190
+ {
191
+ 'First Name' => 'What',
192
+ 'Last Name' => 'What',
193
+ 'City' => 'What'
194
+ }
195
+ ])
196
+ end
151
197
  end
152
198
  end
@@ -111,6 +111,41 @@ Capybara::SpecHelper.spec '#has_text?' do
111
111
  expect(@session).not_to have_text(/xxxxyzzz/)
112
112
  end
113
113
 
114
+ context 'with object implementing to_s and to_hash' do
115
+ it 'should work if the object is passed alone' do
116
+ with_to_hash = Class.new do
117
+ def to_s; '42' end
118
+ def to_hash; { value: 'Other hash' } end
119
+ end.new
120
+ @session.visit('/with_html')
121
+ expect(@session).to have_text(with_to_hash)
122
+ end
123
+
124
+ it 'should work if passed with empty options' do
125
+ with_to_hash = Class.new do
126
+ def to_s; '42' end
127
+ def to_hash; { value: 'Other hash' } end
128
+ end.new
129
+ @session.visit('/with_html')
130
+ if RUBY_VERSION >= '2.7'
131
+ expect(@session).to have_text(:visible, with_to_hash, **{})
132
+ else
133
+ expect(@session).to have_text(:visible, with_to_hash, {})
134
+ end
135
+ end
136
+
137
+ it 'should fail if passed without empty options' do
138
+ with_to_hash = Class.new do
139
+ def to_s; '42' end
140
+ def to_hash; { blah: 'Other hash' } end
141
+ end.new
142
+ @session.visit('/with_html')
143
+ expect do
144
+ expect(@session).to have_text(:visible, with_to_hash)
145
+ end.to raise_error(ArgumentError)
146
+ end
147
+ end
148
+
114
149
  context 'with exact: true option' do
115
150
  it 'should be true if text matches exactly' do
116
151
  @session.visit('/with_html')
@@ -270,13 +270,13 @@ Capybara::SpecHelper.spec 'node' do
270
270
  end
271
271
 
272
272
  it 'works when details is toggled open and closed' do
273
- @session.find(:css, '#closed_details').click
274
- expect(@session).to have_css('#closed_details *', visible: true, count: 5)
273
+ @session.find(:css, '#closed_details > summary').click
274
+ expect(@session).to have_css('#closed_details *', visible: :visible, count: 5)
275
275
  .and(have_no_css('#closed_details *', visible: :hidden))
276
276
 
277
- @session.find(:css, '#closed_details').click
277
+ @session.find(:css, '#closed_details > summary').click
278
278
  descendants_css = '#closed_details > *:not(summary), #closed_details > *:not(summary) *'
279
- expect(@session).to have_no_css(descendants_css, visible: true)
279
+ expect(@session).to have_no_css(descendants_css, visible: :visible)
280
280
  .and(have_css(descendants_css, visible: :hidden, count: 3))
281
281
  end
282
282
  end
@@ -446,7 +446,7 @@ Capybara::SpecHelper.spec 'node' do
446
446
 
447
447
  it 'should work with Dragula' do
448
448
  @session.visit('/with_dragula')
449
- @session.within(:css, '#sortable') do
449
+ @session.within(:css, '#sortable.ready') do
450
450
  src = @session.find('div', text: 'Item 1')
451
451
  target = @session.find('div', text: 'Item 3')
452
452
  src.drag_to target
@@ -468,6 +468,50 @@ Capybara::SpecHelper.spec 'node' do
468
468
  end
469
469
  end
470
470
 
471
+ it 'should simulate a single held down modifier key' do
472
+ %I[
473
+ alt
474
+ ctrl
475
+ meta
476
+ shift
477
+ ].each do |modifier_key|
478
+ @session.visit('/with_js')
479
+
480
+ element = @session.find('//div[@id="drag"]')
481
+ target = @session.find('//div[@id="drop"]')
482
+
483
+ element.drag_to(target, drop_modifiers: modifier_key)
484
+ expect(@session).to have_css('div.drag_start', exact_text: 'Dragged!')
485
+ expect(@session).to have_xpath("//div[contains(., 'Dropped!-#{modifier_key}')]")
486
+ end
487
+ end
488
+
489
+ it 'should simulate multiple held down modifier keys' do
490
+ @session.visit('/with_js')
491
+
492
+ element = @session.find('//div[@id="drag"]')
493
+ target = @session.find('//div[@id="drop"]')
494
+
495
+ modifier_keys = %I[alt ctrl meta shift]
496
+
497
+ element.drag_to(target, drop_modifiers: modifier_keys)
498
+ expect(@session).to have_xpath("//div[contains(., 'Dropped!-#{modifier_keys.join('-')}')]")
499
+ end
500
+
501
+ it 'should support key aliases' do
502
+ { control: :ctrl,
503
+ command: :meta,
504
+ cmd: :meta }.each do |(key_alias, key)|
505
+ @session.visit('/with_js')
506
+
507
+ element = @session.find('//div[@id="drag"]')
508
+ target = @session.find('//div[@id="drop"]')
509
+
510
+ element.drag_to(target, drop_modifiers: [key_alias])
511
+ expect(target).to have_text("Dropped!-#{key}", exact: true)
512
+ end
513
+ end
514
+
471
515
  context 'HTML5', requires: %i[js html5_drag] do
472
516
  it 'should HTML5 drag and drop an object' do
473
517
  @session.visit('/with_js')
@@ -493,6 +537,26 @@ Capybara::SpecHelper.spec 'node' do
493
537
  expect(@session).to have_css('div.log', text: /DragOver with client position: [1-9]\d*,[1-9]\d*/, count: 2)
494
538
  end
495
539
 
540
+ it 'should preserve clientX/Y from last dragover event' do
541
+ @session.visit('/with_js')
542
+ element = @session.find('//div[@id="drag_html5"]')
543
+ target = @session.find('//div[@id="drop_html5"]')
544
+ element.drag_to(target)
545
+
546
+ conditions = %w[DragLeave Drop DragEnd].map do |text|
547
+ have_css('div.log', text: text)
548
+ end
549
+ expect(@session).to(conditions.reduce { |memo, cond| memo.and(cond) })
550
+
551
+ # The first "DragOver" div is inserted by the last dragover event dispatched
552
+ drag_over_div = @session.first('//div[@class="log" and starts-with(text(), "DragOver")]')
553
+ position = drag_over_div.text.sub('DragOver ', '')
554
+
555
+ expect(@session).to have_css('div.log', text: /DragLeave #{position}/, count: 1)
556
+ expect(@session).to have_css('div.log', text: /Drop #{position}/, count: 1)
557
+ expect(@session).to have_css('div.log', text: /DragEnd #{position}/, count: 1)
558
+ end
559
+
496
560
  it 'should not HTML5 drag and drop on a non HTML5 drop element' do
497
561
  @session.visit('/with_js')
498
562
  element = @session.find('//div[@id="drag_html5"]')
@@ -536,6 +600,46 @@ Capybara::SpecHelper.spec 'node' do
536
600
  source.drag_to target
537
601
  expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped")]')
538
602
  end
603
+
604
+ it 'should simulate a single held down modifier key' do
605
+ %I[alt ctrl meta shift].each do |modifier_key|
606
+ @session.visit('/with_js')
607
+
608
+ element = @session.find('//div[@id="drag_html5"]')
609
+ target = @session.find('//div[@id="drop_html5"]')
610
+
611
+ element.drag_to(target, drop_modifiers: modifier_key)
612
+
613
+ expect(@session).to have_css('div.drag_start', exact_text: 'HTML5 Dragged!')
614
+ expect(@session).to have_xpath("//div[contains(., 'HTML5 Dropped string: text/plain drag_html5-#{modifier_key}')]")
615
+ end
616
+ end
617
+
618
+ it 'should simulate multiple held down modifier keys' do
619
+ @session.visit('/with_js')
620
+
621
+ element = @session.find('//div[@id="drag_html5"]')
622
+ target = @session.find('//div[@id="drop_html5"]')
623
+
624
+ modifier_keys = %I[alt ctrl meta shift]
625
+
626
+ element.drag_to(target, drop_modifiers: modifier_keys)
627
+ expect(@session).to have_xpath("//div[contains(., 'HTML5 Dropped string: text/plain drag_html5-#{modifier_keys.join('-')}')]")
628
+ end
629
+
630
+ it 'should support key aliases' do
631
+ { control: :ctrl,
632
+ command: :meta,
633
+ cmd: :meta }.each do |(key_alias, key)|
634
+ @session.visit('/with_js')
635
+
636
+ element = @session.find('//div[@id="drag_html5"]')
637
+ target = @session.find('//div[@id="drop_html5"]')
638
+
639
+ element.drag_to(target, drop_modifiers: [key_alias])
640
+ expect(target).to have_text(%r{^HTML5 Dropped string: text/plain drag_html5-#{key}$}m, exact: true)
641
+ end
642
+ end
539
643
  end
540
644
  end
541
645
 
@@ -654,7 +758,7 @@ Capybara::SpecHelper.spec 'node' do
654
758
  @session.visit('with_js')
655
759
  @session.find(:css, '#click-test').click(x: 5, y: 5)
656
760
  link = @session.find(:link, 'has-been-clicked')
657
- locations = link.text.match(/^Has been clicked at (?<x>[\d\.-]+),(?<y>[\d\.-]+)$/)
761
+ locations = link.text.match(/^Has been clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
658
762
  # 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
659
763
  # integer/float conversions and rounding.
660
764
  expect(locations[:x].to_f).to be_within(1).of(5)
@@ -704,7 +808,10 @@ Capybara::SpecHelper.spec 'node' do
704
808
  context 'offset', requires: [:js] do
705
809
  before do
706
810
  @session.visit('/offset')
707
- @clicker = @session.find(:id, 'clicker')
811
+ end
812
+
813
+ let :clicker do
814
+ @session.find(:id, 'clicker')
708
815
  end
709
816
 
710
817
  context 'when w3c_click_offset is false' do
@@ -713,17 +820,17 @@ Capybara::SpecHelper.spec 'node' do
713
820
  end
714
821
 
715
822
  it 'should offset from top left of element' do
716
- @clicker.click(x: 10, y: 5)
823
+ clicker.click(x: 10, y: 5)
717
824
  expect(@session).to have_text(/clicked at 110,105/)
718
825
  end
719
826
 
720
827
  it 'should offset outside the element' do
721
- @clicker.click(x: -15, y: -10)
828
+ clicker.click(x: -15, y: -10)
722
829
  expect(@session).to have_text(/clicked at 85,90/)
723
830
  end
724
831
 
725
832
  it 'should default to click the middle' do
726
- @clicker.click
833
+ clicker.click
727
834
  expect(@session).to have_text(/clicked at 150,150/)
728
835
  end
729
836
  end
@@ -734,21 +841,30 @@ Capybara::SpecHelper.spec 'node' do
734
841
  end
735
842
 
736
843
  it 'should offset from center of element' do
737
- @clicker.click(x: 10, y: 5)
844
+ clicker.click(x: 10, y: 5)
738
845
  expect(@session).to have_text(/clicked at 160,155/)
739
846
  end
740
847
 
741
848
  it 'should offset outside from center of element' do
742
- @clicker.click(x: -65, y: -60)
849
+ clicker.click(x: -65, y: -60)
743
850
  expect(@session).to have_text(/clicked at 85,90/)
744
851
  end
745
852
 
746
853
  it 'should default to click the middle' do
747
- @clicker.click
854
+ clicker.click
748
855
  expect(@session).to have_text(/clicked at 150,150/)
749
856
  end
750
857
  end
751
858
  end
859
+
860
+ context 'delay', requires: [:js] do
861
+ it 'should delay the mouse up' do
862
+ @session.visit('with_js')
863
+ @session.find(:css, '#click-test').click(delay: 2)
864
+ delay = @session.evaluate_script('window.click_delay')
865
+ expect(delay).to be >= 2
866
+ end
867
+ end
752
868
  end
753
869
 
754
870
  describe '#double_click', requires: [:js] do
@@ -768,7 +884,7 @@ Capybara::SpecHelper.spec 'node' do
768
884
  @session.visit('with_js')
769
885
  @session.find(:css, '#click-test').double_click(x: 10, y: 5)
770
886
  link = @session.find(:link, 'has-been-double-clicked')
771
- locations = link.text.match(/^Has been double clicked at (?<x>[\d\.-]+),(?<y>[\d\.-]+)$/)
887
+ locations = link.text.match(/^Has been double clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
772
888
  # Resulting click location should be very close to 10, 5 relative to top left corner of the element, but may not be exact due
773
889
  # to integer/float conversions and rounding.
774
890
  expect(locations[:x].to_f).to be_within(1).of(10)
@@ -787,7 +903,10 @@ Capybara::SpecHelper.spec 'node' do
787
903
  context 'offset', requires: [:js] do
788
904
  before do
789
905
  @session.visit('/offset')
790
- @clicker = @session.find(:id, 'clicker')
906
+ end
907
+
908
+ let :clicker do
909
+ @session.find(:id, 'clicker')
791
910
  end
792
911
 
793
912
  context 'when w3c_click_offset is false' do
@@ -796,17 +915,17 @@ Capybara::SpecHelper.spec 'node' do
796
915
  end
797
916
 
798
917
  it 'should offset from top left of element' do
799
- @clicker.double_click(x: 10, y: 5)
918
+ clicker.double_click(x: 10, y: 5)
800
919
  expect(@session).to have_text(/clicked at 110,105/)
801
920
  end
802
921
 
803
922
  it 'should offset outside the element' do
804
- @clicker.double_click(x: -15, y: -10)
923
+ clicker.double_click(x: -15, y: -10)
805
924
  expect(@session).to have_text(/clicked at 85,90/)
806
925
  end
807
926
 
808
927
  it 'should default to click the middle' do
809
- @clicker.double_click
928
+ clicker.double_click
810
929
  expect(@session).to have_text(/clicked at 150,150/)
811
930
  end
812
931
  end
@@ -817,17 +936,17 @@ Capybara::SpecHelper.spec 'node' do
817
936
  end
818
937
 
819
938
  it 'should offset from center of element' do
820
- @clicker.double_click(x: 10, y: 5)
939
+ clicker.double_click(x: 10, y: 5)
821
940
  expect(@session).to have_text(/clicked at 160,155/)
822
941
  end
823
942
 
824
943
  it 'should offset outside from center of element' do
825
- @clicker.double_click(x: -65, y: -60)
944
+ clicker.double_click(x: -65, y: -60)
826
945
  expect(@session).to have_text(/clicked at 85,90/)
827
946
  end
828
947
 
829
948
  it 'should default to click the middle' do
830
- @clicker.double_click
949
+ clicker.double_click
831
950
  expect(@session).to have_text(/clicked at 150,150/)
832
951
  end
833
952
  end
@@ -851,7 +970,7 @@ Capybara::SpecHelper.spec 'node' do
851
970
  @session.visit('with_js')
852
971
  @session.find(:css, '#click-test').right_click(x: 10, y: 10)
853
972
  link = @session.find(:link, 'has-been-right-clicked')
854
- locations = link.text.match(/^Has been right clicked at (?<x>[\d\.-]+),(?<y>[\d\.-]+)$/)
973
+ locations = link.text.match(/^Has been right clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
855
974
  # Resulting click location should be very close to 10, 10 relative to top left corner of the element, but may not be exact due
856
975
  # to integer/float conversions and rounding
857
976
  expect(locations[:x].to_f).to be_within(1).of(10)
@@ -870,7 +989,10 @@ Capybara::SpecHelper.spec 'node' do
870
989
  context 'offset', requires: [:js] do
871
990
  before do
872
991
  @session.visit('/offset')
873
- @clicker = @session.find(:id, 'clicker')
992
+ end
993
+
994
+ let :clicker do
995
+ @session.find(:id, 'clicker')
874
996
  end
875
997
 
876
998
  context 'when w3c_click_offset is false' do
@@ -879,17 +1001,17 @@ Capybara::SpecHelper.spec 'node' do
879
1001
  end
880
1002
 
881
1003
  it 'should offset from top left of element' do
882
- @clicker.right_click(x: 10, y: 5)
1004
+ clicker.right_click(x: 10, y: 5)
883
1005
  expect(@session).to have_text(/clicked at 110,105/)
884
1006
  end
885
1007
 
886
1008
  it 'should offset outside the element' do
887
- @clicker.right_click(x: -15, y: -10)
1009
+ clicker.right_click(x: -15, y: -10)
888
1010
  expect(@session).to have_text(/clicked at 85,90/)
889
1011
  end
890
1012
 
891
1013
  it 'should default to click the middle' do
892
- @clicker.right_click
1014
+ clicker.right_click
893
1015
  expect(@session).to have_text(/clicked at 150,150/)
894
1016
  end
895
1017
  end
@@ -900,21 +1022,30 @@ Capybara::SpecHelper.spec 'node' do
900
1022
  end
901
1023
 
902
1024
  it 'should offset from center of element' do
903
- @clicker.right_click(x: 10, y: 5)
1025
+ clicker.right_click(x: 10, y: 5)
904
1026
  expect(@session).to have_text(/clicked at 160,155/)
905
1027
  end
906
1028
 
907
1029
  it 'should offset outside from center of element' do
908
- @clicker.right_click(x: -65, y: -60)
1030
+ clicker.right_click(x: -65, y: -60)
909
1031
  expect(@session).to have_text(/clicked at 85,90/)
910
1032
  end
911
1033
 
912
1034
  it 'should default to click the middle' do
913
- @clicker.right_click
1035
+ clicker.right_click
914
1036
  expect(@session).to have_text(/clicked at 150,150/)
915
1037
  end
916
1038
  end
917
1039
  end
1040
+
1041
+ context 'delay', requires: [:js] do
1042
+ it 'should delay the mouse up' do
1043
+ @session.visit('with_js')
1044
+ @session.find(:css, '#click-test').right_click(delay: 2)
1045
+ delay = @session.evaluate_script('window.right_click_delay')
1046
+ expect(delay).to be >= 2
1047
+ end
1048
+ end
918
1049
  end
919
1050
 
920
1051
  describe '#send_keys', requires: [:send_keys] do