/**
* Candybean is a next generation automation and testing framework suite.
* It is a collection of components that foster test automation, execution
* configuration, data abstraction, results illustration, tag-based execution,
* top-down and bottom-up batches, mobile variants, test translation across
* languages, plain-language testing, and web service testing.
* Copyright (C) 2013 SugarCRM, Inc. <candybean@sugarcrm.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sugarcrm.candybean.automation.webdriver;
import java.util.List;
import org.openqa.selenium.*;
import org.openqa.selenium.interactions.Actions;
import com.sugarcrm.candybean.automation.element.Element;
import com.sugarcrm.candybean.automation.element.Hook;
import com.sugarcrm.candybean.automation.element.Location;
import com.sugarcrm.candybean.automation.element.Hook.Strategy;
import com.sugarcrm.candybean.exceptions.CandybeanException;
/**
* Represents an identifiable (via {@link By}) element or element on a page that
* can be interacted with. A {@link WebDriverElement} object should be used to
* automate tasks that require interaction with elements on a page.
*
* @author Conrad Warmbold
*/
public class WebDriverElement extends Element {
protected WebDriver wd;
protected WebElement we;
public WebDriverElement(Strategy strategy, String hookString, WebDriver wd) throws CandybeanException {
this(new Hook(strategy, hookString), wd);
}
public WebDriverElement(Strategy strategy, String hookString, int index, WebDriver wd) throws CandybeanException {
this(new Hook(strategy, hookString), index, wd);
}
public WebDriverElement(Hook hook, WebDriver wd) throws CandybeanException {
this(hook, 0, wd);
}
public WebDriverElement(Hook hook, int index, WebDriver wd) throws CandybeanException {
super(hook, index);
this.wd = wd;
List<WebElement> wes = this.wd.findElements(Hook.getBy(hook));
if (wes.size() == 0) {
throw new CandybeanException("Control not found; zero web elements returned.");
}
this.we = wes.get(index);
}
public WebDriverElement(Hook hook, int index, WebDriver wd, WebElement we) throws CandybeanException {
super(hook, index);
this.wd = wd;
this.we = we;
}
/**
* Get the value of an attribute of the element.
*
* @param attribute
* name of the attribute to get
* @return the value of the specified attribute
* @throws CandybeanException
* if the attribute does not exist or the element cannot be
* found
*/
public String getAttribute(String attribute) throws CandybeanException {
logger.info("Getting attribute: " + attribute
+ " for element: " + this.toString());
String value = we.getAttribute(attribute);
if (value == null)
throw new CandybeanException("Attribute does not exist.");
else
return value;
}
public String getSource() throws CandybeanException {
logger.info("Getting source for element: " + this.toString());
return (String) ((JavascriptExecutor) this.wd)
.executeScript("return arguments[0].innerHTML;", this.we);
}
/**
* Get the visible text of this element.
* Return the value if it is an input element, otherwise, return the innnerText
*
* @return the visible text of this element
*/
public String getText() throws CandybeanException {
logger.info("Getting text for element: " + this.toString());
if ("input".equals(we.getTagName())){
return this.we.getAttribute("value");
}
return this.we.getText();
}
/**
* Click the element.
*/
public void click() throws CandybeanException {
logger.info("Clicking on element: " + this.toString());
we.click();
}
/**
* Returns true if the element visibly contains the given string in any
* non-visible=false element.
*
* @param s
* The target string searched for in the interface
* @param caseSensitive
* Whether or not the search is case sensitive
* @return Returns true if the interface visibly contains the given string
*/
public boolean contains(String s, boolean caseSensitive) {
logger.info("Searching if the element contains the following string: '"
+ s + "' with case sensitivity: " + caseSensitive);
String lowercase = s;
if (!caseSensitive) {
lowercase = s.toLowerCase();
}
List<WebElement> wes = this.we.findElements(By
.xpath(".//*[not(@visible='false')]"));
wes.add(this.we);
for (WebElement webElement : wes) {
String text = webElement.getText();
if (!caseSensitive) {
text = text.toLowerCase();
}
if (text.contains(lowercase)) {
return true;
}
}
return false;
}
/**
* Double-click the element.
*/
public void doubleClick() throws CandybeanException {
logger.info("Double-clicking on element: " + this.toString());
Actions action = new Actions(this.wd);
action.doubleClick(we).perform();
}
/**
* Drag this element and drop onto another element.
*
* @param dropControl
* target of the drag and drop
*/
public void dragNDrop(WebDriverElement dropControl)
throws CandybeanException {
logger.info("Dragging element: " + this.toString()
+ " to element: " + dropControl.toString());
Actions action = new Actions(this.wd);
action.dragAndDrop(this.we, dropControl.we).build().perform();
}
/**
* Get the select child of the given the hook of a parent element
*
* @param hook
* The hook of the parent element
* @param index
* The index of where the child locates
* @return Return a WebDriverSelector that is the child of the parent element
* @throws CandybeanException
*/
public Element getSelect(Hook hook, int index) throws CandybeanException {
logger.info("Getting select: " + hook.toString()
+ " from element: " + this.toString() + " with index: " + index);
WebElement childWe = this.we.findElements(Hook.getBy(hook)).get(index);
return new WebDriverSelector(hook, index, this.wd, childWe);
}
/**
* Get the element child of the given the hook of a parent element
*
* @param hook
* The hook of the parent element
* @param index
* The index of where the child locates
* @return Return a WebDriverSelector that is the child of the parent element
* @throws CandybeanException
*/
@Override
public Element getElement(Hook hook, int index) throws CandybeanException {
logger.info("Getting element: " + hook.toString()
+ " from element: " + this.toString() + " with index: " + index);
WebElement childWe = this.we.findElements(Hook.getBy(hook)).get(index);
return new WebDriverElement(hook, index, this.wd, childWe);
}
/**
* Executes a javascript command against this element.
*
* @param javascript The javascript code to execute
* @param args The arguments to pass. Note that indices of the arguments passed will
* be incremented by 1 because this element will be used as the first arg
* @return an object representation of the return value of the executed Javascript. Depending
* on the type returned from the Javascript, this may be of type WebElement, Double,
* Long, Boolean, String, List<Object>, or null.
*/
@Override
public Object executeJavascript(String javascript, Object... args) {
logger.info("Executing explicit javascript against " + toString());
return ((JavascriptExecutor) wd).executeScript(javascript, we, args);
}
/**
* Executes an asynchronous javascript command against this element. The script executed with
* this method must explicitly signal they are finished by invoking the provided callback. This
* callback is always injected into the executed function as the last argument
*
* @param javascript The javascript code to execute
* @param args The arguments to pass. Note that indices of the arguments passed will
* be incremented by 1 because this element will be used as the first arg
* @return an object representation of the first argument passed to the callback
* of the executed Javascript. Depending on the type returned from the
* Javascript, this may be of type WebElement, Long, Boolean, String,
* List<Object>, or null.
*/
@Override
public Object executeAsyncJavascript(String javascript, Object... args) {
logger.info("Executing explicit async javascript against " + toString());
return ((JavascriptExecutor) wd).executeAsyncScript(javascript, we, args);
}
/**
* @return an int containing the element's width.
*/
@Override
public int getWidth() {
return we.getSize().getWidth();
}
/**
* @return an int containing the element's height.
*/
@Override
public int getHeight() {
return we.getSize().getHeight();
}
/**
* Returns the value of the specified CSS property, or null if it does not exist.
* <p>
* Note that shorthand CSS properties (e.g. background, font, border, border-top, margin,
* margin-top, padding, padding-top, list-style, outline, pause, cue) are not returned, in
* accordance with the DOM CSS2 specification - you should directly access the longhand
* properties (e.g. background-color) to access the desired values.
* </p>
* @return String the value of the specified property, or null if it does not exist.
*/
@Override
public String getCssValue(String propertyName) {
return we.getCssValue(propertyName);
}
/**
* Hover over this element.
*/
public void hover() throws CandybeanException {
logger.info("Hovering over element: " + this.toString());
Actions action = new Actions(this.wd);
action.moveToElement(this.we).perform();
}
/**
* Returns true if and only if the element is displayed
* <a href="http://selenium.googlecode.com/svn/trunk/docs/api/java/index.html"> according to Selenium</a>
*/
public boolean isDisplayed() throws CandybeanException {
logger.info("Determining if element is visible: " + this.toString());
return we.isDisplayed();
}
/**
* Returns true if and only if part of the element is located on the screen/page
*/
public boolean isOnScreen() throws CandybeanException {
logger.info("Determining if element is on screen " + this.toString());
Point location = we.getLocation();
Dimension windowSize = wd.manage().window().getSize();
return ( location.getY() + we.getSize().getHeight() > 0
&& location.getY() < windowSize.getHeight()
&& location.getX() + we.getSize().getWidth() > 0
&& location.getX() < windowSize.getWidth());
}
/**
* Right-click this element.
*/
public void rightClick() throws CandybeanException {
logger.info("Right-clicking element: " + this.toString());
Actions action = new Actions(this.wd);
action.contextClick(this.we).perform();
}
/**
* Scroll this element to the top of the window
*/
public void scroll() throws CandybeanException {
scroll(Location.TOP);
}
/**
* Scroll the element to the specified location in the window.
*/
public void scroll(Location loc) throws CandybeanException {
int y = this.we.getLocation().getY();
int height = this.we.getSize().getHeight();
switch (loc) {
case TOP:
logger.info("Scrolling element to the top of the viewport: " + this.toString());
((JavascriptExecutor) this.wd).executeScript("window.scrollTo(0," + y + ");");
break;
case BOTTOM:
logger.info("Scrolling element to the bottom of the viewport: " + this.toString());
((JavascriptExecutor) this.wd).executeScript("window.scrollTo(0," + y + "- window.innerHeight + " + height + ");");
break;
case MIDDLE:
logger.info("Scrolling element to the middle of the viewport: " + this.toString());
((JavascriptExecutor) this.wd).executeScript("window.scrollTo(0," + y + "- (window.innerHeight/2) + " + height/2 + ");");
break;
default:
break;
}
}
/**
* Selects all text in and focuses on a Javascript HTMLInputElement.
* Selects only from input fields, and will not currently work on
* textAreas (CB-260) but support is planned
*
* @throws CandybeanException If used on a non INPUT element
*/
public void selectAll() throws CandybeanException {
String tag = we.getTagName();
// The Javascript select() method is INPUT specific, throw if used
// on a non-INPUT tag
if (!"input".equals(tag)) {
throw new CandybeanException("Cannot select text of a non-" +
"INPUT element. Actual element was of tag: " + tag);
}
executeJavascript("arguments[0].select()");
}
/**
* Clears an element. If the element is an "input" element, clear
* it by selecting all the text and pressing back space to avoid
* triggering an onChange event. We don't want to trigger the onChange
* event while clearing a textfield as doing so may interfere with the
* following set call
*
* If the field is not an "input" element, use Selenium's clear method
* instead
*
* Also focuses the cleared element
*/
public void clear() throws CandybeanException {
String tag = we.getTagName();
// Theoretically, the .select() call in selectAll should focus the element
// according to https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/select
// However, this seems to have issues in iframes, so we call focus regardless
executeJavascript("arguments[0].focus()");
if ("input".equals(tag)) {
selectAll();
we.sendKeys(Keys.BACK_SPACE);
} else {
we.clear();
}
}
/**
* Clear the element and send a string to it.
*
* @param input The string to send
*/
public void sendString(String input) throws CandybeanException {
sendString(input, false);
}
/**
* Send a string to this element.
*
* @param input string to send
* @param append if append is false, the element will be cleared first
* @throws CandybeanException
*/
public void sendString(String input, boolean append) throws CandybeanException {
logger.info((append ? "Appending" : "Clearing field and sending") + " text: \"" + input +
"\" to element: " + toString());
if (!append) {
clear();
}
we.sendKeys(input);
}
/**
* Get the By using the hook in this WebDriverElement
* @return
* @throws CandybeanException
*/
public By getBy() throws CandybeanException {
return Hook.getBy(this.hook);
}
}