capybara 2.7.1 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +22 -0
  3. data/README.md +27 -3
  4. data/lib/capybara.rb +19 -4
  5. data/lib/capybara/driver/base.rb +6 -2
  6. data/lib/capybara/driver/node.rb +13 -5
  7. data/lib/capybara/helpers.rb +2 -2
  8. data/lib/capybara/node/actions.rb +116 -17
  9. data/lib/capybara/node/base.rb +7 -1
  10. data/lib/capybara/node/element.rb +23 -3
  11. data/lib/capybara/node/finders.rb +45 -29
  12. data/lib/capybara/node/matchers.rb +13 -15
  13. data/lib/capybara/queries/selector_query.rb +22 -5
  14. data/lib/capybara/queries/text_query.rb +42 -6
  15. data/lib/capybara/rack_test/node.rb +13 -1
  16. data/lib/capybara/result.rb +80 -8
  17. data/lib/capybara/rspec/features.rb +13 -6
  18. data/lib/capybara/selector.rb +98 -71
  19. data/lib/capybara/selector/filter_set.rb +46 -0
  20. data/lib/capybara/selenium/driver.rb +22 -23
  21. data/lib/capybara/selenium/node.rb +14 -6
  22. data/lib/capybara/server.rb +20 -10
  23. data/lib/capybara/session.rb +44 -8
  24. data/lib/capybara/spec/session/all_spec.rb +4 -4
  25. data/lib/capybara/spec/session/assert_text.rb +23 -0
  26. data/lib/capybara/spec/session/check_spec.rb +66 -8
  27. data/lib/capybara/spec/session/choose_spec.rb +20 -0
  28. data/lib/capybara/spec/session/click_button_spec.rb +0 -3
  29. data/lib/capybara/spec/session/click_link_spec.rb +7 -0
  30. data/lib/capybara/spec/session/execute_script_spec.rb +6 -1
  31. data/lib/capybara/spec/session/find_button_spec.rb +19 -1
  32. data/lib/capybara/spec/session/find_field_spec.rb +21 -1
  33. data/lib/capybara/spec/session/find_link_spec.rb +19 -1
  34. data/lib/capybara/spec/session/find_spec.rb +32 -4
  35. data/lib/capybara/spec/session/first_spec.rb +4 -4
  36. data/lib/capybara/spec/session/has_field_spec.rb +4 -0
  37. data/lib/capybara/spec/session/has_text_spec.rb +2 -2
  38. data/lib/capybara/spec/session/node_spec.rb +24 -3
  39. data/lib/capybara/spec/session/reset_session_spec.rb +7 -0
  40. data/lib/capybara/spec/session/selectors_spec.rb +14 -0
  41. data/lib/capybara/spec/session/uncheck_spec.rb +39 -0
  42. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +4 -2
  43. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +2 -1
  44. data/lib/capybara/spec/session/window/window_spec.rb +36 -22
  45. data/lib/capybara/spec/session/within_frame_spec.rb +19 -0
  46. data/lib/capybara/spec/spec_helper.rb +3 -0
  47. data/lib/capybara/spec/views/form.erb +34 -6
  48. data/lib/capybara/spec/views/with_html.erb +5 -1
  49. data/lib/capybara/spec/views/with_unload_alert.erb +3 -1
  50. data/lib/capybara/spec/views/with_windows.erb +2 -0
  51. data/lib/capybara/version.rb +1 -1
  52. data/spec/capybara_spec.rb +34 -0
  53. data/spec/rack_test_spec.rb +24 -0
  54. data/spec/result_spec.rb +25 -0
  55. data/spec/rspec/features_spec.rb +3 -3
  56. data/spec/selenium_spec.rb +6 -3
  57. data/spec/server_spec.rb +2 -2
  58. metadata +18 -4
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  if RSpec::Core::Version::STRING.to_f >= 3.0
3
- RSpec.shared_context "Capybara Features", :capybara_feature => true do
3
+ RSpec.shared_context "Capybara Features", capybara_feature: true do
4
4
  instance_eval do
5
5
  alias background before
6
6
  alias given let
@@ -8,13 +8,20 @@ if RSpec::Core::Version::STRING.to_f >= 3.0
8
8
  end
