diff --git a/README.md b/README.md index 87d4f4bd..b15e61db 100644 --- a/README.md +++ b/README.md @@ -497,6 +497,44 @@ that this may break with more complicated expressions: result = page.evaluate_script('4 + 4'); ``` +### Modals + +In drivers which support it, you can accept, dismiss and respond to alerts, confirms and prompts. + +You can accept or dismiss alert messages by wrapping the code that produces an alert in a block: + +```ruby +accept_alert do + click_link('Show Alert') +end +``` + +You can accept or dismiss a confirmation by wrapping it in a block, as well: + +```ruby +dismiss_confirm do + click_link('Show Confirm') +end +``` + +You can accept or dismiss prompts as well, and also provide text to fill in for the response: + +```ruby +accept_prompt(with: 'Linus Torvalds') do + click_link('Show Prompt About Linux') +end +``` + +All modal methods return the message that was presented. So, you can access the prompt message +by assigning the return to a variable: + +```ruby +message = accept_prompt(with: 'Linus Torvalds') do + click_link('Show Prompt About Linux') +end +expect(message).to eq('Who is the chief architect of Linux?') +``` + ### Debugging It can be useful to take a snapshot of the page as it currently is and take a diff --git a/lib/capybara.rb b/lib/capybara.rb index 9c82ebd5..dd0f7d60 100644 --- a/lib/capybara.rb +++ b/lib/capybara.rb @@ -7,6 +7,7 @@ module Capybara class DriverNotFoundError < CapybaraError; end class FrozenInTime < CapybaraError; end class ElementNotFound < CapybaraError; end + class ModalNotFound < CapybaraError; end class Ambiguous < ElementNotFound; end class ExpectationNotMet < ElementNotFound; end class FileNotFound < CapybaraError; end diff --git a/lib/capybara/driver/base.rb b/lib/capybara/driver/base.rb index aa14942d..6fc3b4fe 100644 --- a/lib/capybara/driver/base.rb +++ b/lib/capybara/driver/base.rb @@ -90,6 +90,34 @@ class Capybara::Driver::Base def no_such_window_error raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#no_such_window_error' end + + + ## + # + # Execute the block, and then accept the modal opened. + # @param type [:alert, :confirm, :prompt] + # @option options [Numeric] :wait How long to wait for the modal to appear after executing the block. + # @option options [String, Regexp] :text Text to verify is in the message shown in the modal + # @option options [String] :with Text to fill in in the case of a prompt + # @return [String] the message shown in the modal + # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found + # + def accept_modal(type, options={}, &blk) + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#accept_modal' + end + + ## + # + # Execute the block, and then dismiss the modal opened. + # @param type [:alert, :confirm, :prompt] + # @option options [Numeric] :wait How long to wait for the modal to appear after executing the block. + # @option options [String, Regexp] :text Text to verify is in the message shown in the modal + # @return [String] the message shown in the modal + # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found + # + def dismiss_modal(type, &blk) + raise Capybara::NotSupportedByDriverError, 'Capybara::Driver::Base#dismiss_modal' + end def invalid_element_errors [] diff --git a/lib/capybara/selenium/driver.rb b/lib/capybara/selenium/driver.rb index a5f43204..274a1cba 100644 --- a/lib/capybara/selenium/driver.rb +++ b/lib/capybara/selenium/driver.rb @@ -91,13 +91,26 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base def reset! # Use instance variable directly so we avoid starting the browser just to reset the session if @browser - begin @browser.manage.delete_all_cookies - rescue Selenium::WebDriver::Error::UnhandledError - # delete_all_cookies fails when we've previously gone - # to about:blank, so we rescue this error and do nothing - # instead. + begin + begin @browser.manage.delete_all_cookies + rescue Selenium::WebDriver::Error::UnhandledError + # delete_all_cookies fails when we've previously gone + # to about:blank, so we rescue this error and do nothing + # instead. + end + @browser.navigate.to("about:blank") + rescue Selenium::WebDriver::Error::UnhandledAlertError + # This error is thrown if an unhandled alert is on the page + # Firefox appears to automatically dismiss this alert, chrome does not + # We'll try to accept it + begin + @browser.switch_to.alert.accept + rescue Selenium::WebDriver::Error::NoAlertPresentError + # The alert is now gone - nothing to do + end + # try cleaning up the browser again + retry end - @browser.navigate.to("about:blank") end end @@ -191,6 +204,23 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base browser.switch_to.window(handle) { yield } end + def accept_modal(type, options={}, &blk) + yield + modal = find_modal(options) + modal.send_keys options[:with] if options[:with] + message = modal.text + modal.accept + message + end + + def dismiss_modal(type, options={}, &blk) + yield + modal = find_modal(options) + message = modal.text + modal.dismiss + message + end + def quit @browser.quit if @browser rescue Errno::ECONNREFUSED @@ -220,4 +250,22 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base result end end + + def find_modal(options={}) + # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time + # Actual wait time may be longer than specified + wait = Selenium::WebDriver::Wait.new( + timeout: (options[:wait] || Capybara.default_wait_time), + ignore: Selenium::WebDriver::Error::NoAlertPresentError) + begin + modal = wait.until do + alert = @browser.switch_to.alert + regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s) + alert.text.match(regexp) ? alert : nil + end + rescue Selenium::WebDriver::Error::TimeOutError + raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}") + end + end + end diff --git a/lib/capybara/session.rb b/lib/capybara/session.rb index 922e4c2e..5dae9cd1 100644 --- a/lib/capybara/session.rb +++ b/lib/capybara/session.rb @@ -46,7 +46,11 @@ module Capybara :save_and_open_screenshot, :reset_session!, :response_headers, :status_code, :title, :has_title?, :has_no_title?, :current_scope ] - DSL_METHODS = NODE_METHODS + SESSION_METHODS + MODAL_METHODS = [ + :accept_alert, :accept_confirm, :dismiss_confirm, :accept_prompt, + :dismiss_prompt + ] + DSL_METHODS = NODE_METHODS + SESSION_METHODS + MODAL_METHODS attr_reader :mode, :app, :server attr_accessor :synchronized @@ -526,6 +530,112 @@ module Capybara driver.evaluate_script(script) end + ## + # + # Execute the block, accepting a alert. + # + # @overload accept_alert(text, options = {}, &blk) + # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any prompt modal is matched + # @overload accept_alert(options = {}, &blk) + # @option options [Numeric] :wait How long to wait for the modal to appear after executing the block. + # @return [String] the message shown in the modal + # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found + # + def accept_alert(text_or_options=nil, options={}, &blk) + if text_or_options.is_a? Hash + options=text_or_options + else + options[:text]=text_or_options + end + + driver.accept_modal(:alert, options, &blk) + end + + ## + # + # Execute the block, accepting a confirm. + # + # @overload accept_confirm(text, options = {}, &blk) + # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any prompt modal is matched + # @overload accept_confirm(options = {}, &blk) + # @option options [Numeric] :wait How long to wait for the modal to appear after executing the block. + # @return [String] the message shown in the modal + # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found + # + def accept_confirm(text_or_options=nil, options={}, &blk) + if text_or_options.is_a? Hash + options=text_or_options + else + options[:text]=text_or_options + end + + driver.accept_modal(:confirm, options, &blk) + end + + ## + # + # Execute the block, dismissing a confirm. + # + # @overload dismiss_confirm(text, options = {}, &blk) + # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any prompt modal is matched + # @overload dismiss_confirm(options = {}, &blk) + # @option options [Numeric] :wait How long to wait for the modal to appear after executing the block. + # @return [String] the message shown in the modal + # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found + # + def dismiss_confirm(text_or_options=nil, options={}, &blk) + if text_or_options.is_a? Hash + options=text_or_options + else + options[:text]=text_or_options + end + + driver.dismiss_modal(:confirm, options, &blk) + end + + ## + # + # Execute the block, accepting a prompt, optionally responding to the prompt. + # + # @overload accept_prompt(text, options = {}, &blk) + # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any prompt modal is matched + # @overload accept_prompt(options = {}, &blk) + # @option options [String] :with Response to provide to the prompt + # @option options [Numeric] :wait How long to wait for the prompt to appear after executing the block. + # @return [String] the message shown in the modal + # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found + # + def accept_prompt(text_or_options=nil, options={}, &blk) + if text_or_options.is_a? Hash + options=text_or_options + else + options[:text]=text_or_options + end + + driver.accept_modal(:prompt, options, &blk) + end + + ## + # + # Execute the block, dismissing a prompt. + # + # @overload dismiss_prompt(text, options = {}, &blk) + # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any prompt modal is matched + # @overload dismiss_prompt(options = {}, &blk) + # @option options [Numeric] :wait How long to wait for the prompt to appear after executing the block. + # @return [String] the message shown in the modal + # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found + # + def dismiss_prompt(text_or_options=nil, options={}, &blk) + if text_or_options.is_a? Hash + options=text_or_options + else + options[:text]=text_or_options + end + + driver.dismiss_modal(:prompt, options, &blk) + end + ## # # Save a snapshot of the page. diff --git a/lib/capybara/spec/public/test.js b/lib/capybara/spec/public/test.js index e5db4d30..678f0ce7 100644 --- a/lib/capybara/spec/public/test.js +++ b/lib/capybara/spec/public/test.js @@ -67,4 +67,37 @@ $(function() { e.preventDefault(); $(this).after('Has been right clicked'); }); + $('#open-alert').click(function() { + alert('Alert opened'); + $(this).attr('opened', 'true'); + }); + $('#open-delayed-alert').click(function() { + var link = this; + setTimeout(function() { + alert('Delayed alert opened'); + $(link).attr('opened', 'true'); + }, 250); + }); + $('#open-slow-alert').click(function() { + var link = this; + setTimeout(function() { + alert('Delayed alert opened'); + $(link).attr('opened', 'true'); + }, 3000); + }); + $('#open-confirm').click(function() { + if(confirm('Confirm opened')) { + $(this).attr('confirmed', 'true'); + } else { + $(this).attr('confirmed', 'false'); + } + }); + $('#open-prompt').click(function() { + var response = prompt('Prompt opened'); + if(response === null) { + $(this).attr('response', 'dismissed'); + } else { + $(this).attr('response', response); + } + }); }); diff --git a/lib/capybara/spec/session/accept_alert_spec.rb b/lib/capybara/spec/session/accept_alert_spec.rb new file mode 100644 index 00000000..8d3451fe --- /dev/null +++ b/lib/capybara/spec/session/accept_alert_spec.rb @@ -0,0 +1,58 @@ +Capybara::SpecHelper.spec '#accept_alert', :requires => [:modals] do + before do + @session.visit('/with_js') + end + + it "should accept the alert" do + @session.accept_alert do + @session.click_link('Open alert') + end + expect(@session).to have_xpath("//a[@id='open-alert' and @opened='true']") + end + + it "should accept the alert if the text matches" do + @session.accept_alert 'Alert opened' do + @session.click_link('Open alert') + end + expect(@session).to have_xpath("//a[@id='open-alert' and @opened='true']") + end + + it "should not accept the alert if the text doesnt match" do + expect do + @session.accept_alert 'Incorrect Text' do + @session.click_link('Open alert') + end + end.to raise_error(Capybara::ModalNotFound) + # @session.accept_alert {} # clear the alert so browser continues to function + end + + it "should return the message presented" do + message = @session.accept_alert do + @session.click_link('Open alert') + end + expect(message).to eq('Alert opened') + end + + context "with an asynchronous alert" do + it "should accept the alert" do + @session.accept_alert do + @session.click_link('Open delayed alert') + end + expect(@session).to have_xpath("//a[@id='open-delayed-alert' and @opened='true']") + end + + it "should return the message presented" do + message = @session.accept_alert do + @session.click_link('Open delayed alert') + end + expect(message).to eq('Delayed alert opened') + end + + it "should allow to adjust the delay" do + @session.accept_alert wait: 4 do + @session.click_link('Open slow alert') + end + expect(@session).to have_xpath("//a[@id='open-slow-alert' and @opened='true']") + end + end +end \ No newline at end of file diff --git a/lib/capybara/spec/session/accept_confirm_spec.rb b/lib/capybara/spec/session/accept_confirm_spec.rb new file mode 100644 index 00000000..be9cb8c9 --- /dev/null +++ b/lib/capybara/spec/session/accept_confirm_spec.rb @@ -0,0 +1,19 @@ +Capybara::SpecHelper.spec '#accept_confirm', :requires => [:modals] do + before do + @session.visit('/with_js') + end + + it "should accept the confirm" do + @session.accept_confirm do + @session.click_link('Open confirm') + end + expect(@session).to have_xpath("//a[@id='open-confirm' and @confirmed='true']") + end + + it "should return the message presented" do + message = @session.accept_confirm do + @session.click_link('Open confirm') + end + expect(message).to eq('Confirm opened') + end +end \ No newline at end of file diff --git a/lib/capybara/spec/session/accept_prompt_spec.rb b/lib/capybara/spec/session/accept_prompt_spec.rb new file mode 100644 index 00000000..1a3b4c07 --- /dev/null +++ b/lib/capybara/spec/session/accept_prompt_spec.rb @@ -0,0 +1,49 @@ +Capybara::SpecHelper.spec '#accept_prompt', :requires => [:modals] do + before do + @session.visit('/with_js') + end + + it "should accept the prompt with no message" do + @session.accept_prompt do + @session.click_link('Open prompt') + end + expect(@session).to have_xpath("//a[@id='open-prompt' and @response='']") + end + + it "should return the message presented" do + message = @session.accept_prompt do + @session.click_link('Open prompt') + end + expect(message).to eq('Prompt opened') + end + + it "should accept the prompt with a response" do + @session.accept_prompt with: 'the response' do + @session.click_link('Open prompt') + end + expect(@session).to have_xpath("//a[@id='open-prompt' and @response='the response']") + end + + it "should accept the prompt if the message matches" do + @session.accept_prompt 'Prompt opened', with: 'matched' do + @session.click_link('Open prompt') + end + expect(@session).to have_xpath("//a[@id='open-prompt' and @response='matched']") + end + + it "should not accept the prompt if the message doesn't match" do + expect do + @session.accept_prompt 'Incorrect Text', with: 'not matched' do + @session.click_link('Open prompt') + end + end.to raise_error(Capybara::ModalNotFound) + end + + + it "should return the message presented" do + message = @session.accept_prompt with: 'the response' do + @session.click_link('Open prompt') + end + expect(message).to eq('Prompt opened') + end +end \ No newline at end of file diff --git a/lib/capybara/spec/session/dismiss_confirm_spec.rb b/lib/capybara/spec/session/dismiss_confirm_spec.rb new file mode 100644 index 00000000..9bb65ccf --- /dev/null +++ b/lib/capybara/spec/session/dismiss_confirm_spec.rb @@ -0,0 +1,35 @@ +Capybara::SpecHelper.spec '#dismiss_confirm', :requires => [:modals] do + before do + @session.visit('/with_js') + end + + it "should dismiss the confirm" do + @session.dismiss_confirm do + @session.click_link('Open confirm') + end + expect(@session).to have_xpath("//a[@id='open-confirm' and @confirmed='false']") + end + + it "should dismiss the confirm if the message matches" do + @session.dismiss_confirm 'Confirm opened' do + @session.click_link('Open confirm') + end + expect(@session).to have_xpath("//a[@id='open-confirm' and @confirmed='false']") + end + + it "should not dismiss the confirm if the message doesn't match" do + expect do + @session.dismiss_confirm 'Incorrect Text' do + @session.click_link('Open confirm') + end + end.to raise_error(Capybara::ModalNotFound) + end + + + it "should return the message presented" do + message = @session.dismiss_confirm do + @session.click_link('Open confirm') + end + expect(message).to eq('Confirm opened') + end +end \ No newline at end of file diff --git a/lib/capybara/spec/session/dismiss_prompt_spec.rb b/lib/capybara/spec/session/dismiss_prompt_spec.rb new file mode 100644 index 00000000..42ae2f87 --- /dev/null +++ b/lib/capybara/spec/session/dismiss_prompt_spec.rb @@ -0,0 +1,19 @@ +Capybara::SpecHelper.spec '#dismiss_prompt', :requires => [:modals] do + before do + @session.visit('/with_js') + end + + it "should dismiss the prompt" do + @session.dismiss_prompt do + @session.click_link('Open prompt') + end + expect(@session).to have_xpath("//a[@id='open-prompt' and @response='dismissed']") + end + + it "should return the message presented" do + message = @session.dismiss_prompt do + @session.click_link('Open prompt') + end + expect(message).to eq('Prompt opened') + end +end \ No newline at end of file diff --git a/lib/capybara/spec/views/with_js.erb b/lib/capybara/spec/views/with_js.erb index d07a3884..dad73d97 100644 --- a/lib/capybara/spec/views/with_js.erb +++ b/lib/capybara/spec/views/with_js.erb @@ -72,6 +72,23 @@

Click me

+

+ Open alert +

+ +

+ Open delayed alert + Open slow alert +

+ +

+ Open confirm +

+ +

+ Open prompt +

+