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/web_view.h"
18 #include "chrome/test/chromedriver/session.h"
19 #include "third_party/webdriver/atoms.h"
23 const char kElementKey
[] = "ELEMENT";
25 bool ParseFromValue(base::Value
* value
, WebPoint
* point
) {
26 base::DictionaryValue
* dict_value
;
27 if (!value
->GetAsDictionary(&dict_value
))
30 if (!dict_value
->GetDouble("x", &x
) ||
31 !dict_value
->GetDouble("y", &y
))
33 point
->x
= static_cast<int>(x
);
34 point
->y
= static_cast<int>(y
);
38 bool ParseFromValue(base::Value
* value
, WebSize
* size
) {
39 base::DictionaryValue
* dict_value
;
40 if (!value
->GetAsDictionary(&dict_value
))
43 if (!dict_value
->GetDouble("width", &width
) ||
44 !dict_value
->GetDouble("height", &height
))
46 size
->width
= static_cast<int>(width
);
47 size
->height
= static_cast<int>(height
);
51 bool ParseFromValue(base::Value
* value
, WebRect
* rect
) {
52 base::DictionaryValue
* dict_value
;
53 if (!value
->GetAsDictionary(&dict_value
))
55 double x
, y
, width
, height
;
56 if (!dict_value
->GetDouble("left", &x
) ||
57 !dict_value
->GetDouble("top", &y
) ||
58 !dict_value
->GetDouble("width", &width
) ||
59 !dict_value
->GetDouble("height", &height
))
61 rect
->origin
.x
= static_cast<int>(x
);
62 rect
->origin
.y
= static_cast<int>(y
);
63 rect
->size
.width
= static_cast<int>(width
);
64 rect
->size
.height
= static_cast<int>(height
);
68 base::Value
* CreateValueFrom(const WebRect
& rect
) {
69 base::DictionaryValue
* dict
= new base::DictionaryValue();
70 dict
->SetInteger("left", rect
.X());
71 dict
->SetInteger("top", rect
.Y());
72 dict
->SetInteger("width", rect
.Width());
73 dict
->SetInteger("height", rect
.Height());
78 const std::string
& frame
,
80 const char* const* atom_function
,
81 const base::ListValue
& args
,
82 scoped_ptr
<base::Value
>* result
) {
83 return web_view
->CallFunction(
84 frame
, webdriver::atoms::asString(atom_function
), args
, result
);
87 Status
VerifyElementClickable(
88 const std::string
& frame
,
90 const std::string
& element_id
,
91 const WebPoint
& location
) {
93 args
.Append(CreateElement(element_id
));
94 args
.Append(CreateValueFrom(location
));
95 scoped_ptr
<base::Value
> result
;
96 Status status
= CallAtomsJs(
97 frame
, web_view
, webdriver::atoms::IS_ELEMENT_CLICKABLE
,
101 base::DictionaryValue
* dict
;
103 if (!result
->GetAsDictionary(&dict
) ||
104 !dict
->GetBoolean("clickable", &is_clickable
)) {
105 return Status(kUnknownError
,
106 "failed to parse value of IS_ELEMENT_CLICKABLE");
111 if (!dict
->GetString("message", &message
))
112 message
= "element is not clickable";
113 return Status(kUnknownError
, message
);
118 Status
ScrollElementRegionIntoViewHelper(
119 const std::string
& frame
,
121 const std::string
& element_id
,
122 const WebRect
& region
,
124 const std::string
& clickable_element_id
,
125 WebPoint
* location
) {
126 WebPoint tmp_location
= *location
;
127 base::ListValue args
;
128 args
.Append(CreateElement(element_id
));
129 args
.AppendBoolean(center
);
130 args
.Append(CreateValueFrom(region
));
131 scoped_ptr
<base::Value
> result
;
132 Status status
= web_view
->CallFunction(
133 frame
, webdriver::atoms::asString(webdriver::atoms::GET_LOCATION_IN_VIEW
),
135 if (status
.IsError())
137 if (!ParseFromValue(result
.get(), &tmp_location
)) {
138 return Status(kUnknownError
,
139 "failed to parse value of GET_LOCATION_IN_VIEW");
141 if (!clickable_element_id
.empty()) {
142 WebPoint middle
= tmp_location
;
143 middle
.Offset(region
.Width() / 2, region
.Height() / 2);
144 status
= VerifyElementClickable(
145 frame
, web_view
, clickable_element_id
, middle
);
146 if (status
.IsError())
149 *location
= tmp_location
;
153 Status
GetElementEffectiveStyle(
154 const std::string
& frame
,
156 const std::string
& element_id
,
157 const std::string
& property
,
158 std::string
* value
) {
159 base::ListValue args
;
160 args
.Append(CreateElement(element_id
));
161 args
.AppendString(property
);
162 scoped_ptr
<base::Value
> result
;
163 Status status
= web_view
->CallFunction(
164 frame
, webdriver::atoms::asString(webdriver::atoms::GET_EFFECTIVE_STYLE
),
166 if (status
.IsError())
168 if (!result
->GetAsString(value
)) {
169 return Status(kUnknownError
,
170 "failed to parse value of GET_EFFECTIVE_STYLE");
175 Status
GetElementBorder(
176 const std::string
& frame
,
178 const std::string
& element_id
,
181 std::string border_left_str
;
182 Status status
= GetElementEffectiveStyle(
183 frame
, web_view
, element_id
, "border-left-width", &border_left_str
);
184 if (status
.IsError())
186 std::string border_top_str
;
187 status
= GetElementEffectiveStyle(
188 frame
, web_view
, element_id
, "border-top-width", &border_top_str
);
189 if (status
.IsError())
191 int border_left_tmp
= -1;
192 int border_top_tmp
= -1;
193 base::StringToInt(border_left_str
, &border_left_tmp
);
194 base::StringToInt(border_top_str
, &border_top_tmp
);
195 if (border_left_tmp
== -1 || border_top_tmp
== -1)
196 return Status(kUnknownError
, "failed to get border width of element");
197 *border_left
= border_left_tmp
;
198 *border_top
= border_top_tmp
;
204 base::DictionaryValue
* CreateElement(const std::string
& element_id
) {
205 base::DictionaryValue
* element
= new base::DictionaryValue();
206 element
->SetString(kElementKey
, element_id
);
210 base::Value
* CreateValueFrom(const WebPoint
& point
) {
211 base::DictionaryValue
* dict
= new base::DictionaryValue();
212 dict
->SetInteger("x", point
.x
);
213 dict
->SetInteger("y", point
.y
);
220 const std::string
* root_element_id
,
223 const base::DictionaryValue
& params
,
224 scoped_ptr
<base::Value
>* value
) {
225 std::string strategy
;
226 if (!params
.GetString("using", &strategy
))
227 return Status(kUnknownError
, "'using' must be a string");
229 if (!params
.GetString("value", &target
))
230 return Status(kUnknownError
, "'value' must be a string");
234 script
= webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENT
);
236 script
= webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENTS
);
237 scoped_ptr
<base::DictionaryValue
> locator(new base::DictionaryValue());
238 locator
->SetString(strategy
, target
);
239 base::ListValue arguments
;
240 arguments
.Append(locator
.release());
242 arguments
.Append(CreateElement(*root_element_id
));
244 base::TimeTicks start_time
= base::TimeTicks::Now();
246 scoped_ptr
<base::Value
> temp
;
247 Status status
= web_view
->CallFunction(
248 session
->GetCurrentFrameId(), script
, arguments
, &temp
);
249 if (status
.IsError())
252 if (!temp
->IsType(base::Value::TYPE_NULL
)) {
254 value
->reset(temp
.release());
257 base::ListValue
* result
;
258 if (!temp
->GetAsList(&result
))
259 return Status(kUnknownError
, "script returns unexpected result");
261 if (result
->GetSize() > 0U) {
262 value
->reset(temp
.release());
268 if (base::TimeTicks::Now() - start_time
>= session
->implicit_wait
) {
270 return Status(kNoSuchElement
);
272 value
->reset(new base::ListValue());
276 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(interval_ms
));
279 return Status(kUnknownError
);
282 Status
GetActiveElement(
285 scoped_ptr
<base::Value
>* value
) {
286 base::ListValue args
;
287 return web_view
->CallFunction(
288 session
->GetCurrentFrameId(),
289 "function() { return document.activeElement || document.body }",
294 Status
IsElementFocused(
297 const std::string
& element_id
,
299 scoped_ptr
<base::Value
> result
;
300 Status status
= GetActiveElement(session
, web_view
, &result
);
301 if (status
.IsError())
303 scoped_ptr
<base::Value
> element_dict(CreateElement(element_id
));
304 *is_focused
= result
->Equals(element_dict
.get());
308 Status
GetElementAttribute(
311 const std::string
& element_id
,
312 const std::string
& attribute_name
,
313 scoped_ptr
<base::Value
>* value
) {
314 base::ListValue args
;
315 args
.Append(CreateElement(element_id
));
316 args
.AppendString(attribute_name
);
318 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::GET_ATTRIBUTE
,
322 Status
IsElementAttributeEqualToIgnoreCase(
325 const std::string
& element_id
,
326 const std::string
& attribute_name
,
327 const std::string
& attribute_value
,
329 scoped_ptr
<base::Value
> result
;
330 Status status
= GetElementAttribute(
331 session
, web_view
, element_id
, attribute_name
, &result
);
332 if (status
.IsError())
334 std::string actual_value
;
335 if (result
->GetAsString(&actual_value
))
336 *is_equal
= LowerCaseEqualsASCII(actual_value
, attribute_value
.c_str());
342 Status
GetElementClickableLocation(
345 const std::string
& element_id
,
346 WebPoint
* location
) {
347 std::string tag_name
;
348 Status status
= GetElementTagName(session
, web_view
, element_id
, &tag_name
);
349 if (status
.IsError())
351 std::string target_element_id
= element_id
;
352 if (tag_name
== "area") {
353 // Scroll the image into view instead of the area.
354 const char* kGetImageElementForArea
=
355 "function (element) {"
356 " var map = element.parentElement;"
357 " if (map.tagName.toLowerCase() != 'map')"
358 " throw new Error('the area is not within a map');"
359 " var mapName = map.getAttribute('name');"
360 " if (mapName == null)"
361 " throw new Error ('area\\'s parent map must have a name');"
362 " mapName = '#' + mapName.toLowerCase();"
363 " var images = document.getElementsByTagName('img');"
364 " for (var i = 0; i < images.length; i++) {"
365 " if (images[i].useMap.toLowerCase() == mapName)"
368 " throw new Error('no img is found for the area');"
370 base::ListValue args
;
371 args
.Append(CreateElement(element_id
));
372 scoped_ptr
<base::Value
> result
;
373 status
= web_view
->CallFunction(
374 session
->GetCurrentFrameId(), kGetImageElementForArea
, args
, &result
);
375 if (status
.IsError())
377 const base::DictionaryValue
* element_dict
;
378 if (!result
->GetAsDictionary(&element_dict
) ||
379 !element_dict
->GetString(kElementKey
, &target_element_id
))
380 return Status(kUnknownError
, "no element reference returned by script");
382 bool is_displayed
= false;
383 status
= IsElementDisplayed(
384 session
, web_view
, target_element_id
, true, &is_displayed
);
385 if (status
.IsError())
388 return Status(kElementNotVisible
);
391 status
= GetElementRegion(session
, web_view
, element_id
, &rect
);
392 if (status
.IsError())
395 std::string tmp_element_id
= element_id
;
396 if (tag_name
== "area" && session
->chrome
->GetBuildNo() < 1799 &&
397 session
->chrome
->GetBuildNo() >= 1666) {
398 // This is to skip clickable verification for <area>.
399 // The problem is caused by document.ElementFromPoint(crbug.com/338601).
400 // It was introduced by blink r159012, which rolled into chromium r227489.
401 // And it was fixed in blink r165426, which rolled into chromium r245994.
402 // TODO(stgao): Revert after 33 is not supported.
403 tmp_element_id
= std::string();
406 status
= ScrollElementRegionIntoView(
407 session
, web_view
, target_element_id
, rect
,
408 true /* center */, tmp_element_id
, location
);
409 if (status
.IsError())
411 location
->Offset(rect
.Width() / 2, rect
.Height() / 2);
415 Status
GetElementEffectiveStyle(
418 const std::string
& element_id
,
419 const std::string
& property_name
,
420 std::string
* property_value
) {
421 return GetElementEffectiveStyle(session
->GetCurrentFrameId(), web_view
,
422 element_id
, property_name
, property_value
);
425 Status
GetElementRegion(
428 const std::string
& element_id
,
430 base::ListValue args
;
431 args
.Append(CreateElement(element_id
));
432 scoped_ptr
<base::Value
> result
;
433 Status status
= web_view
->CallFunction(
434 session
->GetCurrentFrameId(), kGetElementRegionScript
, args
, &result
);
435 if (status
.IsError())
437 if (!ParseFromValue(result
.get(), rect
)) {
438 return Status(kUnknownError
,
439 "failed to parse value of getElementRegion");
444 Status
GetElementTagName(
447 const std::string
& element_id
,
449 base::ListValue args
;
450 args
.Append(CreateElement(element_id
));
451 scoped_ptr
<base::Value
> result
;
452 Status status
= web_view
->CallFunction(
453 session
->GetCurrentFrameId(),
454 "function(elem) { return elem.tagName.toLowerCase(); }",
456 if (status
.IsError())
458 if (!result
->GetAsString(name
))
459 return Status(kUnknownError
, "failed to get element tag name");
463 Status
GetElementSize(
466 const std::string
& element_id
,
468 base::ListValue args
;
469 args
.Append(CreateElement(element_id
));
470 scoped_ptr
<base::Value
> result
;
471 Status status
= CallAtomsJs(
472 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::GET_SIZE
,
474 if (status
.IsError())
476 if (!ParseFromValue(result
.get(), size
))
477 return Status(kUnknownError
, "failed to parse value of GET_SIZE");
481 Status
IsElementDisplayed(
484 const std::string
& element_id
,
486 bool* is_displayed
) {
487 base::ListValue args
;
488 args
.Append(CreateElement(element_id
));
489 args
.AppendBoolean(ignore_opacity
);
490 scoped_ptr
<base::Value
> result
;
491 Status status
= CallAtomsJs(
492 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_DISPLAYED
,
494 if (status
.IsError())
496 if (!result
->GetAsBoolean(is_displayed
))
497 return Status(kUnknownError
, "IS_DISPLAYED should return a boolean value");
501 Status
IsElementEnabled(
504 const std::string
& element_id
,
506 base::ListValue args
;
507 args
.Append(CreateElement(element_id
));
508 scoped_ptr
<base::Value
> result
;
509 Status status
= CallAtomsJs(
510 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_ENABLED
,
512 if (status
.IsError())
514 if (!result
->GetAsBoolean(is_enabled
))
515 return Status(kUnknownError
, "IS_ENABLED should return a boolean value");
519 Status
IsOptionElementSelected(
522 const std::string
& element_id
,
524 base::ListValue args
;
525 args
.Append(CreateElement(element_id
));
526 scoped_ptr
<base::Value
> result
;
527 Status status
= CallAtomsJs(
528 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_SELECTED
,
530 if (status
.IsError())
532 if (!result
->GetAsBoolean(is_selected
))
533 return Status(kUnknownError
, "IS_SELECTED should return a boolean value");
537 Status
IsOptionElementTogglable(
540 const std::string
& element_id
,
541 bool* is_togglable
) {
542 base::ListValue args
;
543 args
.Append(CreateElement(element_id
));
544 scoped_ptr
<base::Value
> result
;
545 Status status
= web_view
->CallFunction(
546 session
->GetCurrentFrameId(), kIsOptionElementToggleableScript
,
548 if (status
.IsError())
550 if (!result
->GetAsBoolean(is_togglable
))
551 return Status(kUnknownError
, "failed check if option togglable or not");
555 Status
SetOptionElementSelected(
558 const std::string
& element_id
,
560 // TODO(171034): need to fix throwing error if an alert is triggered.
561 base::ListValue args
;
562 args
.Append(CreateElement(element_id
));
563 args
.AppendBoolean(selected
);
564 scoped_ptr
<base::Value
> result
;
566 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::CLICK
,
570 Status
ToggleOptionElement(
573 const std::string
& element_id
) {
575 Status status
= IsOptionElementSelected(
576 session
, web_view
, element_id
, &is_selected
);
577 if (status
.IsError())
579 return SetOptionElementSelected(session
, web_view
, element_id
, !is_selected
);
582 Status
ScrollElementIntoView(
585 const std::string
& id
,
586 WebPoint
* location
) {
588 Status status
= GetElementSize(session
, web_view
, id
, &size
);
589 if (status
.IsError())
591 return ScrollElementRegionIntoView(
592 session
, web_view
, id
, WebRect(WebPoint(0, 0), size
),
593 false /* center */, std::string(), location
);
596 Status
ScrollElementRegionIntoView(
599 const std::string
& element_id
,
600 const WebRect
& region
,
602 const std::string
& clickable_element_id
,
603 WebPoint
* location
) {
604 WebPoint region_offset
= region
.origin
;
605 WebSize region_size
= region
.size
;
606 Status status
= ScrollElementRegionIntoViewHelper(
607 session
->GetCurrentFrameId(), web_view
, element_id
, region
,
608 center
, clickable_element_id
, ®ion_offset
);
609 if (status
.IsError())
611 const char* kFindSubFrameScript
=
613 " return document.evaluate(xpath, document, null,"
614 " XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
616 for (std::list
<FrameInfo
>::reverse_iterator rit
= session
->frames
.rbegin();
617 rit
!= session
->frames
.rend(); ++rit
) {
618 base::ListValue args
;
620 base::StringPrintf("//*[@cd_frame_id_ = '%s']",
621 rit
->chromedriver_frame_id
.c_str()));
622 scoped_ptr
<base::Value
> result
;
623 status
= web_view
->CallFunction(
624 rit
->parent_frame_id
, kFindSubFrameScript
, args
, &result
);
625 if (status
.IsError())
627 const base::DictionaryValue
* element_dict
;
628 if (!result
->GetAsDictionary(&element_dict
))
629 return Status(kUnknownError
, "no element reference returned by script");
630 std::string frame_element_id
;
631 if (!element_dict
->GetString(kElementKey
, &frame_element_id
))
632 return Status(kUnknownError
, "failed to locate a sub frame");
634 // Modify |region_offset| by the frame's border.
635 int border_left
= -1;
637 status
= GetElementBorder(
638 rit
->parent_frame_id
, web_view
, frame_element_id
,
639 &border_left
, &border_top
);
640 if (status
.IsError())
642 region_offset
.Offset(border_left
, border_top
);
644 status
= ScrollElementRegionIntoViewHelper(
645 rit
->parent_frame_id
, web_view
, frame_element_id
,
646 WebRect(region_offset
, region_size
),
647 center
, frame_element_id
, ®ion_offset
);
648 if (status
.IsError())
651 *location
= region_offset
;