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.
5 #include "chrome/test/chromedriver/element_util.h"
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"
24 const char kElementKey[] = "ELEMENT";
26 bool ParseFromValue(base::Value* value, WebPoint* point) {
27 base::DictionaryValue* dict_value;
28 if (!value->GetAsDictionary(&dict_value))
32 if (!dict_value->GetDouble("x", &x) ||
33 !dict_value->GetDouble("y", &y))
35 point->x = static_cast<int>(x);
36 point->y = static_cast<int>(y);
40 bool ParseFromValue(base::Value* value, WebSize* size) {
41 base::DictionaryValue* dict_value;
42 if (!value->GetAsDictionary(&dict_value))
46 if (!dict_value->GetDouble("width", &width) ||
47 !dict_value->GetDouble("height", &height))
49 size->width = static_cast<int>(width);
50 size->height = static_cast<int>(height);
54 bool ParseFromValue(base::Value* value, WebRect* rect) {
55 base::DictionaryValue* dict_value;
56 if (!value->GetAsDictionary(&dict_value))
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))
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);
74 base::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());
84 const std::string& frame,
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);
93 Status VerifyElementClickable(
94 const std::string& frame,
96 const std::string& element_id,
97 const WebPoint& location) {
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,
105 if (status.IsError())
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");
117 if (!dict->GetString("message", &message))
118 message = "element is not clickable";
119 return Status(kUnknownError, message);
124 Status ScrollElementRegionIntoViewHelper(
125 const std::string& frame,
127 const std::string& element_id,
128 const WebRect& region,
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),
141 if (status.IsError())
143 if (!ParseFromValue(result.get(), &tmp_location)) {
144 return Status(kUnknownError,
145 "failed to parse value of GET_LOCATION_IN_VIEW");
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())
155 *location = tmp_location;
159 Status GetElementEffectiveStyle(
160 const std::string& frame,
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),
172 if (status.IsError())
174 if (!result->GetAsString(value)) {
175 return Status(kUnknownError,
176 "failed to parse value of GET_EFFECTIVE_STYLE");
181 Status GetElementBorder(
182 const std::string& frame,
184 const std::string& element_id,
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())
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())
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;
210 base::DictionaryValue* CreateElement(const std::string& element_id) {
211 base::DictionaryValue* element = new base::DictionaryValue();
212 element->SetString(kElementKey, element_id);
216 base::Value* CreateValueFrom(const WebPoint& point) {
217 base::DictionaryValue* dict = new base::DictionaryValue();
218 dict->SetInteger("x", point.x);
219 dict->SetInteger("y", point.y);
226 const std::string* root_element_id,
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");
235 if (!params.GetString("value", &target))
236 return Status(kUnknownError, "'value' must be a string");
240 script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENT);
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());
248 arguments.Append(CreateElement(*root_element_id));
250 base::TimeTicks start_time = base::TimeTicks::Now();
252 scoped_ptr<base::Value> temp;
253 Status status = web_view->CallFunction(
254 session->GetCurrentFrameId(), script, arguments, &temp);
255 if (status.IsError())
258 if (!temp->IsType(base::Value::TYPE_NULL)) {
260 value->reset(temp.release());
263 base::ListValue* result;
264 if (!temp->GetAsList(&result))
265 return Status(kUnknownError, "script returns unexpected result");
267 if (result->GetSize() > 0U) {
268 value->reset(temp.release());
274 if (base::TimeTicks::Now() - start_time >= session->implicit_wait) {
276 return Status(kNoSuchElement);
278 value->reset(new base::ListValue());
282 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(interval_ms));
285 return Status(kUnknownError);
288 Status GetActiveElement(
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 }",
300 Status IsElementFocused(
303 const std::string& element_id,
305 scoped_ptr<base::Value> result;
306 Status status = GetActiveElement(session, web_view, &result);
307 if (status.IsError())
309 scoped_ptr<base::Value> element_dict(CreateElement(element_id));
310 *is_focused = result->Equals(element_dict.get());
314 Status GetElementAttribute(
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);
324 session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_ATTRIBUTE,
328 Status IsElementAttributeEqualToIgnoreCase(
331 const std::string& element_id,
332 const std::string& attribute_name,
333 const std::string& attribute_value,
335 scoped_ptr<base::Value> result;
336 Status status = GetElementAttribute(
337 session, web_view, element_id, attribute_name, &result);
338 if (status.IsError())
340 std::string actual_value;
341 if (result->GetAsString(&actual_value))
342 *is_equal = LowerCaseEqualsASCII(actual_value, attribute_value.c_str());
348 Status GetElementClickableLocation(
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())
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)"
374 " throw new Error('no img is found for the area');"
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())
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");
388 bool is_displayed = false;
389 status = IsElementDisplayed(
390 session, web_view, target_element_id, true, &is_displayed);
391 if (status.IsError())
394 return Status(kElementNotVisible);
397 status = GetElementRegion(session, web_view, element_id, &rect);
398 if (status.IsError())
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();
412 status = ScrollElementRegionIntoView(
413 session, web_view, target_element_id, rect,
414 true /* center */, tmp_element_id, location);
415 if (status.IsError())
417 location->Offset(rect.Width() / 2, rect.Height() / 2);
421 Status GetElementEffectiveStyle(
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);
431 Status GetElementRegion(
434 const std::string& element_id,
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())
443 if (!ParseFromValue(result.get(), rect)) {
444 return Status(kUnknownError,
445 "failed to parse value of getElementRegion");
450 Status GetElementTagName(
453 const std::string& element_id,
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(); }",
462 if (status.IsError())
464 if (!result->GetAsString(name))
465 return Status(kUnknownError, "failed to get element tag name");
469 Status GetElementSize(
472 const std::string& element_id,
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,
480 if (status.IsError())
482 if (!ParseFromValue(result.get(), size))
483 return Status(kUnknownError, "failed to parse value of GET_SIZE");
487 Status IsElementDisplayed(
490 const std::string& element_id,
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,
500 if (status.IsError())
502 if (!result->GetAsBoolean(is_displayed))
503 return Status(kUnknownError, "IS_DISPLAYED should return a boolean value");
507 Status IsElementEnabled(
510 const std::string& element_id,
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,
518 if (status.IsError())
520 if (!result->GetAsBoolean(is_enabled))
521 return Status(kUnknownError, "IS_ENABLED should return a boolean value");
525 Status IsOptionElementSelected(
528 const std::string& element_id,
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,
536 if (status.IsError())
538 if (!result->GetAsBoolean(is_selected))
539 return Status(kUnknownError, "IS_SELECTED should return a boolean value");
543 Status IsOptionElementTogglable(
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,
554 if (status.IsError())
556 if (!result->GetAsBoolean(is_togglable))
557 return Status(kUnknownError, "failed check if option togglable or not");
561 Status SetOptionElementSelected(
564 const std::string& element_id,
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;
572 session->GetCurrentFrameId(), web_view, webdriver::atoms::CLICK,
576 Status ToggleOptionElement(
579 const std::string& element_id) {
581 Status status = IsOptionElementSelected(
582 session, web_view, element_id, &is_selected);
583 if (status.IsError())
585 return SetOptionElementSelected(session, web_view, element_id, !is_selected);
588 Status ScrollElementIntoView(
591 const std::string& id,
592 WebPoint* location) {
594 Status status = GetElementSize(session, web_view, id, &size);
595 if (status.IsError())
597 return ScrollElementRegionIntoView(
598 session, web_view, id, WebRect(WebPoint(0, 0), size),
599 false /* center */, std::string(), location);
602 Status ScrollElementRegionIntoView(
605 const std::string& element_id,
606 const WebRect& region,
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, ®ion_offset);
615 if (status.IsError())
617 const char kFindSubFrameScript[] =
619 " return document.evaluate(xpath, document, null,"
620 " XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
622 for (std::list<FrameInfo>::reverse_iterator rit = session->frames.rbegin();
623 rit != session->frames.rend(); ++rit) {
624 base::ListValue args;
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())
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");
640 // Modify |region_offset| by the frame's border.
641 int border_left = -1;
643 status = GetElementBorder(
644 rit->parent_frame_id, web_view, frame_element_id,
645 &border_left, &border_top);
646 if (status.IsError())
648 region_offset.Offset(border_left, border_top);
650 status = ScrollElementRegionIntoViewHelper(
651 rit->parent_frame_id, web_view, frame_element_id,
652 WebRect(region_offset, region_size),
653 center, frame_element_id, ®ion_offset);
654 if (status.IsError())
657 *location = region_offset;