9
9
  end
10
10
 
11
+ # ensure shared_context is included if default shared_context_metadata_behavior is changed
12
+ if RSpec::Core::Version::STRING.to_f >= 3.5
13
+ RSpec.configure do |config|
14
+ config.include_context "Capybara Features", capybara_feature: true
15
+ end
16
+ end
17
+
11
18
  RSpec.configure do |config|
12
- config.alias_example_group_to :feature, :capybara_feature => true, :type => :feature
13
- config.alias_example_group_to :xfeature, :capybara_feature => true, :type => :feature, :skip => "Temporarily disabled with xfeature"
14
- config.alias_example_group_to :ffeature, :capybara_feature => true, :type => :feature, :focus => true
19
+ config.alias_example_group_to :feature, capybara_feature: true, type: :feature
20
+ config.alias_example_group_to :xfeature, capybara_feature: true, type: :feature, skip: "Temporarily disabled with xfeature"
21
+ config.alias_example_group_to :ffeature, capybara_feature: true, type: :feature, focus: true
15
22
  config.alias_example_to :scenario
16
- config.alias_example_to :xscenario, :skip => "Temporarily disabled with xscenario"
17
- config.alias_example_to :fscenario, :focus => true
23
+ config.alias_example_to :xscenario, skip: "Temporarily disabled with xscenario"
24
+ config.alias_example_to :fscenario, focus: true
18
25
  end
19
26
  else
20
27
  module Capybara
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
- require 'capybara/selector/filter'
2
+ require 'capybara/selector/filter_set'
3
3
 
4
4
  module Capybara
5
5
  class Selector
6
6
 
7
- attr_reader :name, :custom_filters, :format
7
+ attr_reader :name, :format
8
8
 
9
9
  class << self
10
10
  def all
@@ -26,7 +26,7 @@ module Capybara
26
26
 
27
27
  def initialize(name, &block)
28
28
  @name = name
29
- @custom_filters = {}
29
+ @filter_set = FilterSet.add(name){}
30
30
  @match = nil
31
31
  @label = nil
32
32
  @failure_message = nil
@@ -36,6 +36,10 @@ module Capybara
36
36
  instance_eval(&block)
37
37
  end
38
38
 
39
+ def custom_filters
40
+ @filter_set.filters
41
+ end
42
+
39
43
  def xpath(&block)
40
44
  @format, @expression = :xpath, block if block
41
45
  format == :xpath ? @expression : nil
@@ -57,7 +61,7 @@ module Capybara
57
61
  end
58
62
 
59
63
  def description(options={})
60
- (@description && @description.call(options)).to_s
64
+ @filter_set.description(options)
61
65
  end
62
66
 
63
67
  def call(locator)
@@ -73,20 +77,31 @@ module Capybara
73
77
  end
74
78
 
75
79
  def filter(name, options={}, &block)
76
- @custom_filters[name] = Filter.new(name, block, options)
80
+ custom_filters[name] = Filter.new(name, block, options)
81
+ end
82
+
83
+ def filter_set(name, filters_to_use = nil)
84
+ f_set = FilterSet.all[name]
85
+ f_set.filters.each do | name, filter |
86
+ custom_filters[name] = filter if filters_to_use.nil? || filters_to_use.include?(name)
87
+ end
88
+ f_set.descriptions.each { |desc| @filter_set.describe &desc }
77
89
  end
78
90
 
79
91
  def describe &block
80
- @description = block
92
+ @filter_set.describe &block
81
93
  end
82
94
 
83
95
  private
84
96
 
85
97
  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))]
98
+ attr_matchers = XPath.attr(:id).equals(locator) |
99
+ XPath.attr(:name).equals(locator) |
100
+ XPath.attr(:placeholder).equals(locator) |
101
+ XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
102
+ attr_matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
103
+
104
+ locate_field = xpath[attr_matchers]
90
105
  locate_field += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
91
106
  locate_field
92
107
  end
@@ -105,35 +120,55 @@ Capybara.add_selector(:id) do
105
120
  xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
106
121
  end
107
122
 
