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