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
))
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
;