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
);
277 value
->reset(new base::ListValue());
281 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(interval_ms
));
284 return Status(kUnknownError
);
287 Status
GetActiveElement(
290 scoped_ptr
<base::Value
>* value
) {
291 base::ListValue args
;
292 return web_view
->CallFunction(
293 session
->GetCurrentFrameId(),
294 "function() { return document.activeElement || document.body }",
299 Status
IsElementFocused(
302 const std::string
& element_id
,
304 scoped_ptr
<base::Value
> result
;
305 Status status
= GetActiveElement(session
, web_view
, &result
);
306 if (status
.IsError())
308 scoped_ptr
<base::Value
> element_dict(CreateElement(element_id
));
309 *is_focused
= result
->Equals(element_dict
.get());
313 Status
GetElementAttribute(
316 const std::string
& element_id
,
317 const std::string
& attribute_name
,
318 scoped_ptr
<base::Value
>* value
) {
319 base::ListValue args
;
320 args
.Append(CreateElement(element_id
));
321 args
.AppendString(attribute_name
);
323 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::GET_ATTRIBUTE
,
327 Status
IsElementAttributeEqualToIgnoreCase(
330 const std::string
& element_id
,
331 const std::string
& attribute_name
,
332 const std::string
& attribute_value
,
334 scoped_ptr
<base::Value
> result
;
335 Status status
= GetElementAttribute(
336 session
, web_view
, element_id
, attribute_name
, &result
);
337 if (status
.IsError())
339 std::string actual_value
;
340 if (result
->GetAsString(&actual_value
))
341 *is_equal
= LowerCaseEqualsASCII(actual_value
, attribute_value
.c_str());
347 Status
GetElementClickableLocation(
350 const std::string
& element_id
,
351 WebPoint
* location
) {
352 std::string tag_name
;
353 Status status
= GetElementTagName(session
, web_view
, element_id
, &tag_name
);
354 if (status
.IsError())
356 std::string target_element_id
= element_id
;
357 if (tag_name
== "area") {
358 // Scroll the image into view instead of the area.
359 const char kGetImageElementForArea
[] =
360 "function (element) {"
361 " var map = element.parentElement;"
362 " if (map.tagName.toLowerCase() != 'map')"
363 " throw new Error('the area is not within a map');"
364 " var mapName = map.getAttribute('name');"
365 " if (mapName == null)"
366 " throw new Error ('area\\'s parent map must have a name');"
367 " mapName = '#' + mapName.toLowerCase();"
368 " var images = document.getElementsByTagName('img');"
369 " for (var i = 0; i < images.length; i++) {"
370 " if (images[i].useMap.toLowerCase() == mapName)"
373 " throw new Error('no img is found for the area');"
375 base::ListValue args
;
376 args
.Append(CreateElement(element_id
));
377 scoped_ptr
<base::Value
> result
;
378 status
= web_view
->CallFunction(
379 session
->GetCurrentFrameId(), kGetImageElementForArea
, args
, &result
);
380 if (status
.IsError())
382 const base::DictionaryValue
* element_dict
;
383 if (!result
->GetAsDictionary(&element_dict
) ||
384 !element_dict
->GetString(kElementKey
, &target_element_id
))
385 return Status(kUnknownError
, "no element reference returned by script");
387 bool is_displayed
= false;
388 status
= IsElementDisplayed(
389 session
, web_view
, target_element_id
, true, &is_displayed
);
390 if (status
.IsError())
393 return Status(kElementNotVisible
);
396 status
= GetElementRegion(session
, web_view
, element_id
, &rect
);
397 if (status
.IsError())
400 status
= ScrollElementRegionIntoView(
401 session
, web_view
, target_element_id
, rect
,
402 true /* center */, element_id
, location
);
403 if (status
.IsError())
405 location
->Offset(rect
.Width() / 2, rect
.Height() / 2);
409 Status
GetElementEffectiveStyle(
412 const std::string
& element_id
,
413 const std::string
& property_name
,
414 std::string
* property_value
) {
415 return GetElementEffectiveStyle(session
->GetCurrentFrameId(), web_view
,
416 element_id
, property_name
, property_value
);
419 Status
GetElementRegion(
422 const std::string
& element_id
,
424 base::ListValue args
;
425 args
.Append(CreateElement(element_id
));
426 scoped_ptr
<base::Value
> result
;
427 Status status
= web_view
->CallFunction(
428 session
->GetCurrentFrameId(), kGetElementRegionScript
, args
, &result
);
429 if (status
.IsError())
431 if (!ParseFromValue(result
.get(), rect
)) {
432 return Status(kUnknownError
,
433 "failed to parse value of getElementRegion");
438 Status
GetElementTagName(
441 const std::string
& element_id
,
443 base::ListValue args
;
444 args
.Append(CreateElement(element_id
));
445 scoped_ptr
<base::Value
> result
;
446 Status status
= web_view
->CallFunction(
447 session
->GetCurrentFrameId(),
448 "function(elem) { return elem.tagName.toLowerCase(); }",
450 if (status
.IsError())
452 if (!result
->GetAsString(name
))
453 return Status(kUnknownError
, "failed to get element tag name");
457 Status
GetElementSize(
460 const std::string
& element_id
,
462 base::ListValue args
;
463 args
.Append(CreateElement(element_id
));
464 scoped_ptr
<base::Value
> result
;
465 Status status
= CallAtomsJs(
466 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::GET_SIZE
,
468 if (status
.IsError())
470 if (!ParseFromValue(result
.get(), size
))
471 return Status(kUnknownError
, "failed to parse value of GET_SIZE");
475 Status
IsElementDisplayed(
478 const std::string
& element_id
,
480 bool* is_displayed
) {
481 base::ListValue args
;
482 args
.Append(CreateElement(element_id
));
483 args
.AppendBoolean(ignore_opacity
);
484 scoped_ptr
<base::Value
> result
;
485 Status status
= CallAtomsJs(
486 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_DISPLAYED
,
488 if (status
.IsError())
490 if (!result
->GetAsBoolean(is_displayed
))
491 return Status(kUnknownError
, "IS_DISPLAYED should return a boolean value");
495 Status
IsElementEnabled(
498 const std::string
& element_id
,
500 base::ListValue args
;
501 args
.Append(CreateElement(element_id
));
502 scoped_ptr
<base::Value
> result
;
503 Status status
= CallAtomsJs(
504 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_ENABLED
,
506 if (status
.IsError())
508 if (!result
->GetAsBoolean(is_enabled
))
509 return Status(kUnknownError
, "IS_ENABLED should return a boolean value");
513 Status
IsOptionElementSelected(
516 const std::string
& element_id
,
518 base::ListValue args
;
519 args
.Append(CreateElement(element_id
));
520 scoped_ptr
<base::Value
> result
;
521 Status status
= CallAtomsJs(
522 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_SELECTED
,
524 if (status
.IsError())
526 if (!result
->GetAsBoolean(is_selected
))
527 return Status(kUnknownError
, "IS_SELECTED should return a boolean value");
531 Status
IsOptionElementTogglable(
534 const std::string
& element_id
,
535 bool* is_togglable
) {
536 base::ListValue args
;
537 args
.Append(CreateElement(element_id
));
538 scoped_ptr
<base::Value
> result
;
539 Status status
= web_view
->CallFunction(
540 session
->GetCurrentFrameId(), kIsOptionElementToggleableScript
,
542 if (status
.IsError())
544 if (!result
->GetAsBoolean(is_togglable
))
545 return Status(kUnknownError
, "failed check if option togglable or not");
549 Status
SetOptionElementSelected(
552 const std::string
& element_id
,
554 // TODO(171034): need to fix throwing error if an alert is triggered.
555 base::ListValue args
;
556 args
.Append(CreateElement(element_id
));
557 args
.AppendBoolean(selected
);
558 scoped_ptr
<base::Value
> result
;
560 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::CLICK
,
564 Status
ToggleOptionElement(
567 const std::string
& element_id
) {
569 Status status
= IsOptionElementSelected(
570 session
, web_view
, element_id
, &is_selected
);
571 if (status
.IsError())
573 return SetOptionElementSelected(session
, web_view
, element_id
, !is_selected
);
576 Status
ScrollElementIntoView(
579 const std::string
& id
,
580 const WebPoint
* offset
,
581 WebPoint
* location
) {
583 Status status
= GetElementRegion(session
, web_view
, id
, ®ion
);
584 if (status
.IsError())
586 status
= ScrollElementRegionIntoView(session
, web_view
, id
, region
,
587 false /* center */, std::string(), location
);
588 if (status
.IsError())
591 location
->Offset(offset
->x
, offset
->y
);
593 location
->Offset(region
.size
.width
/ 2, region
.size
.height
/ 2);
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
;