From b6fdc65bf105d042e16e1b5aec2a008760d6313d Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Mon, 6 Feb 2017 17:29:11 -0800 Subject: [PATCH] Support most of the keys specified by Capybara for Node#send_keys --- lib/capybara/webkit/node.rb | 57 +++++++++++++++++++++-------- spec/driver_spec.rb | 25 +++++++++++++ spec/spec_helper.rb | 16 ++++----- src/JavascriptInvocation.cpp | 69 ++++++++++++++++++++++++++++++++++-- src/JavascriptInvocation.h | 7 ++++ src/capybara.js | 41 +++++++++++++++++---- 6 files changed, 184 insertions(+), 31 deletions(-) diff --git a/lib/capybara/webkit/node.rb b/lib/capybara/webkit/node.rb index c519515..1e24c82 100644 --- a/lib/capybara/webkit/node.rb +++ b/lib/capybara/webkit/node.rb @@ -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 diff --git a/spec/driver_spec.rb b/spec/driver_spec.rb index f7df360..9b09ec9 100644 --- a/spec/driver_spec.rb +++ b/spec/driver_spec.rb @@ -1273,6 +1273,17 @@ describe Capybara::Webkit::Driver do +
+ 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 } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 80d2a00..4ae1332 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/src/JavascriptInvocation.cpp b/src/JavascriptInvocation.cpp index ecce3c0..2ab1cad 100644 --- a/src/JavascriptInvocation.cpp +++ b/src/JavascriptInvocation.cpp @@ -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"); diff --git a/src/JavascriptInvocation.h b/src/JavascriptInvocation.h index 4d18a66..830c3d6 100644 --- a/src/JavascriptInvocation.h +++ b/src/JavascriptInvocation.h @@ -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; }; diff --git a/src/capybara.js b/src/capybara.js index b77b654..623a3bf 100644 --- a/src/capybara.js +++ b/src/capybara.js @@ -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); + } } },