capybara 2.3.0 → 2.4.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +21 -0
  3. data/README.md +50 -12
  4. data/lib/capybara.rb +8 -1
  5. data/lib/capybara/driver/base.rb +28 -0
  6. data/lib/capybara/driver/node.rb +3 -2
  7. data/lib/capybara/helpers.rb +2 -3
  8. data/lib/capybara/node/actions.rb +5 -2
  9. data/lib/capybara/node/base.rb +10 -0
  10. data/lib/capybara/node/document.rb +2 -0
  11. data/lib/capybara/node/document_matchers.rb +68 -0
  12. data/lib/capybara/node/element.rb +17 -2
  13. data/lib/capybara/node/finders.rb +5 -20
  14. data/lib/capybara/node/matchers.rb +101 -71
  15. data/lib/capybara/node/simple.rb +9 -15
  16. data/lib/capybara/queries/base_query.rb +29 -0
  17. data/lib/capybara/queries/text_query.rb +56 -0
  18. data/lib/capybara/queries/title_query.rb +40 -0
  19. data/lib/capybara/query.rb +30 -20
  20. data/lib/capybara/rack_test/node.rb +11 -3
  21. data/lib/capybara/result.rb +1 -1
  22. data/lib/capybara/rspec/features.rb +38 -21
  23. data/lib/capybara/rspec/matchers.rb +53 -38
  24. data/lib/capybara/selector.rb +68 -14
  25. data/lib/capybara/selenium/driver.rb +54 -6
  26. data/lib/capybara/selenium/node.rb +4 -2
  27. data/lib/capybara/session.rb +109 -35
  28. data/lib/capybara/spec/public/test.js +34 -1
  29. data/lib/capybara/spec/session/accept_alert_spec.rb +57 -0
  30. data/lib/capybara/spec/session/accept_confirm_spec.rb +19 -0
  31. data/lib/capybara/spec/session/accept_prompt_spec.rb +49 -0
  32. data/lib/capybara/spec/session/assert_text.rb +195 -0
  33. data/lib/capybara/spec/session/assert_title.rb +69 -0
  34. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +35 -0
  35. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +19 -0
  36. data/lib/capybara/spec/session/find_field_spec.rb +6 -0
  37. data/lib/capybara/spec/session/has_text_spec.rb +1 -1
  38. data/lib/capybara/spec/session/node_spec.rb +16 -1
  39. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +1 -1
  40. data/lib/capybara/spec/session/visit_spec.rb +5 -0
  41. data/lib/capybara/spec/views/with_html.erb +4 -0
  42. data/lib/capybara/spec/views/with_js.erb +17 -0
  43. data/lib/capybara/version.rb +1 -1
  44. data/spec/dsl_spec.rb +3 -1
  45. data/spec/rack_test_spec.rb +12 -1
  46. data/spec/rspec/features_spec.rb +1 -1
  47. data/spec/rspec/matchers_spec.rb +113 -20
  48. data/spec/selenium_spec.rb +10 -1
  49. metadata +13 -2
@@ -91,13 +91,26 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
91
91
  def reset!
92
92
  # Use instance variable directly so we avoid starting the browser just to reset the session
93
93
  if @browser
94
- begin @browser.manage.delete_all_cookies
95
- rescue Selenium::WebDriver::Error::UnhandledError
96
- # delete_all_cookies fails when we've previously gone
97
- # to about:blank, so we rescue this error and do nothing
98
- # instead.
94
+ 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.
100
+ end
101
+ @browser.navigate.to("about:blank")
102
+ rescue Selenium::WebDriver::Error::UnhandledAlertError
103
+ # This error is thrown if an unhandled alert is on the page
104
+ # Firefox appears to automatically dismiss this alert, chrome does not
105
+ # We'll try to accept it
106
+ begin
107
+ @browser.switch_to.alert.accept
108
+ rescue Selenium::WebDriver::Error::NoAlertPresentError
109
+ # The alert is now gone - nothing to do
110
+ end
111
+ # try cleaning up the browser again
112
+ retry
99
113
  end
