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
20namespace {
21
22const char kElementKey[] = "ELEMENT";
23
24bool 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
37bool 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
50bool 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
67base::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
76Status 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
86Status 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
117Status 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
152Status 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
174Status 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
203base::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
209base::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
216Status 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
281Status 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
293Status 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
307Status 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
321Status 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
341Status 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
403Status 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
413Status 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
432Status 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
451Status 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
469Status 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
489Status 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
507Status 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
525Status 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
543Status 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
558Status 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
570Status 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
584Status 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, &region_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, &region_offset);
636    if (status.IsError())
637      return status;
638  }
639  *location = region_offset;
640  return Status(kOk);
641}
642