capybara 2.6.1 → 2.7.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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +47 -16
  3. data/README.md +45 -38
  4. data/lib/capybara/cucumber.rb +1 -0
  5. data/lib/capybara/driver/base.rb +5 -2
  6. data/lib/capybara/driver/node.rb +1 -0
  7. data/lib/capybara/dsl.rb +1 -0
  8. data/lib/capybara/helpers.rb +3 -2
  9. data/lib/capybara/node/actions.rb +6 -0
  10. data/lib/capybara/node/base.rb +1 -0
  11. data/lib/capybara/node/document.rb +1 -0
  12. data/lib/capybara/node/document_matchers.rb +1 -0
  13. data/lib/capybara/node/element.rb +1 -0
  14. data/lib/capybara/node/finders.rb +3 -2
  15. data/lib/capybara/node/matchers.rb +72 -2
  16. data/lib/capybara/node/simple.rb +6 -3
  17. data/lib/capybara/queries/base_query.rb +1 -0
  18. data/lib/capybara/queries/current_path_query.rb +5 -4
  19. data/lib/capybara/queries/match_query.rb +21 -0
  20. data/lib/capybara/queries/selector_query.rb +138 -0
  21. data/lib/capybara/queries/text_query.rb +1 -0
  22. data/lib/capybara/queries/title_query.rb +1 -0
  23. data/lib/capybara/query.rb +3 -131
  24. data/lib/capybara/rack_test/browser.rb +1 -0
  25. data/lib/capybara/rack_test/css_handlers.rb +1 -0
  26. data/lib/capybara/rack_test/driver.rb +6 -4
  27. data/lib/capybara/rack_test/form.rb +1 -0
  28. data/lib/capybara/rack_test/node.rb +1 -0
  29. data/lib/capybara/rails.rb +2 -1
  30. data/lib/capybara/result.rb +1 -0
  31. data/lib/capybara/rspec/features.rb +1 -0
  32. data/lib/capybara/rspec/matchers.rb +54 -3
  33. data/lib/capybara/rspec.rb +1 -0
  34. data/lib/capybara/selector/filter.rb +48 -0
  35. data/lib/capybara/selector.rb +175 -75
  36. data/lib/capybara/selenium/driver.rb +24 -7
  37. data/lib/capybara/selenium/node.rb +1 -0
  38. data/lib/capybara/server.rb +38 -6
  39. data/lib/capybara/session/matchers.rb +2 -1
  40. data/lib/capybara/session.rb +30 -14
  41. data/lib/capybara/spec/public/test.js +6 -0
  42. data/lib/capybara/spec/session/accept_alert_spec.rb +2 -1
  43. data/lib/capybara/spec/session/accept_confirm_spec.rb +2 -1
  44. data/lib/capybara/spec/session/accept_prompt_spec.rb +2 -1
  45. data/lib/capybara/spec/session/all_spec.rb +1 -0
  46. data/lib/capybara/spec/session/assert_current_path.rb +1 -0
  47. data/lib/capybara/spec/session/assert_selector.rb +1 -0
  48. data/lib/capybara/spec/session/assert_text.rb +1 -0
  49. data/lib/capybara/spec/session/assert_title.rb +1 -0
  50. data/lib/capybara/spec/session/attach_file_spec.rb +1 -0
  51. data/lib/capybara/spec/session/body_spec.rb +1 -0
  52. data/lib/capybara/spec/session/check_spec.rb +1 -0
  53. data/lib/capybara/spec/session/choose_spec.rb +1 -0
  54. data/lib/capybara/spec/session/click_button_spec.rb +1 -0
  55. data/lib/capybara/spec/session/click_link_or_button_spec.rb +1 -0
  56. data/lib/capybara/spec/session/click_link_spec.rb +1 -0
  57. data/lib/capybara/spec/session/current_scope_spec.rb +2 -1
  58. data/lib/capybara/spec/session/current_url_spec.rb +1 -0
  59. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +2 -1
  60. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +2 -1
  61. data/lib/capybara/spec/session/element/assert_match_selector.rb +31 -0
  62. data/lib/capybara/spec/session/element/match_css_spec.rb +17 -0
  63. data/lib/capybara/spec/session/element/match_xpath_spec.rb +23 -0
  64. data/lib/capybara/spec/session/element/matches_selector_spec.rb +63 -0
  65. data/lib/capybara/spec/session/evaluate_script_spec.rb +1 -0
  66. data/lib/capybara/spec/session/execute_script_spec.rb +1 -0
  67. data/lib/capybara/spec/session/fill_in_spec.rb +1 -0
  68. data/lib/capybara/spec/session/find_button_spec.rb +1 -0
  69. data/lib/capybara/spec/session/find_by_id_spec.rb +1 -0
  70. data/lib/capybara/spec/session/find_field_spec.rb +1 -0
  71. data/lib/capybara/spec/session/find_link_spec.rb +1 -0
  72. data/lib/capybara/spec/session/find_spec.rb +1 -0
  73. data/lib/capybara/spec/session/first_spec.rb +1 -0
  74. data/lib/capybara/spec/session/go_back_spec.rb +1 -0
  75. data/lib/capybara/spec/session/go_forward_spec.rb +1 -0
  76. data/lib/capybara/spec/session/has_button_spec.rb +1 -0
  77. data/lib/capybara/spec/session/has_css_spec.rb +1 -0
  78. data/lib/capybara/spec/session/has_current_path_spec.rb +1 -0
  79. data/lib/capybara/spec/session/has_field_spec.rb +13 -0
  80. data/lib/capybara/spec/session/has_link_spec.rb +1 -0
  81. data/lib/capybara/spec/session/has_select_spec.rb +19 -0
  82. data/lib/capybara/spec/session/has_selector_spec.rb +6 -0
  83. data/lib/capybara/spec/session/has_table_spec.rb +1 -0
  84. data/lib/capybara/spec/session/has_text_spec.rb +1 -0
  85. data/lib/capybara/spec/session/has_title_spec.rb +1 -0
  86. data/lib/capybara/spec/session/has_xpath_spec.rb +1 -0
  87. data/lib/capybara/spec/session/headers.rb +1 -0
  88. data/lib/capybara/spec/session/html_spec.rb +1 -0
  89. data/lib/capybara/spec/session/node_spec.rb +29 -0
  90. data/lib/capybara/spec/session/reset_session_spec.rb +35 -1
  91. data/lib/capybara/spec/session/response_code.rb +1 -0
  92. data/lib/capybara/spec/session/save_and_open_page_spec.rb +1 -0
  93. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +3 -2
  94. data/lib/capybara/spec/session/save_page_spec.rb +34 -2
  95. data/lib/capybara/spec/session/save_screenshot_spec.rb +31 -1
  96. data/lib/capybara/spec/session/screenshot_spec.rb +3 -1
  97. data/lib/capybara/spec/session/select_spec.rb +1 -0
  98. data/lib/capybara/spec/session/selectors_spec.rb +35 -0
  99. data/lib/capybara/spec/session/text_spec.rb +1 -0
  100. data/lib/capybara/spec/session/title_spec.rb +2 -1
  101. data/lib/capybara/spec/session/uncheck_spec.rb +1 -0
  102. data/lib/capybara/spec/session/unselect_spec.rb +1 -0
  103. data/lib/capybara/spec/session/visit_spec.rb +1 -0
  104. data/lib/capybara/spec/session/window/become_closed_spec.rb +1 -0
  105. data/lib/capybara/spec/session/window/current_window_spec.rb +1 -0
  106. data/lib/capybara/spec/session/window/open_new_window_spec.rb +1 -0
  107. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -0
  108. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -0
  109. data/lib/capybara/spec/session/window/window_spec.rb +1 -8
  110. data/lib/capybara/spec/session/window/windows_spec.rb +1 -0
  111. data/lib/capybara/spec/session/window/within_window_spec.rb +2 -3
  112. data/lib/capybara/spec/session/within_frame_spec.rb +1 -0
  113. data/lib/capybara/spec/session/within_spec.rb +1 -0
  114. data/lib/capybara/spec/spec_helper.rb +2 -0
  115. data/lib/capybara/spec/test_app.rb +1 -0
  116. data/lib/capybara/spec/views/buttons.erb +1 -0
  117. data/lib/capybara/spec/views/fieldsets.erb +2 -1
  118. data/lib/capybara/spec/views/form.erb +24 -0
  119. data/lib/capybara/spec/views/frame_child.erb +2 -1
  120. data/lib/capybara/spec/views/frame_one.erb +2 -1
  121. data/lib/capybara/spec/views/frame_parent.erb +2 -1
  122. data/lib/capybara/spec/views/frame_two.erb +2 -1
  123. data/lib/capybara/spec/views/header_links.erb +1 -0
  124. data/lib/capybara/spec/views/host_links.erb +1 -0
  125. data/lib/capybara/spec/views/path.erb +1 -0
  126. data/lib/capybara/spec/views/popup_one.erb +1 -0
  127. data/lib/capybara/spec/views/popup_two.erb +1 -0
  128. data/lib/capybara/spec/views/postback.erb +2 -1
  129. data/lib/capybara/spec/views/tables.erb +1 -0
  130. data/lib/capybara/spec/views/with_base_tag.erb +1 -0
  131. data/lib/capybara/spec/views/with_count.erb +2 -1
  132. data/lib/capybara/spec/views/with_hover.erb +2 -1
  133. data/lib/capybara/spec/views/with_html.erb +3 -0
  134. data/lib/capybara/spec/views/with_html_entities.erb +1 -0
  135. data/lib/capybara/spec/views/with_js.erb +5 -0
  136. data/lib/capybara/spec/views/with_scope.erb +1 -0
  137. data/lib/capybara/spec/views/with_simple_html.erb +2 -1
  138. data/lib/capybara/spec/views/with_slow_unload.erb +17 -0
  139. data/lib/capybara/spec/views/with_title.erb +2 -1
  140. data/lib/capybara/spec/views/with_unload_alert.erb +12 -0
  141. data/lib/capybara/spec/views/with_windows.erb +1 -0
  142. data/lib/capybara/spec/views/within_frames.erb +2 -1
  143. data/lib/capybara/version.rb +2 -1
  144. data/lib/capybara/window.rb +1 -0
  145. data/lib/capybara.rb +86 -10
  146. data/spec/basic_node_spec.rb +1 -0
  147. data/spec/capybara_spec.rb +28 -1
  148. data/spec/dsl_spec.rb +1 -0
  149. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -0
  150. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -0
  151. data/spec/rack_test_spec.rb +1 -0
  152. data/spec/result_spec.rb +1 -0
  153. data/spec/rspec/features_spec.rb +1 -0
  154. data/spec/rspec/matchers_spec.rb +1 -0
  155. data/spec/rspec/scenarios_spec.rb +1 -0
  156. data/spec/rspec/views_spec.rb +1 -0
  157. data/spec/rspec_spec.rb +1 -0
  158. data/spec/selector_spec.rb +55 -1
  159. data/spec/selenium_spec.rb +6 -2
  160. data/spec/selenium_spec_chrome.rb +1 -0
  161. data/spec/server_spec.rb +101 -13
  162. data/spec/spec_helper.rb +1 -0
  163. metadata +16 -6
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ module Capybara
3
+ class Selector
4
+ class Filter
5
+ def initialize(name, block, options={})
6
+ @name = name
7
+ @block = block
8
+ @options = options
9
+ @options[:valid_values] = [true,false] if options[:boolean]
10
+ end
11
+
12
+ def default?
13
+ @options.has_key?(:default)
14
+ end
15
+
16
+ def default
17
+ @options[:default]
18
+ end
19
+
20
+ def matches?(node, value)
21
+ return true if skip?(value)
22
+
23
+ if !valid_value?(value)
24
+ msg = "Invalid value #{value.inspect} passed to filter #{@name} - "
25
+ if default?
26
+ warn msg + "defaulting to #{default}"
27
+ value = default
28
+ else
29
+ warn msg + "skipping"
30
+ return true
31
+ end
32
+ end
33
+
34
+ @block.call(node, value)
35
+ end
36
+
37
+ def skip?(value)
38
+ @options.has_key?(:skip_if) && value == @options[:skip_if]
39
+ end
40
+
41
+ private
42
+
43
+ def valid_value?(value)
44
+ !@options.has_key?(:valid_values) || Array(@options[:valid_values]).include?(value)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,42 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require 'capybara/selector/filter'
3
+
1
4
  module Capybara