100
- @browser.navigate.to("about:blank")
101
114
  end
102
115
  end
103
116
 
@@ -191,6 +204,23 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
191
204
  browser.switch_to.window(handle) { yield }
192
205
  end
193
206
 
207
+ def accept_modal(type, options={}, &blk)
208
+ yield if block_given?
209
+ modal = find_modal(options)
210
+ modal.send_keys options[:with] if options[:with]
211
+ message = modal.text
212
+ modal.accept
213
+ message
214
+ end
215
+
216
+ def dismiss_modal(type, options={}, &blk)
217
+ yield if block_given?
218
+ modal = find_modal(options)
219
+ message = modal.text
220
+ modal.dismiss
221
+ message
222
+ end
223
+
194
224
  def quit
195
225
  @browser.quit if @browser
196
226
  rescue Errno::ECONNREFUSED
@@ -220,4 +250,22 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
220
250
  result
221
251
  end
222
252
  end
253
+
254
+ def find_modal(options={})
255
+ # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
256
+ # Actual wait time may be longer than specified
257
+ wait = Selenium::WebDriver::Wait.new(
258
+ timeout: (options[:wait] || Capybara.default_wait_time),
259
+ ignore: Selenium::WebDriver::Error::NoAlertPresentError)
260
+ begin
261
+ modal = wait.until do
262
+ alert = @browser.switch_to.alert
263
+ regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
264
+ alert.text.match(regexp) ? alert : nil
265
+ end
266
+ rescue Selenium::WebDriver::Error::TimeOutError
267
+ raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
268
+ end
269
+ end
270
+
223
271
  end
@@ -37,11 +37,13 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
37
37
  path_names = value.to_s.empty? ? [] : value
38
38
  native.send_keys(*path_names)
39
39
  elsif tag_name == 'textarea' or tag_name == 'input'
40
- if value.to_s.empty?
40
+ if self[:readonly]
41
+ warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
42
+ elsif value.to_s.empty?
41
43
  native.clear
42
44
  else
43
45
  #script can change a readonly element which user input cannot, so dont execute if readonly
44
- driver.browser.execute_script "arguments[0].value = ''", native unless self[:readonly]
46
+ driver.browser.execute_script "arguments[0].value = ''", native
45
47
  native.send_keys(value.to_s)
46
48
  end
47
49
  elsif native.attribute('isContentEditable')
@@ -35,7 +35,11 @@ module Capybara
35
35
  :has_no_table?, :has_table?, :unselect, :has_select?, :has_no_select?,
36
36
  :has_selector?, :has_no_selector?, :click_on, :has_no_checked_field?,
37
37
  :has_no_unchecked_field?, :query, :assert_selector, :assert_no_selector,
38
- :refute_selector
38
+ :refute_selector, :assert_text, :assert_no_text
39
+ ]
40
+ # @api private
41
+ DOCUMENT_METHODS = [
42
+ :title, :assert_title, :assert_no_title, :has_title?, :has_no_title?
39
43
  ]
40
44
  SESSION_METHODS = [
41
45
  :body, :html, :source, :current_url, :current_host, :current_path,
@@ -44,9 +48,13 @@ module Capybara
44
48
  :windows, :open_new_window, :switch_to_window, :within_window, :window_opened_by,
45
49
  :save_page, :save_and_open_page, :save_screenshot,
46
50
  :save_and_open_screenshot, :reset_session!, :response_headers,
47
- :status_code, :title, :has_title?, :has_no_title?, :current_scope
51
+ :status_code, :current_scope
52
+ ] + DOCUMENT_METHODS
53
+ MODAL_METHODS = [
54
+ :accept_alert, :accept_confirm, :dismiss_confirm, :accept_prompt,
55
+ :dismiss_prompt
48
56
  ]
49
- DSL_METHODS = NODE_METHODS + SESSION_METHODS
57
+ DSL_METHODS = NODE_METHODS + SESSION_METHODS + MODAL_METHODS
50
58
 
