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
))
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
))
45 if (!dict_value
->GetDouble("width", &width
) ||
46 !dict_value
->GetDouble("height", &height
))
48 size
->width
= static_cast<int>(width
);
49 size
->height
= static_cast<int>(height
);
53 bool ParseFromValue(base::Value
* value
, WebRect
* rect
) {
54 base::DictionaryValue
* dict_value
;
55 if (!value
->GetAsDictionary(&dict_value
))
61 if (!dict_value
->GetDouble("left", &x
) ||
62 !dict_value
->GetDouble("top", &y
) ||
63 !dict_value
->GetDouble("width", &width
) ||
64 !dict_value
->GetDouble("height", &height
))
66 rect
->origin
.x
= static_cast<int>(x
);
67 rect
->origin
.y
= static_cast<int>(y
);
68 rect
->size
.width
= static_cast<int>(width
);
69 rect
->size
.height
= static_cast<int>(height
);
73 base::Value
* CreateValueFrom(const WebRect
& rect
) {
74 base::DictionaryValue
* dict
= new base::DictionaryValue();
75 dict
->SetInteger("left", rect
.X());
76 dict
->SetInteger("top", rect
.Y());
77 dict
->SetInteger("width", rect
.Width());
78 dict
->SetInteger("height", rect
.Height());
83 const std::string
& frame
,
85 const char* const* atom_function
,
86 const base::ListValue
& args
,
87 scoped_ptr
<base::Value
>* result
) {
88 return web_view
->CallFunction(
89 frame
, webdriver::atoms::asString(atom_function
), args
, result
);
92 Status
VerifyElementClickable(
93 const std::string
& frame
,
95 const std::string
& element_id
,
96 const WebPoint
& location
) {
98 args
.Append(CreateElement(element_id
));
99 args
.Append(CreateValueFrom(location
));
100 scoped_ptr
<base::Value
> result
;
101 Status status
= CallAtomsJs(
102 frame
, web_view
, webdriver::atoms::IS_ELEMENT_CLICKABLE
,
104 if (status
.IsError())
106 base::DictionaryValue
* dict
;
107 bool is_clickable
= false;
108 if (!result
->GetAsDictionary(&dict
) ||
109 !dict
->GetBoolean("clickable", &is_clickable
)) {
110 return Status(kUnknownError
,
111 "failed to parse value of IS_ELEMENT_CLICKABLE");
116 if (!dict
->GetString("message", &message
))
117 message
= "element is not clickable";
118 return Status(kUnknownError
, message
);
123 Status
ScrollElementRegionIntoViewHelper(
124 const std::string
& frame
,
126 const std::string
& element_id
,
127 const WebRect
& region
,
129 const std::string
& clickable_element_id
,
130 WebPoint
* location
) {
131 WebPoint tmp_location
= *location
;
132 base::ListValue args
;
133 args
.Append(CreateElement(element_id
));
134 args
.AppendBoolean(center
);
135 args
.Append(CreateValueFrom(region
));
136 scoped_ptr
<base::Value
> result
;
137 Status status
= web_view
->CallFunction(
138 frame
, webdriver::atoms::asString(webdriver::atoms::GET_LOCATION_IN_VIEW
),
140 if (status
.IsError())
142 if (!ParseFromValue(result
.get(), &tmp_location
)) {
143 return Status(kUnknownError
,
144 "failed to parse value of GET_LOCATION_IN_VIEW");
146 if (!clickable_element_id
.empty()) {
147 WebPoint middle
= tmp_location
;
148 middle
.Offset(region
.Width() / 2, region
.Height() / 2);
149 status
= VerifyElementClickable(
150 frame
, web_view
, clickable_element_id
, middle
);
151 if (status
.IsError())
154 *location
= tmp_location
;
158 Status
GetElementEffectiveStyle(
159 const std::string
& frame
,
161 const std::string
& element_id
,
162 const std::string
& property
,
163 std::string
* value
) {
164 base::ListValue args
;
165 args
.Append(CreateElement(element_id
));
166 args
.AppendString(property
);
167 scoped_ptr
<base::Value
> result
;
168 Status status
= web_view
->CallFunction(
169 frame
, webdriver::atoms::asString(webdriver::atoms::GET_EFFECTIVE_STYLE
),
171 if (status
.IsError())
173 if (!result
->GetAsString(value
)) {
174 return Status(kUnknownError
,
175 "failed to parse value of GET_EFFECTIVE_STYLE");
180 Status
GetElementBorder(
181 const std::string
& frame
,
183 const std::string
& element_id
,
186 std::string border_left_str
;
187 Status status
= GetElementEffectiveStyle(
188 frame
, web_view
, element_id
, "border-left-width", &border_left_str
);
189 if (status
.IsError())
191 std::string border_top_str
;
192 status
= GetElementEffectiveStyle(
193 frame
, web_view
, element_id
, "border-top-width", &border_top_str
);
194 if (status
.IsError())
196 int border_left_tmp
= -1;
197 int border_top_tmp
= -1;
198 base::StringToInt(border_left_str
, &border_left_tmp
);
199 base::StringToInt(border_top_str
, &border_top_tmp
);
200 if (border_left_tmp
== -1 || border_top_tmp
== -1)
201 return Status(kUnknownError
, "failed to get border width of element");
202 *border_left
= border_left_tmp
;
203 *border_top
= border_top_tmp
;
209 base::DictionaryValue
* CreateElement(const std::string
& element_id
) {
210 base::DictionaryValue
* element
= new base::DictionaryValue();
211 element
->SetString(kElementKey
, element_id
);
215 base::Value
* CreateValueFrom(const WebPoint
& point
) {
216 base::DictionaryValue
* dict
= new base::DictionaryValue();
217 dict
->SetInteger("x", point
.x
);
218 dict
->SetInteger("y", point
.y
);
225 const std::string
* root_element_id
,
228 const base::DictionaryValue
& params
,
229 scoped_ptr
<base::Value
>* value
) {
230 std::string strategy
;
231 if (!params
.GetString("using", &strategy
))
232 return Status(kUnknownError
, "'using' must be a string");
234 if (!params
.GetString("value", &target
))
235 return Status(kUnknownError
, "'value' must be a string");
239 script
= webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENT
);
241 script
= webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENTS
);
242 scoped_ptr
<base::DictionaryValue
> locator(new base::DictionaryValue());
243 locator
->SetString(strategy
, target
);
244 base::ListValue arguments
;
245 arguments
.Append(locator
.release());
247 arguments
.Append(CreateElement(*root_element_id
));
249 base::TimeTicks start_time
= base::TimeTicks::Now();
251 scoped_ptr
<base::Value
> temp
;
252 Status status
= web_view
->CallFunction(
253 session
->GetCurrentFrameId(), script
, arguments
, &temp
);
254 if (status
.IsError())
257 if (!temp
->IsType(base::Value::TYPE_NULL
)) {
259 value
->reset(temp
.release());
262 base::ListValue
* result
;
263 if (!temp
->GetAsList(&result
))
264 return Status(kUnknownError
, "script returns unexpected result");
266 if (result
->GetSize() > 0U) {
267 value
->reset(temp
.release());
273 if (base::TimeTicks::Now() - start_time
>= session
->implicit_wait
) {
275 return Status(kNoSuchElement
, "Unable to locate element: {\"method\":\""
276 + strategy
+ "\",\"selector\":\"" + target
+ "\"}");
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
)) {
343 base::LowerCaseEqualsASCII(actual_value
, attribute_value
.c_str());
350 Status
GetElementClickableLocation(
353 const std::string
& element_id
,
354 WebPoint
* location
) {
355 std::string tag_name
;
356 Status status
= GetElementTagName(session
, web_view
, element_id
, &tag_name
);
357 if (status
.IsError())
359 std::string target_element_id
= element_id
;
360 if (tag_name
== "area") {
361 // Scroll the image into view instead of the area.
362 const char kGetImageElementForArea
[] =
363 "function (element) {"
364 " var map = element.parentElement;"
365 " if (map.tagName.toLowerCase() != 'map')"
366 " throw new Error('the area is not within a map');"
367 " var mapName = map.getAttribute('name');"
368 " if (mapName == null)"
369 " throw new Error ('area\\'s parent map must have a name');"
370 " mapName = '#' + mapName.toLowerCase();"
371 " var images = document.getElementsByTagName('img');"
372 " for (var i = 0; i < images.length; i++) {"
373 " if (images[i].useMap.toLowerCase() == mapName)"
376 " throw new Error('no img is found for the area');"
378 base::ListValue args
;
379 args
.Append(CreateElement(element_id
));
380 scoped_ptr
<base::Value
> result
;
381 status
= web_view
->CallFunction(
382 session
->GetCurrentFrameId(), kGetImageElementForArea
, args
, &result
);
383 if (status
.IsError())
385 const base::DictionaryValue
* element_dict
;
386 if (!result
->GetAsDictionary(&element_dict
) ||
387 !element_dict
->GetString(kElementKey
, &target_element_id
))
388 return Status(kUnknownError
, "no element reference returned by script");
390 bool is_displayed
= false;
391 status
= IsElementDisplayed(
392 session
, web_view
, target_element_id
, true, &is_displayed
);
393 if (status
.IsError())
396 return Status(kElementNotVisible
);
399 status
= GetElementRegion(session
, web_view
, element_id
, &rect
);
400 if (status
.IsError())
403 status
= ScrollElementRegionIntoView(
404 session
, web_view
, target_element_id
, rect
,
405 true /* center */, element_id
, location
);
406 if (status
.IsError())
408 location
->Offset(rect
.Width() / 2, rect
.Height() / 2);
412 Status
GetElementEffectiveStyle(
415 const std::string
& element_id
,
416 const std::string
& property_name
,
417 std::string
* property_value
) {
418 return GetElementEffectiveStyle(session
->GetCurrentFrameId(), web_view
,
419 element_id
, property_name
, property_value
);
422 Status
GetElementRegion(
425 const std::string
& element_id
,
427 base::ListValue args
;
428 args
.Append(CreateElement(element_id
));
429 scoped_ptr
<base::Value
> result
;
430 Status status
= web_view
->CallFunction(
431 session
->GetCurrentFrameId(), kGetElementRegionScript
, args
, &result
);
432 if (status
.IsError())
434 if (!ParseFromValue(result
.get(), rect
)) {
435 return Status(kUnknownError
,
436 "failed to parse value of getElementRegion");
441 Status
GetElementTagName(
444 const std::string
& element_id
,
446 base::ListValue args
;
447 args
.Append(CreateElement(element_id
));
448 scoped_ptr
<base::Value
> result
;
449 Status status
= web_view
->CallFunction(
450 session
->GetCurrentFrameId(),
451 "function(elem) { return elem.tagName.toLowerCase(); }",
453 if (status
.IsError())
455 if (!result
->GetAsString(name
))
456 return Status(kUnknownError
, "failed to get element tag name");
460 Status
GetElementSize(
463 const std::string
& element_id
,
465 base::ListValue args
;
466 args
.Append(CreateElement(element_id
));
467 scoped_ptr
<base::Value
> result
;
468 Status status
= CallAtomsJs(
469 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::GET_SIZE
,
471 if (status
.IsError())
473 if (!ParseFromValue(result
.get(), size
))
474 return Status(kUnknownError
, "failed to parse value of GET_SIZE");
478 Status
IsElementDisplayed(
481 const std::string
& element_id
,
483 bool* is_displayed
) {
484 base::ListValue args
;
485 args
.Append(CreateElement(element_id
));
486 args
.AppendBoolean(ignore_opacity
);
487 scoped_ptr
<base::Value
> result
;
488 Status status
= CallAtomsJs(
489 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_DISPLAYED
,
491 if (status
.IsError())
493 if (!result
->GetAsBoolean(is_displayed
))
494 return Status(kUnknownError
, "IS_DISPLAYED should return a boolean value");
498 Status
IsElementEnabled(
501 const std::string
& element_id
,
503 base::ListValue args
;
504 args
.Append(CreateElement(element_id
));
505 scoped_ptr
<base::Value
> result
;
506 Status status
= CallAtomsJs(
507 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_ENABLED
,
509 if (status
.IsError())
511 if (!result
->GetAsBoolean(is_enabled
))
512 return Status(kUnknownError
, "IS_ENABLED should return a boolean value");
516 Status
IsOptionElementSelected(
519 const std::string
& element_id
,
521 base::ListValue args
;
522 args
.Append(CreateElement(element_id
));
523 scoped_ptr
<base::Value
> result
;
524 Status status
= CallAtomsJs(
525 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_SELECTED
,
527 if (status
.IsError())
529 if (!result
->GetAsBoolean(is_selected
))
530 return Status(kUnknownError
, "IS_SELECTED should return a boolean value");
534 Status
IsOptionElementTogglable(
537 const std::string
& element_id
,
538 bool* is_togglable
) {
539 base::ListValue args
;
540 args
.Append(CreateElement(element_id
));
541 scoped_ptr
<base::Value
> result
;
542 Status status
= web_view
->CallFunction(
543 session
->GetCurrentFrameId(), kIsOptionElementToggleableScript
,
545 if (status
.IsError())
547 if (!result
->GetAsBoolean(is_togglable
))
548 return Status(kUnknownError
, "failed check if option togglable or not");
552 Status
SetOptionElementSelected(
555 const std::string
& element_id
,
557 // TODO(171034): need to fix throwing error if an alert is triggered.
558 base::ListValue args
;
559 args
.Append(CreateElement(element_id
));
560 args
.AppendBoolean(selected
);
561 scoped_ptr
<base::Value
> result
;
563 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::CLICK
,
567 Status
ToggleOptionElement(
570 const std::string
& element_id
) {
572 Status status
= IsOptionElementSelected(
573 session
, web_view
, element_id
, &is_selected
);
574 if (status
.IsError())
576 return SetOptionElementSelected(session
, web_view
, element_id
, !is_selected
);
579 Status
ScrollElementIntoView(
582 const std::string
& id
,
583 const WebPoint
* offset
,
584 WebPoint
* location
) {
586 Status status
= GetElementRegion(session
, web_view
, id
, ®ion
);
587 if (status
.IsError())
589 status
= ScrollElementRegionIntoView(session
, web_view
, id
, region
,
590 false /* center */, std::string(), location
);
591 if (status
.IsError())
594 location
->Offset(offset
->x
, offset
->y
);
596 location
->Offset(region
.size
.width
/ 2, region
.size
.height
/ 2);
600 Status
ScrollElementRegionIntoView(
603 const std::string
& element_id
,
604 const WebRect
& region
,
606 const std::string
& clickable_element_id
,
607 WebPoint
* location
) {
608 WebPoint region_offset
= region
.origin
;
609 WebSize region_size
= region
.size
;
610 Status status
= ScrollElementRegionIntoViewHelper(
611 session
->GetCurrentFrameId(), web_view
, element_id
, region
,
612 center
, clickable_element_id
, ®ion_offset
);
613 if (status
.IsError())
615 const char kFindSubFrameScript
[] =
617 " return document.evaluate(xpath, document, null,"
618 " XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
620 for (std::list
<FrameInfo
>::reverse_iterator rit
= session
->frames
.rbegin();
621 rit
!= session
->frames
.rend(); ++rit
) {
622 base::ListValue args
;
624 base::StringPrintf("//*[@cd_frame_id_ = '%s']",
625 rit
->chromedriver_frame_id
.c_str()));
626 scoped_ptr
<base::Value
> result
;
627 status
= web_view
->CallFunction(
628 rit
->parent_frame_id
, kFindSubFrameScript
, args
, &result
);
629 if (status
.IsError())
631 const base::DictionaryValue
* element_dict
;
632 if (!result
->GetAsDictionary(&element_dict
))
633 return Status(kUnknownError
, "no element reference returned by script");
634 std::string frame_element_id
;
635 if (!element_dict
->GetString(kElementKey
, &frame_element_id
))
636 return Status(kUnknownError
, "failed to locate a sub frame");
638 // Modify |region_offset| by the frame's border.
639 int border_left
= -1;
641 status
= GetElementBorder(
642 rit
->parent_frame_id
, web_view
, frame_element_id
,
643 &border_left
, &border_top
);
644 if (status
.IsError())
646 region_offset
.Offset(border_left
, border_top
);
648 status
= ScrollElementRegionIntoViewHelper(
649 rit
->parent_frame_id
, web_view
, frame_element_id
,
650 WebRect(region_offset
, region_size
),
651 center
, frame_element_id
, ®ion_offset
);
652 if (status
.IsError())
655 *location
= region_offset
;