2
5
  class Selector
3
- class Filter
4
- def initialize(name, block, options={})
5
- @name = name
6
- @block = block
7
- @options = options
8
- @options[:valid_values] = [true,false] if options[:boolean]
9
- end
10
-
11
- def default?
12
- @options.has_key?(:default)
13
- end
14
-
15
- def default
16
- @options[:default]
17
- end
18
-
19
- def matches?(node, value)
20
- return true if skip?(value)
21
-
22
- if @options.has_key?(:valid_values) && !Array(@options[:valid_values]).include?(value)
23
- msg = "Invalid value #{value.inspect} passed to filter #{@name} - "
24
- if default?
25
- warn msg + "defaulting to #{default}"
26
- value = default
27
- else
28
- warn msg + "skipping"
29
- return true
30
- end
31
- end
32
-
33
- @block.call(node, value)
34
- end
35
-
36
- def skip?(value)
37
- @options.has_key?(:skip_if) && value == @options[:skip_if]
38
- end
39
- end
40
6
 
41
7
  attr_reader :name, :custom_filters, :format
42
8
 
@@ -65,24 +31,19 @@ module Capybara
65
31
  @label = nil
66
32
  @failure_message = nil
67
33
  @description = nil
34
+ @format = nil
35
+ @expression = nil
68
36
  instance_eval(&block)
