// 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_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.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/version.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/session.h"
#include "third_party/webdriver/atoms.h"
namespace {
const char kElementKey[] = "ELEMENT";
bool ParseFromValue(base::Value* value, WebPoint* point) {
base::DictionaryValue* dict_value;
if (!value->GetAsDictionary(&dict_value))
return false;
double x, y;
if (!dict_value->GetDouble("x", &x) ||
!dict_value->GetDouble("y", &y))
return false;
point->x = static_cast<int>(x);
point->y = static_cast<int>(y);
return true;
}
bool ParseFromValue(base::Value* value, WebSize* size) {
base::DictionaryValue* dict_value;
if (!value->GetAsDictionary(&dict_value))
return false;
double width, height;
if (!dict_value->GetDouble("width", &width) ||
!dict_value->GetDouble("height", &height))
return false;
size->width = static_cast<int>(width);
size->height = static_cast<int>(height);
return true;
}
bool ParseFromValue(base::Value* value, WebRect* rect) {
base::DictionaryValue* dict_value;
if (!value->GetAsDictionary(&dict_value))
return false;
double x, y, width, height;
if (!dict_value->GetDouble("left", &x) ||
!dict_value->GetDouble("top", &y) ||
!dict_value->GetDouble("width", &width) ||
!dict_value->GetDouble("height", &height))
return false;
rect->origin.x = static_cast<int>(x);
rect->origin.y = static_cast<int>(y);
rect->size.width = static_cast<int>(width);
rect->size.height = static_cast<int>(height);
return true;
}
base::Value* CreateValueFrom(const WebRect& rect) {
base::DictionaryValue* dict = new base::DictionaryValue();
dict->SetInteger("left", rect.X());
dict->SetInteger("top", rect.Y());
dict->SetInteger("width", rect.Width());
dict->SetInteger("height", rect.Height());
return dict;
}
Status CallAtomsJs(
const std::string& frame,
WebView* web_view,
const char* const* atom_function,
const base::ListValue& args,
scoped_ptr<base::Value>* result) {
return web_view->CallFunction(
frame, webdriver::atoms::asString(atom_function), args, result);
}
Status VerifyElementClickable(
const std::string& frame,
WebView* web_view,
const std::string& element_id,
const WebPoint& location) {
base::ListValue args;
args.Append(CreateElement(element_id));
args.Append(CreateValueFrom(location));
scoped_ptr<base::Value> result;
Status status = CallAtomsJs(
frame, web_view, webdriver::atoms::IS_ELEMENT_CLICKABLE,
args, &result);
if (status.IsError())
return status;
base::DictionaryValue* dict;
bool is_clickable;
if (!result->GetAsDictionary(&dict) ||
!dict->GetBoolean("clickable", &is_clickable)) {
return Status(kUnknownError,
"failed to parse value of IS_ELEMENT_CLICKABLE");
}
if (!is_clickable) {
std::string message;
if (!dict->GetString("message", &message))
message = "element is not clickable";
return Status(kUnknownError, message);
}
return Status(kOk);
}
Status ScrollElementRegionIntoViewHelper(
const std::string& frame,
WebView* web_view,
const std::string& element_id,
const WebRect& region,
bool center,
const std::string& clickable_element_id,
WebPoint* location) {
WebPoint tmp_location = *location;
base::ListValue args;
args.Append(CreateElement(element_id));
args.AppendBoolean(center);
args.Append(CreateValueFrom(region));
scoped_ptr<base::Value> result;
Status status = web_view->CallFunction(
frame, webdriver::atoms::asString(webdriver::atoms::GET_LOCATION_IN_VIEW),
args, &result);
if (status.IsError())
return status;
if (!ParseFromValue(result.get(), &tmp_location)) {
return Status(kUnknownError,
"failed to parse value of GET_LOCATION_IN_VIEW");
}
if (!clickable_element_id.empty()) {
WebPoint middle = tmp_location;
middle.Offset(region.Width() / 2, region.Height() / 2);
status = VerifyElementClickable(
frame, web_view, clickable_element_id, middle);
if (status.IsError())
return status;
}
*location = tmp_location;
return Status(kOk);
}
Status GetElementEffectiveStyle(
const std::string& frame,
WebView* web_view,
const std::string& element_id,
const std::string& property,
std::string* value) {
base::ListValue args;
args.Append(CreateElement(element_id));
args.AppendString(property);
scoped_ptr<base::Value> result;
Status status = web_view->CallFunction(
frame, webdriver::atoms::asString(webdriver::atoms::GET_EFFECTIVE_STYLE),
args, &result);
if (status.IsError())
return status;
if (!result->GetAsString(value)) {
return Status(kUnknownError,
"failed to parse value of GET_EFFECTIVE_STYLE");
}
return Status(kOk);
}
Status GetElementBorder(
const std::string& frame,
WebView* web_view,
const std::string& element_id,
int* border_left,
int* border_top) {
std::string border_left_str;
Status status = GetElementEffectiveStyle(
frame, web_view, element_id, "border-left-width", &border_left_str);
if (status.IsError())
return status;
std::string border_top_str;
status = GetElementEffectiveStyle(
frame, web_view, element_id, "border-top-width", &border_top_str);
if (status.IsError())
return status;
int border_left_tmp = -1;
int border_top_tmp = -1;
base::StringToInt(border_left_str, &border_left_tmp);
base::StringToInt(border_top_str, &border_top_tmp);
if (border_left_tmp == -1 || border_top_tmp == -1)
return Status(kUnknownError, "failed to get border width of element");
*border_left = border_left_tmp;
*border_top = border_top_tmp;
return Status(kOk);
}
} // namespace
base::DictionaryValue* CreateElement(const std::string& element_id) {
base::DictionaryValue* element = new base::DictionaryValue();
element->SetString(kElementKey, element_id);
return element;
}
base::Value* CreateValueFrom(const WebPoint& point) {
base::DictionaryValue* dict = new base::DictionaryValue();
dict->SetInteger("x", point.x);
dict->SetInteger("y", point.y);
return dict;
}
Status FindElement(
int interval_ms,
bool only_one,
const std::string* root_element_id,
Session* session,
WebView* web_view,
const base::DictionaryValue& params,
scoped_ptr<base::Value>* value) {
std::string strategy;
if (!params.GetString("using", &strategy))
return Status(kUnknownError, "'using' must be a string");
std::string target;
if (!params.GetString("value", &target))
return Status(kUnknownError, "'value' must be a string");
std::string script;
if (only_one)
script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENT);
else
script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENTS);
scoped_ptr<base::DictionaryValue> locator(new base::DictionaryValue());
locator->SetString(strategy, target);
base::ListValue arguments;
arguments.Append(locator.release());
if (root_element_id)
arguments.Append(CreateElement(*root_element_id));
base::TimeTicks start_time = base::TimeTicks::Now();
while (true) {
scoped_ptr<base::Value> temp;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(), script, arguments, &temp);
if (status.IsError())
return status;
if (!temp->IsType(base::Value::TYPE_NULL)) {
if (only_one) {
value->reset(temp.release());
return Status(kOk);
} else {
base::ListValue* result;
if (!temp->GetAsList(&result))
return Status(kUnknownError, "script returns unexpected result");
if (result->GetSize() > 0U) {
value->reset(temp.release());
return Status(kOk);
}
}
}
if (base::TimeTicks::Now() - start_time >= session->implicit_wait) {
if (only_one) {
return Status(kNoSuchElement);
} else {
value->reset(new base::ListValue());
return Status(kOk);
}
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(interval_ms));
}
return Status(kUnknownError);
}
Status GetActiveElement(
Session* session,
WebView* web_view,
scoped_ptr<base::Value>* value) {
base::ListValue args;
return web_view->CallFunction(
session->GetCurrentFrameId(),
"function() { return document.activeElement || document.body }",
args,
value);
}
Status IsElementFocused(
Session* session,
WebView* web_view,
const std::string& element_id,
bool* is_focused) {
scoped_ptr<base::Value> result;
Status status = GetActiveElement(session, web_view, &result);
if (status.IsError())
return status;
scoped_ptr<base::Value> element_dict(CreateElement(element_id));
*is_focused = result->Equals(element_dict.get());
return Status(kOk);
}
Status GetElementAttribute(
Session* session,
WebView* web_view,
const std::string& element_id,
const std::string& attribute_name,
scoped_ptr<base::Value>* value) {
base::ListValue args;
args.Append(CreateElement(element_id));
args.AppendString(attribute_name);
return CallAtomsJs(
session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_ATTRIBUTE,
args, value);
}
Status IsElementAttributeEqualToIgnoreCase(
Session* session,
WebView* web_view,
const std::string& element_id,
const std::string& attribute_name,
const std::string& attribute_value,
bool* is_equal) {
scoped_ptr<base::Value> result;
Status status = GetElementAttribute(
session, web_view, element_id, attribute_name, &result);
if (status.IsError())
return status;
std::string actual_value;
if (result->GetAsString(&actual_value))
*is_equal = LowerCaseEqualsASCII(actual_value, attribute_value.c_str());
else
*is_equal = false;
return status;
}
Status GetElementClickableLocation(
Session* session,
WebView* web_view,
const std::string& element_id,
WebPoint* location) {
std::string tag_name;
Status status = GetElementTagName(session, web_view, element_id, &tag_name);
if (status.IsError())
return status;
std::string target_element_id = element_id;
if (tag_name == "area") {
// Scroll the image into view instead of the area.
const char* kGetImageElementForArea =
"function (element) {"
" var map = element.parentElement;"
" if (map.tagName.toLowerCase() != 'map')"
" throw new Error('the area is not within a map');"
" var mapName = map.getAttribute('name');"
" if (mapName == null)"
" throw new Error ('area\\'s parent map must have a name');"
" mapName = '#' + mapName.toLowerCase();"
" var images = document.getElementsByTagName('img');"
" for (var i = 0; i < images.length; i++) {"
" if (images[i].useMap.toLowerCase() == mapName)"
" return images[i];"
" }"
" throw new Error('no img is found for the area');"
"}";
base::ListValue args;
args.Append(CreateElement(element_id));
scoped_ptr<base::Value> result;
status = web_view->CallFunction(
session->GetCurrentFrameId(), kGetImageElementForArea, args, &result);
if (status.IsError())
return status;
const base::DictionaryValue* element_dict;
if (!result->GetAsDictionary(&element_dict) ||
!element_dict->GetString(kElementKey, &target_element_id))
return Status(kUnknownError, "no element reference returned by script");
}
bool is_displayed = false;
status = IsElementDisplayed(
session, web_view, target_element_id, true, &is_displayed);
if (status.IsError())
return status;
if (!is_displayed)
return Status(kElementNotVisible);
WebRect rect;
status = GetElementRegion(session, web_view, element_id, &rect);
if (status.IsError())
return status;
std::string tmp_element_id = element_id;
int build_no = session->chrome->GetBrowserInfo()->build_no;
if (tag_name == "area" && build_no < 1799 && build_no >= 1666) {
// This is to skip clickable verification for <area>.
// The problem is caused by document.ElementFromPoint(crbug.com/338601).
// It was introduced by blink r159012, which rolled into chromium r227489.
// And it was fixed in blink r165426, which rolled into chromium r245994.
// TODO(stgao): Revert after 33 is not supported.
tmp_element_id = std::string();
}
status = ScrollElementRegionIntoView(
session, web_view, target_element_id, rect,
true /* center */, tmp_element_id, location);
if (status.IsError())
return status;
location->Offset(rect.Width() / 2, rect.Height() / 2);
return Status(kOk);
}
Status GetElementEffectiveStyle(
Session* session,
WebView* web_view,
const std::string& element_id,
const std::string& property_name,
std::string* property_value) {
return GetElementEffectiveStyle(session->GetCurrentFrameId(), web_view,
element_id, property_name, property_value);
}
Status GetElementRegion(
Session* session,
WebView* web_view,
const std::string& element_id,
WebRect* rect) {
base::ListValue args;
args.Append(CreateElement(element_id));
scoped_ptr<base::Value> result;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(), kGetElementRegionScript, args, &result);
if (status.IsError())
return status;
if (!ParseFromValue(result.get(), rect)) {
return Status(kUnknownError,
"failed to parse value of getElementRegion");
}
return Status(kOk);
}
Status GetElementTagName(
Session* session,
WebView* web_view,
const std::string& element_id,
std::string* name) {
base::ListValue args;
args.Append(CreateElement(element_id));
scoped_ptr<base::Value> result;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(),
"function(elem) { return elem.tagName.toLowerCase(); }",
args, &result);
if (status.IsError())
return status;
if (!result->GetAsString(name))
return Status(kUnknownError, "failed to get element tag name");
return Status(kOk);
}
Status GetElementSize(
Session* session,
WebView* web_view,
const std::string& element_id,
WebSize* size) {
base::ListValue args;
args.Append(CreateElement(element_id));
scoped_ptr<base::Value> result;
Status status = CallAtomsJs(
session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_SIZE,
args, &result);
if (status.IsError())
return status;
if (!ParseFromValue(result.get(), size))
return Status(kUnknownError, "failed to parse value of GET_SIZE");
return Status(kOk);
}
Status IsElementDisplayed(
Session* session,
WebView* web_view,
const std::string& element_id,
bool ignore_opacity,
bool* is_displayed) {
base::ListValue args;
args.Append(CreateElement(element_id));
args.AppendBoolean(ignore_opacity);
scoped_ptr<base::Value> result;
Status status = CallAtomsJs(
session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_DISPLAYED,
args, &result);
if (status.IsError())
return status;
if (!result->GetAsBoolean(is_displayed))
return Status(kUnknownError, "IS_DISPLAYED should return a boolean value");
return Status(kOk);
}
Status IsElementEnabled(
Session* session,
WebView* web_view,
const std::string& element_id,
bool* is_enabled) {
base::ListValue args;
args.Append(CreateElement(element_id));
scoped_ptr<base::Value> result;
Status status = CallAtomsJs(
session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_ENABLED,
args, &result);
if (status.IsError())
return status;
if (!result->GetAsBoolean(is_enabled))
return Status(kUnknownError, "IS_ENABLED should return a boolean value");
return Status(kOk);
}
Status IsOptionElementSelected(
Session* session,
WebView* web_view,
const std::string& element_id,
bool* is_selected) {
base::ListValue args;
args.Append(CreateElement(element_id));
scoped_ptr<base::Value> result;
Status status = CallAtomsJs(
session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_SELECTED,
args, &result);
if (status.IsError())
return status;
if (!result->GetAsBoolean(is_selected))
return Status(kUnknownError, "IS_SELECTED should return a boolean value");
return Status(kOk);
}
Status IsOptionElementTogglable(
Session* session,
WebView* web_view,
const std::string& element_id,
bool* is_togglable) {
base::ListValue args;
args.Append(CreateElement(element_id));
scoped_ptr<base::Value> result;
Status status = web_view->CallFunction(
session->GetCurrentFrameId(), kIsOptionElementToggleableScript,
args, &result);
if (status.IsError())
return status;
if (!result->GetAsBoolean(is_togglable))
return Status(kUnknownError, "failed check if option togglable or not");
return Status(kOk);
}
Status SetOptionElementSelected(
Session* session,
WebView* web_view,
const std::string& element_id,
bool selected) {
// TODO(171034): need to fix throwing error if an alert is triggered.
base::ListValue args;
args.Append(CreateElement(element_id));
args.AppendBoolean(selected);
scoped_ptr<base::Value> result;
return CallAtomsJs(
session->GetCurrentFrameId(), web_view, webdriver::atoms::CLICK,
args, &result);
}
Status ToggleOptionElement(
Session* session,
WebView* web_view,
const std::string& element_id) {
bool is_selected;
Status status = IsOptionElementSelected(
session, web_view, element_id, &is_selected);
if (status.IsError())
return status;
return SetOptionElementSelected(session, web_view, element_id, !is_selected);
}
Status ScrollElementIntoView(
Session* session,
WebView* web_view,
const std::string& id,
WebPoint* location) {
WebSize size;
Status status = GetElementSize(session, web_view, id, &size);
if (status.IsError())
return status;
return ScrollElementRegionIntoView(
session, web_view, id, WebRect(WebPoint(0, 0), size),
false /* center */, std::string(), location);
}
Status ScrollElementRegionIntoView(
Session* session,
WebView* web_view,
const std::string& element_id,
const WebRect& region,
bool center,
const std::string& clickable_element_id,
WebPoint* location) {
WebPoint region_offset = region.origin;
WebSize region_size = region.size;
Status status = ScrollElementRegionIntoViewHelper(
session->GetCurrentFrameId(), web_view, element_id, region,
center, clickable_element_id, ®ion_offset);
if (status.IsError())
return status;
const char* kFindSubFrameScript =
"function(xpath) {"
" return document.evaluate(xpath, document, null,"
" XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
"}";
for (std::list<FrameInfo>::reverse_iterator rit = session->frames.rbegin();
rit != session->frames.rend(); ++rit) {
base::ListValue args;
args.AppendString(
base::StringPrintf("//*[@cd_frame_id_ = '%s']",
rit->chromedriver_frame_id.c_str()));
scoped_ptr<base::Value> result;
status = web_view->CallFunction(
rit->parent_frame_id, kFindSubFrameScript, args, &result);
if (status.IsError())
return status;
const base::DictionaryValue* element_dict;
if (!result->GetAsDictionary(&element_dict))
return Status(kUnknownError, "no element reference returned by script");
std::string frame_element_id;
if (!element_dict->GetString(kElementKey, &frame_element_id))
return Status(kUnknownError, "failed to locate a sub frame");
// Modify |region_offset| by the frame's border.
int border_left = -1;
int border_top = -1;
status = GetElementBorder(
rit->parent_frame_id, web_view, frame_element_id,
&border_left, &border_top);
if (status.IsError())
return status;
region_offset.Offset(border_left, border_top);
status = ScrollElementRegionIntoViewHelper(
rit->parent_frame_id, web_view, frame_element_id,
WebRect(region_offset, region_size),
center, frame_element_id, ®ion_offset);
if (status.IsError())
return status;
}
*location = region_offset;
return Status(kOk);
}