[ChromeDriver] Key type actions for the actions API
This CL contains the key type actions. Follow up CLs for tests and
pointer/pause actions
spec: https://w3c.github.io/webdriver/webdriver-spec.html#actions
Bug: chromedriver:1897
Change-Id: I135df58d97a45494e51a5ac09dfce021f7d094d3
Reviewed-on: https://chromium-review.googlesource.com/692599
Commit-Queue: Jonathon Kereliuk <kereliuk@chromium.org>
Reviewed-by: John Chen <johnchen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#508958}
diff --git a/chrome/test/chromedriver/chrome/status.cc b/chrome/test/chromedriver/chrome/status.cc
index d970eca..dc12c12 100644
--- a/chrome/test/chromedriver/chrome/status.cc
+++ b/chrome/test/chromedriver/chrome/status.cc
@@ -32,6 +32,8 @@
return "unknown error";
case kInvalidArgument:
return "invalid argument";
+ case kMoveTargetOutOfBounds:
+ return "move target out of bounds";
case kElementNotInteractable:
return "element not interactable";
case kUnsupportedOperation:
diff --git a/chrome/test/chromedriver/chrome/status.h b/chrome/test/chromedriver/chrome/status.h
index 27b9ca4a..bf5c3f3 100644
--- a/chrome/test/chromedriver/chrome/status.h
+++ b/chrome/test/chromedriver/chrome/status.h
@@ -22,6 +22,7 @@
kElementNotInteractable = 15,
kUnsupportedOperation = 16,
kJavaScriptError = 17,
+ kMoveTargetOutOfBounds = 18,
kXPathLookupError = 19,
kUnableToSetCookie = 20,
kTimeout = 21,
diff --git a/chrome/test/chromedriver/server/http_handler.cc b/chrome/test/chromedriver/server/http_handler.cc
index 8f166cc4..f6d254e 100644
--- a/chrome/test/chromedriver/server/http_handler.cc
+++ b/chrome/test/chromedriver/server/http_handler.cc
@@ -493,9 +493,9 @@
CommandMapping(kGet, "session/:sessionId/log/types",
WrapToCommand("GetLogTypes",
base::Bind(&ExecuteGetAvailableLogTypes))),
- CommandMapping(kPost, "session/:sessionId/actions",
- WrapToCommand("PerformActions",
- base::Bind(&ExecuteUnimplementedCommand))),
+ CommandMapping(
+ kPost, "session/:sessionId/actions",
+ WrapToCommand("PerformActions", base::Bind(&ExecutePerformActions))),
CommandMapping(kDelete, "session/:sessionId/actions",
WrapToCommand("DeleteActions",
base::Bind(&ExecuteUnimplementedCommand))),
@@ -724,6 +724,10 @@
case kJavaScriptError:
response.reset(new net::HttpServerResponseInfo(net::HTTP_BAD_REQUEST));
break;
+ case kMoveTargetOutOfBounds:
+ response.reset(
+ new net::HttpServerResponseInfo(net::HTTP_INTERNAL_SERVER_ERROR));
+ break;
case kNoSuchCookie:
response.reset(new net::HttpServerResponseInfo(net::HTTP_NOT_FOUND));
break;
diff --git a/chrome/test/chromedriver/session.h b/chrome/test/chromedriver/session.h
index da05fe632..2b59d22 100644
--- a/chrome/test/chromedriver/session.h
+++ b/chrome/test/chromedriver/session.h
@@ -11,6 +11,7 @@
#include <vector>
#include "base/time/time.h"
+#include "base/values.h"
#include "chrome/test/chromedriver/basic_types.h"
#include "chrome/test/chromedriver/chrome/device_metrics.h"
#include "chrome/test/chromedriver/chrome/geoposition.h"
@@ -68,6 +69,12 @@
std::unique_ptr<Chrome> chrome;
std::string window;
int sticky_modifiers;
+ // List of input sources for each active input. Everytime a new input source
+ // is added, there must be a corresponding entry made in input_state_table.
+ std::unique_ptr<base::ListValue> active_input_sources;
+ // Map between input id and input source state for the corresponding input
+ // source. One entry for each item in active_input_sources
+ std::unique_ptr<base::DictionaryValue> input_state_table;
// List of |FrameInfo|s for each frame to the current target frame from the
// first frame element in the root document. If target frame is window.top,
// this list will be empty.
diff --git a/chrome/test/chromedriver/window_commands.cc b/chrome/test/chromedriver/window_commands.cc
index bac9811..20fb925 100644
--- a/chrome/test/chromedriver/window_commands.cc
+++ b/chrome/test/chromedriver/window_commands.cc
@@ -14,6 +14,7 @@
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/values.h"
@@ -31,6 +32,8 @@
#include "chrome/test/chromedriver/chrome/ui_events.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/element_util.h"
+#include "chrome/test/chromedriver/key_converter.h"
+#include "chrome/test/chromedriver/keycode_text_conversion.h"
#include "chrome/test/chromedriver/net/timeout.h"
#include "chrome/test/chromedriver/session.h"
#include "chrome/test/chromedriver/util.h"
@@ -47,6 +50,9 @@
// Defaults to 20 years into the future when adding a cookie.
const double kDefaultCookieExpiryTime = 20*365*24*60*60;
+// for pointer actions
+enum class PointerActionType { NOT_INITIALIZED, PRESS, MOVE, RELEASE, IDLE };
+
Status GetMouseButton(const base::DictionaryValue& params,
MouseButton* button) {
int button_num;
@@ -745,6 +751,234 @@
Status ProcessInputActionSequence(Session* session,
const base::DictionaryValue* action_sequence,
std::unique_ptr<base::ListValue>* result) {
+ std::string id;
+ std::string type;
+ const base::DictionaryValue* source;
+ const base::DictionaryValue* parameters;
+ std::string pointer_type = "mouse";
+
+ if (!action_sequence->GetString("type", &type) ||
+ ((type != "key") && (type != "pointer") && (type != "none"))) {
+ return Status(
+ kInvalidArgument,
+ "'type' must be one of the strings 'key', 'pointer' or 'none'");
+ }
+
+ if (!action_sequence->GetString("id", &id))
+ return Status(kInvalidArgument, "'id' must be a string");
+
+ if (type == "pointer") {
+ if (action_sequence->GetDictionary("parameters", ¶meters)) {
+ // error check arguments
+ if (parameters->GetString("pointerType", &pointer_type) &&
+ (pointer_type != "mouse" && pointer_type != "pen" &&
+ pointer_type != "touch"))
+ return Status(kInvalidArgument,
+ "'pointerType' must be one of mouse, pen or touch");
+ }
+ }
+
+ bool found = false;
+ for (size_t i = 0; i < session->active_input_sources->GetSize(); i++) {
+ session->active_input_sources->GetDictionary(i, &source);
+ DCHECK(source);
+
+ std::string source_id;
+ source->GetString("id", &source_id);
+ if (source_id == id) {
+ found = true;
+ if (type == "pointer") {
+ std::string source_pointer_type;
+ if (!source->GetString("pointerType", &source_pointer_type) ||
+ pointer_type != source_pointer_type) {
+ return Status(kInvalidArgument,
+ "'pointerType' must be a string that matches sources "
+ "pointer type");
+ }
+ }
+ std::string source_type;
+ source->GetString("type", &source_type);
+ if (source_type != type) {
+ return Status(kInvalidArgument,
+ "input state with same id has a different type");
+ }
+ break;
+ }
+ }
+
+ // if we found no matching active input source
+ base::DictionaryValue tmp_source;
+ if (!found) {
+ // create input source
+ tmp_source.SetString("id", id);
+ tmp_source.SetString("type", type);
+ if (type == "pointer") {
+ tmp_source.SetString("pointerType", pointer_type);
+ }
+
+ session->active_input_sources->Append(
+ base::MakeUnique<base::DictionaryValue>(std::move(tmp_source)));
+
+ base::DictionaryValue tmp_state;
+ if (type == "key") {
+ std::unique_ptr<base::ListValue> pressed(new base::ListValue);
+ bool alt = false;
+ bool shift = false;
+ bool ctrl = false;
+ bool meta = false;
+
+ tmp_state.SetList("pressed", std::move(pressed));
+ tmp_state.SetBoolean("alt", alt);
+ tmp_state.SetBoolean("shift", shift);
+ tmp_state.SetBoolean("ctrl", ctrl);
+ tmp_state.SetBoolean("meta", meta);
+ } else if (type == "pointer") {
+ std::unique_ptr<base::ListValue> pressed(new base::ListValue);
+ int x = 0;
+ int y = 0;
+
+ tmp_state.SetList("pressed", std::move(pressed));
+ tmp_state.SetString("subtype", pointer_type);
+
+ tmp_state.SetInteger("x", x);
+ tmp_state.SetInteger("y", y);
+ }
+ session->input_state_table->SetDictionary(
+ id, base::MakeUnique<base::DictionaryValue>(std::move(tmp_state)));
+ }
+
+ const base::ListValue* actions;
+ if (!action_sequence->GetList("actions", &actions)) {
+ return Status(kInvalidArgument, "actions must be an array");
+ }
+
+ std::unique_ptr<base::ListValue> ret(new base::ListValue);
+ for (size_t i = 0; i < actions->GetSize(); i++) {
+ std::unique_ptr<base::DictionaryValue> action(new base::DictionaryValue());
+ const base::DictionaryValue* action_item;
+ if (!actions->GetDictionary(i, &action_item))
+ return Status(
+ kInvalidArgument,
+ "each argument in the action sequence must be a dictionary");
+
+ if (type == "none") {
+ // process null action
+ std::string subtype;
+ if (!action_item->GetString("type", &subtype) || subtype != "pause")
+ return Status(kInvalidArgument,
+ "type of action must be the string 'pause'");
+
+ action->SetString("id", id);
+ action->SetString("type", "none");
+ action->SetString("subtype", subtype);
+
+ int duration;
+ if (action_item->GetInteger("duration", &duration)) {
+ if (duration < 0)
+ return Status(kInvalidArgument,
+ "duration must be a non-negative int");
+ action->SetInteger("duration", duration);
+ }
+ } else if (type == "key") {
+ // process key action
+ std::string subtype;
+ if (!action_item->GetString("type", &subtype) ||
+ (subtype != "keyUp" && subtype != "keyDown" && subtype != "pause"))
+ return Status(
+ kInvalidArgument,
+ "type of action must be the string 'keyUp', 'keyDown' or 'pause'");
+
+ action->SetString("id", id);
+ action->SetString("type", "key");
+ action->SetString("subtype", subtype);
+
+ if (subtype == "pause") {
+ int duration;
+ if (action_item->GetInteger("duration", &duration)) {
+ if (duration < 0)
+ return Status(kInvalidArgument,
+ "duration must be a non-negative int");
+ action->SetInteger("duration", duration);
+ }
+ }
+ std::string key;
+ // TODO: check if key is a single unicode code point
+ if (!action_item->GetString("value", &key)) {
+ return Status(kInvalidArgument,
+ "'value' must be a single unicode point");
+ }
+ action->SetString("value", key);
+ } else if (type == "pointer") {
+ std::string subtype;
+ if (!action_item->GetString("type", &subtype) ||
+ (subtype != "pointerUp" && subtype != "pointerDown" &&
+ subtype != "pointerMove" && subtype != "pointerCancel" &&
+ subtype != "pause"))
+ return Status(kInvalidArgument,
+ "type of action must be the string 'pointerUp', "
+ "'pointerDown', 'pointerMove' or 'pause'");
+
+ action->SetString("id", id);
+ action->SetString("type", "pointer");
+ action->SetString("subtype", subtype);
+
+ if (subtype == "pause") {
+ int duration;
+ if (action_item->GetInteger("duration", &duration)) {
+ if (duration < 0)
+ return Status(kInvalidArgument,
+ "duration must be a non-negative int");
+ action->SetInteger("duration", duration);
+ }
+ }
+
+ action->SetString("pointerType", pointer_type);
+ if (subtype == "pointerUp" || subtype == "pointerDown") {
+ int button;
+ if (!action_item->GetInteger("button", &button) || button < 0)
+ return Status(kInvalidArgument,
+ "'button' must be a non-negative int");
+ action->SetInteger("button", button);
+ if (subtype == "pointerDown") {
+ int x;
+ if (!action_item->GetInteger("x", &x))
+ return Status(kInvalidArgument, "'x' must be an integer");
+ int y;
+ if (!action_item->GetInteger("y", &y))
+ return Status(kInvalidArgument, "'y' must be an integer");
+
+ action->SetInteger("x", x);
+ action->SetInteger("y", y);
+ }
+ } else {
+ // pointerMove
+ int duration;
+ if (!action_item->GetInteger("duration", &duration) || duration < 0)
+ return Status(kInvalidArgument,
+ "'duration' must be a non-negative int");
+
+ std::string origin;
+ if (!action_item->GetString("origin", &origin))
+ origin = "viewport";
+ if (origin != "viewport" && origin != "pointer")
+ return Status(kInvalidArgument, "'origin' must be a string");
+
+ action->SetString("origin", origin);
+
+ int x;
+ if (!action_item->GetInteger("x", &x))
+ return Status(kInvalidArgument, "'x' must be an integer");
+ int y;
+ if (!action_item->GetInteger("y", &y))
+ return Status(kInvalidArgument, "'y' must be an integer");
+
+ action->SetInteger("x", x);
+ action->SetInteger("y", y);
+ }
+ }
+ ret->Append(std::move(action));
+ }
+ *result = std::move(ret);
return Status(kOk);
}
@@ -763,23 +997,117 @@
if (!params.GetList("actions", &actions))
return Status(kInvalidArgument, "'actions' must be an array");
+ // the processed actions
base::ListValue actions_by_tick;
- std::unique_ptr<base::ListValue> input_source_actions(new base::ListValue());
+ // the type of each action list in actions_by_tick
+ std::list<std::string> action_list_types;
+
for (size_t i = 0; i < actions->GetSize(); i++) {
+ std::unique_ptr<base::ListValue> input_source_actions(
+ new base::ListValue());
// proccess input action sequence
const base::DictionaryValue* action_sequence;
if (!actions->GetDictionary(i, &action_sequence))
return Status(kInvalidArgument, "each argument must be a dictionary");
+ std::string type;
+ if (!action_sequence->GetString("type", &type) ||
+ ((type != "key") && (type != "pointer") && (type != "none"))) {
+ return Status(
+ kInvalidArgument,
+ "'type' must be one of the strings 'key', 'pointer' or 'none'");
+ }
+ action_list_types.push_back(type);
+
Status status = ProcessInputActionSequence(session, action_sequence,
&input_source_actions);
- actions_by_tick.Append(std::move(input_source_actions));
if (status.IsError())
return Status(kInvalidArgument, status);
+
+ actions_by_tick.Append(std::move(input_source_actions));
}
- // TODO(kereliuk): dispatch actions
+ for (size_t i = 0; i < actions_by_tick.GetSize(); i++) {
+ // compute duration
+ int max_duration = 0;
+ int duration;
+ base::ListValue* action_sequence;
+ actions_by_tick.GetList(i, &action_sequence);
+ DCHECK(action_sequence);
+ for (size_t j = 0; j < action_sequence->GetSize(); j++) {
+ base::DictionaryValue* action;
+ if (!action_sequence->GetDictionary(i, &action))
+ return Status(kInvalidArgument, "each argument must be a dictionary");
+ if (action->GetInteger("duration", &duration) &&
+ duration > max_duration) {
+ max_duration = duration;
+ }
+ }
+ // get the type of the actions so we can dispatch all at once for that type
+ std::string type = action_list_types.back();
+ action_list_types.pop_back();
+
+ // pause only
+
+ // key actions
+ if (type == "key") {
+ KeyEventBuilder builder;
+ std::list<KeyEvent> key_events;
+ for (size_t j = 0; j < action_sequence->GetSize(); j++) {
+ base::DictionaryValue* action;
+ if (!action_sequence->GetDictionary(j, &action))
+ return Status(kInvalidArgument, "each argument must be a dictionary");
+ std::string subtype;
+ if (!action->GetString("subtype", &subtype))
+ return Status(kInvalidArgument, "'type' must be a string");
+
+ std::string id;
+ if (!action->GetString("id", &id))
+ return Status(kInvalidArgument, "id");
+
+ if (subtype == "pause") {
+ // TODO: handle this
+ } else {
+ base::DictionaryValue dispatch_params;
+ base::string16 raw_key;
+
+ if (!action->GetString("value", &raw_key))
+ return Status(kInvalidArgument, "value");
+
+ base::char16 key = raw_key[0];
+ // TODO: understand necessary_modifiers
+ int necessary_modifiers = 0;
+ ui::KeyboardCode key_code = ui::VKEY_UNKNOWN;
+ std::string error_msg;
+ ConvertCharToKeyCode(key, &key_code, &necessary_modifiers,
+ &error_msg);
+ if (!error_msg.empty())
+ return Status(kUnknownError, error_msg);
+
+ if (subtype == "keyDown")
+ key_events.push_back(builder.SetType(kKeyDownEventType)
+ ->SetText(base::UTF16ToUTF8(raw_key),
+ base::UTF16ToUTF8(raw_key))
+ ->SetKeyCode(key_code)
+ ->SetModifiers(0)
+ ->Build());
+ else if (subtype == "keyUp")
+ key_events.push_back(builder.SetType(kKeyUpEventType)
+ ->SetText(base::UTF16ToUTF8(raw_key),
+ base::UTF16ToUTF8(raw_key))
+ ->SetKeyCode(key_code)
+ ->SetModifiers(0)
+ ->Build());
+ }
+ }
+ Status status = web_view->DispatchKeyEvents(key_events);
+ if (status.IsError())
+ return status;
+ } else if (type == "pointer") {
+ // TODO:implement this
+ }
+ }
return Status(kOk);
}