69
37
  end
70
38
 
71
39
  def xpath(&block)
72
- if block
73
- @format = :xpath
74
- @xpath, @css = block, nil
75
- end
76
- @xpath
40
+ @format, @expression = :xpath, block if block
41
+ format == :xpath ? @expression : nil
77
42
  end
78
43
 
79
- # Same as xpath, but wrap in XPath.css().
80
44
  def css(&block)
81
- if block
82
- @format = :css
83
- @css, @xpath = block, nil
84
- end
85
- @css
45
+ @format, @expression = :css, block if block
46
+ format == :css ? @expression : nil
86
47
  end
87
48
 
88
49
  def match(&block)
@@ -100,10 +61,10 @@ module Capybara
100
61
  end
101
62
 
102
63
  def call(locator)
103
- if @format==:css
104
- @css.call(locator)
64
+ if format
65
+ @expression.call(locator)
105
66
  else
106
- @xpath.call(locator)
67
+ warn "Selector has no format"
107
68
  end
108
69
  end
109
70
 
@@ -118,6 +79,17 @@ module Capybara
118
79
  def describe &block
119
80
  @description = block
120
81
  end
82
+
83
+ private
84
+
85
+ def locate_field(xpath, locator)
86
+ locate_field = xpath[XPath.attr(:id).equals(locator) |
87
+ XPath.attr(:name).equals(locator) |
88
+ XPath.attr(:placeholder).equals(locator) |
89
+ XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))]
90
+ locate_field += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
91
+ locate_field
92
+ end
121
93
  end
