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, ®ion_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, ®ion_offset); 636 if (status.IsError()) 637 return status; 638 } 639 *location = region_offset; 640 return Status(kOk); 641 } 642