capybara 2.14.0 → 2.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/capybara.rb +16 -15
  5. data/lib/capybara/minitest.rb +92 -113
  6. data/lib/capybara/minitest/spec.rb +12 -37
  7. data/lib/capybara/node/matchers.rb +6 -5
  8. data/lib/capybara/queries/base_query.rb +4 -0
  9. data/lib/capybara/queries/current_path_query.rb +1 -0
  10. data/lib/capybara/queries/selector_query.rb +39 -12
  11. data/lib/capybara/queries/text_query.rb +1 -1
  12. data/lib/capybara/queries/title_query.rb +1 -0
  13. data/lib/capybara/rack_test/driver.rb +1 -0
  14. data/lib/capybara/rspec/matcher_proxies.rb +10 -6
  15. data/lib/capybara/rspec/matchers.rb +29 -0
  16. data/lib/capybara/selector.rb +61 -50
  17. data/lib/capybara/selector/expression_filter.rb +40 -0
  18. data/lib/capybara/selector/filter_set.rb +22 -3
  19. data/lib/capybara/selector/selector.rb +33 -12
  20. data/lib/capybara/selenium/driver.rb +130 -25
  21. data/lib/capybara/selenium/node.rb +3 -3
  22. data/lib/capybara/session/config.rb +29 -23
  23. data/lib/capybara/session/matchers.rb +3 -0
  24. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  25. data/lib/capybara/spec/session/all_spec.rb +2 -1
  26. data/lib/capybara/spec/session/assert_selector.rb +1 -1
  27. data/lib/capybara/spec/session/assert_title.rb +22 -9
  28. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
  29. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +1 -1
  30. data/lib/capybara/spec/session/find_spec.rb +3 -2
  31. data/lib/capybara/spec/session/first_spec.rb +10 -5
  32. data/lib/capybara/spec/session/has_css_spec.rb +11 -0
  33. data/lib/capybara/spec/session/has_current_path_spec.rb +5 -3
  34. data/lib/capybara/spec/session/has_select_spec.rb +62 -4
  35. data/lib/capybara/spec/session/has_text_spec.rb +5 -3
  36. data/lib/capybara/spec/session/has_title_spec.rb +4 -2
  37. data/lib/capybara/spec/session/has_xpath_spec.rb +5 -3
  38. data/lib/capybara/spec/session/node_spec.rb +8 -4
  39. data/lib/capybara/spec/session/window/become_closed_spec.rb +4 -4
  40. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +4 -4
  41. data/lib/capybara/spec/spec_helper.rb +1 -1
  42. data/lib/capybara/spec/views/form.erb +22 -1
  43. data/lib/capybara/spec/views/with_html.erb +1 -1
  44. data/lib/capybara/version.rb +1 -1
  45. data/spec/capybara_spec.rb +16 -0
  46. data/spec/filter_set_spec.rb +28 -0
  47. data/spec/minitest_spec_spec.rb +4 -4
  48. data/spec/per_session_config_spec.rb +4 -4
  49. data/spec/result_spec.rb +20 -0
  50. data/spec/selector_spec.rb +2 -1
  51. data/spec/selenium_spec_chrome.rb +12 -1
  52. data/spec/selenium_spec_firefox.rb +2 -1
  53. data/spec/selenium_spec_marionette.rb +4 -3
  54. data/spec/shared_selenium_session.rb +14 -7
  55. metadata +18 -2
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require 'capybara/selector/filter'
3
+
4
+ module Capybara
5
+ class Selector
6
+ class ExpressionFilter < Filter
7
+ undef_method :matches?
8
+
9
+ def apply_filter(expr, value)
10
+ return expr if skip?(value)
11
+
12
+ if !valid_value?(value)
13
+ msg = "Invalid value #{value.inspect} passed to expression filter #{@name} - "
14
+ if default?
15
+ warn msg + "defaulting to #{default}"
16
+ value = default
17
+ else
18
+ warn msg + "skipping"
19
+ return expr
20
+ end
21
+ end
22
+
23
+ @block.call(expr, value)
24
+ end
25
+ end
26
+
27
+ class IdentityExpressionFilter < ExpressionFilter
28
+ def initialize
29
+ end
30
+
31
+ def default?
32
+ false
33
+ end
34
+
35
+ def apply_filter(expr, _value)
36
+ return expr
37
+ end
38
+ end
39
+ end
40
+ end
@@ -13,9 +13,11 @@ module Capybara
13
13
  end
14
14
 