122
94
  end
123
95
 
@@ -134,7 +106,11 @@ Capybara.add_selector(:id) do
134
106
  end
135
107
 
136
108
  Capybara.add_selector(:field) do
137
- xpath { |locator| XPath::HTML.field(locator) }
109
+ xpath do |locator|
110
+ xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')]
111
+ xpath = locate_field(xpath, locator.to_s) unless locator.nil?
112
+ xpath
113
+ end
138
114
  filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
139
115
  filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
140
116
  filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
@@ -147,31 +123,42 @@ Capybara.add_selector(:field) do
147
123
  node[:type] == type
148
124
  end
149
125
  end
126
+ filter(:multiple, boolean: true) { |node, value| !(value ^ node[:multiple]) }
150
127
  describe do |options|
151
- desc, states = "", []
128
+ desc, states = String.new, []
152
129
  desc << " of type #{options[:type].inspect}" if options[:type]
153
130
  desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
154
131
  states << 'checked' if options[:checked] || (options.has_key?(:unchecked) && !options[:unchecked])
155
132
  states << 'not checked' if options[:unchecked] || (options.has_key?(:checked) && !options[:checked])
156
133
  states << 'disabled' if options[:disabled] == true
157
134
  desc << " that is #{states.join(' and ')}" unless states.empty?
