Add support for keys and offset with mouse clicks coming in Capybara 3

This commit is contained in:
Thomas Walpole 2018-01-06 12:26:31 -08:00
parent 5cd1ad0645
commit 1d4dfdaac9
10 changed files with 86 additions and 49 deletions

View File

@ -6,4 +6,5 @@ end
appraise "master" do appraise "master" do
gem "capybara", github: "jnicklas/capybara" gem "capybara", github: "jnicklas/capybara"
gem "puma"
end end

View File

@ -2,7 +2,7 @@ PATH
remote: . remote: .
specs: specs:
capybara-webkit (1.14.0) capybara-webkit (1.14.0)
capybara (~> 2.3) capybara (>= 2.3, < 4.0)
json json
GEM GEM
@ -14,13 +14,13 @@ GEM
bundler bundler
rake rake
thor (>= 0.14.0) thor (>= 0.14.0)
capybara (2.15.4) capybara (2.17.0)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
xpath (~> 2.0) xpath (>= 2.0, < 4.0)
diff-lcs (1.3) diff-lcs (1.3)
ffi (1.9.18-java) ffi (1.9.18-java)
json (1.8.6) json (1.8.6)
@ -36,14 +36,11 @@ GEM
mustermann (1.0.1) mustermann (1.0.1)
nokogiri (1.8.1) nokogiri (1.8.1)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
nokogiri (1.8.1-java)
nokogiri (1.8.1-x86-mingw32)
mini_portile2 (~> 2.3.0)
public_suffix (3.0.0) public_suffix (3.0.0)
rack (2.0.3) rack (2.0.3)
rack-protection (2.0.0) rack-protection (2.0.0)
rack rack
rack-test (0.7.0) rack-test (0.8.2)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rake (11.3.0) rake (11.3.0)
rspec (3.7.0) rspec (3.7.0)
@ -68,8 +65,8 @@ GEM
ffi ffi
thor (0.20.0) thor (0.20.0)
tilt (2.0.8) tilt (2.0.8)
xpath (2.1.0) xpath (3.0.0)
nokogiri (~> 1.3) nokogiri (~> 1.8)
PLATFORMS PLATFORMS
java java

View File

@ -21,7 +21,7 @@ Gem::Specification.new do |s|
s.requirements << "Qt >= 4.8" s.requirements << "Qt >= 4.8"
s.add_runtime_dependency("capybara", "~>2.3") s.add_runtime_dependency("capybara", ">= 2.3", "< 4.0")
s.add_runtime_dependency("json") s.add_runtime_dependency("json")
s.add_development_dependency("rspec", "~> 3.5") s.add_development_dependency("rspec", "~> 3.5")

View File

@ -2,7 +2,7 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "json", "< 2.0", platforms: [:ruby_19, :jruby_19]
gem "capybara", github: "jnicklas/capybara" gem "capybara", github: "jnicklas/capybara"
gem "puma"
gemspec path: "../" gemspec path: "../"

View File

@ -68,16 +68,16 @@ module Capybara::Webkit
end end
end end
def click def click(keys = [], offset = {})
invoke("leftClick") invoke("leftClick", keys.to_json, offset.to_json)
end end
def double_click def double_click(keys = [], offset = {})
invoke("doubleClick") invoke("doubleClick", keys.to_json, offset.to_json)
end end
def right_click def right_click(keys = [], offset = {})
invoke("rightClick") invoke("rightClick", keys.to_json, offset.to_json)
end end
def hover def hover

View File

