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/chrome.h"
15 #include "chrome/test/chromedriver/chrome/js.h"
16 #include "chrome/test/chromedriver/chrome/status.h"
17 #include "chrome/test/chromedriver/chrome/version.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))
31 if (!dict_value->GetDouble("x", &x) ||
32 !dict_value->GetDouble("y", &y))
34 point->x = static_cast<int>(x);
35 point->y = static_cast<int>(y);
39 bool ParseFromValue(base::Value* value, WebSize* size) {
40 base::DictionaryValue* dict_value;
41 if (!value->GetAsDictionary(&dict_value))
44 if (!dict_value->GetDouble("width", &width) ||
45 !dict_value->GetDouble("height", &height))
47 size->width = static_cast<int>(width);
48 size->height = static_cast<int>(height);
52 bool ParseFromValue(base::Value* value, WebRect* rect) {
53 base::DictionaryValue* dict_value;
54 if (!value->GetAsDictionary(&dict_value))
56 double x, y, width, height;
57 if (!dict_value->GetDouble("left", &x) ||
58 !dict_value->GetDouble("top", &y) ||
59 !dict_value->GetDouble("width", &width) ||
60 !dict_value->GetDouble("height", &height))
62 rect->origin.x = static_cast<int>(x);
63 rect->origin.y = static_cast<int>(y);
64 rect->size.width = static_cast<int>(width);
65 rect->size.height = static_cast<int>(height);
69 base::Value* CreateValueFrom(const WebRect& rect) {
70 base::DictionaryValue* dict = new base::DictionaryValue();
71 dict->SetInteger("left", rect.X());
72 dict->SetInteger("top", rect.Y());
73 dict->SetInteger("width", rect.Width());
74 dict->SetInteger("height", rect.Height());
79 const std::string& frame,
81 const char* const* atom_function,
82 const base::ListValue& args,
83 scoped_ptr<base::Value>* result) {
84 return web_view->CallFunction(
85 frame, webdriver::atoms::asString(atom_function), args, result);
88 Status VerifyElementClickable(
89 const std::string& frame,
91 const std::string& element_id,
92 const WebPoint& location) {
94 args.Append(CreateElement(element_id));
95 args.Append(CreateValueFrom(location));
96 scoped_ptr<base::Value> result;
97 Status status = CallAtomsJs(
98 frame, web_view, webdriver::atoms::IS_ELEMENT_CLICKABLE,
100 if (status.IsError())
102 base::DictionaryValue* dict;
104 if (!result->GetAsDictionary(&dict) ||
105 !dict->GetBoolean("clickable", &is_clickable)) {
106 return Status(kUnknownError,
107 "failed to parse value of IS_ELEMENT_CLICKABLE");
112 if (!dict->GetString("message", &message))
113 message = "element is not clickable";
114 return Status(kUnknownError, message);
119 Status ScrollElementRegionIntoViewHelper(
120 const std::string& frame,
122 const std::string& element_id,
123 const WebRect& region,
125 const std::string& clickable_element_id,
126 WebPoint* location) {
127 WebPoint tmp_location = *location;
128 base::ListValue args;
129 args.Append(CreateElement(element_id));
130 args.AppendBoolean(center);
131 args.Append(CreateValueFrom(region));
132 scoped_ptr<base::Value> result;
133 Status status = web_view->CallFunction(
134 frame, webdriver::atoms::asString(webdriver::atoms::GET_LOCATION_IN_VIEW),
136 if (status.IsError())
138 if (!ParseFromValue(result.get(), &tmp_location)) {
139 return Status(kUnknownError,
140 "failed to parse value of GET_LOCATION_IN_VIEW");
142 if (!clickable_element_id.empty()) {
143 WebPoint middle = tmp_location;
144 middle.Offset(region.Width() / 2, region.Height() / 2);
145 status = VerifyElementClickable(
146 frame, web_view, clickable_element_id, middle);
147 if (status.IsError())
150 *location = tmp_location;
154 Status GetElementEffectiveStyle(
155 const std::string& frame,
157 const std::string& element_id,
158 const std::string& property,
159 std::string* value) {
160 base::ListValue args;
161 args.Append(CreateElement(element_id));
162 args.AppendString(property);
163 scoped_ptr<base::Value> result;
164 Status status = web_view->CallFunction(
165 frame, webdriver::atoms::asString(webdriver::atoms::GET_EFFECTIVE_STYLE),
167 if (status.IsError())
169 if (!result->GetAsString(value)) {
170 return Status(kUnknownError,
171 "failed to parse value of GET_EFFECTIVE_STYLE");
176 Status GetElementBorder(
177 const std::string& frame,
179 const std::string& element_id,
182 std::string border_left_str;
183 Status status = GetElementEffectiveStyle(
184 frame, web_view, element_id, "border-left-width", &border_left_str);
185 if (status.IsError())
187 std::string border_top_str;
188 status = GetElementEffectiveStyle(
189 frame, web_view, element_id, "border-top-width", &border_top_str);
190 if (status.IsError())
192 int border_left_tmp = -1;
193 int border_top_tmp = -1;
194 base::StringToInt(border_left_str, &border_left_tmp);
195 base::StringToInt(border_top_str, &border_top_tmp);
196 if (border_left_tmp == -1 || border_top_tmp == -1)
197 return Status(kUnknownError, "failed to get border width of element");
198 *border_left = border_left_tmp;
199 *border_top = border_top_tmp;
205 base::DictionaryValue* CreateElement(const std::string& element_id) {
206 base::DictionaryValue* element = new base::DictionaryValue();
207 element->SetString(kElementKey, element_id);
211 base::Value* CreateValueFrom(const WebPoint& point) {
212 base::DictionaryValue* dict = new base::DictionaryValue();
213 dict->SetInteger("x", point.x);
214 dict->SetInteger("y", point.y);
221 const std::string* root_element_id,
224 const base::DictionaryValue& params,
225 scoped_ptr<base::Value>* value) {
226 std::string strategy;
227 if (!params.GetString("using", &strategy))
228 return Status(kUnknownError, "'using' must be a string");
230 if (!params.GetString("value", &target))
231 return Status(kUnknownError, "'value' must be a string");
235 script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENT);
237 script = webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENTS);
238 scoped_ptr<base::DictionaryValue> locator(new base::DictionaryValue());
239 locator->SetString(strategy, target);
240 base::ListValue arguments;
241 arguments.Append(locator.release());
243 arguments.Append(CreateElement(*root_element_id));
245 base::TimeTicks start_time = base::TimeTicks::Now();
247 scoped_ptr<base::Value> temp;
248 Status status = web_view->CallFunction(
249 session->GetCurrentFrameId(), script, arguments, &temp);
250 if (status.IsError())
253 if (!temp->IsType(base::Value::TYPE_NULL)) {
255 value->reset(temp.release());
258 base::ListValue* result;
259 if (!temp->GetAsList(&result))
260 return Status(kUnknownError, "script returns unexpected result");
262 if (result->GetSize() > 0U) {
263 value->reset(temp.release());
269 if (base::TimeTicks::Now() - start_time >= session->implicit_wait) {
271 return Status(kNoSuchElement);
273 value->reset(new base::ListValue());
277 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(interval_ms));
280 return Status(kUnknownError);
283 Status GetActiveElement(
286 scoped_ptr<base::Value>* value) {
287 base::ListValue args;
288 return web_view->CallFunction(
289 session->GetCurrentFrameId(),
290 "function() { return document.activeElement || document.body }",
295 Status IsElementFocused(
298 const std::string& element_id,
300 scoped_ptr<base::Value> result;
301 Status status = GetActiveElement(session, web_view, &result);
302 if (status.IsError())
304 scoped_ptr<base::Value> element_dict(CreateElement(element_id));
305 *is_focused = result->Equals(element_dict.get());
309 Status GetElementAttribute(
312 const std::string& element_id,
313 const std::string& attribute_name,
314 scoped_ptr<base::Value>* value) {
315 base::ListValue args;
316 args.Append(CreateElement(element_id));
317 args.AppendString(attribute_name);
319 session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_ATTRIBUTE,
323 Status IsElementAttributeEqualToIgnoreCase(
326 const std::string& element_id,
327 const std::string& attribute_name,
328 const std::string& attribute_value,
330 scoped_ptr<base::Value> result;
331 Status status = GetElementAttribute(
332 session, web_view, element_id, attribute_name, &result);
333 if (status.IsError())
335 std::string actual_value;
336 if (result->GetAsString(&actual_value))
337 *is_equal = LowerCaseEqualsASCII(actual_value, attribute_value.c_str());
343 Status GetElementClickableLocation(
346 const std::string& element_id,
347 WebPoint* location) {
348 std::string tag_name;
349 Status status = GetElementTagName(session, web_view, element_id, &tag_name);
350 if (status.IsError())
352 std::string target_element_id = element_id;
353 if (tag_name == "area") {
354 // Scroll the image into view instead of the area.
355 const char* kGetImageElementForArea =
356 "function (element) {"
357 " var map = element.parentElement;"
358 " if (map.tagName.toLowerCase() != 'map')"
359 " throw new Error('the area is not within a map');"
360 " var mapName = map.getAttribute('name');"
361 " if (mapName == null)"
362 " throw new Error ('area\\'s parent map must have a name');"
363 " mapName = '#' + mapName.toLowerCase();"
364 " var images = document.getElementsByTagName('img');"
365 " for (var i = 0; i < images.length; i++) {"
366 " if (images[i].useMap.toLowerCase() == mapName)"
369 " throw new Error('no img is found for the area');"
371 base::ListValue args;
372 args.Append(CreateElement(element_id));
373 scoped_ptr<base::Value> result;
374 status = web_view->CallFunction(
375 session->GetCurrentFrameId(), kGetImageElementForArea, args, &result);
376 if (status.IsError())
378 const base::DictionaryValue* element_dict;
379 if (!result->GetAsDictionary(&element_dict) ||
380 !element_dict->GetString(kElementKey, &target_element_id))
381 return Status(kUnknownError, "no element reference returned by script");
383 bool is_displayed = false;
384 status = IsElementDisplayed(
385 session, web_view, target_element_id, true, &is_displayed);
386 if (status.IsError())
389 return Status(kElementNotVisible);
392 status = GetElementRegion(session, web_view, element_id, &rect);
393 if (status.IsError())
396 std::string tmp_element_id = element_id;
397 int build_no = session->chrome->GetBrowserInfo()->build_no;
398 if (tag_name == "area" && build_no < 1799 && build_no >= 1666) {
399 // This is to skip clickable verification for <area>.
400 // The problem is caused by document.ElementFromPoint(crbug.com/338601).
401 // It was introduced by blink r159012, which rolled into chromium r227489.
402 // And it was fixed in blink r165426, which rolled into chromium r245994.
403 // TODO(stgao): Revert after 33 is not supported.
404 tmp_element_id = std::string();
407 status = ScrollElementRegionIntoView(
408 session, web_view, target_element_id, rect,
409 true /* center */, tmp_element_id, location);
410 if (status.IsError())
412 location->Offset(rect.Width() / 2, rect.Height() / 2);
416 Status GetElementEffectiveStyle(
419 const std::string& element_id,
420 const std::string& property_name,
421 std::string* property_value) {
422 return GetElementEffectiveStyle(session->GetCurrentFrameId(), web_view,
423 element_id, property_name, property_value);
426 Status GetElementRegion(
429 const std::string& element_id,
431 base::ListValue args;
432 args.Append(CreateElement(element_id));
433 scoped_ptr<base::Value> result;
434 Status status = web_view->CallFunction(
435 session->GetCurrentFrameId(), kGetElementRegionScript, args, &result);
436 if (status.IsError())
438 if (!ParseFromValue(result.get(), rect)) {
439 return Status(kUnknownError,
440 "failed to parse value of getElementRegion");
445 Status GetElementTagName(
448 const std::string& element_id,
450 base::ListValue args;
451 args.Append(CreateElement(element_id));
452 scoped_ptr<base::Value> result;
453 Status status = web_view->CallFunction(
454 session->GetCurrentFrameId(),
455 "function(elem) { return elem.tagName.toLowerCase(); }",
457 if (status.IsError())
459 if (!result->GetAsString(name))
460 return Status(kUnknownError, "failed to get element tag name");
464 Status GetElementSize(
467 const std::string& element_id,
469 base::ListValue args;
470 args.Append(CreateElement(element_id));
471 scoped_ptr<base::Value> result;
472 Status status = CallAtomsJs(
473 session->GetCurrentFrameId(), web_view, webdriver::atoms::GET_SIZE,
475 if (status.IsError())
477 if (!ParseFromValue(result.get(), size))
478 return Status(kUnknownError, "failed to parse value of GET_SIZE");
482 Status IsElementDisplayed(
485 const std::string& element_id,
487 bool* is_displayed) {
488 base::ListValue args;
489 args.Append(CreateElement(element_id));
490 args.AppendBoolean(ignore_opacity);
491 scoped_ptr<base::Value> result;
492 Status status = CallAtomsJs(
493 session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_DISPLAYED,
495 if (status.IsError())
497 if (!result->GetAsBoolean(is_displayed))
498 return Status(kUnknownError, "IS_DISPLAYED should return a boolean value");
502 Status IsElementEnabled(
505 const std::string& element_id,
507 base::ListValue args;
508 args.Append(CreateElement(element_id));
509 scoped_ptr<base::Value> result;
510 Status status = CallAtomsJs(
511 session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_ENABLED,
513 if (status.IsError())
515 if (!result->GetAsBoolean(is_enabled))
516 return Status(kUnknownError, "IS_ENABLED should return a boolean value");
520 Status IsOptionElementSelected(
523 const std::string& element_id,
525 base::ListValue args;
526 args.Append(CreateElement(element_id));
527 scoped_ptr<base::Value> result;
528 Status status = CallAtomsJs(
529 session->GetCurrentFrameId(), web_view, webdriver::atoms::IS_SELECTED,
531 if (status.IsError())
533 if (!result->GetAsBoolean(is_selected))
534 return Status(kUnknownError, "IS_SELECTED should return a boolean value");
538 Status IsOptionElementTogglable(
541 const std::string& element_id,
542 bool* is_togglable) {
543 base::ListValue args;
544 args.Append(CreateElement(element_id));
545 scoped_ptr<base::Value> result;
546 Status status = web_view->CallFunction(
547 session->GetCurrentFrameId(), kIsOptionElementToggleableScript,
549 if (status.IsError())
551 if (!result->GetAsBoolean(is_togglable))
552 return Status(kUnknownError, "failed check if option togglable or not");
556 Status SetOptionElementSelected(
559 const std::string& element_id,
561 // TODO(171034): need to fix throwing error if an alert is triggered.
562 base::ListValue args;
563 args.Append(CreateElement(element_id));
564 args.AppendBoolean(selected);
565 scoped_ptr<base::Value> result;
567 session->GetCurrentFrameId(), web_view, webdriver::atoms::CLICK,
571 Status ToggleOptionElement(
574 const std::string& element_id) {
576 Status status = IsOptionElementSelected(
577 session, web_view, element_id, &is_selected);
578 if (status.IsError())
580 return SetOptionElementSelected(session, web_view, element_id, !is_selected);
583 Status ScrollElementIntoView(
586 const std::string& id,
587 WebPoint* location) {
589 Status status = GetElementSize(session, web_view, id, &size);
590 if (status.IsError())
592 return ScrollElementRegionIntoView(
593 session, web_view, id, WebRect(WebPoint(0, 0), size),
594 false /* center */, std::string(), location);
597 Status ScrollElementRegionIntoView(
600 const std::string& element_id,
601 const WebRect& region,
603 const std::string& clickable_element_id,
604 WebPoint* location) {
605 WebPoint region_offset = region.origin;
606 WebSize region_size = region.size;
607 Status status = ScrollElementRegionIntoViewHelper(
608 session->GetCurrentFrameId(), web_view, element_id, region,
609 center, clickable_element_id, ®ion_offset);
610 if (status.IsError())
612 const char* kFindSubFrameScript =
614 " return document.evaluate(xpath, document, null,"
615 " XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
617 for (std::list<FrameInfo>::reverse_iterator rit = session->frames.rbegin();
618 rit != session->frames.rend(); ++rit) {
619 base::ListValue args;
621 base::StringPrintf("//*[@cd_frame_id_ = '%s']",
622 rit->chromedriver_frame_id.c_str()));
623 scoped_ptr<base::Value> result;
624 status = web_view->CallFunction(
625 rit->parent_frame_id, kFindSubFrameScript, args, &result);
626 if (status.IsError())
628 const base::DictionaryValue* element_dict;
629 if (!result->GetAsDictionary(&element_dict))
630 return Status(kUnknownError, "no element reference returned by script");
631 std::string frame_element_id;
632 if (!element_dict->GetString(kElementKey, &frame_element_id))
633 return Status(kUnknownError, "failed to locate a sub frame");
635 // Modify |region_offset| by the frame's border.
636 int border_left = -1;
638 status = GetElementBorder(
639 rit->parent_frame_id, web_view, frame_element_id,
640 &border_left, &border_top);
641 if (status.IsError())
643 region_offset.Offset(border_left, border_top);
645 status = ScrollElementRegionIntoViewHelper(
646 rit->parent_frame_id, web_view, frame_element_id,
647 WebRect(region_offset, region_size),
648 center, frame_element_id, ®ion_offset);
649 if (status.IsError())
652 *location = region_offset;