135
+ desc << " with the multiple attribute" if options[:multiple] == true
136
+ desc << " without the multiple attribute" if options[:multiple] === false
158
137
  desc
159
138
  end
160
139
  end
161
140
 
162
141
  Capybara.add_selector(:fieldset) do
163
- xpath { |locator| XPath::HTML.fieldset(locator) }
164
- end
165
-
166
- Capybara.add_selector(:link_or_button) do
167
- label "link or button"
168
- xpath { |locator| XPath::HTML.link_or_button(locator) }
169
- filter(:disabled, default: false, boolean: true) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) }
170
- describe { |options| " that is disabled" if options[:disabled] }
142
+ xpath do |locator|
143
+ xpath = XPath.descendant(:fieldset)
144
+ xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]] unless locator.nil?
145
+ xpath
146
+ end
171
147
  end
172
148
 
173
149
  Capybara.add_selector(:link) do
174
- xpath { |locator| XPath::HTML.link(locator) }
150
+ xpath do |locator|
151
+ xpath = XPath.descendant(:a)[XPath.attr(:href)]
152
+ unless locator.nil?
153
+ locator = locator.to_s
154
+ xpath = xpath[XPath.attr(:id).equals(locator) |
155
+ XPath.string.n.is(locator) |
156
+ XPath.attr(:title).is(locator) |
157
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
158
+ end
159
+ xpath
160
+ end
161
+
175
162
  filter(:href) do |node, href|
176
163
  if href.is_a? Regexp
177
164
  node[:href].match href
@@ -179,31 +166,77 @@ Capybara.add_selector(:link) do
179
166
  node.first(:xpath, XPath.axis(:self)[XPath.attr(:href).equals(href.to_s)], minimum: 0)
180
167
  end
181
168
  end
169
+
182
170
  describe { |options| " with href #{options[:href].inspect}" if options[:href] }
183
171
  end
184
172
 
185
173
  Capybara.add_selector(:button) do
186
- xpath { |locator| XPath::HTML.button(locator) }
174
+ xpath do |locator|
175
+ input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
176
+ btn_xpath = XPath.descendant(:button)
177
+ image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).equals('image')]
178
+
179
+ unless locator.nil?
180
+ locator = locator.to_s
181
+ input_btn_xpath = input_btn_xpath[XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)]
182
+ btn_xpath = btn_xpath[XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.string.n.is(locator) | XPath.attr(:title).is(locator)]
183
+ image_btn_xpath = image_btn_xpath[XPath.attr(:alt).is(locator)]
184
+ end
185
+
186
+ input_btn_xpath + btn_xpath + image_btn_xpath
187
+ end
188
+
187
189
  filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
190
+
188
191
  describe { |options| " that is disabled" if options[:disabled] == true }
189
192
  end
190
193
 
