1
0
Fork 0
mirror of https://github.com/thoughtbot/capybara-webkit synced 2023-03-27 23:22:28 -04:00

Implement modal (confirm, prompt and alert) API

* Retain backwards compatibility with legacy capybara-webkit API.
* Confirm dialogs are accepted by default; dialogs are dismissed.
* Legacy API overrides the default action, and does not raise errors
  for unexpected modals.
This commit is contained in:
Matthew Horan 2014-07-03 18:21:21 -04:00
parent 144a43ff7b
commit b411f53cfc
17 changed files with 559 additions and 31 deletions

View file

@ -2,7 +2,7 @@ PATH
remote: .
specs:
capybara-webkit (1.2.0)
capybara (>= 2.0.2, < 2.4.0)
capybara (>= 2.0.2, < 2.5.0)
json
GEM
@ -12,7 +12,7 @@ GEM
appraisal (0.4.0)
bundler
rake
capybara (2.3.0)
capybara (2.4.1)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)

View file

@ -19,7 +19,7 @@ Gem::Specification.new do |s|
s.required_ruby_version = ">= 1.9.0"
s.add_runtime_dependency("capybara", ">= 2.0.2", "< 2.4.0")
s.add_runtime_dependency("capybara", ">= 2.0.2", "< 2.5.0")
s.add_runtime_dependency("json")
s.add_development_dependency("rspec", "~> 2.14.0")

View file

@ -134,18 +134,38 @@ module Capybara::Webkit
alias_method :window_handle, :get_window_handle
def accept_confirm(options)
command("SetConfirmAction", "Yes", options[:text])
end
def accept_js_confirms
command("SetConfirmAction", "Yes")
end
def reject_confirm(options)
command("SetConfirmAction", "No", options[:text])
end
def reject_js_confirms
command("SetConfirmAction", "No")
end
def accept_prompt(options)
if options[:with]
command("SetPromptAction", "Yes", options[:text], options[:with])
else
command("SetPromptAction", "Yes", options[:text])
end
end
def accept_js_prompts
command("SetPromptAction", "Yes")
end
def reject_prompt(options)
command("SetPromptAction", "No", options[:text])
end
def reject_js_prompts
command("SetPromptAction", "No")
end
@ -158,6 +178,14 @@ module Capybara::Webkit
command("ClearPromptText")
end
def accept_alert(options)
command("AcceptAlert", options[:text])
end
def find_modal(id)
command("FindModal", id)
end
def url_blacklist=(black_list)
command("SetUrlBlacklist", *Array(black_list))
end

View file

@ -175,6 +175,38 @@ module Capybara::Webkit
browser.go_forward
end
def accept_modal(type, options={})
options = modal_action_options_for_browser(options)
case type
when :confirm
id = browser.accept_confirm(options)
when :prompt
id = browser.accept_prompt(options)
else
id = browser.accept_alert(options)
end
yield
find_modal(type, id, options)
end
def dismiss_modal(type, options={})
options = modal_action_options_for_browser(options)
case type
when :confirm
id = browser.reject_confirm(options)
else
id = browser.reject_prompt(options)
end
yield
find_modal(type, id, options)
end
def wait?
true
end
@ -217,5 +249,32 @@ module Capybara::Webkit
browser.version
].join("\n")
end
private
def modal_action_options_for_browser(options)
if options[:text].is_a?(Regexp)
options.merge(text: options[:text].source)
else
options.merge(text: Regexp.escape(options[:text].to_s))
end.merge(original_text: options[:text])
end
def find_modal(type, id, options)
Timeout::timeout(options[:wait] || Capybara.default_wait_time) do
begin
browser.find_modal(id)
rescue ModalIndexError
sleep 0.05
retry
end
end
rescue ModalNotFound
raise Capybara::ModalNotFound,
"Unable to find modal dialog#{" with #{options[:original_text]}" if options[:original_text]}"
rescue Timeout::Error
raise Capybara::ModalNotFound,
"Timed out waiting for modal dialog#{" with #{options[:original_text]}" if options[:original_text]}"
end
end
end

