blob: fba128fd31364bcaff2de234451c89ede58fc852 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/chromedriver/element_commands.h"
#include <stddef.h>
#include <cmath>
#include <list>
#include <vector>
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/test/chromedriver/basic_types.h"
#include "chrome/test/chromedriver/chrome/chrome.h"
#include "chrome/test/chromedriver/chrome/js.h"
#include "chrome/test/chromedriver/chrome/status.h"
#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/session.h"
#include "chrome/test/chromedriver/util.h"
#include "third_party/webdriver/atoms.h"
const int kFlickTouchEventsPerSecond = 30;
namespace {
Status FocusToElement(
Session* session,
WebView* web_view,
const std::string& element_id) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
bool is_displayed = false;
bool is_focused = false;
base::TimeTicks start_time = base::TimeTicks::Now();
while (true) {
status = IsElementDisplayed(
session, web_view, element_id, true, &is_displayed);
if (status.IsError())
return status;
if (is_displayed)
break;
status = IsElementFocused(session, web_view, element_id, &is_focused);
if (status.IsError())
return status;
if (is_focused)
break;
if (base::TimeTicks::Now() - start_time >= session->implicit_wait) {
return Status(kElementNotVisible);
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
}
bool is_enabled = false;
status = IsElementEnabled(session, web_view, element_id, &is_enabled);
if (status.IsError())
return status;
if (!is_enabled)
return Status(kInvalidElementState);
if (!is_focused) {
base::ListValue args;
args.Append(CreateElement(element_id));
std::unique_ptr<base::Value> result;
status = web_view->CallFunction(
session->GetCurrentFrameId(), kFocusScript, args, &result);
if (status.IsError())
return status;
}
return Status(kOk);
}
Status SendKeysToElement(
Session* session,
WebView* web_view,
const std::string& element_id,
const base::ListValue* key_list) {
Status status = FocusToElement(session, web_view, element_id);
if (status.IsError())
return Status(kElementNotInteractable);
return SendKeysOnWindow(web_view, key_list, true, &session->sticky_modifiers);
}
} // namespace
Status ExecuteElementCommand(
const ElementCommand& command,
Session* session,
WebView* web_view,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value,
Timeout* timeout) {
std::string id;
if (params.GetString("id", &id) || params.GetString("element", &id))
return command.Run(session, web_view, id, params, value);
return Status(kInvalidArgument, "element identifier must be a string");
}
Status ExecuteFindChildElement(int interval_ms,
Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
return FindElement(
interval_ms, true, &element_id, session, web_view, params, value);
}
Status ExecuteFindChildElements(int interval_ms,
Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
return FindElement(
interval_ms, false, &element_id, session, web_view, params, value);
}
Status ExecuteHoverOverElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
MouseEvent move_event(kMovedMouseEventType, kNoneMouseButton, location.x,
location.y, session->sticky_modifiers, 0, 0);
std::list<MouseEvent> events;
events.push_back(move_event);
status = web_view->DispatchMouseEvents(events, session->GetCurrentFrameId());
if (status.IsOk())
session->mouse_position = location;
return status;
}
Status ExecuteClickElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
std::string tag_name;
Status status = GetElementTagName(session, web_view, element_id, &tag_name);
if (status.IsError())
return status;
if (tag_name == "option") {
bool is_toggleable;
status = IsOptionElementTogglable(
session, web_view, element_id, &is_toggleable);
if (status.IsError())
return status;
if (is_toggleable)
return ToggleOptionElement(session, web_view, element_id);
else
return SetOptionElementSelected(session, web_view, element_id, true);
} else {
WebPoint location;
status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
std::list<MouseEvent> events;
events.push_back(MouseEvent(kMovedMouseEventType, kNoneMouseButton,
location.x, location.y,
session->sticky_modifiers, 0, 0));
events.push_back(MouseEvent(kPressedMouseEventType, kLeftMouseButton,
location.x, location.y,
session->sticky_modifiers, 0, 1));
events.push_back(MouseEvent(kReleasedMouseEventType, kLeftMouseButton,
location.x, location.y,
session->sticky_modifiers, 1, 1));
status =
web_view->DispatchMouseEvents(events, session->GetCurrentFrameId());
if (status.IsOk())
session->mouse_position = location;
return status;
}
}
Status ExecuteTouchSingleTap(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
if (!session->chrome->HasTouchScreen()) {
// TODO(samuong): remove this once we stop supporting M44.
std::list<TouchEvent> events;
events.push_back(
TouchEvent(kTouchStart, location.x, location.y));
events.push_back(
TouchEvent(kTouchEnd, location.x, location.y));
return web_view->DispatchTouchEvents(events);
}
return web_view->SynthesizeTapGesture(location.x, location.y, 1, false);
}
Status ExecuteTouchDoubleTap(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
if (!session->chrome->HasTouchScreen()) {
// TODO(samuong): remove this once we stop supporting M44.
return Status(kUnknownCommand, "Double tap command requires Chrome 44+");
}
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
return web_view->SynthesizeTapGesture(location.x, location.y, 2, false);
}
Status ExecuteTouchLongPress(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
if (!session->chrome->HasTouchScreen()) {
// TODO(samuong): remove this once we stop supporting M44.
return Status(kUnknownCommand, "Long press command requires Chrome 44+");
}
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
return web_view->SynthesizeTapGesture(location.x, location.y, 1, true);
}
Status ExecuteFlick(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
WebPoint location;
Status status = GetElementClickableLocation(
session, web_view, element_id, &location);
if (status.IsError())
return status;
int xoffset, yoffset, speed;
if (!params.GetInteger("xoffset", &xoffset))
return Status(kInvalidArgument, "'xoffset' must be an integer");
if (!params.GetInteger("yoffset", &yoffset))
return Status(kInvalidArgument, "'yoffset' must be an integer");
if (!params.GetInteger("speed", &speed))
return Status(kInvalidArgument, "'speed' must be an integer");
if (speed < 1)
return Status(kInvalidArgument, "'speed' must be a positive integer");
status = web_view->DispatchTouchEvent(
TouchEvent(kTouchStart, location.x, location.y));
if (status.IsError())
return status;
const double offset =
std::sqrt(static_cast<double>(xoffset * xoffset + yoffset * yoffset));
const double xoffset_per_event =
(speed * xoffset) / (kFlickTouchEventsPerSecond * offset);
const double yoffset_per_event =
(speed * yoffset) / (kFlickTouchEventsPerSecond * offset);
const int total_events =
(offset * kFlickTouchEventsPerSecond) / speed;
for (int i = 0; i < total_events; i++) {
status = web_view->DispatchTouchEvent(
TouchEvent(kTouchMove,
location.x + xoffset_per_event * i,
location.y + yoffset_per_event * i));
if (status.IsError())
return status;
base::PlatformThread::Sleep(
base::TimeDelta::FromMilliseconds(1000 / kFlickTouchEventsPerSecond));
}
return web_view->DispatchTouchEvent(
TouchEvent(kTouchEnd, location.x + xoffset, location.y + yoffset));
}
Status ExecuteClearElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
// Scrolling to element is done by webdriver::atoms::CLEAR
bool is_displayed = false;
base::TimeTicks start_time = base::TimeTicks::Now();
while (true) {
status = IsElementDisplayed(
session, web_view, element_id, true, &is_displayed);
if (status.IsError())
return status;
if (is_displayed)
break;
if (base::TimeTicks::Now() - start_time >= session->implicit_wait) {
return Status(kElementNotVisible);
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
}
static bool isClearWarningNotified = false;
if (!isClearWarningNotified) {
std::string messageClearWarning =
"\n\t=== NOTE: ===\n"
"\tThe Clear command in ChromeDriver 2.43 and above\n"
"\thas been updated to conform to the current standard,\n"
"\tincluding raising blur event after clearing.\n";
VLOG(0) << messageClearWarning;
isClearWarningNotified = true;
}
base::ListValue args;
args.Append(CreateElement(element_id));
std::unique_ptr<base::Value> result;
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::CLEAR),
args, &result);
}
Status ExecuteSendKeysToElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
const base::ListValue* key_list;
base::ListValue key_list_local;
if (session->w3c_compliant) {
const base::Value* text;
if (!params.Get("text", &text) || !text->is_string())
return Status(kInvalidArgument, "'text' must be a string");
key_list_local.Set(0, std::make_unique<base::Value>(text->Clone()));
key_list = &key_list_local;
} else {
if (!params.GetList("value", &key_list))
return Status(kInvalidArgument, "'value' must be a list");
}
bool is_input = false;
status = IsElementAttributeEqualToIgnoreCase(
session, web_view, element_id, "tagName", "input", &is_input);
if (status.IsError())
return status;
bool is_file = false;
status = IsElementAttributeEqualToIgnoreCase(
session, web_view, element_id, "type", "file", &is_file);
if (status.IsError())
return status;
if (is_input && is_file) {
if (session->strict_file_interactability) {
status = FocusToElement(session, web_view,element_id);
if (status.IsError())
return status;
}
// Compress array into a single string.
base::FilePath::StringType paths_string;
for (size_t i = 0; i < key_list->GetSize(); ++i) {
base::FilePath::StringType path_part;
if (!key_list->GetString(i, &path_part))
return Status(kInvalidArgument, "'value' is invalid");
paths_string.append(path_part);
}
// w3c spec specifies empty path_part should throw invalidArgument error
if (paths_string.empty())
return Status(kInvalidArgument, "'text' is empty");
ChromeDesktopImpl* chrome_desktop = nullptr;
bool is_desktop = session->chrome->GetAsDesktop(&chrome_desktop).IsOk();
// Separate the string into separate paths, delimited by '\n'.
std::vector<base::FilePath> paths;
for (const auto& path_piece : base::SplitStringPiece(
paths_string, base::FilePath::StringType(1, '\n'),
base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) {
// For local desktop browser, verify that the file exists.
// No easy way to do that for remote or mobile browser.
if (is_desktop && !base::PathExists(base::FilePath(path_piece))) {
return Status(
kInvalidArgument,
base::StringPrintf("File not found : %" PRFilePath,
base::FilePath(path_piece).value().c_str()));
}
paths.push_back(base::FilePath(path_piece));
}
bool multiple = false;
status = IsElementAttributeEqualToIgnoreCase(
session, web_view, element_id, "multiple", "true", &multiple);
if (status.IsError())
return status;
if (!multiple && paths.size() > 1)
return Status(kInvalidArgument,
"the element can not hold multiple files");
std::unique_ptr<base::DictionaryValue> element(CreateElement(element_id));
return web_view->SetFileInputFiles(session->GetCurrentFrameId(), *element,
paths, multiple);
} else {
return SendKeysToElement(session, web_view, element_id, key_list);
}
}
Status ExecuteSubmitElement(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::SUBMIT),
args,
value);
}
Status ExecuteGetElementText(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_TEXT),
args,
value);
}
Status ExecuteGetElementValue(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
"function(elem) { return elem['value'] }",
args,
value);
}
Status ExecuteGetElementProperty(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
std::string name;
if (!params.GetString("name", &name))
return Status(kInvalidArgument, "missing 'name'");
args.AppendString(name);
return web_view->CallFunction(
session->GetCurrentFrameId(),
"function(elem, name) { return elem[name] }",
args,
value);
}
Status ExecuteGetElementTagName(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
"function(elem) { return elem.tagName.toLowerCase() }",
args,
value);
}
Status ExecuteIsElementSelected(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::IS_SELECTED),
args,
value);
}
Status ExecuteIsElementEnabled(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
bool is_xml = false;
status = IsDocumentTypeXml(session, web_view, &is_xml);
if (status.IsError())
return status;
if (is_xml) {
value->reset(new base::Value(false));
return Status(kOk);
} else {
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::IS_ENABLED),
args,
value);
}
}
Status ExecuteIsElementDisplayed(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::IS_DISPLAYED),
args,
value);
}
Status ExecuteGetElementLocation(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_LOCATION),
args,
value);
}
Status ExecuteGetElementRect(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
std::unique_ptr<base::Value> location;
status = web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_LOCATION), args,
&location);
if (status.IsError())
return status;
std::unique_ptr<base::Value> size;
web_view->CallFunction(session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_SIZE),
args, &size);
// do type conversions
base::DictionaryValue* size_dict;
if (!size->GetAsDictionary(&size_dict))
return Status(kUnknownError, "could not convert to DictionaryValue");
base::DictionaryValue* location_dict;
if (!location->GetAsDictionary(&location_dict))
return Status(kUnknownError, "could not convert to DictionaryValue");
// grab values
double x, y, width, height;
if (!location_dict->GetDouble("x", &x))
return Status(kUnknownError, "x coordinate is missing in element location");
if (!location_dict->GetDouble("y", &y))
return Status(kUnknownError, "y coordinate is missing in element location");
if (!size_dict->GetDouble("height", &height))
return Status(kUnknownError, "height is missing in element size");
if (!size_dict->GetDouble("width", &width))
return Status(kUnknownError, "width is missing in element size");
base::DictionaryValue ret;
ret.SetDouble("x", x);
ret.SetDouble("y", y);
ret.SetDouble("width", width);
ret.SetDouble("height", height);
value->reset(ret.DeepCopy());
return Status(kOk);
}
Status ExecuteGetElementLocationOnceScrolledIntoView(
Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
WebPoint offset(0, 0);
WebPoint location;
Status status = ScrollElementIntoView(
session, web_view, element_id, &offset, &location);
if (status.IsError())
return status;
*value = CreateValueFrom(location);
return Status(kOk);
}
Status ExecuteGetElementSize(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = CheckElement(element_id);
if (status.IsError())
return status;
base::ListValue args;
args.Append(CreateElement(element_id));
return web_view->CallFunction(
session->GetCurrentFrameId(),
webdriver::atoms::asString(webdriver::atoms::GET_SIZE),
args,
value);
}
Status ExecuteGetElementAttribute(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
std::string name;
if (!params.GetString("name", &name))
return Status(kInvalidArgument, "missing 'name'");
return GetElementAttribute(session, web_view, element_id, name, value);
}
Status ExecuteGetElementValueOfCSSProperty(
Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
bool is_xml = false;
Status status = IsDocumentTypeXml(session, web_view, &is_xml);
if (status.IsError())
return status;
if (is_xml) {
value->reset(new base::Value(""));
} else {
std::string property_name;
if (!params.GetString("propertyName", &property_name))
return Status(kInvalidArgument, "missing 'propertyName'");
std::string property_value;
status = GetElementEffectiveStyle(
session, web_view, element_id, property_name, &property_value);
if (status.IsError())
return status;
value->reset(new base::Value(property_value));
}
return Status(kOk);
}
Status ExecuteElementEquals(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
std::string other_element_id;
if (!params.GetString("other", &other_element_id))
return Status(kInvalidArgument, "'other' must be a string");
value->reset(new base::Value(element_id == other_element_id));
return Status(kOk);
}
Status ExecuteElementScreenshot(Session* session,
WebView* web_view,
const std::string& element_id,
const base::DictionaryValue& params,
std::unique_ptr<base::Value>* value) {
Status status = session->chrome->ActivateWebView(web_view->GetId());
if (status.IsError())
return status;
WebPoint offset(0, 0);
WebPoint location;
status =
ScrollElementIntoView(session, web_view, element_id, &offset, &location);
if (status.IsError())
return status;
std::unique_ptr<base::Value> clip;
status = ExecuteGetElementRect(session, web_view, element_id, params, &clip);
if (status.IsError())
return status;
// |location| returned by ScrollElementIntoView is relative to the current
// view port. However, CaptureScreenshot expects a location relative to the
// document origin. We make the adjustment using the scroll amount of the top
// level window. Scrolling of frames has already been included in |location|.
// Scroll information can be in either document.documentElement or
// document.body, depending on document compatibility mode. The parentheses
// around the JavaScript code below is needed because JavaScript syntax
// doesn't allow a statement to start with an object literal.
std::unique_ptr<base::Value> scroll;
status = web_view->EvaluateScript(
std::string(),
"({x: document.documentElement.scrollLeft || document.body.scrollLeft,"
" y: document.documentElement.scrollTop || document.body.scrollTop})",
&scroll);
if (status.IsError())
return status;
int scroll_left = scroll->FindKey("x")->GetInt();
int scroll_top = scroll->FindKey("y")->GetInt();
std::unique_ptr<base::DictionaryValue> clip_dict =
base::DictionaryValue::From(std::move(clip));
if (!clip_dict)
return Status(kUnknownError, "Element Rect is not a dictionary");
// |clip_dict| already contains the right width and height of the target
// element, but its x and y are relative to containing frame. We replace them
// with the x and y relative to top-level document origin, as expected by
// CaptureScreenshot.
clip_dict->SetInteger("x", location.x + scroll_left);
clip_dict->SetInteger("y", location.y + scroll_top);
clip_dict->SetDouble("scale", 1.0);
base::DictionaryValue screenshot_params;
screenshot_params.SetDictionary("clip", std::move(clip_dict));
std::string screenshot;
status = web_view->CaptureScreenshot(&screenshot, screenshot_params);
if (status.IsError())
return status;
value->reset(new base::Value(screenshot));
return Status(kOk);
}