15
15
  def filter(name, *types_and_options, &block)
16
- options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
17
- types_and_options.each { |k| options[k] = true}
18
- filters[name] = Filter.new(name, block, options)
16
+ add_filter(name, Filter, *types_and_options, &block)
17
+ end
18
+
19
+ def expression_filter(name, *types_and_options, &block)
20
+ add_filter(name, ExpressionFilter, *types_and_options, &block)
19
21
  end
20
22
 
21
23
  def describe(&block)
@@ -30,7 +32,16 @@ module Capybara
30
32
  @filters ||= {}
31
33
  end
32
34
 
35
+ def node_filters
36
+ filters.reject { |_n, f| f.nil? || f.is_a?(ExpressionFilter) }.freeze
37
+ end
38
+
39
+ def expression_filters
40
+ filters.select { |_n, f| f.nil? || f.is_a?(ExpressionFilter) }.freeze
41
+ end
42
+
33
43
  class << self
44
+
34
45
  def all
35
46
  @filter_sets ||= {}
36
47
  end
@@ -43,6 +54,14 @@ module Capybara
43
54
  all.delete(name.to_sym)
44
55
  end
45
56
  end
57
+
58
+ private
59
+
60
+ def add_filter(name, filter_class, *types_and_options, &block)
61
+ options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
62
+ types_and_options.each { |k| options[k] = true}
63
+ filters[name] = filter_class.new(name, block, options)
64
+ end
46
65
  end
47
66
  end
48
67
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'capybara/selector/expression_filter'
2
3
  require 'capybara/selector/filter_set'
3
4
  require 'capybara/selector/css'
4
5
  require 'xpath'
@@ -21,7 +22,7 @@ end
21
22
  module Capybara
22
23
  class Selector
23
24
 
24
- attr_reader :name, :format, :expression_filters
25
+ attr_reader :name, :format
25
26
 
26
27
  class << self
27
28
  def all
@@ -50,7 +51,7 @@ module Capybara
50
51
  @description = nil
51
52
  @format = nil
52
53
  @expression = nil
53
- @expression_filters = []
54
+ @expression_filters = {}
54
55
  @default_visibility = nil
55
56
  instance_eval(&block)
56
57
  end
@@ -59,6 +60,14 @@ module Capybara
59
60
  @filter_set.filters
60
61
  end
61
62
 
63
+ def node_filters
64
+ @filter_set.node_filters
65
+ end
66
+
67
+ def expression_filters
68
+ @filter_set.expression_filters
69
+ end
70
+
62
71
  ##
63
72
  #
64
73
  # Define a selector by an xpath expression
@@ -74,7 +83,10 @@ module Capybara
74
83
  # @return [#call] The block that will be called to generate the XPath expression
75
84
  #
76
85
  def xpath(*expression_filters, &block)
77
- @format, @expression_filters, @expression = :xpath, expression_filters.flatten, block if block
86
+ if block
87
+ @format, @expression = :xpath, block
88
+ expression_filters.flatten.each { |ef| custom_filters[ef] = IdentityExpressionFilter.new }
89
+ end
78
90
  format == :xpath ? @expression : nil
79
91
  end
80
92
 
@@ -93,7 +105,10 @@ module Capybara
93
105
  # @return [#call] The block that will be called to generate the CSS selector
94
106
  #
95
107
  def css(*expression_filters, &block)
96
- @format, @expression_filters, @expression = :css, expression_filters.flatten, block if block
108
+ if block
109
+ @format, @expression = :css, block
110
+ expression_filters.flatten.each { |ef| custom_filters[ef] = nil }
111
+ end
97
112
  format == :css ? @expression : nil
98
113
  end
99
114
 
@@ -172,10 +187,16 @@ module Capybara
172
187
  #
173
188
  def filter(name, *types_and_options, &block)
174
189
  options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
175
- types_and_options.each { |k| options[k] = true}
190
+ types_and_options.each { |k| options[k] = true }
176
191
  custom_filters[name] = Filter.new(name, block, options)
177
192
  end
178
193
 
194
+ def expression_filter(name, *types_and_options, &block)
195
+ options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
196
+ types_and_options.each { |k| options[k] = true }
197
+ custom_filters[name] = ExpressionFilter.new(name, block, options)
198
+ end
199
+
179
200
  def filter_set(name, filters_to_use = nil)
180
201
  f_set = FilterSet.all[name]
181
202
  f_set.filters.each do |n, filter|
