// 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); }