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
22namespace {
23
24const char kElementKey[] = "ELEMENT";
25
26bool 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
40bool 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
54bool 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
74base::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
83Status 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
93Status 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
124Status 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
159Status 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
181Status 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
210base::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
216base::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
223Status 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
288Status 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
300Status 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
314Status 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
328Status 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
348Status 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
421Status 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
431Status 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
450Status 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
469Status 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
487Status 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
507Status 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
525Status 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
543Status 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
561Status 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
576Status 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
588Status 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
602Status 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, &region_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, &region_offset);
654    if (status.IsError())
655      return status;
656  }
657  *location = region_offset;
658  return Status(kOk);
659}
660