194
+ Capybara.add_selector(:link_or_button) do
195
+ label "link or button"
196
+ xpath do |locator|
197
+ self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator)}.reduce(:+)
198
+ end
199
+
200
+ filter(:disabled, default: false, boolean: true) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) }
201
+
202
+ describe { |options| " that is disabled" if options[:disabled] }
203
+ end
204
+
191
205
  Capybara.add_selector(:fillable_field) do
192
206
  label "field"
193
- xpath { |locator| XPath::HTML.fillable_field(locator) }
207
+ xpath do |locator|
208
+ xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
209
+ xpath = locate_field(xpath, locator.to_s) unless locator.nil?
210
+ xpath
211
+ end
212
+
194
213
  filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
195
- describe { |options| " that is disabled" if options[:disabled] == true }
214
+ filter(:multiple, boolean: true) { |node, value| !(value ^ node[:multiple]) }
215
+
216
+ describe do |options|
217
+ desc = String.new
218
+ desc << " that is disabled" if options[:disabled] == true
219
+ desc << " with the multiple attribute" if options[:multiple] == true
220
+ desc << " without the multiple attribute" if options[:multiple] === false
221
+ desc
222
+ end
196
223
  end
197
224
 
198
225
  Capybara.add_selector(:radio_button) do
199
226
  label "radio button"
200
- xpath { |locator| XPath::HTML.radio_button(locator) }
227
+ xpath do |locator|
228
+ xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')]
229
+ xpath = locate_field(xpath, locator.to_s) unless locator.nil?
230
+ xpath
231
+ end
232
+
201
233
  filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
202
234
  filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
203
235
  filter(:option) { |node, value| node.value == value.to_s }
204
236
  filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
237
+
205
238
  describe do |options|
206
- desc, states = "", []
239
+ desc, states = String.new, []
207
240
  desc << " with value #{options[:option].inspect}" if options[:option]
208
241
  states << 'checked' if options[:checked] || (options.has_key?(:unchecked) && !options[:unchecked])
209
242
  states << 'not checked' if options[:unchecked] || (options.has_key?(:checked) && !options[:checked])
@@ -214,13 +247,19 @@ Capybara.add_selector(:radio_button) do
214
247
  end
215
248
 
216
249
  Capybara.add_selector(:checkbox) do
217
- xpath { |locator| XPath::HTML.checkbox(locator) }
250
+ xpath do |locator|
251
+ xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')]
252
+ xpath = locate_field(xpath, locator.to_s) unless locator.nil?
253
+ xpath
254
+ end
255
+
218
256
  filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
219
257
  filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
220
258
  filter(:option) { |node, value| node.value == value.to_s }
221
259
  filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
260
+
222
261
  describe do |options|
223
- desc, states = "", []
262
+ desc, states = String.new, []
224
263
  desc << " with value #{options[:option].inspect}" if options[:option]
225
264
  states << 'checked' if options[:checked] || (options.has_key?(:unchecked) && !options[:unchecked])
226
265
  states << 'not checked' if options[:unchecked] || (options.has_key?(:checked) && !options[:checked])
@@ -232,7 +271,12 @@ end
232
271
 
233
272
  Capybara.add_selector(:select) do
234
273
  label "select box"
235
- xpath { |locator| XPath::HTML.select(locator) }
274
+ xpath do |locator|
275
+ xpath = XPath.descendant(:select)
276
+ xpath = locate_field(xpath, locator.to_s) unless locator.nil?
277
+ xpath
278
+ end
279
+
236
280
  filter(:options) do |node, options|
237
281
  if node.visible?
238
282
  actual = node.all(:xpath, './/option').map { |option| option.text }
@@ -253,27 +297,83 @@ Capybara.add_selector(:select) do
253
297
  [selected].flatten.sort == actual.sort
254
298
  end
255
299
  filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
300
+ filter(:multiple, boolean: true) { |node, value| !(value ^ node[:multiple]) }
301
+
256
302
  describe do |options|