@@ -215,17 +236,17 @@ module Capybara
215
236
  locate_xpath = xpath #need to save original xpath for the label wrap
216
237
  if locator
217
238
  locator = locator.to_s
218
- attr_matchers = XPath.attr(:id).equals(locator) |
219
- XPath.attr(:name).equals(locator) |
220
- XPath.attr(:placeholder).equals(locator) |
221
- XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
222
- attr_matchers |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
239
+ attr_matchers = XPath.attr(:id).equals(locator).or(
240
+ XPath.attr(:name).equals(locator)).or(
241
+ XPath.attr(:placeholder).equals(locator)).or(
242
+ XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)))
243
+ attr_matchers = attr_matchers.or XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
223
244
 
224
245
  locate_xpath = locate_xpath[attr_matchers]
225
- locate_xpath += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
246
+ locate_xpath = locate_xpath.union(XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath))
226
247
  end
227
248
 
228
- locate_xpath = [:name, :placeholder].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
249
+ # locate_xpath = [:name, :placeholder].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
229
250
  locate_xpath
230
251
  end
231
252
 
@@ -14,12 +14,16 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
14
14
 
15
15
  def browser
16
16
  unless @browser
17
- if options[:browser].to_s == "firefox"
18
- options[:desired_capabilities] ||= Selenium::WebDriver::Remote::Capabilities.firefox
17
+ if firefox?
18
+ options[:desired_capabilities] ||= {}
19
19
  options[:desired_capabilities].merge!({ unexpectedAlertBehaviour: "ignore" })
20
20
  end
21
21
 
22
- @browser = Selenium::WebDriver.for(options[:browser], options.reject { |key,_val| SPECIAL_OPTIONS.include?(key) })
22
+ @processed_options = options.reject { |key,_val| SPECIAL_OPTIONS.include?(key) }
23
+ @browser = Selenium::WebDriver.for(options[:browser], @processed_options)
24
+
25
+ @w3c = ((defined?(Selenium::WebDriver::Remote::W3CCapabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)) ||
26
+ (defined?(Selenium::WebDriver::Remote::W3C::Capabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3C::Capabilities)))
23
27
 
24
28
  main = Process.pid
25
29
  at_exit do
@@ -33,6 +37,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
33
37
  end
34
38
 
35
39
  def initialize(app, options={})
40
+ @session = nil
36
41
  begin
37
42
  require 'selenium-webdriver'
38
43
  # Fix for selenium-webdriver 3.4.0 which misnamed these
@@ -145,7 +150,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
145
150
  raise Capybara::ExpectationNotMet.new('Timed out waiting for Selenium session reset') if (Capybara::Helpers.monotonic_time - start_time) >= 10
146
151
  sleep 0.05
147
152
  end
148
- rescue Selenium::WebDriver::Error::UnhandledAlertError
153
+ rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
149
154
  # This error is thrown if an unhandled alert is on the page
150
155
  # Firefox appears to automatically dismiss this alert, chrome does not
151
156
  # We'll try to accept it
@@ -232,20 +237,32 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
232
237
  end
233
238
 
234
239
  def accept_modal(_type, options={})
235
- yield if block_given?
236
- modal = find_modal(options)
237
- modal.send_keys options[:with] if options[:with]
238
- message = modal.text
239
- modal.accept
240
- message
240
+ if headless_chrome?
241
+ insert_modal_handlers(true, options[:with], options[:text])
242
+ yield if block_given?
243
+ find_headless_modal(options)
244
+ else
245
+ yield if block_given?
246
+ modal = find_modal(options)
247
+ modal.send_keys options[:with] if options[:with]
248
+ message = modal.text
249
+ modal.accept
250
+ message
251
+ end
241
252
  end
242
253
 
243
254
  def dismiss_modal(_type, options={})
244
- yield if block_given?
245
- modal = find_modal(options)
246
- message = modal.text
247
- modal.dismiss
248
- message
255
+ if headless_chrome?
256
+ insert_modal_handlers(false, options[:with], options[:text])
257
+ yield if block_given?
258
+ find_headless_modal(options)
259
+ else
260
+ yield if block_given?
261
+ modal = find_modal(options)
262
+ message = modal.text
263
+ modal.dismiss
264
+ message
265
+ end
249
266
  end
250
267
 
251
268
  def quit
@@ -275,6 +292,37 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
275
292
  end
276
293
 
277
294
  # @api private
