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