Support most of the keys specified by Capybara for Node#send_keys

This commit is contained in:
Thomas Walpole 2017-02-06 17:29:11 -08:00 committed by Matthew Horan
parent 702bce9ce9
commit b6fdc65bf1
6 changed files with 184 additions and 31 deletions

View File

@ -48,20 +48,9 @@ module Capybara::Webkit
end
def send_keys(*keys)
invoke("sendKeys", keys.map { |key|
case key
when :space
" "
when :enter
"\r"
when :backspace
"\b"
when String
key.to_s
else
raise Capybara::NotSupportedByDriverError.new, "Unrecognized key(s) in #{key}"
end
}.join)
# Currently unsupported keys specified by Capybara
# :separator
invoke("sendKeys", convert_to_named_keys(keys).to_json)
end
def select_option
@ -172,5 +161,45 @@ module Capybara::Webkit
def ==(other)
invoke("equals", other.native) == "true"
end
private
def convert_to_named_keys(key)
if key.is_a? Array
key.map {|k| convert_to_named_keys(k)}
else
case key
when :cancel, :help, :backspace, :tab, :clear, :return, :enter, :insert, :delete, :pause, :escape,
:space, :end, :home, :left, :up, :right, :down, :semicolon,
:f1, :f2, :f3, :f4, :f5, :f6, :f7, :f8, :f9, :f10, :f11, :f12,
:shift, :control, :alt, :meta
{ "key" => key.to_s.capitalize }
when :equals
{ "key" => "Equal" }
when :page_up
{ "key" => "PageUp" }
when :page_down
{ "key" => "PageDown" }
when :numpad0, :numpad1, :numpad2, :numpad3, :numpad4, :numpad5, :numpad6, :numpad7, :numpad9, :numpad9
{ "key" => key[-1], "modifier" => 'Keypad' }
when :multiply
{ "key" => "Asterisk", "modifier" => 'Keypad' }
when :divide
{ "key" => "Slash", "modifier" => 'Keypad' }
when :add
{ "key" => "Plus", "modifier" => 'Keypad' }
when :subtract
{ "key" => "Minus", "modifier" => 'Keypad' }
when :decimal
{"key" => "Period", "modifier" => 'Keypad'}
when :command
{ "key" => "Meta" }
when String
key.to_s
else
raise Capybara::NotSupportedByDriverError.new
end
end
end
end
end

View File

@ -1273,6 +1273,17 @@ describe Capybara::Webkit::Driver do
<input type="radio" id="only-radio" value="1"/>
<button type="reset">Reset Form</button>
</form>
<div id="key_events"></div>
<script>
var form = document.getElementsByTagName('form')[0];
var output = document.getElementById('key_events');
form.addEventListener('keydown', function(e){
output.innerHTML = output.innerHTML + " d:" + (e.key || e.which);
});
form.addEventListener('keyup', function(e){
output.innerHTML = output.innerHTML + " u:" + (e.key || e.which);
});
</script>
</body></html>
HTML
end
@ -1341,6 +1352,20 @@ describe Capybara::Webkit::Driver do
input.send_keys(*[:backspace])
expect(input.value).to eq "do"
end
it "should support :modifiers" do
input = driver.find_xpath("//input").first
input.send_keys("abc", [:shift, :left], "def")
expect(input.value).to eq "abdef"
input.send_keys([:control, "a"], [:shift, "upper"])
expect(input.value).to eq "UPPER"
end
it "releases modifiers correctly" do
input = driver.find_xpath("//input").first
input.send_keys("a", [:shift, :left], "a")
expect(driver.find_css("#key_events").first.text).to eq "d:65 u:65 d:16 d:37 u:37 u:16 d:65 u:65"
end
end
let(:monkey_option) { driver.find_xpath("//option[@id='select-option-monkey']").first }

View File

