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/js.h"
15 #include "chrome/test/chromedriver/chrome/status.h"
16 #include "chrome/test/chromedriver/chrome/web_view.h"
17 #include "chrome/test/chromedriver/session.h"
18 #include "third_party/webdriver/atoms.h"
22 const char kElementKey
[] = "ELEMENT";
24 bool ParseFromValue(base::Value
* value
, WebPoint
* point
) {
25 base::DictionaryValue
* dict_value
;
26 if (!value
->GetAsDictionary(&dict_value
))
29 if (!dict_value
->GetDouble("x", &x
) ||
30 !dict_value
->GetDouble("y", &y
))
32 point
->x
= static_cast<int>(x
);
33 point
->y
= static_cast<int>(y
);
37 bool ParseFromValue(base::Value
* value
, WebSize
* size
) {
38 base::DictionaryValue
* dict_value
;
39 if (!value
->GetAsDictionary(&dict_value
))
42 if (!dict_value
->GetDouble("width", &width
) ||
43 !dict_value
->GetDouble("height", &height
))
45 size
->width
= static_cast<int>(width
);
46 size
->height
= static_cast<int>(height
);
50 bool ParseFromValue(base::Value
* value
, WebRect
* rect
) {
51 base::DictionaryValue
* dict_value
;
52 if (!value
->GetAsDictionary(&dict_value
))
54 double x
, y
, width
, height
;
55 if (!dict_value
->GetDouble("left", &x
) ||
56 !dict_value
->GetDouble("top", &y
) ||
57 !dict_value
->GetDouble("width", &width
) ||
58 !dict_value
->GetDouble("height", &height
))
60 rect
->origin
.x
= static_cast<int>(x
);
61 rect
->origin
.y
= static_cast<int>(y
);
62 rect
->size
.width
= static_cast<int>(width
);
63 rect
->size
.height
= static_cast<int>(height
);
67 base::Value
* CreateValueFrom(const WebRect
& rect
) {
68 base::DictionaryValue
* dict
= new base::DictionaryValue();
69 dict
->SetInteger("left", rect
.X());
70 dict
->SetInteger("top", rect
.Y());
71 dict
->SetInteger("width", rect
.Width());
72 dict
->SetInteger("height", rect
.Height());
77 const std::string
& frame
,
79 const char* const* atom_function
,
80 const base::ListValue
& args
,
81 scoped_ptr
<base::Value
>* result
) {
82 return web_view
->CallFunction(
83 frame
, webdriver::atoms::asString(atom_function
), args
, result
);
86 Status
VerifyElementClickable(
87 const std::string
& frame
,
89 const std::string
& element_id
,
90 const WebPoint
& location
) {
92 args
.Append(CreateElement(element_id
));
93 args
.Append(CreateValueFrom(location
));
94 scoped_ptr
<base::Value
> result
;
95 Status status
= CallAtomsJs(
96 frame
, web_view
, webdriver::atoms::IS_ELEMENT_CLICKABLE
,
100 base::DictionaryValue
* dict
;
102 if (!result
->GetAsDictionary(&dict
) ||
103 !dict
->GetBoolean("clickable", &is_clickable
)) {
104 return Status(kUnknownError
,
105 "failed to parse value of IS_ELEMENT_CLICKABLE");
110 if (!dict
->GetString("message", &message
))
111 message
= "element is not clickable";
112 return Status(kUnknownError
, message
);
117 Status
ScrollElementRegionIntoViewHelper(
118 const std::string
& frame
,
120 const std::string
& element_id
,
121 const WebRect
& region
,
123 const std::string
& clickable_element_id
,
124 WebPoint
* location
) {
125 WebPoint tmp_location
= *location
;
126 base::ListValue args
;
127 args
.Append(CreateElement(element_id
));
128 args
.AppendBoolean(center
);
129 args
.Append(CreateValueFrom(region
));
130 scoped_ptr
<base::Value
> result
;
131 Status status
= web_view
->CallFunction(
132 frame
, webdriver::atoms::asString(webdriver::atoms::GET_LOCATION_IN_VIEW
),
134 if (status
.IsError())
136 if (!ParseFromValue(result
.get(), &tmp_location
)) {
137 return Status(kUnknownError
,
138 "failed to parse value of GET_LOCATION_IN_VIEW");
140 if (!clickable_element_id
.empty()) {
141 WebPoint middle
= tmp_location
;
142 middle
.Offset(region
.Width() / 2, region
.Height() / 2);
143 status
= VerifyElementClickable(
144 frame
, web_view
, clickable_element_id
, middle
);
145 if (status
.IsError())
148 *location
= tmp_location
;
152 Status
GetElementEffectiveStyle(
153 const std::string
& frame
,
155 const std::string
& element_id
,
156 const std::string
& property
,
157 std::string
* value
) {
158 base::ListValue args
;
159 args
.Append(CreateElement(element_id
));
160 args
.AppendString(property
);
161 scoped_ptr
<base::Value
> result
;
162 Status status
= web_view
->CallFunction(
163 frame
, webdriver::atoms::asString(webdriver::atoms::GET_EFFECTIVE_STYLE
),
165 if (status
.IsError())
167 if (!result
->GetAsString(value
)) {
168 return Status(kUnknownError
,
169 "failed to parse value of GET_EFFECTIVE_STYLE");
174 Status
GetElementBorder(
175 const std::string
& frame
,
177 const std::string
& element_id
,
180 std::string border_left_str
;
181 Status status
= GetElementEffectiveStyle(
182 frame
, web_view
, element_id
, "border-left-width", &border_left_str
);
183 if (status
.IsError())
185 std::string border_top_str
;
186 status
= GetElementEffectiveStyle(
187 frame
, web_view
, element_id
, "border-top-width", &border_top_str
);
188 if (status
.IsError())
190 int border_left_tmp
= -1;
191 int border_top_tmp
= -1;
192 base::StringToInt(border_left_str
, &border_left_tmp
);
193 base::StringToInt(border_top_str
, &border_top_tmp
);
194 if (border_left_tmp
== -1 || border_top_tmp
== -1)
195 return Status(kUnknownError
, "failed to get border width of element");
196 *border_left
= border_left_tmp
;
197 *border_top
= border_top_tmp
;
203 base::DictionaryValue
* CreateElement(const std::string
& element_id
) {
204 base::DictionaryValue
* element
= new base::DictionaryValue();
205 element
->SetString(kElementKey
, element_id
);
209 base::Value
* CreateValueFrom(const WebPoint
& point
) {
210 base::DictionaryValue
* dict
= new base::DictionaryValue();
211 dict
->SetInteger("x", point
.x
);
212 dict
->SetInteger("y", point
.y
);
219 const std::string
* root_element_id
,
222 const base::DictionaryValue
& params
,
223 scoped_ptr
<base::Value
>* value
) {
224 std::string strategy
;
225 if (!params
.GetString("using", &strategy
))
226 return Status(kUnknownError
, "'using' must be a string");
228 if (!params
.GetString("value", &target
))
229 return Status(kUnknownError
, "'value' must be a string");
233 script
= webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENT
);
235 script
= webdriver::atoms::asString(webdriver::atoms::FIND_ELEMENTS
);
236 scoped_ptr
<base::DictionaryValue
> locator(new base::DictionaryValue());
237 locator
->SetString(strategy
, target
);
238 base::ListValue arguments
;
239 arguments
.Append(locator
.release());
241 arguments
.Append(CreateElement(*root_element_id
));
243 base::TimeTicks start_time
= base::TimeTicks::Now();
245 scoped_ptr
<base::Value
> temp
;
246 Status status
= web_view
->CallFunction(
247 session
->GetCurrentFrameId(), script
, arguments
, &temp
);
248 if (status
.IsError())
251 if (!temp
->IsType(base::Value::TYPE_NULL
)) {
253 value
->reset(temp
.release());
256 base::ListValue
* result
;
257 if (!temp
->GetAsList(&result
))
258 return Status(kUnknownError
, "script returns unexpected result");
260 if (result
->GetSize() > 0U) {
261 value
->reset(temp
.release());
267 if (base::TimeTicks::Now() - start_time
>= session
->implicit_wait
) {
269 return Status(kNoSuchElement
);
271 value
->reset(new base::ListValue());
275 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(interval_ms
));
278 return Status(kUnknownError
);
281 Status
GetActiveElement(
284 scoped_ptr
<base::Value
>* value
) {
285 base::ListValue args
;
286 return web_view
->CallFunction(
287 session
->GetCurrentFrameId(),
288 "function() { return document.activeElement || document.body }",
293 Status
IsElementFocused(
296 const std::string
& element_id
,
298 scoped_ptr
<base::Value
> result
;
299 Status status
= GetActiveElement(session
, web_view
, &result
);
300 if (status
.IsError())
302 scoped_ptr
<base::Value
> element_dict(CreateElement(element_id
));
303 *is_focused
= result
->Equals(element_dict
.get());
307 Status
GetElementAttribute(
310 const std::string
& element_id
,
311 const std::string
& attribute_name
,
312 scoped_ptr
<base::Value
>* value
) {
313 base::ListValue args
;
314 args
.Append(CreateElement(element_id
));
315 args
.AppendString(attribute_name
);
317 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::GET_ATTRIBUTE
,
321 Status
IsElementAttributeEqualToIgnoreCase(
324 const std::string
& element_id
,
325 const std::string
& attribute_name
,
326 const std::string
& attribute_value
,
328 scoped_ptr
<base::Value
> result
;
329 Status status
= GetElementAttribute(
330 session
, web_view
, element_id
, attribute_name
, &result
);
331 if (status
.IsError())
333 std::string actual_value
;
334 if (result
->GetAsString(&actual_value
))
335 *is_equal
= LowerCaseEqualsASCII(actual_value
, attribute_value
.c_str());
341 Status
GetElementClickableLocation(
344 const std::string
& element_id
,
345 WebPoint
* location
) {
346 std::string tag_name
;
347 Status status
= GetElementTagName(session
, web_view
, element_id
, &tag_name
);
348 if (status
.IsError())
350 std::string target_element_id
= element_id
;
351 if (tag_name
== "area") {
352 // Scroll the image into view instead of the area.
353 const char* kGetImageElementForArea
=
354 "function (element) {"
355 " var map = element.parentElement;"
356 " if (map.tagName.toLowerCase() != 'map')"
357 " throw new Error('the area is not within a map');"
358 " var mapName = map.getAttribute('name');"
359 " if (mapName == null)"
360 " throw new Error ('area\\'s parent map must have a name');"
361 " mapName = '#' + mapName.toLowerCase();"
362 " var images = document.getElementsByTagName('img');"
363 " for (var i = 0; i < images.length; i++) {"
364 " if (images[i].useMap.toLowerCase() == mapName)"
367 " throw new Error('no img is found for the area');"
369 base::ListValue args
;
370 args
.Append(CreateElement(element_id
));
371 scoped_ptr
<base::Value
> result
;
372 status
= web_view
->CallFunction(
373 session
->GetCurrentFrameId(), kGetImageElementForArea
, args
, &result
);
374 if (status
.IsError())
376 const base::DictionaryValue
* element_dict
;
377 if (!result
->GetAsDictionary(&element_dict
) ||
378 !element_dict
->GetString(kElementKey
, &target_element_id
))
379 return Status(kUnknownError
, "no element reference returned by script");
381 bool is_displayed
= false;
382 status
= IsElementDisplayed(
383 session
, web_view
, target_element_id
, true, &is_displayed
);
384 if (status
.IsError())
387 return Status(kElementNotVisible
);
390 status
= GetElementRegion(session
, web_view
, element_id
, &rect
);
391 if (status
.IsError())
394 status
= ScrollElementRegionIntoView(
395 session
, web_view
, target_element_id
, rect
,
396 true /* center */, element_id
, location
);
397 if (status
.IsError())
399 location
->Offset(rect
.Width() / 2, rect
.Height() / 2);
403 Status
GetElementEffectiveStyle(
406 const std::string
& element_id
,
407 const std::string
& property_name
,
408 std::string
* property_value
) {
409 return GetElementEffectiveStyle(session
->GetCurrentFrameId(), web_view
,
410 element_id
, property_name
, property_value
);
413 Status
GetElementRegion(
416 const std::string
& element_id
,
418 base::ListValue args
;
419 args
.Append(CreateElement(element_id
));
420 scoped_ptr
<base::Value
> result
;
421 Status status
= web_view
->CallFunction(
422 session
->GetCurrentFrameId(), kGetElementRegionScript
, args
, &result
);
423 if (status
.IsError())
425 if (!ParseFromValue(result
.get(), rect
)) {
426 return Status(kUnknownError
,
427 "failed to parse value of getElementRegion");
432 Status
GetElementTagName(
435 const std::string
& element_id
,
437 base::ListValue args
;
438 args
.Append(CreateElement(element_id
));
439 scoped_ptr
<base::Value
> result
;
440 Status status
= web_view
->CallFunction(
441 session
->GetCurrentFrameId(),
442 "function(elem) { return elem.tagName.toLowerCase(); }",
444 if (status
.IsError())
446 if (!result
->GetAsString(name
))
447 return Status(kUnknownError
, "failed to get element tag name");
451 Status
GetElementSize(
454 const std::string
& element_id
,
456 base::ListValue args
;
457 args
.Append(CreateElement(element_id
));
458 scoped_ptr
<base::Value
> result
;
459 Status status
= CallAtomsJs(
460 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::GET_SIZE
,
462 if (status
.IsError())
464 if (!ParseFromValue(result
.get(), size
))
465 return Status(kUnknownError
, "failed to parse value of GET_SIZE");
469 Status
IsElementDisplayed(
472 const std::string
& element_id
,
474 bool* is_displayed
) {
475 base::ListValue args
;
476 args
.Append(CreateElement(element_id
));
477 args
.AppendBoolean(ignore_opacity
);
478 scoped_ptr
<base::Value
> result
;
479 Status status
= CallAtomsJs(
480 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_DISPLAYED
,
482 if (status
.IsError())
484 if (!result
->GetAsBoolean(is_displayed
))
485 return Status(kUnknownError
, "IS_DISPLAYED should return a boolean value");
489 Status
IsElementEnabled(
492 const std::string
& element_id
,
494 base::ListValue args
;
495 args
.Append(CreateElement(element_id
));
496 scoped_ptr
<base::Value
> result
;
497 Status status
= CallAtomsJs(
498 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::IS_ENABLED
,
500 if (status
.IsError())
502 if (!result
->GetAsBoolean(is_enabled
))
503 return Status(kUnknownError
, "IS_ENABLED should return a boolean value");
507 Status
IsOptionElementSelected(
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_SELECTED
,
518 if (status
.IsError())
520 if (!result
->GetAsBoolean(is_selected
))
521 return Status(kUnknownError
, "IS_SELECTED should return a boolean value");
525 Status
IsOptionElementTogglable(
528 const std::string
& element_id
,
529 bool* is_togglable
) {
530 base::ListValue args
;
531 args
.Append(CreateElement(element_id
));
532 scoped_ptr
<base::Value
> result
;
533 Status status
= web_view
->CallFunction(
534 session
->GetCurrentFrameId(), kIsOptionElementToggleableScript
,
536 if (status
.IsError())
538 if (!result
->GetAsBoolean(is_togglable
))
539 return Status(kUnknownError
, "failed check if option togglable or not");
543 Status
SetOptionElementSelected(
546 const std::string
& element_id
,
548 // TODO(171034): need to fix throwing error if an alert is triggered.
549 base::ListValue args
;
550 args
.Append(CreateElement(element_id
));
551 args
.AppendBoolean(selected
);
552 scoped_ptr
<base::Value
> result
;
554 session
->GetCurrentFrameId(), web_view
, webdriver::atoms::CLICK
,
558 Status
ToggleOptionElement(
561 const std::string
& element_id
) {
563 Status status
= IsOptionElementSelected(
564 session
, web_view
, element_id
, &is_selected
);
565 if (status
.IsError())
567 return SetOptionElementSelected(session
, web_view
, element_id
, !is_selected
);
570 Status
ScrollElementIntoView(
573 const std::string
& id
,
574 WebPoint
* location
) {
576 Status status
= GetElementSize(session
, web_view
, id
, &size
);
577 if (status
.IsError())
579 return ScrollElementRegionIntoView(
580 session
, web_view
, id
, WebRect(WebPoint(0, 0), size
),
581 false /* center */, std::string(), location
);
584 Status
ScrollElementRegionIntoView(
587 const std::string
& element_id
,
588 const WebRect
& region
,
590 const std::string
& clickable_element_id
,
591 WebPoint
* location
) {
592 WebPoint region_offset
= region
.origin
;
593 WebSize region_size
= region
.size
;
594 Status status
= ScrollElementRegionIntoViewHelper(
595 session
->GetCurrentFrameId(), web_view
, element_id
, region
,
596 center
, clickable_element_id
, ®ion_offset
);
597 if (status
.IsError())
599 const char* kFindSubFrameScript
=
601 " return document.evaluate(xpath, document, null,"
602 " XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
604 for (std::list
<FrameInfo
>::reverse_iterator rit
= session
->frames
.rbegin();
605 rit
!= session
->frames
.rend(); ++rit
) {
606 base::ListValue args
;
608 base::StringPrintf("//*[@cd_frame_id_ = '%s']",
609 rit
->chromedriver_frame_id
.c_str()));
610 scoped_ptr
<base::Value
> result
;
611 status
= web_view
->CallFunction(
612 rit
->parent_frame_id
, kFindSubFrameScript
, args
, &result
);
613 if (status
.IsError())
615 const base::DictionaryValue
* element_dict
;
616 if (!result
->GetAsDictionary(&element_dict
))
617 return Status(kUnknownError
, "no element reference returned by script");
618 std::string frame_element_id
;
619 if (!element_dict
->GetString(kElementKey
, &frame_element_id
))
620 return Status(kUnknownError
, "failed to locate a sub frame");
622 // Modify |region_offset| by the frame's border.
623 int border_left
= -1;
625 status
= GetElementBorder(
626 rit
->parent_frame_id
, web_view
, frame_element_id
,
627 &border_left
, &border_top
);
628 if (status
.IsError())
630 region_offset
.Offset(border_left
, border_top
);
632 status
= ScrollElementRegionIntoViewHelper(
633 rit
->parent_frame_id
, web_view
, frame_element_id
,
634 WebRect(region_offset
, region_size
),
635 center
, frame_element_id
, ®ion_offset
);
636 if (status
.IsError())
639 *location
= region_offset
;