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, ®ion_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, ®ion_offset); 618 if (status.IsError()) 619 return status; 620 } 621 *location = region_offset; 622 return Status(kOk); 623 } 624