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, ®ion_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, ®ion_offset); 654 if (status.IsError()) 655 return status; 656 } 657 *location = region_offset; 658 return Status(kOk); 659 } 660