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
)) {
342 base::LowerCaseEqualsASCII(actual_value
, attribute_value
.c_str());
349 Status
GetElementClickableLocation(
352 const std::string
& element_id
,
353 WebPoint
* location
) {
354 std::string tag_name
;
355 Status status
= GetElementTagName(session
, web_view
, element_id
, &tag_name
);
356 if (status
.IsError())
358 std::string target_element_id
= element_id
;
359 if (tag_name
== "area") {
360 // Scroll the image into view instead of the area.
361 const char kGetImageElementForArea
[] =
362 "function (element) {"
363 " var map = element.parentElement;"
364 " if (map.tagName.toLowerCase() != 'map')"
365 " throw new Error('the area is not within a map');"
366 " var mapName = map.getAttribute('name');"
367 " if (mapName == null)"
368 " throw new Error ('area\\'s parent map must have a name');"
369 " mapName = '#' + mapName.toLowerCase();"
370 " var images = document.getElementsByTagName('img');"
371 " for (var i = 0; i < images.length; i++) {"
372 " if (images[i].useMap.toLowerCase() == mapName)"
375 " throw new Error('no img is found for the area');"
377 base::ListValue args
;
378 args
.Append(CreateElement(element_id
));
379 scoped_ptr
<base::Value
> result
;
380 status
= web_view
->CallFunction(
381 session
->GetCurrentFrameId(), kGetImageElementForArea
, args
, &result
);
382 if (status
.IsError())
384 const base::DictionaryValue
* element_dict
;
385 if (!result
->GetAsDictionary(&element_dict
) ||
386 !element_dict
->GetString(kElementKey
, &target_element_id
))
387 return Status(kUnknownError
, "no element reference returned by script");
389 bool is_displayed
= false;
390 status
= IsElementDisplayed(
391 session
, web_view
, target_element_id
, true, &is_displayed
);
392 if (status
.IsError())
395 return Status(kElementNotVisible
);
398 status
= GetElementRegion(session
, web_view
, element_id
, &rect
);
399 if (status
.IsError())
402 status
= ScrollElementRegionIntoView(
403 session
, web_view
, target_element_id
, rect
,
404 true /* center */, element_id
, location
);
405 if (status
.IsError())
407 location
->Offset(rect
.Width() / 2, rect
.Height() / 2);
411 Status
GetElementEffectiveStyle(
414 const std::string
& element_id
,
415 const std::string
& property_name
,
416 std::string
* property_value
) {
417 return GetElementEffectiveStyle(session
->GetCurrentFrameId(), web_view
,
418 element_id
, property_name
, property_value
);
421 Status
GetElementRegion(
424 const std::string
& element_id
,
426 base::ListValue args
;
427 args
.Append(CreateElement(element_id
));
428 scoped_ptr
<base::Value
> result
;
429 Status status
= web_view
->CallFunction(
430 session
->GetCurrentFrameId(), kGetElementRegionScript
, args
, &result
);
431 if (status
.IsError())
433 if (!ParseFromValue(result
.get(), rect
)) {
434 return Status(kUnknownError
,
435 "failed to parse value of getElementRegion");
440 Status
GetElementTagName(
443 const std::string
& element_id
,
445 base::ListValue args
;
446 args
.Append(CreateElement(element_id
));
447 scoped_ptr
<base::Value
> result
;
448 Status status
= web_view
->CallFunction(
449 session
->GetCurrentFrameId(),
450 "function(elem) { return elem.tagName.toLowerCase(); }",
452 if (status
.IsError())
454 if (!result
->GetAsString(name
))
455 return Status(kUnknownError
, "failed to get element tag name");
459 Status
GetElementSize(
462 const std::string
& element_id
,
464 base::ListValue args
;
465 args
.Append(CreateElement(element_id
));
466 scoped_ptr
<base::Value
> result
;
467 Status status
= CallAtomsJs(
468 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::GET_SIZE
,
470 if (status
.IsError())
472 if (!ParseFromValue(result
.get(), size
))
473 return Status(kUnknownError
, "failed to parse value of GET_SIZE");
477 Status
IsElementDisplayed(
480 const std::string
& element_id
,
482 bool* is_displayed
) {
483 base::ListValue args
;
484 args
.Append(CreateElement(element_id
));
485 args
.AppendBoolean(ignore_opacity
);
486 scoped_ptr
<base::Value
> result
;
487 Status status
= CallAtomsJs(
488 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_DISPLAYED
,
490 if (status
.IsError())
492 if (!result
->GetAsBoolean(is_displayed
))
493 return Status(kUnknownError
, "IS_DISPLAYED should return a boolean value");
497 Status
IsElementEnabled(
500 const std::string
& element_id
,
502 base::ListValue args
;
503 args
.Append(CreateElement(element_id
));
504 scoped_ptr
<base::Value
> result
;
505 Status status
= CallAtomsJs(
506 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_ENABLED
,
508 if (status
.IsError())
510 if (!result
->GetAsBoolean(is_enabled
))
511 return Status(kUnknownError
, "IS_ENABLED should return a boolean value");
515 Status
IsOptionElementSelected(
518 const std::string
& element_id
,
520 base::ListValue args
;
521 args
.Append(CreateElement(element_id
));
522 scoped_ptr
<base::Value
> result
;
523 Status status
= CallAtomsJs(
524 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_SELECTED
,
526 if (status
.IsError())
528 if (!result
->GetAsBoolean(is_selected
))
529 return Status(kUnknownError
, "IS_SELECTED should return a boolean value");
533 Status
IsOptionElementTogglable(
536 const std::string
& element_id
,
537 bool* is_togglable
) {
538 base::ListValue args
;
539 args
.Append(CreateElement(element_id
));
540 scoped_ptr
<base::Value
> result
;
541 Status status
= web_view
->CallFunction(
542 session
->GetCurrentFrameId(), kIsOptionElementToggleableScript
,
544 if (status
.IsError())
546 if (!result
->GetAsBoolean(is_togglable
))
547 return Status(kUnknownError
, "failed check if option togglable or not");
551 Status
SetOptionElementSelected(
554 const std::string
& element_id
,
556 // TODO(171034): need to fix throwing error if an alert is triggered.
557 base::ListValue args
;
558 args
.Append(CreateElement(element_id
));
559 args
.AppendBoolean(selected
);
560 scoped_ptr
<base::Value
> result
;
562 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::CLICK
,
566 Status
ToggleOptionElement(
569 const std::string
& element_id
) {
571 Status status
= IsOptionElementSelected(
572 session
, web_view
, element_id
, &is_selected
);
573 if (status
.IsError())
575 return SetOptionElementSelected(session
, web_view
, element_id
, !is_selected
);
578 Status
ScrollElementIntoView(
581 const std::string
& id
,
582 const WebPoint
* offset
,
583 WebPoint
* location
) {
585 Status status
= GetElementRegion(session
, web_view
, id
, ®ion
);
586 if (status
.IsError())
588 status
= ScrollElementRegionIntoView(session
, web_view
, id
, region
,
589 false /* center */, std::string(), location
);
590 if (status
.IsError())
593 location
->Offset(offset
->x
, offset
->y
);
595 location
->Offset(region
.size
.width
/ 2, region
.size
.height
/ 2);
599 Status
ScrollElementRegionIntoView(
602 const std::string
& element_id
,
603 const WebRect
& region
,
605 const std::string
& clickable_element_id
,
606 WebPoint
* location
) {
607 WebPoint region_offset
= region
.origin
;
608 WebSize region_size
= region
.size
;
609 Status status
= ScrollElementRegionIntoViewHelper(
610 session
->GetCurrentFrameId(), web_view
, element_id
, region
,
611 center
, clickable_element_id
, ®ion_offset
);
612 if (status
.IsError())
614 const char kFindSubFrameScript
[] =
616 " return document.evaluate(xpath, document, null,"
617 " XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
619 for (std::list
<FrameInfo
>::reverse_iterator rit
= session
->frames
.rbegin();
620 rit
!= session
->frames
.rend(); ++rit
) {
621 base::ListValue args
;
623 base::StringPrintf("//*[@cd_frame_id_ = '%s']",
624 rit
->chromedriver_frame_id
.c_str()));
625 scoped_ptr
<base::Value
> result
;
626 status
= web_view
->CallFunction(
627 rit
->parent_frame_id
, kFindSubFrameScript
, args
, &result
);
628 if (status
.IsError())
630 const base::DictionaryValue
* element_dict
;
631 if (!result
->GetAsDictionary(&element_dict
))
632 return Status(kUnknownError
, "no element reference returned by script");
633 std::string frame_element_id
;
634 if (!element_dict
->GetString(kElementKey
, &frame_element_id
))
635 return Status(kUnknownError
, "failed to locate a sub frame");
637 // Modify |region_offset| by the frame's border.
638 int border_left
= -1;
640 status
= GetElementBorder(
641 rit
->parent_frame_id
, web_view
, frame_element_id
,
642 &border_left
, &border_top
);
643 if (status
.IsError())
645 region_offset
.Offset(border_left
, border_top
);
647 status
= ScrollElementRegionIntoViewHelper(
648 rit
->parent_frame_id
, web_view
, frame_element_id
,
649 WebRect(region_offset
, region_size
),
650 center
, frame_element_id
, ®ion_offset
);
651 if (status
.IsError())
654 *location
= region_offset
;