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/chrome/web_view_impl.h" 6 7#include "base/bind.h" 8#include "base/files/file_path.h" 9#include "base/json/json_writer.h" 10#include "base/logging.h" 11#include "base/strings/string_util.h" 12#include "base/strings/stringprintf.h" 13#include "base/threading/platform_thread.h" 14#include "base/time/time.h" 15#include "base/values.h" 16#include "chrome/test/chromedriver/chrome/devtools_client_impl.h" 17#include "chrome/test/chromedriver/chrome/dom_tracker.h" 18#include "chrome/test/chromedriver/chrome/frame_tracker.h" 19#include "chrome/test/chromedriver/chrome/geolocation_override_manager.h" 20#include "chrome/test/chromedriver/chrome/javascript_dialog_manager.h" 21#include "chrome/test/chromedriver/chrome/js.h" 22#include "chrome/test/chromedriver/chrome/log.h" 23#include "chrome/test/chromedriver/chrome/navigation_tracker.h" 24#include "chrome/test/chromedriver/chrome/status.h" 25#include "chrome/test/chromedriver/chrome/ui_events.h" 26 27namespace { 28 29Status GetContextIdForFrame(FrameTracker* tracker, 30 const std::string& frame, 31 int* context_id) { 32 if (frame.empty()) { 33 *context_id = 0; 34 return Status(kOk); 35 } 36 Status status = tracker->GetContextIdForFrame(frame, context_id); 37 if (status.IsError()) 38 return status; 39 return Status(kOk); 40} 41 42const char* GetAsString(MouseEventType type) { 43 switch (type) { 44 case kPressedMouseEventType: 45 return "mousePressed"; 46 case kReleasedMouseEventType: 47 return "mouseReleased"; 48 case kMovedMouseEventType: 49 return "mouseMoved"; 50 default: 51 return ""; 52 } 53} 54 55const char* GetAsString(TouchEventType type) { 56 switch (type) { 57 case kTouchStart: 58 return "touchStart"; 59 case kTouchEnd: 60 return "touchEnd"; 61 case kTouchMove: 62 return "touchMove"; 63 default: 64 return ""; 65 } 66} 67 68const char* GetPointStateString(TouchEventType type) { 69 switch (type) { 70 case kTouchStart: 71 return "touchPressed"; 72 case kTouchEnd: 73 return "touchReleased"; 74 case kTouchMove: 75 return "touchMoved"; 76 default: 77 return ""; 78 } 79} 80 81const char* GetAsString(MouseButton button) { 82 switch (button) { 83 case kLeftMouseButton: 84 return "left"; 85 case kMiddleMouseButton: 86 return "middle"; 87 case kRightMouseButton: 88 return "right"; 89 case kNoneMouseButton: 90 return "none"; 91 default: 92 return ""; 93 } 94} 95 96const char* GetAsString(KeyEventType type) { 97 switch (type) { 98 case kKeyDownEventType: 99 return "keyDown"; 100 case kKeyUpEventType: 101 return "keyUp"; 102 case kRawKeyDownEventType: 103 return "rawKeyDown"; 104 case kCharEventType: 105 return "char"; 106 default: 107 return ""; 108 } 109} 110 111} // namespace 112 113WebViewImpl::WebViewImpl(const std::string& id, 114 int build_no, 115 scoped_ptr<DevToolsClient> client, 116 Log* log) 117 : id_(id), 118 build_no_(build_no), 119 dom_tracker_(new DomTracker(client.get())), 120 frame_tracker_(new FrameTracker(client.get())), 121 navigation_tracker_(new NavigationTracker(client.get())), 122 dialog_manager_(new JavaScriptDialogManager(client.get())), 123 geolocation_override_manager_( 124 new GeolocationOverrideManager(client.get())), 125 client_(client.release()), 126 log_(log) {} 127 128WebViewImpl::~WebViewImpl() {} 129 130std::string WebViewImpl::GetId() { 131 return id_; 132} 133 134Status WebViewImpl::ConnectIfNecessary() { 135 return client_->ConnectIfNecessary(); 136} 137 138Status WebViewImpl::HandleReceivedEvents() { 139 return client_->HandleReceivedEvents(); 140} 141 142Status WebViewImpl::Load(const std::string& url) { 143 // Javascript URLs will cause a hang while waiting for the page to stop 144 // loading, so just disallow. 145 if (StartsWithASCII(url, "javascript:", false)) 146 return Status(kUnknownError, "unsupported protocol"); 147 base::DictionaryValue params; 148 params.SetString("url", url); 149 return client_->SendCommand("Page.navigate", params); 150} 151 152Status WebViewImpl::Reload() { 153 base::DictionaryValue params; 154 params.SetBoolean("ignoreCache", false); 155 return client_->SendCommand("Page.reload", params); 156} 157 158Status WebViewImpl::EvaluateScript(const std::string& frame, 159 const std::string& expression, 160 scoped_ptr<base::Value>* result) { 161 int context_id; 162 Status status = GetContextIdForFrame(frame_tracker_.get(), frame, 163 &context_id); 164 if (status.IsError()) 165 return status; 166 return internal::EvaluateScriptAndGetValue( 167 client_.get(), context_id, expression, result); 168} 169 170Status WebViewImpl::CallFunction(const std::string& frame, 171 const std::string& function, 172 const base::ListValue& args, 173 scoped_ptr<base::Value>* result) { 174 std::string json; 175 base::JSONWriter::Write(&args, &json); 176 // TODO(zachconrad): Second null should be array of shadow host ids. 177 std::string expression = base::StringPrintf( 178 "(%s).apply(null, [null, %s, %s])", 179 kCallFunctionScript, 180 function.c_str(), 181 json.c_str()); 182 scoped_ptr<base::Value> temp_result; 183 Status status = EvaluateScript(frame, expression, &temp_result); 184 if (status.IsError()) 185 return status; 186 187 return internal::ParseCallFunctionResult(*temp_result, result); 188} 189 190Status WebViewImpl::CallAsyncFunction(const std::string& frame, 191 const std::string& function, 192 const base::ListValue& args, 193 const base::TimeDelta& timeout, 194 scoped_ptr<base::Value>* result) { 195 return CallAsyncFunctionInternal( 196 frame, function, args, false, timeout, result); 197} 198 199Status WebViewImpl::CallUserAsyncFunction(const std::string& frame, 200 const std::string& function, 201 const base::ListValue& args, 202 const base::TimeDelta& timeout, 203 scoped_ptr<base::Value>* result) { 204 return CallAsyncFunctionInternal( 205 frame, function, args, true, timeout, result); 206} 207 208Status WebViewImpl::GetFrameByFunction(const std::string& frame, 209 const std::string& function, 210 const base::ListValue& args, 211 std::string* out_frame) { 212 int context_id; 213 Status status = GetContextIdForFrame(frame_tracker_.get(), frame, 214 &context_id); 215 if (status.IsError()) 216 return status; 217 bool found_node; 218 int node_id; 219 status = internal::GetNodeIdFromFunction( 220 client_.get(), context_id, function, args, &found_node, &node_id); 221 if (status.IsError()) 222 return status; 223 if (!found_node) 224 return Status(kNoSuchFrame); 225 return dom_tracker_->GetFrameIdForNode(node_id, out_frame); 226} 227 228Status WebViewImpl::DispatchMouseEvents(const std::list<MouseEvent>& events, 229 const std::string& frame) { 230 for (std::list<MouseEvent>::const_iterator it = events.begin(); 231 it != events.end(); ++it) { 232 base::DictionaryValue params; 233 params.SetString("type", GetAsString(it->type)); 234 params.SetInteger("x", it->x); 235 params.SetInteger("y", it->y); 236 params.SetInteger("modifiers", it->modifiers); 237 params.SetString("button", GetAsString(it->button)); 238 params.SetInteger("clickCount", it->click_count); 239 Status status = client_->SendCommand("Input.dispatchMouseEvent", params); 240 if (status.IsError()) 241 return status; 242 if (build_no_ < 1569 && it->button == kRightMouseButton && 243 it->type == kReleasedMouseEventType) { 244 base::ListValue args; 245 args.AppendInteger(it->x); 246 args.AppendInteger(it->y); 247 args.AppendInteger(it->modifiers); 248 scoped_ptr<base::Value> result; 249 status = CallFunction( 250 frame, kDispatchContextMenuEventScript, args, &result); 251 if (status.IsError()) 252 return status; 253 } 254 } 255 return Status(kOk); 256} 257 258Status WebViewImpl::DispatchTouchEvents(const std::list<TouchEvent>& events) { 259 for (std::list<TouchEvent>::const_iterator it = events.begin(); 260 it != events.end(); ++it) { 261 base::DictionaryValue params; 262 params.SetString("type", GetAsString(it->type)); 263 scoped_ptr<base::ListValue> point_list(new base::ListValue); 264 scoped_ptr<base::DictionaryValue> point(new base::DictionaryValue); 265 point->SetString("state", GetPointStateString(it->type)); 266 point->SetInteger("x", it->x); 267 point->SetInteger("y", it->y); 268 point_list->Set(0, point.release()); 269 params.Set("touchPoints", point_list.release()); 270 Status status = client_->SendCommand("Input.dispatchTouchEvent", params); 271 if (status.IsError()) 272 return status; 273 } 274 return Status(kOk); 275} 276 277Status WebViewImpl::DispatchKeyEvents(const std::list<KeyEvent>& events) { 278 for (std::list<KeyEvent>::const_iterator it = events.begin(); 279 it != events.end(); ++it) { 280 base::DictionaryValue params; 281 params.SetString("type", GetAsString(it->type)); 282 if (it->modifiers & kNumLockKeyModifierMask) { 283 params.SetBoolean("isKeypad", true); 284 params.SetInteger("modifiers", 285 it->modifiers & ~kNumLockKeyModifierMask); 286 } else { 287 params.SetInteger("modifiers", it->modifiers); 288 } 289 params.SetString("text", it->modified_text); 290 params.SetString("unmodifiedText", it->unmodified_text); 291 params.SetInteger("nativeVirtualKeyCode", it->key_code); 292 params.SetInteger("windowsVirtualKeyCode", it->key_code); 293 Status status = client_->SendCommand("Input.dispatchKeyEvent", params); 294 if (status.IsError()) 295 return status; 296 } 297 return Status(kOk); 298} 299 300Status WebViewImpl::GetCookies(scoped_ptr<base::ListValue>* cookies) { 301 base::DictionaryValue params; 302 scoped_ptr<base::DictionaryValue> result; 303 Status status = client_->SendCommandAndGetResult( 304 "Page.getCookies", params, &result); 305 if (status.IsError()) 306 return status; 307 base::ListValue* cookies_tmp; 308 if (!result->GetList("cookies", &cookies_tmp)) 309 return Status(kUnknownError, "DevTools didn't return cookies"); 310 cookies->reset(cookies_tmp->DeepCopy()); 311 return Status(kOk); 312} 313 314Status WebViewImpl::DeleteCookie(const std::string& name, 315 const std::string& url) { 316 base::DictionaryValue params; 317 params.SetString("cookieName", name); 318 params.SetString("url", url); 319 return client_->SendCommand("Page.deleteCookie", params); 320} 321 322Status WebViewImpl::WaitForPendingNavigations(const std::string& frame_id, 323 int timeout) { 324 log_->AddEntry(Log::kLog, "waiting for pending navigations..."); 325 Status status = client_->HandleEventsUntil( 326 base::Bind(&WebViewImpl::IsNotPendingNavigation, base::Unretained(this), 327 frame_id), 328 base::TimeDelta::FromMilliseconds(timeout)); 329 if (status.code() == kTimeout) { 330 log_->AddEntry(Log::kLog, "timed out. stopping navigations..."); 331 scoped_ptr<base::Value> unused_value; 332 EvaluateScript(std::string(), "window.stop();", &unused_value); 333 Status new_status = client_->HandleEventsUntil( 334 base::Bind(&WebViewImpl::IsNotPendingNavigation, base::Unretained(this), 335 frame_id), 336 base::TimeDelta::FromSeconds(10)); 337 if (new_status.IsError()) 338 status = new_status; 339 } 340 log_->AddEntry(Log::kLog, "done waiting for pending navigations"); 341 return status; 342} 343 344Status WebViewImpl::IsPendingNavigation(const std::string& frame_id, 345 bool* is_pending) { 346 return navigation_tracker_->IsPendingNavigation(frame_id, is_pending); 347} 348 349JavaScriptDialogManager* WebViewImpl::GetJavaScriptDialogManager() { 350 return dialog_manager_.get(); 351} 352 353Status WebViewImpl::OverrideGeolocation(const Geoposition& geoposition) { 354 return geolocation_override_manager_->OverrideGeolocation(geoposition); 355} 356 357Status WebViewImpl::CaptureScreenshot(std::string* screenshot) { 358 base::DictionaryValue params; 359 scoped_ptr<base::DictionaryValue> result; 360 Status status = client_->SendCommandAndGetResult( 361 "Page.captureScreenshot", params, &result); 362 if (status.IsError()) 363 return status; 364 if (!result->GetString("data", screenshot)) 365 return Status(kUnknownError, "expected string 'data' in response"); 366 return Status(kOk); 367} 368 369Status WebViewImpl::SetFileInputFiles( 370 const std::string& frame, 371 const base::DictionaryValue& element, 372 const std::vector<base::FilePath>& files) { 373 base::ListValue file_list; 374 for (size_t i = 0; i < files.size(); ++i) { 375 if (!files[i].IsAbsolute()) { 376 return Status(kUnknownError, 377 "path is not absolute: " + files[i].AsUTF8Unsafe()); 378 } 379 if (files[i].ReferencesParent()) { 380 return Status(kUnknownError, 381 "path is not canonical: " + files[i].AsUTF8Unsafe()); 382 } 383 file_list.AppendString(files[i].value()); 384 } 385 386 int context_id; 387 Status status = GetContextIdForFrame(frame_tracker_.get(), frame, 388 &context_id); 389 if (status.IsError()) 390 return status; 391 base::ListValue args; 392 args.Append(element.DeepCopy()); 393 bool found_node; 394 int node_id; 395 status = internal::GetNodeIdFromFunction( 396 client_.get(), context_id, "function(element) { return element; }", 397 args, &found_node, &node_id); 398 if (status.IsError()) 399 return status; 400 if (!found_node) 401 return Status(kUnknownError, "no node ID for file input"); 402 base::DictionaryValue params; 403 params.SetInteger("nodeId", node_id); 404 params.Set("files", file_list.DeepCopy()); 405 return client_->SendCommand("DOM.setFileInputFiles", params); 406} 407 408Status WebViewImpl::CallAsyncFunctionInternal(const std::string& frame, 409 const std::string& function, 410 const base::ListValue& args, 411 bool is_user_supplied, 412 const base::TimeDelta& timeout, 413 scoped_ptr<base::Value>* result) { 414 base::ListValue async_args; 415 async_args.AppendString("return (" + function + ").apply(null, arguments);"); 416 async_args.Append(args.DeepCopy()); 417 async_args.AppendBoolean(is_user_supplied); 418 async_args.AppendInteger(timeout.InMilliseconds()); 419 scoped_ptr<base::Value> tmp; 420 Status status = CallFunction( 421 frame, kExecuteAsyncScriptScript, async_args, &tmp); 422 if (status.IsError()) 423 return status; 424 425 const char* kDocUnloadError = "document unloaded while waiting for result"; 426 std::string kQueryResult = base::StringPrintf( 427 "function() {" 428 " var info = document.$chrome_asyncScriptInfo;" 429 " if (!info)" 430 " return {status: %d, value: '%s'};" 431 " var result = info.result;" 432 " if (!result)" 433 " return {status: 0};" 434 " delete info.result;" 435 " return result;" 436 "}", 437 kJavaScriptError, 438 kDocUnloadError); 439 440 while (true) { 441 base::ListValue no_args; 442 scoped_ptr<base::Value> query_value; 443 Status status = CallFunction(frame, kQueryResult, no_args, &query_value); 444 if (status.IsError()) { 445 if (status.code() == kNoSuchFrame) 446 return Status(kJavaScriptError, kDocUnloadError); 447 return status; 448 } 449 450 base::DictionaryValue* result_info = NULL; 451 if (!query_value->GetAsDictionary(&result_info)) 452 return Status(kUnknownError, "async result info is not a dictionary"); 453 int status_code; 454 if (!result_info->GetInteger("status", &status_code)) 455 return Status(kUnknownError, "async result info has no int 'status'"); 456 if (status_code != kOk) { 457 std::string message; 458 result_info->GetString("value", &message); 459 return Status(static_cast<StatusCode>(status_code), message); 460 } 461 462 base::Value* value = NULL; 463 if (result_info->Get("value", &value)) { 464 result->reset(value->DeepCopy()); 465 return Status(kOk); 466 } 467 468 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100)); 469 } 470} 471 472Status WebViewImpl::IsNotPendingNavigation(const std::string& frame_id, 473 bool* is_not_pending) { 474 bool is_pending; 475 Status status = 476 navigation_tracker_->IsPendingNavigation(frame_id, &is_pending); 477 if (status.IsError()) 478 return status; 479 // An alert may block the pending navigation. 480 if (is_pending && dialog_manager_->IsDialogOpen()) 481 return Status(kUnexpectedAlertOpen); 482 483 *is_not_pending = !is_pending; 484 return Status(kOk); 485} 486 487namespace internal { 488 489Status EvaluateScript(DevToolsClient* client, 490 int context_id, 491 const std::string& expression, 492 EvaluateScriptReturnType return_type, 493 scoped_ptr<base::DictionaryValue>* result) { 494 base::DictionaryValue params; 495 params.SetString("expression", expression); 496 if (context_id) 497 params.SetInteger("contextId", context_id); 498 params.SetBoolean("returnByValue", return_type == ReturnByValue); 499 scoped_ptr<base::DictionaryValue> cmd_result; 500 Status status = client->SendCommandAndGetResult( 501 "Runtime.evaluate", params, &cmd_result); 502 if (status.IsError()) 503 return status; 504 505 bool was_thrown; 506 if (!cmd_result->GetBoolean("wasThrown", &was_thrown)) 507 return Status(kUnknownError, "Runtime.evaluate missing 'wasThrown'"); 508 if (was_thrown) { 509 std::string description = "unknown"; 510 cmd_result->GetString("result.description", &description); 511 return Status(kUnknownError, 512 "Runtime.evaluate threw exception: " + description); 513 } 514 515 base::DictionaryValue* unscoped_result; 516 if (!cmd_result->GetDictionary("result", &unscoped_result)) 517 return Status(kUnknownError, "evaluate missing dictionary 'result'"); 518 result->reset(unscoped_result->DeepCopy()); 519 return Status(kOk); 520} 521 522Status EvaluateScriptAndGetObject(DevToolsClient* client, 523 int context_id, 524 const std::string& expression, 525 bool* got_object, 526 std::string* object_id) { 527 scoped_ptr<base::DictionaryValue> result; 528 Status status = EvaluateScript(client, context_id, expression, ReturnByObject, 529 &result); 530 if (status.IsError()) 531 return status; 532 if (!result->HasKey("objectId")) { 533 *got_object = false; 534 return Status(kOk); 535 } 536 if (!result->GetString("objectId", object_id)) 537 return Status(kUnknownError, "evaluate has invalid 'objectId'"); 538 *got_object = true; 539 return Status(kOk); 540} 541 542Status EvaluateScriptAndGetValue(DevToolsClient* client, 543 int context_id, 544 const std::string& expression, 545 scoped_ptr<base::Value>* result) { 546 scoped_ptr<base::DictionaryValue> temp_result; 547 Status status = EvaluateScript(client, context_id, expression, ReturnByValue, 548 &temp_result); 549 if (status.IsError()) 550 return status; 551 552 std::string type; 553 if (!temp_result->GetString("type", &type)) 554 return Status(kUnknownError, "Runtime.evaluate missing string 'type'"); 555 556 if (type == "undefined") { 557 result->reset(base::Value::CreateNullValue()); 558 } else { 559 base::Value* value; 560 if (!temp_result->Get("value", &value)) 561 return Status(kUnknownError, "Runtime.evaluate missing 'value'"); 562 result->reset(value->DeepCopy()); 563 } 564 return Status(kOk); 565} 566 567Status ParseCallFunctionResult(const base::Value& temp_result, 568 scoped_ptr<base::Value>* result) { 569 const base::DictionaryValue* dict; 570 if (!temp_result.GetAsDictionary(&dict)) 571 return Status(kUnknownError, "call function result must be a dictionary"); 572 int status_code; 573 if (!dict->GetInteger("status", &status_code)) { 574 return Status(kUnknownError, 575 "call function result missing int 'status'"); 576 } 577 if (status_code != kOk) { 578 std::string message; 579 dict->GetString("value", &message); 580 return Status(static_cast<StatusCode>(status_code), message); 581 } 582 const base::Value* unscoped_value; 583 if (!dict->Get("value", &unscoped_value)) { 584 return Status(kUnknownError, 585 "call function result missing 'value'"); 586 } 587 result->reset(unscoped_value->DeepCopy()); 588 return Status(kOk); 589} 590 591Status GetNodeIdFromFunction(DevToolsClient* client, 592 int context_id, 593 const std::string& function, 594 const base::ListValue& args, 595 bool* found_node, 596 int* node_id) { 597 std::string json; 598 base::JSONWriter::Write(&args, &json); 599 // TODO(zachconrad): Second null should be array of shadow host ids. 600 std::string expression = base::StringPrintf( 601 "(%s).apply(null, [null, %s, %s, true])", 602 kCallFunctionScript, 603 function.c_str(), 604 json.c_str()); 605 606 bool got_object; 607 std::string element_id; 608 Status status = internal::EvaluateScriptAndGetObject( 609 client, context_id, expression, &got_object, &element_id); 610 if (status.IsError()) 611 return status; 612 if (!got_object) { 613 *found_node = false; 614 return Status(kOk); 615 } 616 617 scoped_ptr<base::DictionaryValue> cmd_result; 618 { 619 base::DictionaryValue params; 620 params.SetString("objectId", element_id); 621 status = client->SendCommandAndGetResult( 622 "DOM.requestNode", params, &cmd_result); 623 } 624 { 625 // Release the remote object before doing anything else. 626 base::DictionaryValue params; 627 params.SetString("objectId", element_id); 628 Status release_status = 629 client->SendCommand("Runtime.releaseObject", params); 630 if (release_status.IsError()) { 631 LOG(ERROR) << "Failed to release remote object: " 632 << release_status.message(); 633 } 634 } 635 if (status.IsError()) 636 return status; 637 638 if (!cmd_result->GetInteger("nodeId", node_id)) 639 return Status(kUnknownError, "DOM.requestNode missing int 'nodeId'"); 640 *found_node = true; 641 return Status(kOk); 642} 643 644} // namespace internal 645