295
+ def marionette?
296
+ firefox? && browser && @w3c
297
+ end
298
+
299
+ # @api private
300
+ def firefox?
301
+ browser_name == "firefox"
302
+ end
303
+
304
+ # @api private
305
+ def chrome?
306
+ browser_name == "chrome"
307
+ end
308
+
309
+ # @api private
310
+ def headless_chrome?
311
+ chrome? && ((@processed_options[:desired_capabilities][:chrome_options] || {})['args'] || []).include?("headless")
312
+ end
313
+
314
+ # @deprecated This method is being removed
315
+ def browser_initialized?
316
+ super && !@browser.nil?
317
+ end
318
+
319
+ private
320
+
321
+ # @api private
322
+ def browser_name
323
+ options[:browser].to_s
324
+ end
325
+
278
326
  def find_window(locator)
279
327
  handles = browser.window_handles
280
328
  return locator if handles.include? locator
@@ -292,18 +340,49 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
292
340
  raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}"
293
341
  end
294
342
 
295
- #@api private
296
- def marionette?
297
- (options[:browser].to_s == "firefox") && browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)
343
+ def insert_modal_handlers(accept, response_text, expected_text=nil)
344
+ script = <<-JS
345
+ if (typeof window.capybara === 'undefined') {
346
+ window.capybara = {
347
+ modal_handlers: [],
348
+ current_modal_status: function() {
349
+ return [this.modal_handlers[0].called, this.modal_handlers[0].modal_text];
350
+ },
351
+ add_handler: function(handler) {
352
+ this.modal_handlers.unshift(handler);
353
+ },
354
+ remove_handler: function(handler) {
355
+ window.alert = handler.alert;
356
+ window.confirm = handler.confirm;
357
+ window.prompt = handler.prompt;
358
+ },
359
+ handler_called: function(handler, str) {
360
+ handler.called = true;
361
+ handler.modal_text = str;
362
+ this.remove_handler(handler);
363
+ }
364
+ };
365
+ };
366
+
367
+ var modal_handler = {
368
+ prompt: window.prompt,
369
+ confirm: window.confirm,
370
+ alert: window.alert,
371
+ }
372
+ window.capybara.add_handler(modal_handler);
373
+
374
+ window.alert = window.confirm = function(str) {
375
+ window.capybara.handler_called(modal_handler, str);
376
+ return #{accept ? 'true' : 'false'};
377
+ };
378
+ window.prompt = function(str) {
379
+ window.capybara.handler_called(modal_handler, str);
380
+ return #{accept ? "'#{response_text}'" : 'null'};
381
+ }
382
+ JS
383
+ execute_script script
298
384
  end
299
385
 
300
- # @deprecated This method is being removed
301
- def browser_initialized?
302
- super && !@browser.nil?
303
- end
304
-
305
- private
306
-
307
386
  def within_given_window(handle)
308
387
  original_handle = self.current_window_handle
309
388
  if handle == original_handle
@@ -333,6 +412,32 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
333
412
  end
334
413
  end
335
414
 
415
+ def find_headless_modal(options={})
416
+ # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
417
+ # Actual wait time may be longer than specified
418
+ wait = Selenium::WebDriver::Wait.new(
419
+ timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
420
+ ignore: Selenium::WebDriver::Error::NoAlertPresentError)
421
+ begin
422
+ wait.until do
423
+ called, alert_text = evaluate_script('window.capybara.current_modal_status()')
424
+ if called
425
+ execute_script('window.capybara.modal_handlers.shift()')
426
+ regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
427
+ if alert_text.match(regexp)
428
+ alert_text
429
+ else
430
+ raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
431
+ end
432
+ else
433
+ nil
434
+ end
435
+ end
436
+ rescue Selenium::WebDriver::Error::TimeOutError
437
+ raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
438
+ end
439
+ end
440
+
336
441
  def silenced_unknown_error_message?(msg)
337
442
  silenced_unknown_error_messages.any? { |r| msg =~ r }
338
443
  end
@@ -47,7 +47,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
47
47
  click if value ^ native.attribute('checked').to_s.eql?("true")
48
48
  elsif tag_name == 'input' and type == 'file'
49
49
  path_names = value.to_s.empty? ? [] : value
50
- if driver.options[:browser].to_s == "chrome"
50
+ if driver.chrome?
51
51
  native.send_keys(Array(path_names).join("\n"))
52
52
  else
53
53
  native.send_keys(*path_names)
@@ -88,8 +88,8 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
88
88
  JS
89
89
  driver.execute_script script, self
90
90
 
