Ver Fonte

LibWeb: Begin implementing the Element Send Keys endpoint

Timothy Flynn há 10 meses atrás
pai
commit
23d134708c

+ 135 - 0
Userland/Libraries/LibWeb/WebDriver/Actions.cpp

@@ -841,6 +841,14 @@ static KeyCodeData key_code_data(u32 code_point)
     return *it;
 }
 
+// https://w3c.github.io/webdriver/#dfn-shifted-character
+static bool is_shifted_character(u32 code_point)
+{
+    // A shifted character is one that appears in the second column of the following table.
+    auto code = key_code_data(code_point);
+    return code.alternate_key == code_point;
+}
+
 struct KeyEvent {
     u32 code_point { 0 };
     UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None };
@@ -1404,4 +1412,131 @@ JS::NonnullGCPtr<JS::Cell> dispatch_list_of_actions(InputState& input_state, Vec
     // 3. Return the result of dispatch actions with input state, actions by tick, browsing context, and actions options.
     return dispatch_actions(input_state, move(actions_by_tick), browsing_context, move(actions_options), on_complete);
 }
+
+// https://w3c.github.io/webdriver/#dfn-dispatch-the-events-for-a-typeable-string
+static JS::NonnullGCPtr<JS::Cell> dispatch_the_events_for_a_typeable_string(Web::WebDriver::InputState& input_state, String const& input_id, Web::WebDriver::InputSource& source, StringView text, Web::HTML::BrowsingContext& browsing_context, Web::WebDriver::OnActionsComplete on_complete)
+{
+    auto& input_source = source.get<Web::WebDriver::KeyInputSource>();
+
+    // NOTE: Rather than dispatching each action list individually below, we collect a list of "actions by tick" to
+    //       dispatch, to make handling the asynchronous nature of actions simpler.
+    Vector<Vector<Web::WebDriver::ActionObject>> actions_by_tick;
+
+    // 1. Let actions options be a new actions options with the is element origin steps set to represents a web element,
+    //    and the get element origin steps set to get a WebElement origin.
+    Web::WebDriver::ActionsOptions actions_options {
+        .is_element_origin = &Web::WebDriver::represents_a_web_element,
+        .get_element_origin = &Web::WebDriver::get_web_element_origin,
+    };
+
+    // 2. For each char of text:
+    for (auto code_point : Utf8View { text }) {
+        auto char_is_shifted = Web::WebDriver::is_shifted_character(code_point);
+
+        // 1. Let global key state be the result of get the global key state with input state.
+        auto global_key_state = Web::WebDriver::get_global_key_state(input_state);
+
+        // 2. If char is a shifted character, and the shifted state of source is false:
+        if (char_is_shifted && !input_source.shift) {
+            // 1. Let action be an action object constructed with input id, "key", and "keyDown", and set its value
+            //    property to U+E008 ("left shift").
+            Web::WebDriver::ActionObject action { input_id, Web::WebDriver::InputSourceType::Key, Web::WebDriver::ActionObject::Subtype::KeyDown };
+            action.key_fields().value = 0xE008;
+
+            // 2. Let actions be the list «action».
+            Vector actions { move(action) };
+
+            // 3. Dispatch a list of actions with input state, actions, and browsing context.
+            actions_by_tick.append(move(actions));
+            input_source.shift = true;
+        }
+
+        // 3. If char is not a shifted character and the shifted state of source is true:
+        if (!char_is_shifted && input_source.shift) {
+            // 1. Let action be an action object constructed with input id, "key", and "keyUp", and set its value
+            //    property to U+E008 ("left shift").
+            Web::WebDriver::ActionObject action { input_id, Web::WebDriver::InputSourceType::Key, Web::WebDriver::ActionObject::Subtype::KeyUp };
+            action.key_fields().value = 0xE008;
+
+            // 2. Let tick actions be the list «action».
+            Vector actions { move(action) };
+
+            // 3. Dispatch a list of actions with input state, actions, browsing context, and actions options.
+            actions_by_tick.append(move(actions));
+            input_source.shift = false;
+        }
+
+        // 4. Let keydown action be an action object constructed with arguments input id, "key", and "keyDown".
+        Web::WebDriver::ActionObject keydown_action { input_id, Web::WebDriver::InputSourceType::Key, Web::WebDriver::ActionObject::Subtype::KeyDown };
+
+        // 5. Set the value property of keydown action to char.
+        keydown_action.key_fields().value = code_point;
+
+        // 6. Let keyup action be a copy of keydown action with the subtype property changed to "keyUp".
+        auto keyup_action = keydown_action;
+        keyup_action.subtype = Web::WebDriver::ActionObject::Subtype::KeyUp;
+
+        // 7. Let actions be the list «keydown action, keyup action».
+        Vector actions { move(keydown_action), move(keyup_action) };
+
+        // 8. Dispatch a list of actions with input state, actions, browsing context, and actions options.
+        actions_by_tick.append(move(actions));
+    }
+
+    return dispatch_actions(input_state, move(actions_by_tick), browsing_context, move(actions_options), on_complete);
+}
+
+// https://w3c.github.io/webdriver/#dfn-dispatch-actions-for-a-string
+JS::NonnullGCPtr<JS::Cell> dispatch_actions_for_a_string(Web::WebDriver::InputState& input_state, String const& input_id, Web::WebDriver::InputSource& source, StringView text, Web::HTML::BrowsingContext& browsing_context, Web::WebDriver::OnActionsComplete on_complete)
+{
+    // FIXME: 1. Let clusters be an array created by breaking text into extended grapheme clusters.
+    // FIXME: 2. Let undo actions be an empty map.
+    // FIXME: 3. Let current typeable text be an empty list.
+    // FIXME: 4. For each cluster corresponding to an indexed property in clusters run the substeps of the first matching statement:
+    {
+        // -> cluster is the null key
+        {
+            // FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context. Empty current typeable text.
+            // FIXME: 2. Try to clear the modifier key state with input state, input id, source, undo actions and browsing context.
+            // FIXME: 3. Clear undo actions.
+        }
+        // -> cluster is a modifier key
+        {
+
+            // FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context.
+            // FIXME: 2. Empty current typeable text.
+            // FIXME: 3. Let keydown action be an action object constructed with arguments input id, "key", and "keyDown".
+            // FIXME: 4. Set the value property of keydown action to cluster.
+            // FIXME: 5. Let actions be the list «keydown action»
+            // FIXME: 6. Dispatch a list of actions with input state, actions, browsing context, and actions options.
+            // FIXME: 7. Add an entry to undo actions with key cluster and value being a copy of keydown action with the subtype property modified to "keyUp".
+        }
+        // -> cluster is typeable
+        {
+            // FIXME: Append cluster to current typeable text.
+        }
+        // -> Otherwise
+        {
+            // FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context.
+            // FIXME: 2. Empty current typeable text.
+            // FIXME: 3. Dispatch a composition event with arguments "compositionstart", undefined, and browsing context.
+            // FIXME: 4. Dispatch a composition event with arguments "compositionupdate", cluster, and browsing context.
+            // FIXME: 5. Dispatch a composition event with arguments "compositionend", cluster, and browsing context.
+        }
+    }
+
+    // FIXME: We currently only support sending single code points to Page. Much of the above loop would break the the
+    //        text into segments, broken by graphemes / modifier keys / null keys. Until we need such support, we take
+    //        the easy road here and dispatch the string as a single list of actions. When we do implement the above
+    //        steps, we will likely need to implement a completely asynchronous driver (like ActionExecutor above).
+
+    // 5. Dispatch the events for a typeable string with input state, input id and source, current typeable text, and
+    //    browsing context.
+    return dispatch_the_events_for_a_typeable_string(input_state, input_id, source, text, browsing_context, JS::create_heap_function(browsing_context.heap(), [on_complete](Web::WebDriver::Response result) {
+        // FIXME: 6. Try to clear the modifier key state with input state, input id, source, undo actions, and browsing context.
+
+        on_complete->function()(move(result));
+    }));
+}
+
 }