257
- desc = ""
303
+ desc = String.new
258
304
  desc << " with options #{options[:options].inspect}" if options[:options]
259
305
  desc << " with at least options #{options[:with_options].inspect}" if options[:with_options]
260
306
  desc << " with #{options[:selected].inspect} selected" if options[:selected]
261
307
  desc << " that is disabled" if options[:disabled] == true
308
+ desc << " that allows multiple selection" if options[:multiple] == true
309
+ desc << " that only allows single selection" if options[:multiple] === false
262
310
  desc
263
311
  end
264
312
  end
265
313
 
266
314
  Capybara.add_selector(:option) do
267
- xpath { |locator| XPath::HTML.option(locator) }
315
+ xpath do |locator|
316
+ xpath = XPath.descendant(:option)
317
+ xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
318
+ xpath
319
+ end
320
+
321
+ filter(:disabled, boolean: true) { |node, value| not(value ^ node.disabled?) }
322
+ filter(:selected, boolean: true) { |node, value| not(value ^ node.selected?) }
323
+
324
+ describe do |options|
325
+ desc = String.new
326
+ desc << " that is#{' not' unless options[:disabled]} disabled" if options.has_key?(:disabled)
327
+ desc << " that is#{' not' unless options[:selected]} selected" if options.has_key?(:selected)
328
+ desc
329
+ end
268
330
  end
269
331
 
270
332
  Capybara.add_selector(:file_field) do
271
333
  label "file field"
272
- xpath { |locator| XPath::HTML.file_field(locator) }
334
+ xpath do |locator|
335
+ xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')]
336
+ xpath = locate_field(xpath, locator.to_s) unless locator.nil?
337
+ xpath
338
+ end
339
+
273
340
  filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
274
- describe { |options| " that is disabled" if options[:disabled] == true}
341
+ filter(:multiple, boolean: true) { |node, value| !(value ^ node[:multiple]) }
342
+
343
+ describe do |options|
344
+ desc = String.new
345
+ desc << " that is disabled" if options[:disabled] == true
346
+ desc << " that allows multiple files" if options[:multiple] == true
347
+ desc << " that only allows a single file" if options[:multiple] === false
348
+ desc
349
+ end
350
+ end
351
+
352
+ Capybara.add_selector(:label) do
353
+ label "label"
354
+ xpath do |locator|
355
+ xpath = XPath.descendant(:label)
356
+ xpath = xpath[XPath.string.n.is(locator.to_s) | XPath.attr(:id).equals(locator.to_s)] unless locator.nil?
357
+ xpath
358
+ end
359
+
360
+ filter(:for) do |node, field_or_value|
361
+ if field_or_value.is_a? Capybara::Node::Element
362
+ if field_or_value[:id] && (field_or_value[:id] == node[:for])
363
+ true
364
+ else
365
+ field_or_value.find_xpath('./ancestor::label[1]').include? node.base
366
+ end
367
+ else
368
+ node[:for] == field_or_value.to_s
369
+ end
370
+ end
275
371
  end
276
372
 
277
373
  Capybara.add_selector(:table) do
278
- xpath { |locator| XPath::HTML.table(locator) }
374
+ xpath do |locator|
375
+ xpath = XPath.descendant(:table)
376
+ xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.descendant(:caption).is(locator.to_s)] unless locator.nil?
377
+ xpath
378
+ end
279
379
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "uri"
2
3
 
3
4
  class Capybara::Selenium::Driver < Capybara::Driver::Base
@@ -91,20 +92,35 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
91
92
  def reset!
92
93
  # Use instance variable directly so we avoid starting the browser just to reset the session
93
94
  if @browser
95
+ navigated = false
96
+ start_time = Capybara::Helpers.monotonic_time
94
97
  begin
