Home | History | Annotate | Download | only in chromedriver
      1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/test/chromedriver/element_util.h"
      6 
      7 #include "base/strings/string_number_conversions.h"
      8 #include "base/strings/string_util.h"
      9 #include "base/strings/stringprintf.h"
     10 #include "base/threading/platform_thread.h"
     11 #include "base/time/time.h"
     12 #include "base/values.h"
     13 #include "chrome/test/chromedriver/basic_types.h"
     14 #include "chrome/test/chromedriver/chrome/browser_info.h"
     15 #include "chrome/test/chromedriver/chrome/chrome.h"
     16 #include "chrome/test/chromedriver/chrome/js.h"
     17 #include "chrome/test/chromedriver/chrome/status.h"
     18 #include "chrome/test/chromedriver/chrome/web_view.h"
     19 #include "chrome/test/chromedriver/session.h"
     20 #include "third_party/webdriver/atoms.h"
     21 
     22 namespace {
     23 
     24 const char kElementKey[] = "ELEMENT";
     25 
     26 bool ParseFromValue(base::Value* value, WebPoint* point) {
     27   base::DictionaryValue* dict_value;
     28   if (!value->GetAsDictionary(&dict_value))
     29     return false;
     30   double x = 0;
     31   double y = 0;
     32   if (!dict_value->GetDouble("x", &x) ||
     33       !dict_value->GetDouble("y", &y))
     34     return false;
     35   point->x = static_cast<int>(x);
     36   point->y = static_cast<int>(y);
     37   return true;
     38 }
     39 
     40 bool ParseFromValue(base::Value* value, WebSize* size) {
     41   base::DictionaryValue* dict_value;
     42   if (!value->GetAsDictionary(&dict_value))
     43     return false;
     44   double width = 0;
     45   double height = 0;
     46   if (!dict_value->GetDouble("width", &width) ||
     47       !dict_value->GetDouble("height", &height))
     48     return false;
     49   size->width = static_cast<int>(width);
     50   size->height = static_cast<int>(height);
     51   return true;
     52 }
     53 
     54 bool ParseFromValue(base::Value* value, WebRect* rect) {
     55   base::DictionaryValue* dict_value;
     56   if (!value->GetAsDictionary(&dict_value))
     57     return false;
     58   double x = 0;
     59   double y = 0;
     60   double width = 0;
     61   double height = 0;
     62   if (!dict_value->GetDouble("left", &x) ||
     63       !dict_value->GetDouble("top", &y) ||
     64       !dict_value->GetDouble("width", &width) ||
     65       !dict_value->GetDouble("height", &height))
     66     return false;
     67   rect->origin.x = static_cast<int>(x);
     68   rect->origin.y = static_cast<int>(y);
     69   rect->size.width = static_cast<int>(width);
     70   rect->size.height = static_cast<int>(height);
     71   return true;
     72 }
     73 
     74 base::Value* CreateValueFrom(const WebRect& rect) {
     75   base::DictionaryValue* dict = new base::DictionaryValue();
     76   dict->SetInteger("left", rect.X());
     77   dict->SetInteger("top", rect.Y());
     78   dict->SetInteger("width", rect.Width());
     79   dict->SetInteger("height", rect.Height());
     80   return dict;
     81 }
     82 
     83 Status CallAtomsJs(
     84     const std::string& frame,
     85     WebView* web_view,
     86     const char* const* atom_function,
     87     const base::ListValue& args,
     88     scoped_ptr<base::Value>* result) {
     89   return web_view->CallFunction(
     90       frame, webdriver::atoms::asString(atom_function), args, result);
     91 }
     92 
     93 Status VerifyElementClickable(
     94     const std::string& frame,
     95     WebView* web_view,
     96     const std::string& element_id,
     97     const WebPoint& location) {
     98   base::ListValue args;
     99   args.Append(CreateElement(element_id));
    100   args.Append(CreateValueFrom(location));
    101   scoped_ptr<base::Value> result;
    102   Status status = CallAtomsJs(
    103       frame, web_view, webdriver::atoms::IS_ELEMENT_CLICKABLE,
    104       args, &result);
    105   if (status.IsError())
    106     return status;
    107   base::DictionaryValue* dict;
    108   bool is_clickable = false;
    109   if (!result->GetAsDictionary(&dict) ||
    110       !dict->GetBoolean("clickable", &is_clickable)) {
    111     return Status(kUnknownError,
    112                   "failed to parse value of IS_ELEMENT_CLICKABLE");
    113   }
    114 
    115   if (!is_clickable) {
    116     std::string message;
    117     if (!dict->GetString("message", &message))
    118       message = "element is not clickable";
    119     return Status(kUnknownError, message);
    120   }
    121   return Status(kOk);
    122 }
    123 
    124 Status ScrollElementRegionIntoViewHelper(
    125     const std::string& frame,
    126     WebView* web_view,
    127     const std::string& element_id,
    128     const WebRect& region,
    129     bool center,
    130     const std::string& clickable_element_id,
    131     WebPoint* location) {
    132   WebPoint tmp_location = *location;
    133   base::ListValue args;
    134   args.Append(CreateElement(element_id));
    135   args.AppendBoolean(center);
    136   args.Append(CreateValueFrom(region));
    137   scoped_ptr<base::Value> result;
    138   Status status = web_view->CallFunction(
    139       frame, webdriver::atoms::asString(webdriver::atoms::GET_LOCATION_IN_VIEW),
    140       args, &result);
    141   if (status.IsError())
    142     return status;
    143   if (!ParseFromValue(result.get(), &tmp_location)) {
    144     return Status(kUnknownError,
    145                   "failed to parse value of GET_LOCATION_IN_VIEW");
    146   }
    147   if (!clickable_element_id.empty()) {
    148     WebPoint middle = tmp_location;
    149     middle.Offset(region.Width() / 2, region.Height() / 2);
    150     status = VerifyElementClickable(
    151         frame, web_view, clickable_element_id, middle);
    152     if (status.IsError())
    153       return status;
    154   }
    155   *location = tmp_location;
    156   return Status(kOk);
    157 }
    158 
    159 Status GetElementEffectiveStyle(
    160     const std::string& frame,
    161     WebView* web_view,
    162     const std::string& element_id,
    163     const std::string& property,
    164     std::string* value) {
    165   base::ListValue args;
    166   args.Append(CreateElement(element_id));
    167   args.AppendString(property);
    168   scoped_ptr<base::Value> result;
    169   Status status = web_view->CallFunction(
    170       frame, webdriver::atoms::asString(webdriver::atoms::GET_EFFECTIVE_STYLE),
    171       args, &result);
    172   if (status.IsError())
    173     return status;
    174   if (!result->GetAsString(value)) {
    175     return Status(kUnknownError,
    176                   "failed to parse value of GET_EFFECTIVE_STYLE");
    177   }
    178   return Status(kOk);
    179 }
    180 
    181 Status GetElementBorder(
    182     const std::string& frame,
    183     WebView* web_view,
    184     const std::string& element_id,
    185     int* border_left,
    186     int* border_top) {
    187   std::string border_left_str;
    188   Status status = GetElementEffectiveStyle(
    189       frame, web_view, element_id, "border-left-width", &border_left_str);
    190   if (status.IsError())
    191     return status;
    192   std::string border_top_str;
    193   status = GetElementEffectiveStyle(
    194       frame, web_view, element_id, "border-top-width", &border_top_str);
    195   if (status.IsError())
    196     return status;
    197   int border_left_tmp = -1;
    198   int border_top_tmp = -1;
    199   base::StringToInt(border_left_str, &border_left_tmp);
    200   base::StringToInt(border_top_str, &border_top_tmp);
    201   if (border_left_tmp == -1 || border_top_tmp == -1)
    202     return Status(kUnknownError, "failed to get border width of element");
    203   *border_left = border_left_tmp;
    204   *border_top = border_top_tmp;
    205   return Status(kOk);
    206 }
    207 
    208 }  // namespace
    209 
    210 base::DictionaryValue* CreateElement(const std::string& element_id) {
    211   base::DictionaryValue* element = new base::DictionaryValue();
    212   element->SetString(kElementKey, element_id);
    213   return element;
    214 }
    215 
    216 base::Value* CreateValueFrom(const WebPoint& point) {
    217   base::DictionaryValue* dict = new base::DictionaryValue();
    218   dict->SetInteger("x", point.x);
    219   dict->SetInteger("y", point.y);
    220   return dict;
    221 }
    222 
    223 Status FindElement(
    224     int interval_ms,
    225     bool only_one,
    226     const std::string* root_element_id,
    227     Session* session,
    228     WebView* web_view,
    229     const base::DictionaryValue& params,
    230     scoped_ptr<base::Value>* value) {
    231   std::string strategy;
    232   if (!params.GetString("using", &strategy))
    233     return Status(kUnknownError, "'using' must be a string");
    234   std::string target;
    235   if (!params.GetString("value", &target))
    236     return Status(kUnknownError, "'value' must be a string");
    237 
    238   std::string script;
    239   if (only_one)
    240     script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENT);
    241   else
    242     script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENTS);
    243   scoped_ptr<base::DictionaryValue> locator(new base::DictionaryValue());
    244   locator->SetString(strategy, target);
    245   base::ListValue arguments;
    246   arguments.Append(locator.release());
    247   if (root_element_id)
    248     arguments.Append(CreateElement(*root_element_id));
    249 
    250   base::TimeTicks start_time = base::TimeTicks::Now();
    251   while (true) {
    252     scoped_ptr<base::Value> temp;
    253     Status status = web_view->CallFunction(
    254         session->GetCurrentFrameId(), script, arguments, &temp);
    255     if (status.IsError())
    256       return status;
    257 
    258     if (!temp->IsType(base::Value::TYPE_NULL)) {
    259       if (only_one) {
    260         value->reset(temp.release());
    261         return Status(kOk);
    262       } else {
    263         base::ListValue* result;
    264         if (!temp->GetAsList(&result))
    265           return Status(kUnknownError, "script returns unexpected result");
    266 
    267         if (result->GetSize() > 0U) {
    268           value->reset(temp.release());
    269           return Status(kOk);
    270         }
    271       }
    272     }
    273 
    274     if (base::TimeTicks::Now() - start_time >= session->implicit_wait) {
    275       if (only_one) {
    276         return Status(kNoSuchElement);
    277       } else {
    278         value->reset(new base::ListValue());
    279         return Status(kOk);
    280       }
    281     }
    282     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(interval_ms));
    283   }
    284 
    285   return Status(kUnknownError);
    286 }
    287 
    288 Status GetActiveElement(
    289     Session* session,
    290     WebView* web_view,
    291     scoped_ptr<base::Value>* value) {
    292   base::ListValue args;
    293   return web_view->CallFunction(
    294       session->GetCurrentFrameId(),
    295       "function() { return document.activeElement || document.body }",
    296       args,
    297       value);
    298 }
    299 
    300 Status IsElementFocused(
    301     Session* session,
    302     WebView* web_view,
    303     const std::string& element_id,
    304     bool* is_focused) {
    305   scoped_ptr<base::Value> result;
    306   Status status = GetActiveElement(session, web_view, &result);
    307   if (status.IsError())
    308     return status;
    309   scoped_ptr<base::Value> element_dict(CreateElement(element_id));
    310   *is_focused = result->Equals(element_dict.get());
    311   return Status(kOk);
    312 }
    313 
    314 Status GetElementAttribute(
    315     Session* session,
    316     WebView* web_view,
    317     const std::string& element_id,
    318     const std::string& attribute_name,
    319     scoped_ptr<base::Value>* value) {
    320   base::ListValue args;
    321   args.Append(CreateElement(element_id));
    322   args.AppendString(attribute_name);
    323   return CallAtomsJs(
    324       session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_ATTRIBUTE,
    325       args, value);
    326 }
    327 
    328 Status IsElementAttributeEqualToIgnoreCase(
    329     Session* session,
    330     WebView* web_view,
    331     const std::string& element_id,
    332     const std::string& attribute_name,
    333     const std::string& attribute_value,
    334     bool* is_equal) {
    335   scoped_ptr<base::Value> result;
    336   Status status = GetElementAttribute(
    337       session, web_view, element_id, attribute_name, &result);
    338   if (status.IsError())
    339     return status;
    340   std::string actual_value;
    341   if (result->GetAsString(&actual_value))
    342     *is_equal = LowerCaseEqualsASCII(actual_value, attribute_value.c_str());
    343   else
    344     *is_equal = false;
    345   return status;
    346 }
    347 
    348 Status GetElementClickableLocation(
    349     Session* session,
    350     WebView* web_view,
    351     const std::string& element_id,
    352     WebPoint* location) {
    353   std::string tag_name;
    354   Status status = GetElementTagName(session, web_view, element_id, &tag_name);
    355   if (status.IsError())
    356     return status;
    357   std::string target_element_id = element_id;
    358   if (tag_name == "area") {
    359     // Scroll the image into view instead of the area.
    360     const char* kGetImageElementForArea =
    361         "function (element) {"
    362         "  var map = element.parentElement;"
    363         "  if (map.tagName.toLowerCase() != 'map')"
    364         "    throw new Error('the area is not within a map');"
    365         "  var mapName = map.getAttribute('name');"
    366         "  if (mapName == null)"
    367         "    throw new Error ('area\\'s parent map must have a name');"
    368         "  mapName = '#' + mapName.toLowerCase();"
    369         "  var images = document.getElementsByTagName('img');"
    370         "  for (var i = 0; i < images.length; i++) {"
    371         "    if (images[i].useMap.toLowerCase() == mapName)"
    372         "      return images[i];"
    373         "  }"
    374         "  throw new Error('no img is found for the area');"
    375         "}";
    376     base::ListValue args;
    377     args.Append(CreateElement(element_id));
    378     scoped_ptr<base::Value> result;
    379     status = web_view->CallFunction(
    380         session->GetCurrentFrameId(), kGetImageElementForArea, args, &result);
    381     if (status.IsError())
    382       return status;
    383     const base::DictionaryValue* element_dict;
    384     if (!result->GetAsDictionary(&element_dict) ||
    385         !element_dict->GetString(kElementKey, &target_element_id))
    386       return Status(kUnknownError, "no element reference returned by script");
    387   }
    388   bool is_displayed = false;
    389   status = IsElementDisplayed(
    390       session, web_view, target_element_id, true, &is_displayed);
    391   if (status.IsError())
    392     return status;
    393   if (!is_displayed)
    394     return Status(kElementNotVisible);
    395 
    396   WebRect rect;
    397   status = GetElementRegion(session, web_view, element_id, &rect);
    398   if (status.IsError())
    399     return status;
    400 
    401   std::string tmp_element_id = element_id;
    402   int build_no = session->chrome->GetBrowserInfo()->build_no;
    403   if (tag_name == "area" && build_no < 1799 && build_no >= 1666) {
    404     // This is to skip clickable verification for <area>.
    405     // The problem is caused by document.ElementFromPoint(crbug.com/338601).
    406     // It was introduced by blink r159012, which rolled into chromium r227489.
    407     // And it was fixed in blink r165426, which rolled into chromium r245994.
    408     // TODO(stgao): Revert after 33 is not supported.
    409     tmp_element_id = std::string();
    410   }
    411 
    412   status = ScrollElementRegionIntoView(
    413       session, web_view, target_element_id, rect,
    414       true /* center */, tmp_element_id, location);
    415   if (status.IsError())
    416     return status;
    417   location->Offset(rect.Width() / 2, rect.Height() / 2);
    418   return Status(kOk);
    419 }
    420 
    421 Status GetElementEffectiveStyle(
    422     Session* session,
    423     WebView* web_view,
    424     const std::string& element_id,
    425     const std::string& property_name,
    426     std::string* property_value) {
    427   return GetElementEffectiveStyle(session->GetCurrentFrameId(), web_view,
    428                                   element_id, property_name, property_value);
    429 }
    430 
    431 Status GetElementRegion(
    432     Session* session,
    433     WebView* web_view,
    434     const std::string& element_id,
    435     WebRect* rect) {
    436   base::ListValue args;
    437   args.Append(CreateElement(element_id));
    438   scoped_ptr<base::Value> result;
    439   Status status = web_view->CallFunction(
    440       session->GetCurrentFrameId(), kGetElementRegionScript, args, &result);
    441   if (status.IsError())
    442     return status;
    443   if (!ParseFromValue(result.get(), rect)) {
    444     return Status(kUnknownError,
    445                   "failed to parse value of getElementRegion");
    446   }
    447   return Status(kOk);
    448 }
    449 
    450 Status GetElementTagName(
    451     Session* session,
    452     WebView* web_view,
    453     const std::string& element_id,
    454     std::string* name) {
    455   base::ListValue args;
    456   args.Append(CreateElement(element_id));
    457   scoped_ptr<base::Value> result;
    458   Status status = web_view->CallFunction(
    459       session->GetCurrentFrameId(),
    460       "function(elem) { return elem.tagName.toLowerCase(); }",
    461       args, &result);
    462   if (status.IsError())
    463     return status;
    464   if (!result->GetAsString(name))
    465     return Status(kUnknownError, "failed to get element tag name");
    466   return Status(kOk);
    467 }
    468 
    469 Status GetElementSize(
    470     Session* session,
    471     WebView* web_view,
    472     const std::string& element_id,
    473     WebSize* size) {
    474   base::ListValue args;
    475   args.Append(CreateElement(element_id));
    476   scoped_ptr<base::Value> result;
    477   Status status = CallAtomsJs(
    478       session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_SIZE,
    479       args, &result);
    480   if (status.IsError())
    481     return status;
    482   if (!ParseFromValue(result.get(), size))
    483     return Status(kUnknownError, "failed to parse value of GET_SIZE");
    484   return Status(kOk);
    485 }
    486 
    487 Status IsElementDisplayed(
    488     Session* session,
    489     WebView* web_view,
    490     const std::string& element_id,
    491     bool ignore_opacity,
    492     bool* is_displayed) {
    493   base::ListValue args;
    494   args.Append(CreateElement(element_id));
    495   args.AppendBoolean(ignore_opacity);
    496   scoped_ptr<base::Value> result;
    497   Status status = CallAtomsJs(
    498       session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_DISPLAYED,
    499       args, &result);
    500   if (status.IsError())
    501     return status;
    502   if (!result->GetAsBoolean(is_displayed))
    503     return Status(kUnknownError, "IS_DISPLAYED should return a boolean value");
    504   return Status(kOk);
    505 }
    506 
    507 Status IsElementEnabled(
    508     Session* session,
    509     WebView* web_view,
    510     const std::string& element_id,
    511     bool* is_enabled) {
    512   base::ListValue args;
    513   args.Append(CreateElement(element_id));
    514   scoped_ptr<base::Value> result;
    515   Status status = CallAtomsJs(
    516       session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_ENABLED,
    517       args, &result);
    518   if (status.IsError())
    519     return status;
    520   if (!result->GetAsBoolean(is_enabled))
    521     return Status(kUnknownError, "IS_ENABLED should return a boolean value");
    522   return Status(kOk);
    523 }
    524 
    525 Status IsOptionElementSelected(
    526     Session* session,
    527     WebView* web_view,
    528     const std::string& element_id,
    529     bool* is_selected) {
    530   base::ListValue args;
    531   args.Append(CreateElement(element_id));
    532   scoped_ptr<base::Value> result;
    533   Status status = CallAtomsJs(
    534       session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_SELECTED,
    535       args, &result);
    536   if (status.IsError())
    537     return status;
    538   if (!result->GetAsBoolean(is_selected))
    539     return Status(kUnknownError, "IS_SELECTED should return a boolean value");
    540   return Status(kOk);
    541 }
    542 
    543 Status IsOptionElementTogglable(
    544     Session* session,
    545     WebView* web_view,
    546     const std::string& element_id,
    547     bool* is_togglable) {
    548   base::ListValue args;
    549   args.Append(CreateElement(element_id));
    550   scoped_ptr<base::Value> result;
    551   Status status = web_view->CallFunction(
    552       session->GetCurrentFrameId(), kIsOptionElementToggleableScript,
    553       args, &result);
    554   if (status.IsError())
    555     return status;
    556   if (!result->GetAsBoolean(is_togglable))
    557     return Status(kUnknownError, "failed check if option togglable or not");
    558   return Status(kOk);
    559 }
    560 
    561 Status SetOptionElementSelected(
    562     Session* session,
    563     WebView* web_view,
    564     const std::string& element_id,
    565     bool selected) {
    566   // TODO(171034): need to fix throwing error if an alert is triggered.
    567   base::ListValue args;
    568   args.Append(CreateElement(element_id));
    569   args.AppendBoolean(selected);
    570   scoped_ptr<base::Value> result;
    571   return CallAtomsJs(
    572       session->GetCurrentFrameId(), web_view, webdriver::atoms::CLICK,
    573       args, &result);
    574 }
    575 
    576 Status ToggleOptionElement(
    577     Session* session,
    578     WebView* web_view,
    579     const std::string& element_id) {
    580   bool is_selected;
    581   Status status = IsOptionElementSelected(
    582       session, web_view, element_id, &is_selected);
    583   if (status.IsError())
    584     return status;
    585   return SetOptionElementSelected(session, web_view, element_id, !is_selected);
    586 }
    587 
    588 Status ScrollElementIntoView(
    589     Session* session,
    590     WebView* web_view,
    591     const std::string& id,
    592     WebPoint* location) {
    593   WebSize size;
    594   Status status = GetElementSize(session, web_view, id, &size);
    595   if (status.IsError())
    596     return status;
    597   return ScrollElementRegionIntoView(
    598       session, web_view, id, WebRect(WebPoint(0, 0), size),
    599       false /* center */, std::string(), location);
    600 }
    601 
    602 Status ScrollElementRegionIntoView(
    603     Session* session,
    604     WebView* web_view,
    605     const std::string& element_id,
    606     const WebRect& region,
    607     bool center,
    608     const std::string& clickable_element_id,
    609     WebPoint* location) {
    610   WebPoint region_offset = region.origin;
    611   WebSize region_size = region.size;
    612   Status status = ScrollElementRegionIntoViewHelper(
    613       session->GetCurrentFrameId(), web_view, element_id, region,
    614       center, clickable_element_id, &region_offset);
    615   if (status.IsError())
    616     return status;
    617   const char* kFindSubFrameScript =
    618       "function(xpath) {"
    619       "  return document.evaluate(xpath, document, null,"
    620       "      XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
    621       "}";
    622   for (std::list<FrameInfo>::reverse_iterator rit = session->frames.rbegin();
    623        rit != session->frames.rend(); ++rit) {
    624     base::ListValue args;
    625     args.AppendString(
    626         base::StringPrintf("//*[@cd_frame_id_ = '%s']",
    627                            rit->chromedriver_frame_id.c_str()));
    628     scoped_ptr<base::Value> result;
    629     status = web_view->CallFunction(
    630         rit->parent_frame_id, kFindSubFrameScript, args, &result);
    631     if (status.IsError())
    632       return status;
    633     const base::DictionaryValue* element_dict;
    634     if (!result->GetAsDictionary(&element_dict))
    635       return Status(kUnknownError, "no element reference returned by script");
    636     std::string frame_element_id;
    637     if (!element_dict->GetString(kElementKey, &frame_element_id))
    638       return Status(kUnknownError, "failed to locate a sub frame");
    639 
    640     // Modify |region_offset| by the frame's border.
    641     int border_left = -1;
    642     int border_top = -1;
    643     status = GetElementBorder(
    644         rit->parent_frame_id, web_view, frame_element_id,
    645         &border_left, &border_top);
    646     if (status.IsError())
    647       return status;
    648     region_offset.Offset(border_left, border_top);
    649 
    650     status = ScrollElementRegionIntoViewHelper(
    651         rit->parent_frame_id, web_view, frame_element_id,
    652         WebRect(region_offset, region_size),
    653         center, frame_element_id, &region_offset);
    654     if (status.IsError())
    655       return status;
    656   }
    657   *location = region_offset;
    658   return Status(kOk);
    659 }
    660