+ 1 - 0
Userland/Libraries/LibWeb/WebDriver/Actions.h

@@ -129,5 +129,6 @@ ErrorOr<Vector<Vector<ActionObject>>, WebDriver::Error> extract_an_action_sequen
 JS::NonnullGCPtr<JS::Cell> dispatch_actions(InputState&, Vector<Vector<ActionObject>>, HTML::BrowsingContext&, ActionsOptions, OnActionsComplete);
 ErrorOr<void, WebDriver::Error> dispatch_tick_actions(InputState&, ReadonlySpan<ActionObject>, AK::Duration, HTML::BrowsingContext&, ActionsOptions const&);
 JS::NonnullGCPtr<JS::Cell> dispatch_list_of_actions(InputState&, Vector<ActionObject>, HTML::BrowsingContext&, ActionsOptions, OnActionsComplete);
+JS::NonnullGCPtr<JS::Cell> dispatch_actions_for_a_string(Web::WebDriver::InputState&, String const& input_id, Web::WebDriver::InputSource&, StringView text, Web::HTML::BrowsingContext&, Web::WebDriver::OnActionsComplete);
 
 }

+ 36 - 0
Userland/Libraries/LibWeb/WebDriver/ElementReference.cpp

@@ -10,6 +10,8 @@
 #include <LibWeb/DOM/ShadowRoot.h>
 #include <LibWeb/Geometry/DOMRect.h>
 #include <LibWeb/Geometry/DOMRectList.h>