91
- if (driver.options[:browser].to_s == "chrome") ||
92
- (driver.options[:browser].to_s == "firefox" && !driver.marionette?)
91
+ if (driver.chrome?) ||
92
+ (driver.firefox? && !driver.marionette?)
93
93
  # chromedriver raises a can't focus element for child elements if we use native.send_keys
94
94
  # we've already focused it so just use action api
95
95
  driver.browser.action.send_keys(value.to_s).perform
@@ -8,53 +8,55 @@ module Capybara
8
8
  :automatic_label_click, :enable_aria_label, :save_path, :exact_options, :asset_host, :default_host, :app_host,
9
9
  :save_and_open_page_path, :server_host, :server_port, :server_errors]
10
10
 
11
- attr_accessor *OPTIONS
11
+ attr_accessor(*OPTIONS)
12
12
 
13
13
  ##
14
14
  #@!method always_include_port
15
- # See {Capybara#configure}
15
+ # See {Capybara.configure}
16
16
  #@!method run_server
17
- # See {Capybara#configure}
17
+ # See {Capybara.configure}
18
18
  #@!method default_selector
19
- # See {Capybara#configure}
19
+ # See {Capybara.configure}
20
20
  #@!method default_max_wait_time
21
- # See {Capybara#configure}
21
+ # See {Capybara.configure}
22
22
  #@!method ignore_hidden_elements
23
- # See {Capybara#configure}
23
+ # See {Capybara.configure}
24
24
  #@!method automatic_reload
25
- # See {Capybara#configure}
25
+ # See {Capybara.configure}
26
26
  #@!method match
27
- # See {Capybara#configure}
27
+ # See {Capybara.configure}
28
28
  #@!method exact
29
- # See {Capybara#configure}
29
+ # See {Capybara.configure}
30
30
  #@!method raise_server_errors
31
- # See {Capybara#configure}
31
+ # See {Capybara.configure}
32
32
  #@!method visible_text_only
33
- # See {Capybara#configure}
33
+ # See {Capybara.configure}
34
34
  #@!method wait_on_first_by_default
35
- # See {Capybara#configure}
35
+ # See {Capybara.configure}
36
36
  #@!method automatic_label_click
37
- # See {Capybara#configure}
37
+ # See {Capybara.configure}
38
38
  #@!method enable_aria_label
39
- # See {Capybara#configure}
39
+ # See {Capybara.configure}
40
40
  #@!method save_path
41
- # See {Capybara#configure}
41
+ # See {Capybara.configure}
42
42
  #@!method exact_options
43
- # See {Capybara#configure}
43
+ # See {Capybara.configure}
44
44
  #@!method asset_host
45
- # See {Capybara#configure}
45
+ # See {Capybara.configure}
46
46
  #@!method default_host
47
- # See {Capybara#configure}
47
+ # See {Capybara.configure}
48
48
  #@!method app_host
49
- # See {Capybara#configure}
49
+ # See {Capybara.configure}
50
50
  #@!method save_and_open_page_path
51
- # See {Capybara#configure}
51
+ # See {Capybara.configure}
52
52
  #@!method server_host
53
- # See {Capybara#configure}
53
+ # See {Capybara.configure}
54
54
  #@!method server_port
55
- # See {Capybara#configure}
55
+ # See {Capybara.configure}
56
56
  #@!method server_errors
57
- # See {Capybara#configure}
57
+ # See {Capybara.configure}
58
+
59
+ remove_method :server_host
58
60
 
59
61
  ##
60
62
  #
@@ -64,20 +66,24 @@ module Capybara
64
66
  @server_host || '127.0.0.1'
65
67
  end
66
68
 
69
+ remove_method :server_errors=
67
70
  def server_errors=(errors)
68
71
  (@server_errors ||= []).replace(errors.dup)
69
72
  end
70
73
 
74
+ remove_method :app_host=
71
75
  def app_host=(url)
72
76
  raise ArgumentError.new("Capybara.app_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
73
77
  @app_host = url
74
78
  end
75
79
 
80
+ remove_method :default_host=
76
81
  def default_host=(url)
77
82
  raise ArgumentError.new("Capybara.default_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
78
83
  @default_host = url
79
84
  end
80
85
 
86
+ remove_method :save_and_open_page_path=
81
87
  def save_and_open_page_path=(path)
82
88
  warn "DEPRECATED: #save_and_open_page_path is deprecated, please use #save_path instead. \n"\
83
89
  "Note: Behavior is slightly different with relative paths - see documentation" unless path.nil?