51
59
  attr_reader :mode, :app, :server
52
60
  attr_accessor :synchronized
@@ -78,7 +86,7 @@ module Capybara
78
86
  #
79
87
  # This method does not:
80
88
  #
81
- # * accept modal dialogs if they are present
89
+ # * accept modal dialogs if they are present (Selenium driver now does, others may not)
82
90
  # * clear browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc.
83
91
  # * modify state of the driver/underlying browser in any other way
84
92
  #
@@ -167,14 +175,6 @@ module Capybara
167
175
  driver.current_url
168
176
  end
169
177
 
170
- ##
171
- #
172
- # @return [String] Title of the current page
173
- #
174
- def title
175
- driver.title
176
- end
177
-
178
178
  ##
179
179
  #
180
180
  # Navigate to the given URL. The URL can either be a relative URL or an absolute URL
@@ -206,12 +206,15 @@ module Capybara
206
206
 
207
207
  @touched = true
208
208
 
209
- if url !~ /^http/ and Capybara.app_host
209
+ url_relative = URI.parse(url.to_s).scheme.nil?
210
+
211
+ if url_relative && Capybara.app_host
210
212
  url = Capybara.app_host + url.to_s
213
+ url_relative = false
211
214
  end
212
215
 
213
216
  if @server
214
- url = "http://#{@server.host}:#{@server.port}" + url.to_s unless url =~ /^http/
217
+ url = "http://#{@server.host}:#{@server.port}" + url.to_s if url_relative
215
218
 
216
219
  if Capybara.always_include_port
217
220
  uri = URI.parse(url)
@@ -523,6 +526,93 @@ module Capybara
523
526
  driver.evaluate_script(script)
524
527
  end
525
528
 
529
+ ##
530
+ #
531
+ # Execute the block, accepting a alert.
532
+ #
533
+ # @!macro modal_params
534
+ # @overload $0(text, options = {}, &blk)
535
+ # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any modal is matched
536
+ # @overload $0(options = {}, &blk)
537
+ # @option options [Numeric] :wait How long to wait for the modal to appear after executing the block.
538
+ # @return [String] the message shown in the modal
539
+ # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found
540
+ #
541
+ def accept_alert(text_or_options=nil, options={}, &blk)
542
+ if text_or_options.is_a? Hash
543
+ options=text_or_options
544
+ else
545
+ options[:text]=text_or_options
546
+ end
547
+
548
+ driver.accept_modal(:alert, options, &blk)
549
+ end
550
+
551
+ ##
552
+ #
553
+ # Execute the block, accepting a confirm.
554
+ #
555
+ # @macro modal_params
556
+ #
557
+ def accept_confirm(text_or_options=nil, options={}, &blk)
558
+ if text_or_options.is_a? Hash
559
+ options=text_or_options
560
+ else
561
+ options[:text]=text_or_options
562
+ end
563
+
564
+ driver.accept_modal(:confirm, options, &blk)
565
+ end
566
+
567
+ ##
568
+ #
569
+ # Execute the block, dismissing a confirm.
570
+ #
571
+ # @macro modal_params
572
+ #
573
+ def dismiss_confirm(text_or_options=nil, options={}, &blk)
574
+ if text_or_options.is_a? Hash
575
+ options=text_or_options
576
+ else
577
+ options[:text]=text_or_options
578
+ end
579
+
580
+ driver.dismiss_modal(:confirm, options, &blk)
581
+ end
582
+
583
+ ##
584
+ #
585
+ # Execute the block, accepting a prompt, optionally responding to the prompt.
586
+ #
587
+ # @macro modal_params
588
+ # @option options [String] :with Response to provide to the prompt
589
+ #
590
+ def accept_prompt(text_or_options=nil, options={}, &blk)
591
+ if text_or_options.is_a? Hash
592
+ options=text_or_options
593
+ else
594
+ options[:text]=text_or_options
595
+ end
596
+
597
+ driver.accept_modal(:prompt, options, &blk)
598
+ end
599
+
600
+ ##
601
+ #
602
+ # Execute the block, dismissing a prompt.
603
+ #
604
+ # @macro modal_params
605
+ #
606
+ def dismiss_prompt(text_or_options=nil, options={}, &blk)
607
+ if text_or_options.is_a? Hash
608
+ options=text_or_options
609
+ else
610
+ options[:text]=text_or_options
611
+ end
612
+
613
+ driver.dismiss_modal(:prompt, options, &blk)
614
+ end
615
+
526
616
  ##