+#include <LibWeb/HTML/HTMLBodyElement.h>
+#include <LibWeb/HTML/HTMLInputElement.h>
 #include <LibWeb/WebDriver/ElementReference.h>
 
 namespace Web::WebDriver {
@@ -123,6 +125,40 @@ bool is_element_stale(Web::DOM::Node const& element)
     return !element.document().is_active() || !element.is_connected();
 }
 
+// https://w3c.github.io/webdriver/#dfn-keyboard-interactable
+bool is_element_keyboard_interactable(Web::DOM::Element const& element)
+{
+    // A keyboard-interactable element is any element that has a focusable area, is a body element, or is the document element.
+    return element.is_focusable() || is<HTML::HTMLBodyElement>(element) || element.is_document_element();
+}
+
+// https://w3c.github.io/webdriver/#dfn-non-typeable-form-control
+bool is_element_non_typeable_form_control(Web::DOM::Element const& element)
+{
+    // A non-typeable form control is an input element whose type attribute state causes the primary input mechanism not
+    // to be through means of a keyboard, whether virtual or physical.
+    if (!is<HTML::HTMLInputElement>(element))
+        return false;
+
+    auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
+
+    switch (input_element.type_state()) {
+    case HTML::HTMLInputElement::TypeAttributeState::Hidden:
+    case HTML::HTMLInputElement::TypeAttributeState::Range:
+    case HTML::HTMLInputElement::TypeAttributeState::Color:
+    case HTML::HTMLInputElement::TypeAttributeState::Checkbox:
+    case HTML::HTMLInputElement::TypeAttributeState::RadioButton:
+    case HTML::HTMLInputElement::TypeAttributeState::FileUpload:
+    case HTML::HTMLInputElement::TypeAttributeState::SubmitButton:
+    case HTML::HTMLInputElement::TypeAttributeState::ImageButton:
+    case HTML::HTMLInputElement::TypeAttributeState::ResetButton:
+    case HTML::HTMLInputElement::TypeAttributeState::Button:
+        return true;
+    default:
+        return false;
+    }
+}
+
 // https://w3c.github.io/webdriver/#dfn-get-or-create-a-shadow-root-reference
 ByteString get_or_create_a_shadow_root_reference(Web::DOM::ShadowRoot const& shadow_root)
 {

+ 3 - 0
Userland/Libraries/LibWeb/WebDriver/ElementReference.h

@@ -23,7 +23,10 @@ ByteString extract_web_element_reference(JsonObject const&);
 bool represents_a_web_element(JsonValue const& value);
 ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, Web::WebDriver::Error> get_web_element_origin(StringView origin);
 ErrorOr<Web::DOM::Element*, Web::WebDriver::Error> get_known_connected_element(StringView element_id);
+
 bool is_element_stale(Web::DOM::Node const& element);
+bool is_element_keyboard_interactable(Web::DOM::Element const&);
+bool is_element_non_typeable_form_control(Web::DOM::Element const&);
 
 ByteString get_or_create_a_shadow_root_reference(Web::DOM::ShadowRoot const& shadow_root);
 JsonObject shadow_root_reference_object(Web::DOM::ShadowRoot const& shadow_root);

+ 162 - 123
Userland/Services/WebContent/WebDriverConnection.cpp

@@ -10,8 +10,10 @@
 
 #include <AK/JsonObject.h>
 #include <AK/JsonValue.h>
+#include <AK/LexicalPath.h>
 #include <AK/Time.h>
 #include <AK/Vector.h>
+#include <LibCore/File.h>
 #include <LibJS/Runtime/JSONObject.h>
 #include <LibJS/Runtime/Value.h>
 #include <LibWeb/CSS/CSSStyleValue.h>
@@ -25,6 +27,7 @@
 #include <LibWeb/DOM/Event.h>
 #include <LibWeb/DOM/NodeFilter.h>
 #include <LibWeb/DOM/NodeIterator.h>
+#include <LibWeb/DOM/Position.h>
 #include <LibWeb/DOM/ShadowRoot.h>
 #include <LibWeb/Geometry/DOMRect.h>
 #include <LibWeb/HTML/AttributeNames.h>
@@ -38,7 +41,9 @@
 #include <LibWeb/HTML/HTMLOptGroupElement.h>
 #include <LibWeb/HTML/HTMLOptionElement.h>
 #include <LibWeb/HTML/HTMLSelectElement.h>
+#include <LibWeb/HTML/HTMLTextAreaElement.h>
 #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
+#include <LibWeb/HTML/SelectedFile.h>
 #include <LibWeb/HTML/TraversableNavigable.h>
 #include <LibWeb/Page/Page.h>
 #include <LibWeb/Platform/EventLoopPlugin.h>
@@ -1535,92 +1540,27 @@ Messages::WebDriverClient::ElementClearResponse WebDriverConnection::element_cle
 // 12.5.3 Element Send Keys, https://w3c.github.io/webdriver/#dfn-element-send-keys
 Messages::WebDriverClient::ElementSendKeysResponse WebDriverConnection::element_send_keys(String const& element_id, JsonValue const& payload)
 {
-    dbgln("FIXME: WebDriverConnection::element_send_keys({}, {})", element_id, payload);
+    // 1. Let text be the result of getting a property named "text" from parameters.
+    // 2. If text is not a String, return an error with error code invalid argument.
+    auto text = TRY(Web::WebDriver::get_property(payload, "text"sv));
 
-    // To clear the modifier key state given input state, input id, source, undo actions, and browsing context:
-    {
-        // FIXME: 1. If source is not a key input source return error with error code invalid argument.
-        // FIXME: 2. Let actions options be a new actions options with the is element origin steps set to represents a web element, and the get element origin steps set to get a WebElement origin.
-        // FIXME: 3. For each entry key in the lexically sorted keys of undo actions:
-        {
-            // FIXME: 1. Let action be the value of undo actions equal to the key entry key.
-            // FIXME: 2. If action is not an action object with type "key" and subtype "keyUp", return error with error code invalid argument.
-            // FIXME: 3. Let actions be the list «action»
-            // FIXME: 4. Dispatch a list of actions with input state, actions, browsing context, and actions options.
-        }
-    }
+    // 3. If session's current browsing context is no longer open, return error with error code no such window.
+    TRY(ensure_current_browsing_context_is_open());
 
-    // To dispatch the events for a typeable string given input state, input id, source, text, and browsing context:
-    {
-        // FIXME: 1. Let actions options be a new actions options with the is element origin steps set to represents a web element, and the get element origin steps set to get a WebElement origin.
-        // FIXME: 2. For each char of text:
-        {
-            // FIXME: 1. Let global key state be the result of get the global key state with input state.
-            // FIXME: 2. Let actions be the list «action».
-            // FIXME: 3. Dispatch a list of actions with input state, actions, and browsing context.
-        }
-        // FIXME: 3. If char is not a shifted character and the shifted state of source is true:
-        {
-            // FIXME: 1. Let action be an action object constructed with input id, "key", and "keyUp", and set its value property to U+E008 ("left shift").
-            // FIXME: 2. Let tick actions be the list «action».
-            // FIXME: 3. Dispatch a list of actions with input state, actions, browsing context, and actions options.
-        }
-        // FIXME: 4. Let keydown action be an action object constructed with arguments input id, "key", and "keyDown".
-        // FIXME: 5. Set the value property of keydown action to char.
-        // FIXME: 6. Let keyup action be a copy of keydown action with the subtype property changed to "keyUp".
-        // FIXME: 7. Let actions be the list «keydown action, keyup action».
-        // FIXME: 8. Dispatch a list of actions with input state, actions, browsing context, and actions options.
-    }
+    // 4. Try to handle any user prompts with session.
+    TRY(handle_any_user_prompts());
 
-    // To dispatch actions for a string given input state, input id, source, text, browsing context, and actions options:
-    {
-        // FIXME: 1. Let clusters be an array created by breaking text into extended grapheme clusters.
-        // FIXME: 2. Let undo actions be an empty map.
-        // FIXME: 3. Let current typeable text be an empty list.
-        // FIXME: 4. For each cluster corresponding to an indexed property in clusters run the substeps of the first matching statement:
-        {
-            // -> cluster is the null key
-            {
-                // FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context. Empty current typeable text.
-                // FIXME: 2. Try to clear the modifier key state with input state, input id, source, undo actions and browsing context.
-                // FIXME: 3. Clear undo actions.
-            }
-            // -> cluster is a modifier key
-            {
-                // FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context.
-                // FIXME: 2. Emptycurrent typeable text.
-                // FIXME: 3. Let keydown action be an action object constructed with arguments input id, "key", and "keyDown".
-                // FIXME: 4. Set the value property of keydown action to cluster.
-                // FIXME: 5. Let actions be the list «keydown action»
-                // FIXME: 6. Dispatch a list of actions with input state, actions, browsing context, and actions options.
-                // FIXME: 7. Add an entry to undo actions with key cluster and value being a copy of keydown action with the subtype property modified to "keyUp".
-            }
-            // -> cluster is typeable
-            {
-                // Append cluster to current typeable text.
-            }
-            // -> otherwise
-            {
-                // FIXME: 1. Dispatch the events for a typeable string with input state, input id, source, current typeable text, and browsing context.
-                // FIXME: 2. Empty current typeable text.
-                // FIXME: 3. Dispatch a composition event with arguments "compositionstart", undefined, and browsing context.
-                // FIXME: 4. Dispatch a composition event with arguments "compositionupdate", cluster, and browsing context.
-                // FIXME: 5. Dispatch a composition event with arguments "compositionend", cluster, and browsing context.
-            }
-        }
-        // FIXME: 5. Dispatch the events for a typeable string with input state, input id and source, current typeable text, and browsing context.
-        // FIXME: 6. Try to clear the modifier key state with input state, input id, source, undo actions, and browsing context.
-    }
+    // 5. Let element be the result of trying to get a known element with session and URL variables[element id].
+    auto* element = TRY(Web::WebDriver::get_known_connected_element(element_id));
+
+    // 6. Let file be true if element is input element in the file upload state, or false otherwise.
+    auto file = is<Web::HTML::HTMLInputElement>(*element) && static_cast<Web::HTML::HTMLInputElement&>(*element).type_state() == Web::HTML::HTMLInputElement::TypeAttributeState::FileUpload;
+
+    // 7. If file is false or the session's strict file interactability, is true run the following substeps:
+    if (!file || m_strict_file_interactability) {
+        // 1. Scroll into view the element.
+        TRY(scroll_element_into_view(*element));
 
-    // FIXME: 1. Let text be the result of getting a property named "text" from parameters.
-    // FIXME: 2. If text is not a String, return an error with error code invalid argument.
-    // FIXME: 3. If session's current browsing context is no longer open, return error with error code no such window.
-    // FIXME: 4. Try to handle any user prompts with session.
-    // FIXME: 5. Let element be the result of trying to get a known element with session and URL variables[element id].
-    // FIXME: 6. Let file be true if element is input element in the file upload state, or false otherwise.
-    // FIXME: 7. If file is false or the session's strict file interactability, is true run the following substeps:
-    {
-        // FIXME: 1. Scroll into view the element.
         // FIXME: 2. Let timeout be session's session timeouts' implicit wait timeout.
         // FIXME: 3. Let timer be a new timer.
         // FIXME: 4. If timeout is not null:
@@ -1628,51 +1568,150 @@ Messages::WebDriverClient::ElementSendKeysResponse WebDriverConnection::element_
             // FIXME: 1. Start the timer with timer and timeout.
         }
         // FIXME: 5. Wait for element to become keyboard-interactable, or timer's timeout fired flag to be set, whichever occurs first.
-        // FIXME: 6. If element is not keyboard-interactable, return error with error code element not interactable.
-        // FIXME: 7. If element is not the active element run the focusing steps for the element.
+
+        // 6. If element is not keyboard-interactable, return error with error code element not interactable.
+        if (!Web::WebDriver::is_element_keyboard_interactable(*element))
+            return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ElementNotInteractable, "Element is not keyboard-interactable"sv);
+
+        // 7. If element is not the active element run the focusing steps for the element.
+        if (!element->is_active())
+            Web::HTML::run_focusing_steps(element);
     }
-    // FIXME: 8. Run the substeps of the first matching condition:
-    {
-        // -> file is true
-        {
-            // FIXME: 1. Let files be the result of splitting text on the newline (\n) character.
-            // FIXME: 2. If files is of 0 length, return an error with error code invalid argument.
-            // FIXME: 3. Let multiple equal the result of calling hasAttribute() with "multiple" on element.
-            // FIXME: 4. if multiple is false and the length of files is not equal to 1, return an error with error code invalid argument.
-            // FIXME: 5. Verify that each file given by the user exists. If any do not, return error with error code invalid argument.
-            // FIXME: 6. Complete implementation specific steps equivalent to setting the selected files on the input element. If multiple is true files are be appended to element's selected files.
-            // FIXME: 7. Fire these events in order on element:
-            //     FIXME: 1. input
-            //     FIXME: 2. change
-            // FIXME: 8. Return success with data null.
-        }
-        // -> element is a non-typeable form control
-        {
-            // FIXME: 1. If element does not have an own property named value return an error with error code element not interactable
-            // FIXME: 2. If element is not mutable return an error with error code element not interactable.
-            // FIXME: 3. Set a property value to text on element.
-            // FIXME: 4. If element is suffering from bad input return an error with error code invalid argument.
-            // FIXME: 5. Return success with data null.
-        }
-        // -> elementis content editable
-        {
-            // If element does not currently have focus, set the text insertion caret after any child content.
+
+    // 8. Run the substeps of the first matching condition:
+
+    // -> file is true
+    if (file) {
+        auto& input_element = static_cast<Web::HTML::HTMLInputElement&>(*element);
+
+        // 1. Let files be the result of splitting text on the newline (\n) character.
+        auto files = text.split('\n');
+
+        // 2. If files is of 0 length, return an error with error code invalid argument.
+        if (files.is_empty())
+            return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "File list is empty"sv);
+
+        // 3. Let multiple equal the result of calling hasAttribute() with "multiple" on element.
+        auto multiple = input_element.has_attribute(Web::HTML::AttributeNames::multiple);
+
+        // 4. if multiple is false and the length of files is not equal to 1, return an error with error code invalid argument.
+        if (!multiple && files.size() != 1)
+            return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Element does not accept multiple files"sv);
+
+        // 5. Verify that each file given by the user exists. If any do not, return error with error code invalid argument.
+        // 6. Complete implementation specific steps equivalent to setting the selected files on the input element. If
+        //    multiple is true files are be appended to element's selected files.
+        auto create_selected_file = [](auto const& path) -> ErrorOr<Web::HTML::SelectedFile> {
+            auto file = TRY(Core::File::open(path, Core::File::OpenMode::Read));
+            auto contents = TRY(file->read_until_eof());
+
+            return Web::HTML::SelectedFile { LexicalPath::basename(path), move(contents) };
+        };
+
+        Vector<Web::HTML::SelectedFile> selected_files;
+        selected_files.ensure_capacity(files.size());
+
+        for (auto const& path : files) {
+            auto selected_file = create_selected_file(path);
+            if (selected_file.is_error())
+                return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, ByteString::formatted("'{}' does not exist", path));
+
+            selected_files.unchecked_append(selected_file.release_value());
         }
-        // -> otherwise
-        {
-            // FIXME: 1. If element does not currently have focus, let current text length be the length of element's API value.
-            // FIXME: 2. Set the text insertion caret using set selection range using current text length for both the start and end parameters.
+
+        input_element.did_select_files(selected_files, Web::HTML::HTMLInputElement::MultipleHandling::Append);
+
+        // 7. Fire these events in order on element:
+        //     1. input
+        //     2. change
+        // NOTE: These events are fired by `did_select_files` as an element task. So instead of firing them here, we spin
+        //       the event loop once before informing the client that the action is complete.
+        Web::HTML::queue_a_task(Web::HTML::Task::Source::Unspecified, nullptr, nullptr, JS::create_heap_function(current_browsing_context().heap(), [this]() {
+            async_actions_performed(JsonValue {});
+        }));
+
+        // 8. Return success with data null.
+        return JsonValue {};
+    }
+    // -> element is a non-typeable form control
+    else if (Web::WebDriver::is_element_non_typeable_form_control(*element)) {
+        // 1. If element does not have an own property named value return an error with error code element not interactable
+        if (!is<Web::HTML::HTMLInputElement>(*element))
+            return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ElementNotInteractable, "Element does not have a property named 'value'"sv);
+
+        auto& input_element = static_cast<Web::HTML::HTMLInputElement&>(*element);
+
+        // 2. If element is not mutable return an error with error code element not interactable.
+        if (input_element.is_mutable())
+            return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ElementNotInteractable, "Element is immutable"sv);
+
+        // 3. Set a property value to text on element.
+        MUST(input_element.set_value(MUST(String::from_byte_string(text))));
+
+        // FIXME: 4. If element is suffering from bad input return an error with error code invalid argument.
+
+        // 5. Return success with data null.
+        async_actions_performed(JsonValue {});
+        return JsonValue {};
+    }
+    // -> element is content editable
+    else if (is<Web::HTML::HTMLElement>(*element) && static_cast<Web::HTML::HTMLElement&>(*element).is_content_editable()) {
+        // If element does not currently have focus, set the text insertion caret after any child content.
+        if (!element->is_focused())
+            element->document().set_cursor_position(Web::DOM::Position::create(element->realm(), *element, element->length()));
+    }
+    // -> otherwise
+    else if (is<Web::HTML::FormAssociatedTextControlElement>(*element)) {
+        Optional<Web::HTML::FormAssociatedTextControlElement&> target;
+
+        if (is<Web::HTML::HTMLInputElement>(*element))
+            target = static_cast<Web::HTML::HTMLInputElement&>(*element);
+        else if (is<Web::HTML::HTMLTextAreaElement>(*element))
+            target = static_cast<Web::HTML::HTMLTextAreaElement&>(*element);
+
+        // NOTE: The spec doesn't dictate this, but these steps only make sense for form-associated text elements.
+        if (target.has_value()) {
+            // 1. If element does not currently have focus, let current text length be the length of element's API value.
+            Optional<Web::WebIDL::UnsignedLong> current_text_length;
+
+            if (element->is_focused()) {
+                auto api_value = target->relevant_value();
+
+                // FIXME: This should be a UTF-16 code unit length, but `set_the_selection_range` is also currently
+                //        implemented in terms of code point length.
+                current_text_length = api_value.code_points().length();
+            }
+
+            // 2. Set the text insertion caret using set selection range using current text length for both the start
+            //    and end parameters.
+            (void)target->set_selection_range(current_text_length, current_text_length, {});
         }
     }