View file

@ -20,6 +20,12 @@ module Capybara::Webkit
class ConnectionError < StandardError
end
class ModalIndexError < StandardError
end
class ModalNotFound < StandardError
end
class JsonError
def initialize(response)
error = JSON.parse response

View file

@ -620,26 +620,95 @@ describe Capybara::Webkit::Driver do
context "javascript dialog interaction" do
context "on an alert app" do
let(:driver) do
driver_for_html(<<-HTML)
<html>
<head>
</head>
<body>
<script type="text/javascript">
alert("Alert Text\\nGoes Here");
</script>
</body>
</html>
HTML
driver_for_app do
get '/' do
<<-HTML
<html>
<head>
</head>
<body>
<script type="text/javascript">
alert("Alert Text\\nGoes Here");
</script>
</body>
</html>
HTML
end
get '/async' do
<<-HTML
<html>
<head>
</head>
<body>
<script type="text/javascript">
function testAlert() {
setTimeout(function() { alert("Alert Text\\nGoes Here"); },
#{params[:sleep] || 100});
}
</script>
<input type="button" onclick="testAlert()" name="test"/>
</body>
</html>
HTML
end
end
end
before { visit("/") }
it 'accepts any alert modal if no match is provided' do
alert_message = driver.accept_modal(:alert) do
visit("/")
end
alert_message.should eq "Alert Text\nGoes Here"
end
it 'accepts an alert modal if it matches' do
alert_message = driver.accept_modal(:alert, text: "Alert Text\nGoes Here") do
visit("/")
end
alert_message.should eq "Alert Text\nGoes Here"
end
it 'raises an error when accepting an alert modal that does not match' do
expect {
driver.accept_modal(:alert, text: 'No?') do
visit('/')
end
}.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with No?"
end
it 'waits to accept an async alert modal' do
visit("/async")
alert_message = driver.accept_modal(:alert) do
driver.find_xpath("//input").first.click
end
alert_message.should eq "Alert Text\nGoes Here"
end
it 'times out waiting for an async alert modal' do
visit("/async?sleep=1000")
expect {
driver.accept_modal(:alert, wait: 0.1) do
driver.find_xpath("//input").first.click
end
}.to raise_error Capybara::ModalNotFound, "Timed out waiting for modal dialog"
end
it 'raises an error when an unexpected modal is displayed' do
expect {
driver.accept_modal(:confirm) do
visit("/")
end
}.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog"
end
it "should let me read my alert messages" do
visit("/")
driver.alert_messages.first.should eq "Alert Text\nGoes Here"
end
it "empties the array when reset" do
visit("/")
driver.reset!
driver.alert_messages.should be_empty
end
@ -659,8 +728,25 @@ describe Capybara::Webkit::Driver do
else
console.log("goodbye");
}
function test_complex_dialog() {
if(confirm("Yes?"))
if(confirm("Really?"))
console.log("hello");
else
console.log("goodbye");
}
function test_async_dialog() {
setTimeout(function() {
if(confirm("Yes?"))
console.log("hello");
else
console.log("goodbye");
}, 100);
}
</script>
<input type="button" onclick="test_dialog()" name="test"/>
<input type="button" onclick="test_complex_dialog()" name="test_complex"/>
<input type="button" onclick="test_async_dialog()" name="test_async"/>
</body>
</html>
HTML
@ -668,6 +754,81 @@ describe Capybara::Webkit::Driver do
before { visit("/") }
it 'accepts any confirm modal if no match is provided' do
driver.accept_modal(:confirm) do
driver.find_xpath("//input").first.click
end
driver.console_messages.first[:message].should eq "hello"
end
it 'dismisses a confirm modal that does not match' do
begin
driver.accept_modal(:confirm, text: 'No?') do
driver.find_xpath("//input").first.click
driver.console_messages.first[:message].should eq "goodbye"
end
rescue Capybara::ModalNotFound
end
end
it 'raises an error when accepting a confirm modal that does not match' do
expect {
driver.accept_modal(:confirm, text: 'No?') do
driver.find_xpath("//input").first.click
end
}.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with No?"
end
it 'dismisses any confirm modal if no match is provided' do
driver.dismiss_modal(:confirm) do
driver.find_xpath("//input").first.click
end
driver.console_messages.first[:message].should eq "goodbye"
end
it 'raises an error when dismissing a confirm modal that does not match' do
expect {
driver.dismiss_modal(:confirm, text: 'No?') do
driver.find_xpath("//input").first.click
end
}.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with No?"
end
it 'waits to accept an async confirm modal' do
visit("/async")
confirm_message = driver.accept_modal(:confirm) do
driver.find_css("input[name=test_async]").first.click
end
confirm_message.should eq "Yes?"
end
it 'allows the nesting of dismiss and accept' do
driver.dismiss_modal(:confirm) do
driver.accept_modal(:confirm) do
driver.find_css("input[name=test_complex]").first.click
end
end
driver.console_messages.first[:message].should eq "goodbye"
end
it 'raises an error when an unexpected modal is displayed' do
expect {
driver.accept_modal(:prompt) do
driver.find_xpath("//input").first.click
end
}.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog"
end
it 'dismisses a confirm modal when prompt is expected' do
begin
driver.accept_modal(:prompt) do
driver.find_xpath("//input").first.click
driver.console_messages.first[:message].should eq "goodbye"
end
rescue Capybara::ModalNotFound
end
end
it "should default to accept the confirm" do
driver.find_xpath("//input").first.click
driver.console_messages.first[:message].should eq "hello"
@ -727,8 +888,23 @@ describe Capybara::Webkit::Driver do
else
console.log("goodbye");
}
function test_complex_dialog() {
var response = prompt("Your name?", "John Smith");
if(response != null)
if(prompt("Your age?"))
console.log("hello " + response);
else
console.log("goodbye");
}
function test_async_dialog() {
setTimeout(function() {
var response = prompt("Your name?", "John Smith");
}, 100);
}
</script>
<input type="button" onclick="test_dialog()" name="test"/>
<input type="button" onclick="test_complex_dialog()" name="test_complex"/>
<input type="button" onclick="test_async_dialog()" name="test_async"/>
</body>
</html>
HTML
@ -736,6 +912,88 @@ describe Capybara::Webkit::Driver do
before { visit("/") }
it 'accepts any prompt modal if no match is provided' do
driver.accept_modal(:prompt) do
driver.find_xpath("//input").first.click
end
driver.console_messages.first[:message].should eq "hello John Smith"
end
it 'accepts any prompt modal with the provided response' do
driver.accept_modal(:prompt, with: 'Capy') do
driver.find_xpath("//input").first.click
end
driver.console_messages.first[:message].should eq "hello Capy"
end
it 'raises an error when accepting a prompt modal that does not match' do
expect {
driver.accept_modal(:prompt, text: 'Your age?') do
driver.find_xpath("//input").first.click
end
}.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with Your age?"
end
it 'dismisses any prompt modal if no match is provided' do
driver.dismiss_modal(:prompt) do
driver.find_xpath("//input").first.click
end
driver.console_messages.first[:message].should eq "goodbye"
end
it 'dismisses a prompt modal that does not match' do
begin
driver.accept_modal(:prompt, text: 'Your age?') do
driver.find_xpath("//input").first.click
driver.console_messages.first[:message].should eq "goodbye"
end
rescue Capybara::ModalNotFound
end
end
it 'raises an error when dismissing a prompt modal that does not match' do
expect {
driver.dismiss_modal(:prompt, text: 'Your age?') do
driver.find_xpath("//input").first.click
end
}.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog with Your age?"
end
it 'waits to accept an async prompt modal' do
visit("/async")
prompt_message = driver.accept_modal(:prompt) do
driver.find_css("input[name=test_async]").first.click
end
prompt_message.should eq "Your name?"
end
it 'allows the nesting of dismiss and accept' do
driver.dismiss_modal(:prompt) do
driver.accept_modal(:prompt) do
driver.find_css("input[name=test_complex]").first.click
end
end
driver.console_messages.first[:message].should eq "goodbye"
end
it 'raises an error when an unexpected modal is displayed' do
expect {
driver.accept_modal(:confirm) do
driver.find_xpath("//input").first.click
end
}.to raise_error Capybara::ModalNotFound, "Unable to find modal dialog"
end
it 'dismisses a prompt modal when confirm is expected' do
begin
driver.accept_modal(:confirm) do
driver.find_xpath("//input").first.click
driver.console_messages.first[:message].should eq "goodbye"
end
rescue Capybara::ModalNotFound
end
end
it "should default to dismiss the prompt" do
driver.find_xpath("//input").first.click
driver.console_messages.first[:message].should eq "goodbye"