95
- begin @browser.manage.delete_all_cookies
96
- rescue Selenium::WebDriver::Error::UnhandledError
97
- # delete_all_cookies fails when we've previously gone
98
- # to about:blank, so we rescue this error and do nothing
99
- # instead.
98
+ if !navigated
99
+ # Only trigger a navigation if we haven't done it already, otherwise it
100
+ # can trigger an endless series of unload modals
101
+ begin
102
+ @browser.manage.delete_all_cookies
103
+ rescue Selenium::WebDriver::Error::UnhandledError
104
+ # delete_all_cookies fails when we've previously gone
105
+ # to about:blank, so we rescue this error and do nothing
106
+ # instead.
107
+ end
108
+ @browser.navigate.to("about:blank")
109
+ end
110
+ navigated = true
111
+
112
+ #Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
113
+ until find_xpath("/html/body/*").empty? do
114
+ raise Capybara::ExpectationNotMet.new('Timed out waiting for Selenium session reset') if (Capybara::Helpers.monotonic_time - start_time) >= 10
115
+ sleep 0.05
100
116
  end
101
- @browser.navigate.to("about:blank")
102
117
  rescue Selenium::WebDriver::Error::UnhandledAlertError
103
118
  # This error is thrown if an unhandled alert is on the page
104
119
  # Firefox appears to automatically dismiss this alert, chrome does not
105
120
  # We'll try to accept it
106
121
  begin
107
122
  @browser.switch_to.alert.accept
123
+ sleep 0.25 # allow time for the modal to be handled
108
124
  rescue Selenium::WebDriver::Error::NoAlertPresentError
109
125
  # The alert is now gone - nothing to do
110
126
  end
@@ -247,8 +263,9 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
247
263
  Selenium::WebDriver::Error::NoSuchWindowError
248
264
  end
249
265
 
266
+ # @deprecated This method is being removed
250
267
  def browser_initialized?
251
- !@browser.nil?
268
+ super && !@browser.nil?
252
269
  end
253
270
 
254
271
  private
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Capybara::Selenium::Node < Capybara::Driver::Node
2
3
  def visible_text
3
4
  # Selenium doesn't normalize Unicode whitespace.
@@ -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,46 @@ 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
28
  def initialize(app)
11
29
  @app = app
30
+ @counter = Counter.new
31
+ end
32
+
33
+ def pending_requests?
34
+ @counter.value > 0
12
35
  end
13
36
 
14
37
  def call(env)
15
38
  if env["PATH_INFO"] == "/__identify__"
16
39
  [200, {}, [@app.object_id.to_s]]
17
40
  else
41
+ @counter.increment
18
42
  begin
19
43
  @app.call(env)
20
44
  rescue *Capybara.server_errors => e
21
45
  @error = e unless @error
22
46
  raise e
47
+ ensure
48
+ @counter.decrement
23
49
  end
24
50
  end
25
51
  end
@@ -38,8 +64,8 @@ module Capybara
38
64
  @middleware = Middleware.new(@app)
39
65
  @server_thread = nil # suppress warnings
40
66
  @host, @port = host, port
41
- @port ||= Capybara::Server.ports[@app.object_id]
42
- @port ||= find_available_port
67
+ @port ||= Capybara::Server.ports[Capybara.reuse_server ? @app.object_id : @middleware.object_id]
68
+ @port ||= find_available_port(host)
43
69
  end
44
70
 
45
71
  def reset_error!
@@ -62,12 +88,18 @@ module Capybara
62
88
  return false
63
89
  end
64
90
 
91
+ def wait_for_pending_requests
92
+ Timeout.timeout(60) { sleep(0.01) while @middleware.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[Capybara.reuse_server ? @app.object_id : @middleware.object_id] = @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,8 +112,8 @@ 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 find_available_port(host)
116
+ server = TCPServer.new(host, 0)
85
117
  server.addr[1]
86
118
  ensure
87
119
  server.close if server
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Capybara
2
3
  module SessionMatchers
3
4
  ##
@@ -66,4 +67,4 @@ module Capybara
66
67
  return false
67
68
  end
68
69
  end
69
- end
70
+ end