-    // FIXME: 9. Let input state be the result of get the input state with session and session's current top-level browsing context.
-    // FIXME: 10. Let input id be a the result of generating a UUID.
-    // FIXME: 11. Let source be the result of create an input source with input state, and "key".
-    // FIXME: 12. Add an input source with input state, input id and source.
-    // FIXME: 13. Dispatch actions for a string with arguments input state, input id, and source, text, and session's current browsing context.
-    // FIXME: 14. Remove an input source with input state and input id.
-    // FIXME: 15. Return success with data null.
-
-    return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnsupportedOperation, "send keys not implemented"sv);
+
+    // 9. Let input state be the result of get the input state with session and session's current top-level browsing context.
+    auto& input_state = Web::WebDriver::get_input_state(*current_top_level_browsing_context());
+
+    // 10. Let input id be a the result of generating a UUID.
+    auto input_id = MUST(Web::Crypto::generate_random_uuid());
+
+    // 11. Let source be the result of create an input source with input state, and "key".
+    auto source = Web::WebDriver::create_input_source(input_state, Web::WebDriver::InputSourceType::Key, {});
+
+    // 12. Add an input source with input state, input id and source.
+    Web::WebDriver::add_input_source(input_state, input_id, move(source));
+
+    // 13. Dispatch actions for a string with arguments input state, input id, and source, text, and session's current browsing context.
+    m_action_executor = Web::WebDriver::dispatch_actions_for_a_string(input_state, input_id, source, text, current_browsing_context(), JS::create_heap_function(current_browsing_context().heap(), [this, &input_state, input_id](Web::WebDriver::Response result) {
+        m_action_executor = nullptr;
+
+        // 14. Remove an input source with input state and input id.
+        Web::WebDriver::remove_input_source(input_state, input_id);
+
+        async_actions_performed(move(result));
+    }));
+
+    // 15. Return success with data null.
+    return JsonValue {};
 }
 
 // 13.1 Get Page Source, https://w3c.github.io/webdriver/#dfn-get-page-source