123
+ Capybara::Selector::FilterSet.add(:_field) do
124
+ filter(:id) { |node, id| node['id'] == id }
125
+ filter(:name) { |node, name| node['name'] == name }
126
+ filter(:placeholder) { |node, placeholder| node['placeholder'] == placeholder }
127
+ filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
128
+ filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
129
+ filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
130
+ filter(:multiple, boolean: true) { |node, value| !(value ^ node.multiple?) }
131
+
132
+ describe do |options|
133
+ desc, states = String.new, []
134
+ [:id, :name, :placeholder].each do |opt|
135
+ desc << " with #{opt.to_s} #{options[opt]}" if options.has_key?(opt)
136
+ end
137
+ states << 'checked' if options[:checked] || (options[:unchecked] === false)
138
+ states << 'not checked' if options[:unchecked] || (options[:checked] === false)
139
+ states << 'disabled' if options[:disabled] == true
140
+ desc << " that is #{states.join(' and ')}" unless states.empty?
141
+ desc << " with the multiple attribute" if options[:multiple] == true
142
+ desc << " without the multiple attribute" if options[:multiple] === false
143
+ desc
144
+ end
145
+ end
146
+
108
147
  Capybara.add_selector(:field) do
109
148
  xpath do |locator|
110
149
  xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')]
111
150
  xpath = locate_field(xpath, locator.to_s) unless locator.nil?
112
151
  xpath
113
152
  end
114
- filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
115
- filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
116
- filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
117
- filter(:readonly, boolean: true) { |node, value| not(value ^ node[:readonly]) }
118
- filter(:with) { |node, with| node.value == with.to_s }
153
+
154
+ filter_set(:_field)
155
+
156
+ filter(:readonly, boolean: true) { |node, value| not(value ^ node.readonly?) }
157
+ filter(:with) do |node, with|
158
+ with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
159
+ end
119
160
  filter(:type) do |node, type|
161
+ type = type.to_s
120
162
  if ['textarea', 'select'].include?(type)
121
163
  node.tag_name == type
122
164
  else
123
165
  node[:type] == type
124
166
  end
125
167
  end
126
- filter(:multiple, boolean: true) { |node, value| !(value ^ node[:multiple]) }
127
168
  describe do |options|
128
169
  desc, states = String.new, []
129
170
  desc << " of type #{options[:type].inspect}" if options[:type]
130
171
  desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
131
- states << 'checked' if options[:checked] || (options.has_key?(:unchecked) && !options[:unchecked])
132
- states << 'not checked' if options[:unchecked] || (options.has_key?(:checked) && !options[:checked])
133
- states << 'disabled' if options[:disabled] == true
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
137
172
  desc
138
173
  end
139
174
  end
@@ -151,10 +186,12 @@ Capybara.add_selector(:link) do
151
186
  xpath = XPath.descendant(:a)[XPath.attr(:href)]
152
187
  unless locator.nil?
153
188
  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)]]
189
+ matchers = XPath.attr(:id).equals(locator) |
190
+ XPath.string.n.is(locator) |
191
+ XPath.attr(:title).is(locator) |
192
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
193
+ matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
194
+ xpath = xpath[matchers]
158
195
  end
159
196
  xpath
160
197
  end
@@ -178,9 +215,16 @@ Capybara.add_selector(:button) do
178
215
 
179
216
  unless locator.nil?
180
217
  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)]
218
+ locator_matches = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
219
+ locator_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
220
+
221
+ input_btn_xpath = input_btn_xpath[locator_matches]
222
+
223
+ btn_xpath = btn_xpath[locator_matches | XPath.string.n.is(locator)]
224
+
225
+ alt_matches = XPath.attr(:alt).is(locator)
226
+ alt_matches |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
227
+ image_btn_xpath = image_btn_xpath[alt_matches]
184
228
  end
185
229
 
186
230
  input_btn_xpath + btn_xpath + image_btn_xpath
@@ -197,7 +241,7 @@ Capybara.add_selector(:link_or_button) do
197
241
  self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator)}.reduce(:+)
198
242
  end
199
243
 
200
- filter(:disabled, default: false, boolean: true) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) }
244
+ filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) }
201
245
 
