Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API for operation with modal dialogs #1322

Merged
merged 5 commits into from Jul 1, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
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
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
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options = {}

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
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
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 @@ -523,6 +527,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing overloads in such way doesn't produce correct documentation -http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Session#accept_alert-instance_method (@param text isn't documented)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm -- generating the docs locally (yard 0.8.7.4) produces the correct output for me -

  • (String) accept_confirm(text, options = {}, &blk) - (String) accept_confirm(options = {}, &blk)

Execute the block, accepting a confirm.

Overloads:

- (String) accept_confirm(text, options = {}, &blk)

Parameters:
    text (String, Regexp) —

    Text or regex to match against the text in the modal. If not provided any prompt modal is matched

Parameters:

options (Hash) (defaults to: {}) —

a customizable set of options

Options Hash (options):

:wait (Numeric) —

How long to wait for the modal to appear after executing the block.

Returns:

(String) —

the message shown in the modal

Raises:

(Capybara::ModalNotFound) —

if modal dialog hasn't been found

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its correct here too - http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Session:accept_alert
not sure why its correct in one place but not the other

# @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
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
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be commented?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - its handled in reset now, so the line can eventually go away

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