+ 1 - 1
Userland/Services/WebDriver/Client.cpp

@@ -606,7 +606,7 @@ Web::WebDriver::Response Client::element_send_keys(Web::WebDriver::Parameters pa
 {
     dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/element/<element_id>/value");
     auto session = TRY(find_session_with_id(parameters[0]));
-    return session->web_content_connection().element_send_keys(move(parameters[1]), move(payload));
+    return session->element_send_keys(move(parameters[1]), move(payload));
 }
 
 // 13.1 Get Page Source, https://w3c.github.io/webdriver/#dfn-get-page-source

+ 7 - 0
Userland/Services/WebDriver/Session.cpp

@@ -220,6 +220,13 @@ Web::WebDriver::Response Session::element_click(String element_id) const
     });
 }
 
+Web::WebDriver::Response Session::element_send_keys(String element_id, JsonValue payload) const
+{
+    return perform_async_action(web_content_connection().on_actions_performed, [&]() {
+        return web_content_connection().element_send_keys(move(element_id), move(payload));
+    });
+}
+
 Web::WebDriver::Response Session::perform_actions(JsonValue payload) const
 {
     return perform_async_action(web_content_connection().on_actions_performed, [&]() {

+ 1 - 0
Userland/Services/WebDriver/Session.h

@@ -62,6 +62,7 @@ public:
     Web::WebDriver::Response execute_script(JsonValue, ScriptMode) const;
 
     Web::WebDriver::Response element_click(String) const;
+    Web::WebDriver::Response element_send_keys(String, JsonValue) const;
     Web::WebDriver::Response perform_actions(JsonValue) const;
 
 private: