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 base::Value* CreateValueFrom(const WebSize& size) {
     51   base::DictionaryValue* dict = new base::DictionaryValue();
     52   dict->SetInteger("width", size.width);
     53   dict->SetInteger("height", size.height);
     54   return dict;
     55 }
     56 
     57 bool ParseFromValue(base::Value* value, WebRect* rect) {
     58   base::DictionaryValue* dict_value;
     59   if (!value->GetAsDictionary(&dict_value))
     60     return false;
     61   double x, y, width, height;
     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;
    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::Time start_time = base::Time::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::Time::Now() - start_time).InMilliseconds() >=
    275         session->implicit_wait) {
    276       if (only_one) {
    277         return Status(kNoSuchElement);
    278       } else {
    279         value->reset(new base::ListValue());
    280         return Status(kOk);
    281       }
    282     }
    283     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(interval_ms));
    284   }
    285 
    286   return Status(kUnknownError);
    287 }
    288 
    289 Status GetElementAttribute(
    290     Session* session,
    291     WebView* web_view,
    292     const std::string& element_id,
    293     const std::string& attribute_name,
    294     scoped_ptr<base::Value>* value) {
    295   base::ListValue args;
    296   args.Append(CreateElement(element_id));
    297   args.AppendString(attribute_name);
    298   return CallAtomsJs(
    299       session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_ATTRIBUTE,
    300       args, value);
    301 }
    302 
    303 Status IsElementAttributeEqualToIgnoreCase(
    304     Session* session,
    305     WebView* web_view,
    306     const std::string& element_id,
    307     const std::string& attribute_name,
    308     const std::string& attribute_value,
    309     bool* is_equal) {
    310   scoped_ptr<base::Value> result;
    311   Status status = GetElementAttribute(
    312       session, web_view, element_id, attribute_name, &result);
    313   if (status.IsError())
    314     return status;
    315   std::string actual_value;
    316   if (result->GetAsString(&actual_value))
    317     *is_equal = LowerCaseEqualsASCII(actual_value, attribute_value.c_str());
    318   else
    319     *is_equal = false;
    320   return status;
    321 }
    322 
    323 Status GetElementClickableLocation(
    324     Session* session,
    325     WebView* web_view,
    326     const std::string& element_id,
    327     WebPoint* location) {
    328   std::string tag_name;
    329   Status status = GetElementTagName(session, web_view, element_id, &tag_name);
    330   if (status.IsError())
    331     return status;
    332   std::string target_element_id = element_id;
    333   if (tag_name == "area") {
    334     // Scroll the image into view instead of the area.
    335     const char* kGetImageElementForArea =
    336         "function (element) {"
    337         "  var map = element.parentElement;"
    338         "  if (map.tagName.toLowerCase() != 'map')"
    339         "    throw new Error('the area is not within a map');"
    340         "  var mapName = map.getAttribute('name');"
    341         "  if (mapName == null)"
    342         "    throw new Error ('area\\'s parent map must have a name');"
    343         "  mapName = '#' + mapName.toLowerCase();"
    344         "  var images = document.getElementsByTagName('img');"
    345         "  for (var i = 0; i < images.length; i++) {"
    346         "    if (images[i].useMap.toLowerCase() == mapName)"
    347         "      return images[i];"
    348         "  }"
    349         "  throw new Error('no img is found for the area');"
    350         "}";
    351     base::ListValue args;
    352     args.Append(CreateElement(element_id));
    353     scoped_ptr<base::Value> result;
    354     status = web_view->CallFunction(
    355         session->GetCurrentFrameId(), kGetImageElementForArea, args, &result);
    356     if (status.IsError())
    357       return status;
    358     const base::DictionaryValue* element_dict;
    359     if (!result->GetAsDictionary(&element_dict) ||
    360         !element_dict->GetString(kElementKey, &target_element_id))
    361       return Status(kUnknownError, "no element reference returned by script");
    362   }
    363   bool is_displayed = false;
    364   status = IsElementDisplayed(
    365       session, web_view, target_element_id, true, &is_displayed);
    366   if (status.IsError())
    367     return status;
    368   if (!is_displayed)
    369     return Status(kElementNotVisible);
    370 
    371   WebRect rect;
    372   status = GetElementRegion(session, web_view, element_id, &rect);
    373   if (status.IsError())
    374     return status;
    375 
    376   status = ScrollElementRegionIntoView(
    377       session, web_view, target_element_id, rect,
    378       true /* center */, element_id, location);
    379   if (status.IsError())
    380     return status;
    381   location->Offset(rect.Width() / 2, rect.Height() / 2);
    382   return Status(kOk);
    383 }
    384 
    385 Status GetElementEffectiveStyle(
    386     Session* session,
    387     WebView* web_view,
    388     const std::string& element_id,
    389     const std::string& property_name,
    390     std::string* property_value) {
    391   return GetElementEffectiveStyle(session->GetCurrentFrameId(), web_view,
    392                                   element_id, property_name, property_value);
    393 }
    394 
    395 Status GetElementRegion(
    396     Session* session,
    397     WebView* web_view,
    398     const std::string& element_id,
    399     WebRect* rect) {
    400   base::ListValue args;
    401   args.Append(CreateElement(element_id));
    402   scoped_ptr<base::Value> result;
    403   Status status = web_view->CallFunction(
    404       session->GetCurrentFrameId(), kGetElementRegionScript, args, &result);
    405   if (status.IsError())
    406     return status;
    407   if (!ParseFromValue(result.get(), rect)) {
    408     return Status(kUnknownError,
    409                   "failed to parse value of getElementRegion");
    410   }
    411   return Status(kOk);
    412 }
    413 
    414 Status GetElementTagName(
    415     Session* session,
    416     WebView* web_view,
    417     const std::string& element_id,
    418     std::string* name) {
    419   base::ListValue args;
    420   args.Append(CreateElement(element_id));
    421   scoped_ptr<base::Value> result;
    422   Status status = web_view->CallFunction(
    423       session->GetCurrentFrameId(),
    424       "function(elem) { return elem.tagName.toLowerCase(); }",
    425       args, &result);
    426   if (status.IsError())
    427     return status;
    428   if (!result->GetAsString(name))
    429     return Status(kUnknownError, "failed to get element tag name");
    430   return Status(kOk);
    431 }
    432 
    433 Status GetElementSize(
    434     Session* session,
    435     WebView* web_view,
    436     const std::string& element_id,
    437     WebSize* size) {
    438   base::ListValue args;
    439   args.Append(CreateElement(element_id));
    440   scoped_ptr<base::Value> result;
    441   Status status = CallAtomsJs(
    442       session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_SIZE,
    443       args, &result);
    444   if (status.IsError())
    445     return status;
    446   if (!ParseFromValue(result.get(), size))
    447     return Status(kUnknownError, "failed to parse value of GET_SIZE");
    448   return Status(kOk);
    449 }
    450 
    451 Status IsElementDisplayed(
    452     Session* session,
    453     WebView* web_view,
    454     const std::string& element_id,
    455     bool ignore_opacity,
    456     bool* is_displayed) {
    457   base::ListValue args;
    458   args.Append(CreateElement(element_id));
    459   args.AppendBoolean(ignore_opacity);
    460   scoped_ptr<base::Value> result;
    461   Status status = CallAtomsJs(
    462       session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_DISPLAYED,
    463       args, &result);
    464   if (status.IsError())
    465     return status;
    466   if (!result->GetAsBoolean(is_displayed))
    467     return Status(kUnknownError, "IS_DISPLAYED should return a boolean value");
    468   return Status(kOk);
    469 }
    470 
    471 Status IsElementEnabled(
    472     Session* session,
    473     WebView* web_view,
    474     const std::string& element_id,
    475     bool* is_enabled) {
    476   base::ListValue args;
    477   args.Append(CreateElement(element_id));
    478   scoped_ptr<base::Value> result;
    479   Status status = CallAtomsJs(
    480       session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_ENABLED,
    481       args, &result);
    482   if (status.IsError())
    483     return status;
    484   if (!result->GetAsBoolean(is_enabled))
    485     return Status(kUnknownError, "IS_ENABLED should return a boolean value");
    486   return Status(kOk);
    487 }
    488 
    489 Status IsOptionElementSelected(
    490     Session* session,
    491     WebView* web_view,
    492     const std::string& element_id,
    493     bool* is_selected) {
    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_SELECTED,
    499       args, &result);
    500   if (status.IsError())
    501     return status;
    502   if (!result->GetAsBoolean(is_selected))
    503     return Status(kUnknownError, "IS_SELECTED should return a boolean value");
    504   return Status(kOk);
    505 }
    506 
    507 Status IsOptionElementTogglable(
    508     Session* session,
    509     WebView* web_view,
    510     const std::string& element_id,
    511     bool* is_togglable) {
    512   base::ListValue args;
    513   args.Append(CreateElement(element_id));
    514   scoped_ptr<base::Value> result;
    515   Status status = web_view->CallFunction(
    516       session->GetCurrentFrameId(), kIsOptionElementToggleableScript,
    517       args, &result);
    518   if (status.IsError())
    519     return status;
    520   if (!result->GetAsBoolean(is_togglable))
    521     return Status(kUnknownError, "failed check if option togglable or not");
    522   return Status(kOk);
    523 }
    524 
    525 Status SetOptionElementSelected(
    526     Session* session,
    527     WebView* web_view,
    528     const std::string& element_id,
    529     bool selected) {
    530   // TODO(171034): need to fix throwing error if an alert is triggered.
    531   base::ListValue args;
    532   args.Append(CreateElement(element_id));
    533   args.AppendBoolean(selected);
    534   scoped_ptr<base::Value> result;
    535   return CallAtomsJs(
    536       session->GetCurrentFrameId(), web_view, webdriver::atoms::CLICK,
    537       args, &result);
    538 }
    539 
    540 Status ToggleOptionElement(
    541     Session* session,
    542     WebView* web_view,
    543     const std::string& element_id) {
    544   bool is_selected;
    545   Status status = IsOptionElementSelected(
    546       session, web_view, element_id, &is_selected);
    547   if (status.IsError())
    548     return status;
    549   return SetOptionElementSelected(session, web_view, element_id, !is_selected);
    550 }
    551 
    552 Status ScrollElementIntoView(
    553     Session* session,
    554     WebView* web_view,
    555     const std::string& id,
    556     WebPoint* location) {
    557   WebSize size;
    558   Status status = GetElementSize(session, web_view, id, &size);
    559   if (status.IsError())
    560     return status;
    561   return ScrollElementRegionIntoView(
    562       session, web_view, id, WebRect(WebPoint(0, 0), size),
    563       false /* center */, std::string(), location);
    564 }
    565 
    566 Status ScrollElementRegionIntoView(
    567     Session* session,
    568     WebView* web_view,
    569     const std::string& element_id,
    570     const WebRect& region,
    571     bool center,
    572     const std::string& clickable_element_id,
    573     WebPoint* location) {
    574   WebPoint region_offset = region.origin;
    575   WebSize region_size = region.size;
    576   Status status = ScrollElementRegionIntoViewHelper(
    577       session->GetCurrentFrameId(), web_view, element_id, region,
    578       center, clickable_element_id, &region_offset);
    579   if (status.IsError())
    580     return status;
    581   const char* kFindSubFrameScript =
    582       "function(xpath) {"
    583       "  return document.evaluate(xpath, document, null,"
    584       "      XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
    585       "}";
    586   for (std::list<FrameInfo>::reverse_iterator rit = session->frames.rbegin();
    587        rit != session->frames.rend(); ++rit) {
    588     base::ListValue args;
    589     args.AppendString(
    590         base::StringPrintf("//*[@cd_frame_id_ = '%s']",
    591                            rit->chromedriver_frame_id.c_str()));
    592     scoped_ptr<base::Value> result;
    593     status = web_view->CallFunction(
    594         rit->parent_frame_id, kFindSubFrameScript, args, &result);
    595     if (status.IsError())
    596       return status;
    597     const base::DictionaryValue* element_dict;
    598     if (!result->GetAsDictionary(&element_dict))
    599       return Status(kUnknownError, "no element reference returned by script");
    600     std::string frame_element_id;
    601     if (!element_dict->GetString(kElementKey, &frame_element_id))
    602       return Status(kUnknownError, "failed to locate a sub frame");
    603 
    604     // Modify |region_offset| by the frame's border.
    605     int border_left = -1;
    606     int border_top = -1;
    607     status = GetElementBorder(
    608         rit->parent_frame_id, web_view, frame_element_id,
    609         &border_left, &border_top);
    610     if (status.IsError())
    611       return status;
    612     region_offset.Offset(border_left, border_top);
    613 
    614     status = ScrollElementRegionIntoViewHelper(
    615         rit->parent_frame_id, web_view, frame_element_id,
    616         WebRect(region_offset, region_size),
    617         center, frame_element_id, &region_offset);
    618     if (status.IsError())
    619       return status;
    620   }
    621   *location = region_offset;
    622   return Status(kOk);
    623 }
    624