527
617
  #
528
618
  # Save a snapshot of the page.
@@ -586,30 +676,14 @@ module Capybara
586
676
  end
587
677
  end
588
678
 
589
- def inspect
590
- %(#<Capybara::Session>)
591
- end
592
-
593
- def has_title?(content)
594
- document.synchronize do
595
- unless title.match(Capybara::Helpers.to_regexp(content))
596
- raise ExpectationNotMet
597
- end
679
+ DOCUMENT_METHODS.each do |method|
680
+ define_method method do |*args, &block|
681
+ document.send(method, *args, &block)
598
682
  end
599
- return true
600
- rescue Capybara::ExpectationNotMet
601
- return false
602
683
  end
603
684
 
604
- def has_no_title?(content)
605
- document.synchronize do
606
- if title.match(Capybara::Helpers.to_regexp(content))
607
- raise ExpectationNotMet
608
- end
609
- end
610
- return true
611
- rescue Capybara::ExpectationNotMet
612
- return false
685
+ def inspect
686
+ %(#<Capybara::Session>)
613
687
  end
614
688
 
615
689
  def current_scope
@@ -53,7 +53,7 @@ $(function() {
53
53
  $('#reload-list').click(function() {
54
54
  setTimeout(function() {
55
55
  $('#the-list').html('<li>Foo</li><li>Bar</li>');
56
- }, 250)
56
+ }, 550)
57
57
  });
58
58
  $('#change-title').click(function() {
59
59
  setTimeout(function() {
@@ -67,4 +67,37 @@ $(function() {
67
67
  e.preventDefault();
68
68
  $(this).after('<a id="has-been-right-clicked" href="#">Has been right clicked</a>');
69
69
  });
70
+ $('#open-alert').click(function() {
71
+ alert('Alert opened');
72
+ $(this).attr('opened', 'true');
73
+ });
74
+ $('#open-delayed-alert').click(function() {
75
+ var link = this;
76
+ setTimeout(function() {
77
+ alert('Delayed alert opened');
78
+ $(link).attr('opened', 'true');
79
+ }, 250);
80
+ });
81
+ $('#open-slow-alert').click(function() {
82
+ var link = this;
83
+ setTimeout(function() {
84
+ alert('Delayed alert opened');
85
+ $(link).attr('opened', 'true');
86
+ }, 3000);
87
+ });
88
+ $('#open-confirm').click(function() {
89
+ if(confirm('Confirm opened')) {
90
+ $(this).attr('confirmed', 'true');
91
+ } else {
92
+ $(this).attr('confirmed', 'false');
93
+ }
94
+ });
95
+ $('#open-prompt').click(function() {
96
+ var response = prompt('Prompt opened');
97
+ if(response === null) {
98
+ $(this).attr('response', 'dismissed');
99
+ } else {
100
+ $(this).attr('response', response);
101
+ }
102
+ });
70
103
  });
@@ -0,0 +1,57 @@
1
+ Capybara::SpecHelper.spec '#accept_alert', :requires => [:modals] do
2
+ before do
3
+ @session.visit('/with_js')
4
+ end
5
+
6
+ it "should accept the alert" do
7
+ @session.accept_alert do
8
+ @session.click_link('Open alert')
9
+ end
10
+ expect(@session).to have_xpath("//a[@id='open-alert' and @opened='true']")
11
+ end
12
+
13
+ it "should accept the alert if the text matches" do
14
+ @session.accept_alert 'Alert opened' do
15
+ @session.click_link('Open alert')
16
+ end
17
+ expect(@session).to have_xpath("//a[@id='open-alert' and @opened='true']")
18
+ end
19
+
20
+ it "should not accept the alert if the text doesnt match" do
21
+ expect do
22
+ @session.accept_alert 'Incorrect Text' do
23
+ @session.click_link('Open alert')
24
+ end
25
+ end.to raise_error(Capybara::ModalNotFound)
26
+ end
27
+
28
+ it "should return the message presented" do
29
+ message = @session.accept_alert do
30
+ @session.click_link('Open alert')
31
+ end
32
+ expect(message).to eq('Alert opened')
33
+ end
34
+
35
+ context "with an asynchronous alert" do
36
+ it "should accept the alert" do
37
+ @session.accept_alert do
38
+ @session.click_link('Open delayed alert')
39
+ end
40
+ expect(@session).to have_xpath("//a[@id='open-delayed-alert' and @opened='true']")
41
+ end
42
+
43
+ it "should return the message presented" do
44
+ message = @session.accept_alert do
45
+ @session.click_link('Open delayed alert')
46
+ end
47
+ expect(message).to eq('Delayed alert opened')
48
+ end
49
+
50
+ it "should allow to adjust the delay" do
51
+ @session.accept_alert wait: 4 do
52
+ @session.click_link('Open slow alert')
53
+ end
54
+ expect(@session).to have_xpath("//a[@id='open-slow-alert' and @opened='true']")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ Capybara::SpecHelper.spec '#accept_confirm', :requires => [:modals] do
2
+ before do
3
+ @session.visit('/with_js')
4
+ end
5
+
6
+ it "should accept the confirm" do
7
+ @session.accept_confirm do
8
+ @session.click_link('Open confirm')
9
+ end
10
+ expect(@session).to have_xpath("//a[@id='open-confirm' and @confirmed='true']")
11
+ end
12
+
13
+ it "should return the message presented" do
14
+ message = @session.accept_confirm do
15
+ @session.click_link('Open confirm')
16
+ end
17
+ expect(message).to eq('Confirm opened')
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ Capybara::SpecHelper.spec '#accept_prompt', :requires => [:modals] do
2
+ before do
3
+ @session.visit('/with_js')
4
+ end
5
+
6
+ it "should accept the prompt with no message" do
7
+ @session.accept_prompt do
8
+ @session.click_link('Open prompt')
9
+ end
10
+ expect(@session).to have_xpath("//a[@id='open-prompt' and @response='']")
11
+ end
12
+
13
+ it "should return the message presented" do
14
+ message = @session.accept_prompt do
15
+ @session.click_link('Open prompt')
16
+ end
17
+ expect(message).to eq('Prompt opened')
18
+ end
19
+
20
+ it "should accept the prompt with a response" do
21
+ @session.accept_prompt with: 'the response' do
22
+ @session.click_link('Open prompt')
23
+ end
24
+ expect(@session).to have_xpath("//a[@id='open-prompt' and @response='the response']")
25
+ end
26
+
27
+ it "should accept the prompt if the message matches" do
28
+ @session.accept_prompt 'Prompt opened', with: 'matched' do
29
+ @session.click_link('Open prompt')
30
+ end
31
+ expect(@session).to have_xpath("//a[@id='open-prompt' and @response='matched']")
32
+ end
33
+
34
+ it "should not accept the prompt if the message doesn't match" do
35
+ expect do
36
+ @session.accept_prompt 'Incorrect Text', with: 'not matched' do
37
+ @session.click_link('Open prompt')
38
+ end
39
+ end.to raise_error(Capybara::ModalNotFound)
40
+ end
41
+
42
+
43
+ it "should return the message presented" do
44
+ message = @session.accept_prompt with: 'the response' do
45
+ @session.click_link('Open prompt')
46
+ end
47
+ expect(message).to eq('Prompt opened')
48
+ end
49
+ end