202
246
  describe { |options| " that is disabled" if options[:disabled] }
203
247
  end
@@ -210,16 +254,7 @@ Capybara.add_selector(:fillable_field) do
210
254
  xpath
211
255
  end
212
256
 
213
- filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
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
257
+ filter_set(:_field, [:id, :name, :placeholder, :disabled, :multiple])
223
258
  end
224
259
 
225
260
  Capybara.add_selector(:radio_button) do
@@ -230,18 +265,13 @@ Capybara.add_selector(:radio_button) do
230
265
  xpath
231
266
  end
232
267
 
233
- filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
234
- filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
268
+ filter_set(:_field, [:id, :name, :checked, :unchecked, :disabled])
269
+
235
270
  filter(:option) { |node, value| node.value == value.to_s }
236
- filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
237
271
 
238
272
  describe do |options|
239
- desc, states = String.new, []
273
+ desc = String.new
240
274
  desc << " with value #{options[:option].inspect}" if options[:option]
241
- states << 'checked' if options[:checked] || (options.has_key?(:unchecked) && !options[:unchecked])
242
- states << 'not checked' if options[:unchecked] || (options.has_key?(:checked) && !options[:checked])
243
- states << 'disabled' if options[:disabled] == true
244
- desc << " that is #{states.join(' and ')}" unless states.empty?
245
275
  desc
246
276
  end
247
277
  end
@@ -253,18 +283,13 @@ Capybara.add_selector(:checkbox) do
253
283
  xpath
254
284
  end
255
285
 
256
- filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
257
- filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
286
+ filter_set(:_field, [:id, :name, :checked, :unchecked, :disabled])
287
+
258
288
  filter(:option) { |node, value| node.value == value.to_s }
259
- filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
260
289
 
261
290
  describe do |options|
262
- desc, states = String.new, []
291
+ desc = String.new
263
292
  desc << " with value #{options[:option].inspect}" if options[:option]
264
- states << 'checked' if options[:checked] || (options.has_key?(:unchecked) && !options[:unchecked])
265
- states << 'not checked' if options[:unchecked] || (options.has_key?(:checked) && !options[:checked])
266
- states << 'disabled' if options[:disabled] == true
267
- desc << " that is #{states.join(' and ')}" unless states.empty?
268
293
  desc
269
294
  end
270
295
  end
@@ -277,6 +302,8 @@ Capybara.add_selector(:select) do
277
302
  xpath
278
303
  end
279
304
 
305
+ filter_set(:_field, [:id, :name, :placeholder, :disabled, :multiple])
306
+
280
307
  filter(:options) do |node, options|
281
308
  if node.visible?
282
309
  actual = node.all(:xpath, './/option').map { |option| option.text }
@@ -296,17 +323,12 @@ Capybara.add_selector(:select) do
296
323
  actual = node.all(:xpath, './/option', visible: false).select { |option| option.selected? }.map { |option| option.text(:all) }
297
324
  [selected].flatten.sort == actual.sort
298
325
  end
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
326
 
302
327
  describe do |options|
303
328
  desc = String.new
304
329
  desc << " with options #{options[:options].inspect}" if options[:options]
305
330
  desc << " with at least options #{options[:with_options].inspect}" if options[:with_options]
306
331
  desc << " with #{options[:selected].inspect} selected" if options[:selected]
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
310
332
  desc
311
333
  end
312
334
  end
@@ -337,16 +359,7 @@ Capybara.add_selector(:file_field) do
337
359
  xpath
338
360
  end
339
361
 
340
- filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
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
362
+ filter_set(:_field, [:id, :name, :disabled, :multiple])
350
363
  end
351
364
 
352
365
  Capybara.add_selector(:label) do
@@ -368,6 +381,12 @@ Capybara.add_selector(:label) do
368
381
  node[:for] == field_or_value.to_s
369
382
  end
370
383
  end
384
+
385
+ describe do |options|
386
+ desc = String.new
387
+ desc << " for #{options[:for]}" if options[:for]
388
+ desc
389
+ end
371
390
  end
372
391
 
373
392
  Capybara.add_selector(:table) do
