Skip to content

Commit

Permalink
Merge pull request teamcapybara#1322 from jnicklas/modal
Browse files Browse the repository at this point in the history
API for operation with modal dialogs
  • Loading branch information
twalpole committed Jul 1, 2014
2 parents ffde0ee + 973fc4f commit c589f6a
Show file tree
Hide file tree
Showing 14 changed files with 464 additions and 7 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/capybara.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class CapybaraError < StandardError; end
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
Expand Down
28 changes: 28 additions & 0 deletions lib/capybara/driver/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,34 @@ def within_window(locator)
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
[]
Expand Down
60 changes: 54 additions & 6 deletions lib/capybara/selenium/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,26 @@ def save_screenshot(path, options={})
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

Expand Down Expand Up @@ -191,6 +204,23 @@ def within_window(locator)
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
Expand Down Expand Up @@ -220,4 +250,22 @@ def within_given_window(handle)
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
112 changes: 111 additions & 1 deletion lib/capybara/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ class Session
: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
Expand Down Expand Up @@ -526,6 +530,112 @@ def evaluate_script(script)
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.
Expand Down
33 changes: 33 additions & 0 deletions lib/capybara/spec/public/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,37 @@ $(function() {
e.preventDefault();
$(this).after('<a id="has-been-right-clicked" href="#">Has been right clicked</a>');
});
$('#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);
}
});
});
58 changes: 58 additions & 0 deletions lib/capybara/spec/session/accept_alert_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit c589f6a

Please sign in to comment.