@ -52,15 +52,13 @@ RSpec.configure do |c|
# Accessing unattached nodes is allowed when reload is disabled - Legacy behavior
# Node#send_keys does not support modifiers and only supports a subset of special keys
c.filter_run_excluding :full_description => lambda { |description, metadata|
(description !~ /Capybara::Session webkit node #send_keys should send a string of keys to an element/) && (
description =~ /Capybara::Session webkit node #send_keys/ ||
description =~ /Capybara::Session webkit node #reload without automatic reload should not automatically reload/ ||
if Gem::Version.new(Capybara::VERSION) < Gem::Version.new("2.12.0")
description =~ /Capybara::Session webkit Capybara::Window\s*#(size|resize_to|maximize|close.*no_such_window_error|send_keys)/ ||
description =~ /Capybara::Session webkit node\s*#set should allow me to change the contents of a contenteditable elements child/
else
description =~ /Capybara::Session webkit Capybara::Window\s*#close.*no_such_window_error/
end
(description =~ /Capybara::Session webkit node #reload without automatic reload should not automatically reload/ ||
if Gem::Version.new(Capybara::VERSION) < Gem::Version.new("2.12.0")
description =~ /Capybara::Session webkit Capybara::Window\s*#(size|resize_to|maximize|close.*no_such_window_error)/ ||
description =~ /Capybara::Session webkit node\s*#set should allow me to change the contents of a contenteditable elements child/
else
description =~ /Capybara::Session webkit Capybara::Window\s*#close.*no_such_window_error/
end
)
}
end

View File

@ -134,14 +134,79 @@ int JavascriptInvocation::keyCodeFor(const QChar &key) {
}
}
int JavascriptInvocation::keyCodeForName(const QString &keyName) {
const QMetaObject &mo = JavascriptInvocation::staticMetaObject;
int prop_index = mo.indexOfProperty("key_enum");
QMetaProperty metaProperty = mo.property(prop_index);
QMetaEnum metaEnum = metaProperty.enumerator();
QByteArray array ((QString("Key_") + keyName).toStdString().c_str());
return metaEnum.keyToValue(array);
// return Qt::Key_unknown;
}
void JavascriptInvocation::keypress(QChar key) {
int keyCode = keyCodeFor(key);
QKeyEvent event(QKeyEvent::KeyPress, keyCode, Qt::NoModifier, key);
QKeyEvent event(QKeyEvent::KeyPress, keyCode, m_currentModifiers, (m_currentModifiers ? QString() : key));
QApplication::sendEvent(m_page, &event);
event = QKeyEvent(QKeyEvent::KeyRelease, keyCode, Qt::NoModifier, key);
event = QKeyEvent(QKeyEvent::KeyRelease, keyCode, m_currentModifiers);
QApplication::sendEvent(m_page, &event);
}
void JavascriptInvocation::namedKeypress(QString keyName, QString modifiers){
Qt::KeyboardModifiers key_modifiers(m_currentModifiers);
if (modifiers == "Keypad") {
key_modifiers |= Qt::KeypadModifier;
};
int keyCode = keyCodeForName(keyName);
QKeyEvent event(QKeyEvent::KeyPress, keyCode, key_modifiers);
QApplication::sendEvent(m_page, &event);
event = QKeyEvent(QKeyEvent::KeyRelease, keyCode, key_modifiers);
QApplication::sendEvent(m_page, &event);
}
void JavascriptInvocation::namedKeydown(QString keyName){
int keyCode = keyCodeForName(keyName);
QKeyEvent event(QKeyEvent::KeyPress, keyCode, m_currentModifiers);
QApplication::sendEvent(m_page, &event);
switch(keyCode){
case Qt::Key_Shift:
m_currentModifiers |= Qt::ShiftModifier;
break;
case Qt::Key_Control:
m_currentModifiers |= Qt::ControlModifier;
break;
case Qt::Key_Alt:
m_currentModifiers |= Qt::AltModifier;
break;
case Qt::Key_Meta:
m_currentModifiers |= Qt::MetaModifier;
break;
};
}
void JavascriptInvocation::namedKeyup(QString keyName){
int keyCode = keyCodeForName(keyName);
QKeyEvent event(QKeyEvent::KeyRelease, keyCode, m_currentModifiers, 0);
QApplication::sendEvent(m_page, &event);
switch(keyCode){
case Qt::Key_Shift:
m_currentModifiers &= ~Qt::ShiftModifier;
break;
case Qt::Key_Control:
m_currentModifiers &= ~Qt::ControlModifier;
break;
case Qt::Key_Alt:
m_currentModifiers &= ~Qt::AltModifier;
break;
case Qt::Key_Meta:
m_currentModifiers &= ~Qt::MetaModifier;
break;
};
}
const QString JavascriptInvocation::render(void) {
QString pathTemplate =
QDir::temp().absoluteFilePath("./click_failed_XXXXXX.png");

View File

@ -13,6 +13,7 @@ class JavascriptInvocation : public QObject {
Q_PROPERTY(bool allowUnattached READ allowUnattached)
Q_PROPERTY(QStringList arguments READ arguments)
Q_PROPERTY(QVariant error READ getError WRITE setError)
Q_PROPERTY(Qt::Key key_enum)
public:
JavascriptInvocation(const QString &functionName, bool allowUnattached, const QStringList &arguments, WebPage *page, QObject *parent = 0);
@ -26,6 +27,9 @@ class JavascriptInvocation : public QObject {
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 keypress(QChar);
Q_INVOKABLE void namedKeydown(QString keyName);
Q_INVOKABLE void namedKeyup(QString keyName);
Q_INVOKABLE void namedKeypress(QString keyName, QString modifiers);
Q_INVOKABLE const QString render(void);
QVariant getError();
void setError(QVariant error);
@ -39,5 +43,8 @@ class JavascriptInvocation : public QObject {
QVariant m_error;
void hover(const QPoint &);
int keyCodeFor(const QChar &);
int keyCodeForName(const QString &);
Qt::Key key_enum;
Qt::KeyboardModifiers m_currentModifiers;
};

View File

@ -2,6 +2,7 @@ Capybara = {
nextIndex: 0,
nodes: {},
attachedFiles: [],
keyModifiersStack: [],
invoke: function () {
try {
@ -285,17 +286,45 @@ Capybara = {
return true;
},
sendKeys: function (index, keys) {
var strindex, length;
sendKeys: function (elem_index, json_keys) {
var idx, length, keys;
keys = JSON.parse(json_keys);
length = keys.length;
if (length) {
this.focus(index);
this.focus(elem_index);
}
for (strindex = 0; strindex < length; strindex++) {
CapybaraInvocation.keypress(keys[strindex]);
for (idx = 0; idx < length; idx++) {
this._sendKeys(keys[idx]);
}
},
_sendKeys: function(keys) {
if (typeof keys == "string") {
var str_len = keys.length;
var str_idx;
for (str_idx = 0; str_idx < str_len; str_idx++) {
CapybaraInvocation.keypress(keys[str_idx]);
}
} else if (Array.isArray(keys)) {
this.keyModifiersStack.push([]);
var idx;
for (idx = 0; idx < keys.length; idx++) {
this._sendKeys(keys[idx]);
}
var mods = this.keyModifiersStack.pop();
while (mods.length) {
CapybaraInvocation.namedKeyup(mods.pop().key);
}
} else {
key = keys.key
if (["Shift", "Control", "Alt", "Meta"].indexOf(key) > -1){
CapybaraInvocation.namedKeydown(key);
this.keyModifiersStack[this.keyModifiersStack.length-1].push(keys);
} else {
CapybaraInvocation.namedKeypress(key, keys.modifier);
}
}
},