@@ -377,3 +396,11 @@ Capybara.add_selector(:table) do
377
396
  xpath
378
397
  end
379
398
  end
399
+
400
+ Capybara.add_selector(:frame) do
401
+ xpath do |locator|
402
+ xpath = XPath.descendant(:iframe) + XPath.descendant(:frame)
403
+ xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.attr(:name).equals(locator)] unless locator.nil?
404
+ xpath
405
+ end
406
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ require 'capybara/selector/filter'
3
+
4
+ module Capybara
5
+ class Selector
6
+ class FilterSet
7
+ attr_reader :descriptions
8
+
9
+ def initialize(name, &block)
10
+ @name = name
11
+ @descriptions = []
12
+ instance_eval(&block)
13
+ end
14
+
15
+ def filter(name, options={}, &block)
16
+ filters[name] = Filter.new(name, block, options)
17
+ end
18
+
19
+ def describe(&block)
20
+ descriptions.push block
21
+ end
22
+
23
+ def description(options={})
24
+ @descriptions.map {|desc| desc.call(options).to_s }.join
25
+ end
26
+
27
+ def filters
28
+ @filters ||= {}
29
+ end
30
+
31
+ class << self
32
+ def all
33
+ @filter_sets ||= {}
34
+ end
35
+
36
+ def add(name, &block)
37
+ all[name.to_sym] = FilterSet.new(name.to_sym, &block)
38
+ end
39
+
40
+ def remove(name)
41
+ all.delete(name.to_sym)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -11,6 +11,11 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
11
11
 
12
12
  def browser
13
13
  unless @browser
14
+ if options[:browser].to_s == "firefox"
15
+ options[:desired_capabilities] ||= Selenium::WebDriver::Remote::Capabilities.firefox
16
+ options[:desired_capabilities].merge!({ unexpectedAlertBehaviour: "ignore" })
17
+ end
18
+
14
19
  @browser = Selenium::WebDriver.for(options[:browser], options.reject { |key,val| SPECIAL_OPTIONS.include?(key) })
15
20
 
16
21
  main = Process.pid
@@ -130,29 +135,22 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
130
135
  end
131
136
  end
132
137
 
133
- ##
134
- #
135
- # Webdriver supports frame name, id, index(zero-based) or {Capybara::Node::Element} to find iframe
136
- #
137
- # @overload within_frame(index)
138
- # @param [Integer] index index of a frame
139
- # @overload within_frame(name_or_id)
140
- # @param [String] name_or_id name or id of a frame
141
- # @overload within_frame(element)
142
- # @param [Capybara::Node::Base] a_node frame element
143
- #
144
- def within_frame(frame_handle)
145
- frame_handle = frame_handle.native if frame_handle.is_a?(Capybara::Node::Base)
146
- @frame_handles[browser.window_handle] ||= []
147
- @frame_handles[browser.window_handle] << frame_handle
148
- browser.switch_to.frame(frame_handle)
149
- yield
150
- ensure
151
- # would love to use browser.switch_to.parent_frame here
152
- # but it has an issue if the current frame is removed from within it
153
- @frame_handles[browser.window_handle].pop
154
- browser.switch_to.default_content
155
- @frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
138
+ def switch_to_frame(frame)
139
+ case frame
140
+ when :top
141
+ @frame_handles[browser.window_handle] = []
142
+ browser.switch_to.default_content
143
+ when :parent
144
+ # would love to use browser.switch_to.parent_frame here
145
+ # but it has an issue if the current frame is removed from within it
146
+ @frame_handles[browser.window_handle].pop
147
+ browser.switch_to.default_content
148
+ @frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
149
+ else
150
+ @frame_handles[browser.window_handle] ||= []
151
+ @frame_handles[browser.window_handle] << frame.native
152
+ browser.switch_to.frame(frame.native)
153
+ end
156
154
  end
157
155
 
158
156
  def current_window_handle
@@ -221,6 +219,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
221
219
  end
222
220
 
223
221
  def accept_modal(type, options={}, &blk)
222
+ options = options.dup
224
223
  yield if block_given?
225
224
  modal = find_modal(options)
226
225
  modal.send_keys options[:with] if options[:with]