/**
* 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.awt.AWTException;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.TimeUnit;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.swing.JOptionPane;
import org.openqa.selenium.By;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.interactions.HasTouchScreen;
import org.openqa.selenium.interactions.TouchScreen;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoAlertPresentException;
import org.openqa.selenium.UnhandledAlertException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.RemoteTouchScreen;
import org.openqa.selenium.remote.RemoteWebDriver;
import com.sugarcrm.candybean.automation.AutomationInterface;
import com.sugarcrm.candybean.automation.Candybean;
import com.sugarcrm.candybean.automation.element.Element;
import com.sugarcrm.candybean.automation.element.Hook;
import com.sugarcrm.candybean.automation.element.Hook.Strategy;
import com.sugarcrm.candybean.exceptions.CandybeanException;
import com.sugarcrm.candybean.utilities.Utils.Pair;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
/**
* Drives the creation of multi-platform automation tests by providing a resourceful API
* containing several helper methods to write automation tests. The {@link Candybean} configuration
* will build a {@link WebDriverInterface} based on the platform specified in the configuration. An appropriate platform-specific
* driver is instantiated for use to write tests.
*
*/
public abstract class WebDriverInterface extends AutomationInterface {
public WebDriver wd = null;
private String baseUrl = "";
private Stack<Pair<Integer, String>> windows = new Stack<Pair<Integer, String>>();
protected WebDriverInterface(Type iType) throws CandybeanException {
super(iType);
}
/**
* Handle the way this interface is to be started.
* This routine must be implemented specific to the type of interface.
*
* @throws CandybeanException
*/
@Override
public void start() throws CandybeanException {
// Set implicit wait and timeout parameters
long implicitWait = Long.parseLong(candybean.config.getValue("perf.implicit.wait.seconds"));
wd.manage().timeouts().implicitlyWait(implicitWait, TimeUnit.SECONDS);
if (System.getProperty("headless") == null && iType != Type.IOS && iType != Type.ANDROID) {
java.awt.Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
wd.manage().window().setSize(new Dimension(screenSize.width, screenSize.height));
this.windows.push(new Pair<Integer, String>(new Integer(0), this.wd.getWindowHandle()));
}
}
/**
* @throws CandybeanException
*/
@Override
public void stop() throws CandybeanException {
this.windows.clear();
this.wd.quit();
}
/**
* @throws CandybeanException
*/
public void restart() throws CandybeanException {
this.stop();
this.start();
}
/**
* Display a modal dialog box to the test user.
*
* @param message String to display on the dialog box
*/
public void interact(String message) {
logger.info("Interaction via popup dialog with message: " + message);
JOptionPane.showInputDialog(message);
}
/**
* Returns a WebDriverPause object for waiting on some condition
* @return
*/
public WebDriverPause getPause() {
long timeoutMs = Long.parseLong(
candybean.config.getValue("perf.explicit.wait.timeout.mseconds", "15000"));
long pollingS = Long.parseLong(
candybean.config.getValue("perf.explicit.wait.polling.seconds", "5"));
return new WebDriverPause(wd, timeoutMs, pollingS);
}
/**
* Takes a full screenshot and saves it to the given file.
*
* @param file The file to which a screenshot is saved
* @throws CandybeanException
*/
public void screenshot(File file) throws CandybeanException {
logger.info("Taking screenshot; saving to file: " + file.toString());
Rectangle screen = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
BufferedImage screenshot;
try {
screenshot = (new Robot()).createScreenCapture(screen);
} catch (AWTException awte) {
throw new CandybeanException(awte);
}
try {
ImageIO.write(screenshot, "png", file);
} catch (IOException ioe) {
throw new CandybeanException(ioe);
}
}
/**
* Executes any javascript command
* @param javascript The javascript code to execute
* @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.
*/
public Object executeJavascript(String javascript, Object... args){
logger.info("Executing explicit javascript");
return ((JavascriptExecutor) this.wd).executeScript(javascript, args);
}
/**
* Executes an asynchronous javascript command. 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
* @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.
*/
public Object executeAsyncJavascript(String javascript, Object... args){
logger.info("Executing explicit async javascript");
return ((JavascriptExecutor) this.wd).executeAsyncScript(javascript, args);
}
/**
* Refreshes the interface. If refresh is undefined, it does nothing.
*
*/
public void refresh() throws CandybeanException {
logger.info("Refreshing the interface.");
this.wd.navigate().refresh();
}
/**
* Load a URL in the browser window.
*
* @param url the URL to be loaded by the browser
*/
public void go(String url) throws CandybeanException {
logger.info("Going to URL and switching to window: " + url);
this.wd.get(url);
}
/**
* Returns the current URL of the current window
*
* @return Returns the current window's URL as a String
*/
public String getURL() {
String url = this.wd.getCurrentUrl();
logger.info("Getting URL " + url);
return url;
}
/**
* Navigates the interface backward. If backward is undefined, it does nothing.
*/
public void backward() throws CandybeanException {
logger.info("Navigating the interface backward.");
this.wd.navigate().back();
}
/**
* Returns true if the interface 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) throws CandybeanException {
logger.info("Searching if the interface contains the following string: " + s + " with case sensitivity: " + caseSensitive);
if (!caseSensitive) s = s.toLowerCase();
List<WebElement> wes = this.wd.findElements(By.xpath("//*[not(@visible='false')]"));
for (WebElement we : wes) {
String text = we.getText();
if (!caseSensitive) text = text.toLowerCase();
if (text.contains(s)) return true;
}
return false;
}
/**
* Switches focus to default content.
*
*/
public void focusDefault() throws CandybeanException {
logger.info("Focusing to default content.");
this.wd.switchTo().defaultContent();
}
/**
* Switches focus to the IFrame identified by the given zero-based index
*
* @param index the serial, zero-based index of the iframe to focus
*/
public void focusFrame(int index) throws CandybeanException {
logger.info("Focusing to frame by index: " + index);
this.getPause().waitUntil(WaitConditions.frameToBeAvailableAndSwitchToIt(index));
}
/**
* Switches focus to the IFrame identified by the given name or ID string
*
* @param nameOrId the name or ID identifying the targeted IFrame
*/
public void focusFrame(String nameOrId) throws CandybeanException {
logger.info("Focusing to frame by name or ID: " + nameOrId);
this.getPause().waitUntil(WaitConditions.frameToBeAvailableAndSwitchToIt(nameOrId));
}
/**
* Switches focus to the IFrame identified by the given {@link Element}
*
* @param wde The element representing a focus-targeted IFrame
*/
public void focusFrame(WebDriverElement wde) throws CandybeanException {
logger.info("Focusing to frame by element: " + wde.toString());
this.getPause().waitUntil(WaitConditions.frameToBeAvailableAndSwitchToIt(wde));
}
/**
* Open a new browser window with specified URL and places focus on the new window.
*
* @param url a String containing the URL to open in the new window.
*/
public void openWindow(String url) throws CandybeanException {
logger.info("Opening a new window and navigating to: " + url);
int numWindows = wd.getWindowHandles().size();
executeJavascript("window.open('" + url + "');");
getPause().waitUntil(WaitConditions.numberOfWindowsToBe(numWindows + 1));
focusWindow(url); // focusWindow automatically pushes the window onto the stack.
}
/**
* Close the current browser window.
*/
public void closeWindow() throws CandybeanException {
logger.info("Closing window with handle: " + windows.peek());
this.wd.close();
this.windows.pop();
logger.info("Refocusing to previous window with handle: " + windows.peek());
this.wd.switchTo().window(windows.peek().y);
}
/**
* Focus a browser window by its index if it is not the current index.
*
* <p>The order of browser windows is somewhat arbitrary and not
* guaranteed, although window creation time ordering seems to be
* the most common.</p>
*
* @param index the window index
* @throws CandybeanException if the specified window index is out of range
*/
public void focusWindow(final int index) throws CandybeanException {
if (index == windows.peek().x) {
logger.warning("No focus was made because the given index matched the current index: " + index);
} else if (index < 0) {
throw new CandybeanException("Given focus window index is out of bounds: " + index + "; current size: " + windows.size());
} else {
// Wait for more than the specified number of windows to exist
getPause().waitUntil(new ExpectedCondition<Boolean>() {
@Override
public Boolean apply(WebDriver driver) {
return wd.getWindowHandles().size() > index;
}
});
Set<String> windowHandlesSet = wd.getWindowHandles();
String[] windowHandles = windowHandlesSet.toArray(new String[] {""});
getPause().waitUntil(WaitConditions.windowToBeAvailableAndSwitchToIt(windowHandles[index]));
windows.push(new Pair<>(index, wd.getWindowHandle()));
logger.info("Focused by index: " + index + " to window: " + windows.peek());
}
}
/**
* Focus a browser window by its window title or URL if it does not
* match the current title or URL.
*
* <p>If more than one window has the same title or URL, the first
* encountered is the one that is focused.</p>
*
* @param titleOrUrl the exact window title or URL to be matched
* @throws CandybeanException if the specified window cannot be found
*/
public void focusWindow(String titleOrUrl) throws CandybeanException {
String curTitle = this.wd.getTitle();
String curUrl = this.wd.getCurrentUrl();
if (titleOrUrl.equals(curTitle) || titleOrUrl.equals(curUrl)) {
logger.warning("No focus was made because the given string matched the current title or URL: " + titleOrUrl);
} else {
Set<String> windowHandlesSet = this.wd.getWindowHandles();
String[] windowHandles = windowHandlesSet.toArray(new String[] {""});
int i = 0;
boolean windowFound = false;
while (i < windowHandles.length && !windowFound) {
WebDriver window = (WebDriver)getPause().waitUntil(WaitConditions.windowToBeAvailableAndSwitchToIt(windowHandles[i]));
if (window.getTitle().equals(titleOrUrl) || window.getCurrentUrl().equals(titleOrUrl)) {
windows.push(new Pair<Integer, String>(new Integer(i), this.wd.getWindowHandle()));
logger.info("Focused by title or URL: " + titleOrUrl + " to window: " + windows.peek());
windowFound = true;
}
i++;
}
if (!windowFound) {
getPause().waitUntil(WaitConditions.windowToBeAvailableAndSwitchToIt(windows.peek().y));
throw new CandybeanException("The given focus window string matched no title or URL: " + titleOrUrl);
}
}
}
/**
* @return The number of active windows
*/
public int getWindowCount() {
return wd.getWindowHandles().size();
}
/**
* Navigates the interface forward. If forward is undefined, it does nothing.
*/
public void forward() throws CandybeanException {
logger.info("Navigating the interface forward.");
this.wd.navigate().forward();
}
/**
* Returns a string with the contents of the windows data structure.
*
* @return A string representation of all focused windows, with
* chronological index of focus and handle
*/
public String getWindowsString() {
String s = "Reverse stack:\n";
Iterator<Pair<Integer, String>> winIter = windows.iterator();
while (winIter.hasNext()) {
s += winIter.next() + "\n";
}
return s;
}
/**
* Maximize the browser window.
*/
public void maximize() {
logger.info("Maximizing window");
this.wd.manage().window().maximize();
}
/**
* Get an element from the current page.
*
* @param hook description of how to find the control
* @throws CandybeanException
*/
public WebDriverElement getWebDriverElement(Hook hook) throws CandybeanException {
return this.getWebDriverElement(hook, 0);
}
/**
* Get an element from the current page by index.
*
* @param strategy method to use to search for the control
* @param hookString string to find using the specified strategy
* @param index
* @throws CandybeanException
*/
public WebDriverElement getWebDriverElement(Strategy strategy, String hookString, int index) throws CandybeanException {
return this.getWebDriverElement(new Hook(strategy, hookString), index);
}
/**
* Get an element from the current page.
*
* @param strategy method to use to search for the control
* @param hookString string to find using the specified strategy
* @throws CandybeanException
*/
public WebDriverElement getWebDriverElement(Strategy strategy, String hookString) throws CandybeanException {
return this.getWebDriverElement(new Hook(strategy, hookString), 0);
}
/**
* Get an element from the current page by index.
*
* @param hook description of how to find the control
* @param index
* @throws CandybeanException
*/
public WebDriverElement getWebDriverElement(Hook hook, int index) throws CandybeanException {
return new WebDriverElement(hook, index, this.wd);
}
/**
* @param strategy The strategy used to search for the control
* @param hook The associated hook for the strategy
* @return The list of all controls that match the strategy and hook
* @throws CandybeanException
*/
public List<WebDriverElement> getWebDriverElements(Strategy strategy, String hook) throws CandybeanException {
return this.getWebDriverElements(new Hook(strategy, hook));
}
/**
* @param hook The associated hook for the strategy
* @return The list of all controls that match the strategy and hook
* @throws CandybeanException
*/
public List<WebDriverElement> getWebDriverElements(Hook hook) throws CandybeanException {
List<WebDriverElement> elements = new ArrayList<WebDriverElement>();
List<WebElement> wes = this.wd.findElements(Hook.getBy(hook));
for (WebElement we : wes)
elements.add(new WebDriverElement(hook, 0, this.wd, we));
return elements;
}
/**
* @param strategy method to use to search for the control
* @param hook string to find using the specified strategy
* @throws CandybeanException
*/
public WebDriverSelector getSelect(Strategy strategy, String hook) throws CandybeanException {
return this.getSelect(new Hook(strategy, hook));
}
/**
* @param hook description of how to find the control
* @throws CandybeanException
*/
public WebDriverSelector getSelect(Hook hook) throws CandybeanException {
return new WebDriverSelector(hook, wd);
}
/**
* Click "OK" on a modal dialog box (usually referred to
* as a "javascript dialog").
*/
public void acceptDialog() {
try {
logger.info("Accepting dialog.");
this.wd.switchTo().alert().accept();
this.waitForAlertDismissal();
} catch(UnhandledAlertException uae) {
logger.warning("Unhandled alert exception");
}
}
/**
* Waits for an alert to be dismissed
* The use of a while loop is not recommended, use this method with caution
*/
private void waitForAlertDismissal() {
long timeoutSec = Long.parseLong(candybean.config.getValue("perf.implicit.wait.seconds", "20"));
logger.info("Waiting for alert to be dismissed, timeout in " + timeoutSec + " seconds.");
long startTime = System.currentTimeMillis();
while(true) {
if(!isDialogVisible()) {
logger.info("Wait for alert dismissal successful, alert was dismissed");
break;
} else if(waitForTimeout(startTime, timeoutSec)) {
logger.info("Waiting for alert to be dismissed timed out, continuing");
break;
}
}
}
/**
* Determines whether a timeout has occurred since the start time
* @param startTimeMs The start time in milliseconds
* @param timeoutSec The time in seconds for timeout
* @return
*/
private boolean waitForTimeout(long startTimeMs, long timeoutSec) {
long timePassed = (System.currentTimeMillis() - startTimeMs) / 1000;
return timePassed > timeoutSec;
}
/**
* Dismisses a modal dialog box (usually referred to
* as a "javascript dialog").
*
*/
public void dismissDialog() {
try {
logger.info("Dismissing dialog.");
this.wd.switchTo().alert().dismiss();
this.waitForAlertDismissal();
} catch(UnhandledAlertException uae) {
logger.warning("Unhandled alert exception");
}
}
/**
* Returns true if a modal dialog can be switched to
* and switched back from; otherwise, returns false.
*
* @return Boolean true only if a modal dialog can
* be switched to, then switched back from.
*/
public boolean isDialogVisible() {
try {
this.wd.switchTo().alert();
logger.info("Dialog present?: true.");
return true;
} catch(UnhandledAlertException uae) {
logger.info("(Unhandled alert in FF?) Dialog present?: true. May have ignored dialog...");
return true;
} catch(NoAlertPresentException nape) {
logger.info("Dialog present?: false.");
return false;
}
}
/**
* This saves the base URL that might be needed for various purposes
* (e.g. Query parameters to control application under test)
* @param baseUrl
*/
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
/**
* This returns the base URL
* @return the base URL
*/
public String getBaseUrl() {
return baseUrl;
}
public class SwipeableWebDriver extends RemoteWebDriver implements HasTouchScreen {
private RemoteTouchScreen touch;
public SwipeableWebDriver(URL remoteAddress,
Capabilities desiredCapabilities) {
super(remoteAddress, desiredCapabilities);
touch = new RemoteTouchScreen(getExecuteMethod());
}
public TouchScreen getTouch() {
return touch;
}
}
}