@ -5,6 +5,8 @@
#include <QEvent> #include <QEvent>
#include <QContextMenuEvent> #include <QContextMenuEvent>
QMap<QString, Qt::KeyboardModifiers> JavascriptInvocation::m_modifiersMap(JavascriptInvocation::makeModifiersMap());
JavascriptInvocation::JavascriptInvocation(const QString &functionName, bool allowUnattached, const QStringList &arguments, WebPage *page, QObject *parent) : QObject(parent) { JavascriptInvocation::JavascriptInvocation(const QString &functionName, bool allowUnattached, const QStringList &arguments, WebPage *page, QObject *parent) : QObject(parent) {
m_functionName = functionName; m_functionName = functionName;
m_allowUnattached = allowUnattached; m_allowUnattached = allowUnattached;
@ -33,6 +35,7 @@ void JavascriptInvocation::setError(QVariant error) {
} }
InvocationResult JavascriptInvocation::invoke(QWebFrame *frame) { InvocationResult JavascriptInvocation::invoke(QWebFrame *frame) {
frame->addToJavaScriptWindowObject("CapybaraInvocation", this); frame->addToJavaScriptWindowObject("CapybaraInvocation", this);
QVariant result = frame->evaluateJavaScript("Capybara.invoke()"); QVariant result = frame->evaluateJavaScript("Capybara.invoke()");
if (getError().isValid()) if (getError().isValid())
@ -41,36 +44,43 @@ InvocationResult JavascriptInvocation::invoke(QWebFrame *frame) {
if (functionName() == "leftClick") { if (functionName() == "leftClick") {
// Don't trigger the left click from JS incase the frame closes // Don't trigger the left click from JS incase the frame closes
QVariantMap qm = result.toMap(); QVariantMap qm = result.toMap();
leftClick(qm["absoluteX"].toInt(), qm["absoluteY"].toInt()); leftClick(qm["absoluteX"].toInt(), qm["absoluteY"].toInt(), qm["keys"].value<QVariantList>());
} }
return InvocationResult(result); return InvocationResult(result);
} }
} }
void JavascriptInvocation::leftClick(int x, int y) { Qt::KeyboardModifiers JavascriptInvocation::modifiers(const QVariantList& keys){
QPoint mousePos(x, y); Qt::KeyboardModifiers modifiers = Qt::NoModifier;
for (int i = 0; i < keys.length(); i++) {
m_page->mouseEvent(QEvent::MouseButtonPress, mousePos, Qt::LeftButton); modifiers |= m_modifiersMap.value(keys[i].toString(), Qt::NoModifier);
m_page->mouseEvent(QEvent::MouseButtonRelease, mousePos, Qt::LeftButton); }
return modifiers;
} }
void JavascriptInvocation::rightClick(int x, int y) { void JavascriptInvocation::leftClick(int x, int y, QVariantList keys) {
QPoint mousePos(x, y); QPoint mousePos(x, y);
m_page->mouseEvent(QEvent::MouseButtonPress, mousePos, Qt::RightButton); m_page->mouseEvent(QEvent::MouseButtonPress, mousePos, Qt::LeftButton, modifiers(keys));
m_page->mouseEvent(QEvent::MouseButtonRelease, mousePos, Qt::LeftButton, modifiers(keys));
}
void JavascriptInvocation::rightClick(int x, int y, QVariantList keys) {
QPoint mousePos(x, y);
m_page->mouseEvent(QEvent::MouseButtonPress, mousePos, Qt::RightButton, modifiers(keys));
// swallowContextMenuEvent tries to fire contextmenu event in html page // swallowContextMenuEvent tries to fire contextmenu event in html page
QContextMenuEvent *event = new QContextMenuEvent(QContextMenuEvent::Mouse, mousePos); QContextMenuEvent *event = new QContextMenuEvent(QContextMenuEvent::Mouse, mousePos, QCursor::pos(), modifiers(keys));
m_page->swallowContextMenuEvent(event); m_page->swallowContextMenuEvent(event);
m_page->mouseEvent(QEvent::MouseButtonRelease, mousePos, Qt::RightButton); m_page->mouseEvent(QEvent::MouseButtonRelease, mousePos, Qt::RightButton, modifiers(keys));
} }
void JavascriptInvocation::doubleClick(int x, int y) { void JavascriptInvocation::doubleClick(int x, int y, QVariantList keys) {
QPoint mousePos(x, y); QPoint mousePos(x, y);
m_page->mouseEvent(QEvent::MouseButtonDblClick, mousePos, Qt::LeftButton); m_page->mouseEvent(QEvent::MouseButtonDblClick, mousePos, Qt::LeftButton, modifiers(keys));
m_page->mouseEvent(QEvent::MouseButtonRelease, mousePos, Qt::LeftButton); m_page->mouseEvent(QEvent::MouseButtonRelease, mousePos, Qt::LeftButton, modifiers(keys));
} }
bool JavascriptInvocation::clickTest(QWebElement element, int absoluteX, int absoluteY) { bool JavascriptInvocation::clickTest(QWebElement element, int absoluteX, int absoluteY) {
@ -86,6 +96,8 @@ QVariantMap JavascriptInvocation::clickPosition(QWebElement element, int left, i
QVariantMap m; QVariantMap m;
m["relativeX"] = mousePos.x(); m["relativeX"] = mousePos.x();
m["relativeY"] = mousePos.y(); m["relativeY"] = mousePos.y();
m["relativeTop"] = boundedBox.top();
m["relativeLeft"] = boundedBox.left();
QWebFrame *parent = element.webFrame(); QWebFrame *parent = element.webFrame();
while (parent) { while (parent) {
@ -95,7 +107,8 @@ QVariantMap JavascriptInvocation::clickPosition(QWebElement element, int left, i
boundedBox = elementBox.intersected(viewport); boundedBox = elementBox.intersected(viewport);
mousePos = boundedBox.center(); mousePos = boundedBox.center();
m["absoluteTop"] = boundedBox.top();
m["absoluteLeft"] = boundedBox.left();
m["absoluteX"] = mousePos.x(); m["absoluteX"] = mousePos.x();
m["absoluteY"] = mousePos.y(); m["absoluteY"] = mousePos.y();
@ -217,3 +230,14 @@ const QString JavascriptInvocation::render(void) {
m_page->render(path, QSize(1024, 768)); m_page->render(path, QSize(1024, 768));
return path; return path;
} }
QMap<QString, Qt::KeyboardModifiers> JavascriptInvocation::makeModifiersMap(){
QMap<QString, Qt::KeyboardModifiers> map;
map["alt"] = Qt::AltModifier;
map["control"] = Qt::ControlModifier;
map["meta"] = Qt::MetaModifier;
map["shift"] = Qt::ShiftModifier;
return map;
}

View File

@ -20,9 +20,9 @@ class JavascriptInvocation : public QObject {
QString &functionName(); QString &functionName();
bool allowUnattached(); bool allowUnattached();
QStringList &arguments(); QStringList &arguments();
Q_INVOKABLE void leftClick(int x, int y); Q_INVOKABLE void leftClick(int x, int y, QVariantList keys);
Q_INVOKABLE void rightClick(int x, int y); Q_INVOKABLE void rightClick(int x, int y, QVariantList keys);
Q_INVOKABLE void doubleClick(int x, int y); Q_INVOKABLE void doubleClick(int x, int y, QVariantList keys);
Q_INVOKABLE bool clickTest(QWebElement element, int absoluteX, int absoluteY); Q_INVOKABLE bool clickTest(QWebElement element, int absoluteX, int absoluteY);
Q_INVOKABLE QVariantMap clickPosition(QWebElement element, int left, int top, int width, int height); Q_INVOKABLE QVariantMap clickPosition(QWebElement element, int left, int top, int width, int height);
Q_INVOKABLE void hover(int absoluteX, int absoluteY); Q_INVOKABLE void hover(int absoluteX, int absoluteY);
@ -46,5 +46,8 @@ class JavascriptInvocation : public QObject {
int keyCodeForName(const QString &); int keyCodeForName(const QString &);
Qt::Key key_enum; Qt::Key key_enum;
Qt::KeyboardModifiers m_currentModifiers; Qt::KeyboardModifiers m_currentModifiers;
Qt::KeyboardModifiers modifiers(const QVariantList& keys);
static QMap<QString, Qt::KeyboardModifiers> makeModifiersMap();
static QMap<QString, Qt::KeyboardModifiers> m_modifiersMap;
}; };

View File

@ -275,9 +275,9 @@ QString WebPage::failureString() {
return message + m_errorPageMessage; return message + m_errorPageMessage;
} }
void WebPage::mouseEvent(QEvent::Type type, const QPoint &position, Qt::MouseButton button) { void WebPage::mouseEvent(QEvent::Type type, const QPoint &position, Qt::MouseButton button, Qt::KeyboardModifiers modifiers) {
m_mousePosition = position; m_mousePosition = position;
QMouseEvent event(type, position, button, button, Qt::NoModifier); QMouseEvent event(type, position, button, button, modifiers);
QApplication::sendEvent(this, &event); QApplication::sendEvent(this, &event);
} }
@ -491,5 +491,3 @@ QWebFrame* WebPage::currentFrameParent() {
void WebPage::setCurrentFrameParent(QWebFrame* frame) { void WebPage::setCurrentFrameParent(QWebFrame* frame) {
m_currentFrameParent = frame; m_currentFrameParent = frame;
} }

View File

@ -50,7 +50,7 @@ class WebPage : public QWebPage {
QVariantMap pageHeaders(); QVariantMap pageHeaders();
QByteArray body(); QByteArray body();
QString contentType(); QString contentType();
void mouseEvent(QEvent::Type type, const QPoint &position, Qt::MouseButton button); void mouseEvent(QEvent::Type type, const QPoint &position, Qt::MouseButton button, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
bool clickTest(QWebElement element, int absoluteX, int absoluteY); bool clickTest(QWebElement element, int absoluteX, int absoluteY);
void resize(int, int); void resize(int, int);
int modalCount(); int modalCount();

View File

@ -7,7 +7,15 @@ Capybara = {
invoke: function () { invoke: function () {
try { try {
if (CapybaraInvocation.functionName == "leftClick") { if (CapybaraInvocation.functionName == "leftClick") {
return this["verifiedClickPosition"].apply(this, CapybaraInvocation.arguments); var args = CapybaraInvocation.arguments;
var leftClickOptions = this["verifiedClickPosition"].apply(this, args);
leftClickOptions["keys"] = JSON.parse(args[1]);
offset = JSON.parse(args[2]);
if (offset && offset.x && offset.y){
leftClickOptions["absoluteX"] = leftClickOptions["absoluteLeft"] + offset.x;
leftClickOptions["absoluteY"] = leftClickOptions["absoluteTop"] + offset.y;
}
return leftClickOptions;
} else { } else {
return this[CapybaraInvocation.functionName].apply(this, CapybaraInvocation.arguments); return this[CapybaraInvocation.functionName].apply(this, CapybaraInvocation.arguments);
} }
@ -208,22 +216,28 @@ Capybara = {
return pos; return pos;
}, },
click: function (index, action) { click: function (index, action, keys, offset) {
var pos = this.verifiedClickPosition(index); var pos = this.verifiedClickPosition(index);
action(pos.absoluteX, pos.absoluteY); keys = keys ? JSON.parse(keys) : [];
offset = offset ? JSON.parse(offset) : {};
if (offset && (offset.x != null) && (offset.y != null)){
action(pos.absoluteLeft + offset.x, pos.absoluteTop + offset.y, keys);
} else {
action(pos.absoluteX, pos.absoluteY, keys);
}
}, },
leftClick: function (index) { leftClick: function (index, keys, offset) {
this.click(index, CapybaraInvocation.leftClick); this.click(index, CapybaraInvocation.leftClick, keys, offset);
}, },
doubleClick: function(index) { doubleClick: function(index, keys, offset) {
this.click(index, CapybaraInvocation.leftClick); this.click(index, CapybaraInvocation.leftClick, keys, offset);
this.click(index, CapybaraInvocation.doubleClick); this.click(index, CapybaraInvocation.doubleClick, keys, offset);
}, },
rightClick: function(index) { rightClick: function(index, keys, offset) {
this.click(index, CapybaraInvocation.rightClick); this.click(index, CapybaraInvocation.rightClick, keys, offset);
}, },
hover: function (index) { hover: function (index) {