package com.ttdev.wicketpagetest;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.wicket.Application;
import org.apache.wicket.Page;
import org.apache.wicket.extensions.breadcrumb.panel.IBreadCrumbPanelFactory;
import org.apache.wicket.request.cycle.IRequestCycleListener;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.openqa.selenium.By;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Predicate;
import com.thoughtworks.selenium.Selenium;
/**
* This class is a wrapper around {@link Selenium} that adds some handy
* functions for Wicket-generated HTML pages. In particular, it allows you to
* wait for Ajax processing to be completed. This way, you don't need to use
* {@link WebDriverWait} which is more complicated and error-prone.
*
* @author Kent Tong
* @author Andy Chu
*/
public class WicketSelenium {
private static final Logger LOGGER = LoggerFactory
.getLogger(WicketSelenium.class);
protected static final int WAIT_TIMEOUT_IN_SECONDS = 10;
private WebDriver selenium;
private Configuration cfg;
private String pageMarker;
private Random randMarkerGenerator;
public WicketSelenium(Configuration cfg, WebDriver selenium) {
this.selenium = selenium;
this.cfg = cfg;
randMarkerGenerator = new Random();
configToWaitOnNotFound();
}
// By default Wicket uses redirects for page navigation, but the
// Selenium's click() method doesn't wait for redirects. So,
// tell Selenium to wait some seconds if an element is not found.
// This will work if the response page is a different page so
// only it has that element.
private void configToWaitOnNotFound() {
selenium.manage().timeouts()
.implicitlyWait(WAIT_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
LOGGER.debug("Setting the implicit wait timeout to {}",
WAIT_TIMEOUT_IN_SECONDS);
}
public void subscribeAjaxDoneHandler() {
JavascriptExecutor jsExec = (JavascriptExecutor) selenium;
String defineAjaxDoneIndicatorExpr = "if (typeof wicketPageTestAjaxDone === 'undefined') { var wicketPageTestAjaxDone = false;"
+ "Wicket.Event.subscribe('/ajax/call/complete', function(jqEvent, attributes, jqXHR, errorThrown, textStatus) { window.wicketPageTestAjaxDone = true; });"
+ " }";
jsExec.executeScript(defineAjaxDoneIndicatorExpr);
}
private void clearAjaxDoneIndicator() {
JavascriptExecutor jsExec = (JavascriptExecutor) selenium;
jsExec.executeScript("window.wicketPageTestAjaxDone = false;");
}
/**
* It waits until the Wicket Ajax processing has completed. It does that by
* waiting until all the Wicket Ajax channels to become idle.
*
* This method heavily depends on the Wicket internal Ajax implementation
* and thus is subject to changes.
*/
public void waitUntilAjaxDone() {
new WebDriverWait(selenium, WAIT_TIMEOUT_IN_SECONDS)
.until(new Predicate<WebDriver>() {
public boolean apply(WebDriver input) {
JavascriptExecutor jsExec = (JavascriptExecutor) input;
Boolean ajaxDone = (Boolean) jsExec
.executeScript("return window.wicketPageTestAjaxDone == true;");
return ajaxDone;
}
});
clearAjaxDoneIndicator();
}
/**
* It tells the server side to include a random string as the marker (a
* cookie) in the response page. See {@link #setResponsePageMarker(String)}.
*/
public void setResponsePageMarker() {
setResponsePageMarker(Long.toString(randMarkerGenerator.nextLong()));
}
/**
* It tells the server side to include a page marker as a cookie in the next
* response page, so that you can then call {@link #waitForMarkedPage()} to
* wait for it. This way you can be sure that it is a new page and won't
* suffer from the StaleElementReferenceException.
*
* @param marker
* the marker to be used
*/
public void setResponsePageMarker(String marker) {
pageMarker = marker;
PageMarkingListener pml = findPageMarkingListener();
LOGGER.debug("Setting page marker to {}", pageMarker);
pml.setMarker(pageMarker);
}
private PageMarkingListener findPageMarkingListener() {
for (IRequestCycleListener l : Application.get()
.getRequestCycleListeners()) {
if (l instanceof PageMarkingListener) {
PageMarkingListener pml = (PageMarkingListener) l;
return pml;
}
}
throw new RuntimeException(
PageMarkingListener.class.getSimpleName()
+ " not found. Have you installed it in your Wicket application?");
}
/**
* It waits until the browser sees a page with the page marker. Usually you
* don't need to use this method because when you try to get an element and
* if it is not found, Selenium has been configured to wait a few seconds.
* But if the response page is the same as the original page, then it won't
* work as that element will be found but when you try to use it, you will
* get an StaleElementReferenceException. To work around the problem, call
* {@link #setResponsePageMarker(String)} and then call this method.
*/
public void waitForMarkedPage() {
if (pageMarker == null) {
throw new RuntimeException(
"Must call setPageMarker() before calling this method");
}
new WebDriverWait(selenium, WAIT_TIMEOUT_IN_SECONDS)
.until(new Predicate<WebDriver>() {
public boolean apply(WebDriver input) {
Cookie marker = input
.manage()
.getCookieNamed(
PageMarkingListener.WPT_PAGE_MARKER_COOKIE_NAME);
if (marker != null) {
LOGGER.debug("Marker retrieved is {}",
marker.getValue());
if (marker.getValue().equals(pageMarker)) {
LOGGER.debug("Marker matched. Clearing it.");
pageMarker = null;
return true;
} else {
LOGGER.debug("Marker not matched");
return false;
}
} else {
LOGGER.debug("No marker found");
return false;
}
}
});
}
/**
* Open a bookmarkable Wicket page. That is, it has a no-arg constructor.
*
* @param pageClass
* the class of the page
*/
public void openBookmarkablePage(Class<? extends Page> pageClass) {
selenium.get(getWicketAppBase()
+ String.format("wicket/bookmarkable/%s", pageClass.getName()));
}
/**
* Open a mounted Wicket page.
*
* @param mountPoint
* the mount point of the page
*
* @param parameters
* the page parameters
*/
public void openMountedPage(String mountPoint, PageParameters parameters) {
String appBase = getWicketAppBase();
String fullQuery = getFullQueryString(parameters);
String url = fullQuery.isEmpty() ? (appBase + mountPoint) : appBase
+ mountPoint + "?" + fullQuery;
selenium.get(url);
}
private String getFullQueryString(PageParameters parameters) {
List<String> queries = new ArrayList<String>();
Set<String> keys = parameters.getNamedKeys();
for (String key : keys) {
queries.add(String.format("%s=%s", key, parameters.get(key)
.toString()));
}
String fullQuery = "";
if (queries.size() == 1) {
fullQuery = queries.get(0);
}
if (queries.size() > 1) {
fullQuery = queries.get(0);
for (int i = 1; i < queries.size(); i++) {
fullQuery += ("&" + queries.get(i));
}
}
return fullQuery;
}
private String getWicketAppBase() {
return String.format("http://localhost:%d/%s",
cfg.getJettyServerPort(), cfg.getWicketFilterPrefix().trim()
.isEmpty() ? "" : (cfg.getWicketFilterPrefix() + "/"));
}
/**
* Open the home page of the Wicket application.
*/
public void openHomePage() {
selenium.get(getWicketAppBase());
}
/**
* Open a non-bookmarkable Wicket page. That is, its constructor takes one
* or more arguments.
*
* @param pageClass
* the class of the page
* @param constructorArgs
* the constructor arguments
*/
public void openNonBookmarkablePage(Class<? extends Page> pageClass,
Object... constructorArgs) {
// use plain mocking possible? unlikely as it is Wicket that creates
// the instance from the class.
MockableBeanInjector.mockBean(LauncherPage.PAGE_FACTORY_FIELD_NAME,
new DefaultPageFactory(pageClass, constructorArgs));
openBookmarkablePage(LauncherPage.class);
}
/**
* Open a page containing ONLY the Component created by the specified
* ComponentFactory.
*
* @param factory
* the ComponentFactory creating the testing component
*/
public void openComponent(ComponentFactory factory) {
openNonBookmarkablePage(ComponentTestPage.class, factory);
}
/**
* Open a BreadCrumbPanel created by the IBreadCrumbPanelFactory in a page
*
* @param factory
* the IBreadCrumbPanelFactory creating the testing panel
*/
public void openBreadCrumbPanel(IBreadCrumbPanelFactory factory) {
openNonBookmarkablePage(BreadCrumbPanelTestPage.class, factory);
}
/**
* Locate the HTML slot of a Wicket component using the specified path. For
* the syntax of the path, please see {@link ByWicketIdPath}.
*
* @param path
* The path to the Wicket component
* @return The corresponding HTML slot
*/
public WebElement findWicketElement(String path) {
return findElement(new ByWicketIdPathFastVersion(path));
}
/**
* Get the {@link WebElement} located by the specified locator.
*
* @param locator
* locate the element using this locator
* @return the element
*/
public WebElement findElement(By locator) {
return selenium.findElement(locator);
}
/**
* Get the underlying Selenium {@link WebDriver}.
*
* @return the Selenium WebDriver
*/
public WebDriver getSelenium() {
return selenium;
}
/**
* Get the body text of the element located by the specified locator.
*
* @param elementLocator
* locate the element using this locator
* @return the body text of that element
*/
public String getText(By elementLocator) {
return findElement(elementLocator).getText();
}
/**
* Get the value of the "value" attribute of the element located by the
* specified locator.
*
* @param elementLocator
* locate the element using this locator
* @return the value of the "value" attribute of that element
*/
public String getValue(By elementLocator) {
return findElement(elementLocator).getAttribute("value");
}
/**
* Click the element located by the specified locator.
*
* @param elementLocator
* locate the element using this locator
*/
public void click(By elementLocator) {
findElement(elementLocator).click();
}
/**
* Send the keys to the element located by the specified locator.
*
* @param elementLocator
* locate the element using this locator
* @param keysToSend
* the keys to be sent to that element
*/
public void sendKeys(By elementLocator, CharSequence... keysToSend) {
findElement(elementLocator).sendKeys(keysToSend);
}
/**
* Clear the value of the text input element located by the specified
* locator.
*
* @param elementLocator
* locate the element using this locator
*/
public void clear(By elementLocator) {
findElement(elementLocator).clear();
}
public void setPreferredLocale(Locale locale) {
openNonBookmarkablePage(SwitchLocalePage.class, locale);
}
public void switchDefaultLocale() {
openNonBookmarkablePage(SwitchLocalePage.class, Locale.getDefault());
}
}