11
src/AcceptAlert.cpp Normal file
View file

@ -0,0 +1,11 @@
#include "AcceptAlert.h"
#include "SocketCommand.h"
#include "WebPage.h"
#include "WebPageManager.h"
AcceptAlert::AcceptAlert(WebPageManager *manager, QStringList &arguments, QObject *parent) : SocketCommand(manager, arguments, parent) {
}
void AcceptAlert::start() {
finish(true, page()->acceptAlert(arguments()[0]));
}

10
src/AcceptAlert.h Normal file
View file

@ -0,0 +1,10 @@
#include "SocketCommand.h"
class AcceptAlert : public SocketCommand {
Q_OBJECT
public:
AcceptAlert(WebPageManager *, QStringList &arguments, QObject *parent = 0);
virtual void start();
};

View file

@ -46,6 +46,8 @@
#include "WindowMaximize.h"
#include "GoBack.h"
#include "GoForward.h"
#include "AcceptAlert.h"
#include "FindModal.h"
CommandFactory::CommandFactory(WebPageManager *manager, QObject *parent) : QObject(parent) {
m_manager = manager;

21
src/FindModal.cpp Normal file
View file

@ -0,0 +1,21 @@
#include "FindModal.h"
#include "SocketCommand.h"
#include "WebPage.h"
#include "WebPageManager.h"
#include "ErrorMessage.h"
FindModal::FindModal(WebPageManager *manager, QStringList &arguments, QObject *parent) : SocketCommand(manager, arguments, parent) {
}
void FindModal::start() {
int modalId = arguments()[0].toInt();
if (page()->modalCount() < modalId) {
finish(false, new ErrorMessage("ModalIndexError", ""));
} else {
if (page()->modalMessage(modalId).isNull()) {
finish(false, new ErrorMessage("ModalNotFound", ""));
} else {
finish(true, page()->modalMessage(modalId));
}
}
}

10
src/FindModal.h Normal file
View file

@ -0,0 +1,10 @@
#include "SocketCommand.h"
class FindModal : public SocketCommand {
Q_OBJECT
public:
FindModal(WebPageManager *, QStringList &arguments, QObject *parent = 0);
virtual void start();
};

View file

@ -6,6 +6,14 @@ SetConfirmAction::SetConfirmAction(WebPageManager *manager, QStringList &argumen
void SetConfirmAction::start()
{
page()->setConfirmAction(arguments()[0]);
finish(true);
QString index;
switch (arguments().length()) {
case 2:
index = page()->setConfirmAction(arguments()[0], arguments()[1]);
break;
default:
page()->setConfirmAction(arguments()[0]);
}
finish(true, index);
}

View file

@ -6,6 +6,17 @@ SetPromptAction::SetPromptAction(WebPageManager *manager, QStringList &arguments
void SetPromptAction::start()
{
page()->setPromptAction(arguments()[0]);
finish(true);
QString index;
switch (arguments().length()) {
case 3:
index = page()->setPromptAction(arguments()[0], arguments()[1], arguments()[2]);
break;
case 2:
index = page()->setPromptAction(arguments()[0], arguments()[1]);
break;
default:
page()->setPromptAction(arguments()[0]);
}
finish(true, index);
}

View file

@ -19,14 +19,13 @@ WebPage::WebPage(WebPageManager *manager, QObject *parent) : QWebPage(parent) {
m_failed = false;
m_manager = manager;
m_uuid = QUuid::createUuid().toString();
m_confirmAction = true;
m_promptAction = false;
setForwardUnsupportedContent(true);
loadJavascript();
setUserStylesheet();
m_confirm = true;
m_prompt = false;
m_prompt_text = QString();
this->setCustomNetworkAccessManager();
connect(this, SIGNAL(loadStarted()), this, SLOT(loadStarted()));
@ -180,26 +179,71 @@ void WebPage::javaScriptConsoleMessage(const QString &message, int lineNumber, c
void WebPage::javaScriptAlert(QWebFrame *frame, const QString &message) {
Q_UNUSED(frame);
m_alertMessages.append(message);
if (m_modalResponses.isEmpty()) {
m_modalMessages << QString();
} else {
QVariantMap alertResponse = m_modalResponses.takeLast();
bool expectedType = alertResponse["type"].toString() == "alert";
QRegExp expectedMessage = alertResponse["message"].toRegExp();
addModalMessage(expectedType, message, expectedMessage);
}
m_manager->logger() << "ALERT:" << qPrintable(message);
}
bool WebPage::javaScriptConfirm(QWebFrame *frame, const QString &message) {
Q_UNUSED(frame);
m_confirmMessages.append(message);
return m_confirm;
if (m_modalResponses.isEmpty()) {
m_modalMessages << QString();
return m_confirmAction;
} else {
QVariantMap confirmResponse = m_modalResponses.takeLast();
bool expectedType = confirmResponse["type"].toString() == "confirm";
QRegExp expectedMessage = confirmResponse["message"].toRegExp();
addModalMessage(expectedType, message, expectedMessage);
return expectedType &&
confirmResponse["action"].toBool() &&
message.contains(expectedMessage);
}
}
bool WebPage::javaScriptPrompt(QWebFrame *frame, const QString &message, const QString &defaultValue, QString *result) {
Q_UNUSED(frame)
m_promptMessages.append(message);
if (m_prompt) {
if (m_prompt_text.isNull()) {
bool action = false;
QString response;
if (m_modalResponses.isEmpty()) {
action = m_promptAction;
response = m_prompt_text;
m_modalMessages << QString();
} else {
QVariantMap promptResponse = m_modalResponses.takeLast();
bool expectedType = promptResponse["type"].toString() == "prompt";
QRegExp expectedMessage = promptResponse["message"].toRegExp();
action = expectedType &&
promptResponse["action"].toBool() &&
message.contains(expectedMessage);
response = promptResponse["response"].toString();
addModalMessage(expectedType, message, expectedMessage);
}
if (action) {
if (response.isNull()) {
*result = defaultValue;
} else {
*result = m_prompt_text;
*result = response;
}
}
return m_prompt;
return action;
}
void WebPage::loadStarted() {
@ -376,15 +420,60 @@ void WebPage::remove() {
m_manager->removePage(this);
}
QString WebPage::setConfirmAction(QString action, QString message) {
QVariantMap confirmResponse;
confirmResponse["type"] = "confirm";
confirmResponse["action"] = (action=="Yes");
confirmResponse["message"] = QRegExp(message);
m_modalResponses << confirmResponse;
return QString::number(m_modalResponses.length());
}
void WebPage::setConfirmAction(QString action) {
m_confirm = (action == "Yes");
m_confirmAction = (action == "Yes");
}
QString WebPage::setPromptAction(QString action, QString message, QString response) {
QVariantMap promptResponse;
promptResponse["type"] = "prompt";
promptResponse["action"] = (action == "Yes");
promptResponse["message"] = QRegExp(message);
promptResponse["response"] = response;
m_modalResponses << promptResponse;
return QString::number(m_modalResponses.length());
}
QString WebPage::setPromptAction(QString action, QString message) {
return setPromptAction(action, message, QString());
}
void WebPage::setPromptAction(QString action) {
m_prompt = (action == "Yes");
m_promptAction = (action == "Yes");
}
void WebPage::setPromptText(QString text) {
m_prompt_text = text;
}
QString WebPage::acceptAlert(QString message) {
QVariantMap alertResponse;
alertResponse["type"] = "alert";
alertResponse["message"] = QRegExp(message);
m_modalResponses << alertResponse;
return QString::number(m_modalResponses.length());
}
int WebPage::modalCount() {
return m_modalMessages.length();
}
QString WebPage::modalMessage(int id) {
return m_modalMessages[id - 1];
}
void WebPage::addModalMessage(bool expectedType, const QString &message, const QRegExp &expectedMessage) {
if (expectedType && message.contains(expectedMessage))
m_modalMessages << message;
else
m_modalMessages << QString();
}

View file

@ -23,8 +23,12 @@ class WebPage : public QWebPage {
QString userAgentForUrl(const QUrl &url ) const;
void setUserAgent(QString userAgent);
void setConfirmAction(QString action);
QString setConfirmAction(QString action, QString message);
QString setPromptAction(QString action, QString message, QString response);
QString setPromptAction(QString action, QString message);
void setPromptAction(QString action);
void setPromptText(QString action);
QString acceptAlert(QString);
int getLastStatus();
void setCustomNetworkAccessManager();
bool render(const QString &fileName, const QSize &minimumSize);
@ -48,6 +52,8 @@ class WebPage : public QWebPage {
void mouseEvent(QEvent::Type type, const QPoint &position, Qt::MouseButton button);
bool clickTest(QWebElement element, int absoluteX, int absoluteY);
void resize(int, int);
int modalCount();
QString modalMessage(int);
public slots:
bool shouldInterruptJavaScript();
@ -82,8 +88,8 @@ class WebPage : public QWebPage {
QStringList getAttachedFileNames();
void loadJavascript();
void setUserStylesheet();
bool m_confirm;
bool m_prompt;
bool m_confirmAction;
bool m_promptAction;
QVariantList m_consoleMessages;
QVariantList m_alertMessages;
QVariantList m_confirmMessages;
@ -94,6 +100,9 @@ class WebPage : public QWebPage {
QString m_errorPageMessage;
void setFrameProperties(QWebFrame *, QUrl &, NetworkReplyProxy *);
QPoint m_mousePosition;
QList<QVariantMap> m_modalResponses;
QStringList m_modalMessages;
void addModalMessage(bool, const QString &, const QRegExp &);
};
#endif //_WEBPAGE_H

View file

@ -48,3 +48,5 @@ CHECK_COMMAND(WindowSize)
CHECK_COMMAND(WindowMaximize)
CHECK_COMMAND(GoBack)
CHECK_COMMAND(GoForward)
CHECK_COMMAND(AcceptAlert)
CHECK_COMMAND(FindModal)

View file

@ -7,6 +7,8 @@ PRECOMPILED_DIR = $${BUILD_DIR}
OBJECTS_DIR = $${BUILD_DIR}
MOC_DIR = $${BUILD_DIR}
HEADERS = \
FindModal.h \
AcceptAlert.h \
GoForward.h \
GoBack.h \
WindowMaximize.h \
@ -79,6 +81,8 @@ HEADERS = \
StdinNotifier.h
SOURCES = \
FindModal.cpp \
AcceptAlert.cpp \
GoForward.cpp \
GoBack.cpp \
WindowMaximize.cpp \