/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.gatein.htmlunit.common;
import java.io.IOException;
import java.util.Arrays;
import java.util.Properties;
import org.testng.Assert;
import com.gargoylesoftware.htmlunit.ConfirmHandler;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlOption;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlSelect;
/**
* WebHelper class contains helper methods to easily perform GateIn portal operations
*
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class WebHelper {
/** HTMLUnit Webclient represents a web session */
private WebClient webClient;
/** Host to use for web session */
private String host = "localhost";
/** Port to use for web session */
private int port = 8080;
/** Portal container to use for web session */
private String portalContainer = "portal";
/** Time to wait (in seconds) for element or text to appear in the page */
private int waitTimeout = 20;
/** Short pause period (in seconds) */
private int shortPause = 3;
/** Dump current page before throwing exception */
private boolean dumpPageOnFail = false;
/** Dump current page on exit */
private boolean dumpPageOnExit = false;
/** Properties to use for configuration overrides */
private Properties props = System.getProperties();
/** Current page */
private HtmlPage page;
/**
* Get host property.
*
* @return value of host property
*/
public String getHost() {
return host;
}
/**
* Get port property.
*
* @return value of port property
*/
public int getPort() {
return port;
}
/**
* Get portalContainer property.
*
* @return value of portalContainer property
*/
public String getPortalContainer() {
return portalContainer;
}
/**
* Get properties used for initialization.
*
* @return Properties
*/
public Properties getProps() {
return props;
}
/**
* Get shortPause property.
*
* @return value of shortPause property
*/
public int getShortPause() {
return shortPause;
}
/**
* Get waitTimeout property.
*
* @return value of waitTimeout property
*/
public int getWaitTimeout() {
return waitTimeout;
}
/**
* Is dumpPageOnFail flag true.
*
* @return value of dumpPageOnFail property
*/
public boolean isDumpPageOnFail() {
return dumpPageOnFail;
}
/**
* Is dumpPageOnExit flag true.
*
* @return value of dumpPageOnExit property
*/
public boolean isDumpPageOnExit() {
return dumpPageOnExit;
}
/**
* Get HTMLUnit WebClient instance representing the current session.
*
* @return WebClient instance representing a browser window
*/
public WebClient getWebClient() {
if (webClient == null) {
webClient = new WebClient();
webClient.setThrowExceptionOnFailingStatusCode(false);
webClient.setThrowExceptionOnScriptError(false);
/*
* // // all this seems to have no effect on logging //
*
* webClient.setHTMLParserListener(new HTMLParserListener() { public void error(String message, URL url, int line,
* int column, String key) {
*
* }
*
* public void warning(String message, URL url, int line, int column, String key) {
*
* } }); webClient.setIncorrectnessListener(new IncorrectnessListener() { public void notify(String message, Object
* origin) {
*
* } });
*
* LogFactory.getFactory().setAttribute("org.apache.commons.logging.Log",
* "org.apache.commons.logging.impl.SimpleLog");
*
* System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "fatal");
* System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.http", "debug");
*/
initFromProps();
}
return webClient;
}
/**
* Get current page.
*
* @return current page
*/
public HtmlPage getPage() {
return page;
}
/**
* Properties object containing configuration overrides.
*
* @return properties object
*/
public Properties getProperties() {
return props;
}
/**
* Set configuration overrides.
*
* @param props Properties object containing configuration overrides
*/
public void setProperties(Properties props) {
if (props == null)
throw new IllegalArgumentException("Null properties");
this.props = props;
}
/**
* Initialize from properties.
*/
private void initFromProps() {
String val = props.getProperty("test.host");
if (val != null)
host = val;
val = props.getProperty("test.port");
if (val != null)
port = Integer.parseInt(val);
val = props.getProperty("test.portalContainer");
if (val != null)
portalContainer = val;
val = props.getProperty("test.waitTimeout");
if (val != null)
waitTimeout = Integer.parseInt(val);
val = props.getProperty("test.shortPause");
if (val != null)
shortPause = Integer.parseInt(val);
val = props.getProperty("test.dumpPageOnFail");
if (val != null)
dumpPageOnFail = Boolean.parseBoolean(val);
val = props.getProperty("test.dumpPageOnExit");
if (val != null)
dumpPageOnExit = Boolean.parseBoolean(val);
log("Initial properties:\n\thost: " + host + "\n\tport: " + port + "\n\tportalContainer: " + portalContainer
+ "\n\twaitTimeout: " + waitTimeout + "\n\tshortPause: " + shortPause + "\n\tdumpPageOnFail: " + dumpPageOnFail
+ "\n\tdumpPageOnExit: " + dumpPageOnExit);
}
/**
* Open GateIn's home page.
*
* @param publicMode if true 'public' portal page is used, otherwise 'private', which requires signing in
* @throws IOException
*/
public void openPortal(boolean publicMode) throws IOException {
log("--Open portal home--");
page = getWebClient().getPage(getStartUrl(publicMode));
HtmlElement desc = page.getFirstByXPath(".//div[contains(text(), 'All rights reserved')]");
if (desc == null)
throw new RuntimeException("Portal page content corrupted\r\n" + page.asText());
}
/**
* Compose portal's home page url.
*
* @param publicMode
* @return
*/
private String getStartUrl(boolean publicMode) {
return "http://" + host + ":" + port + "/" + portalContainer + "/" + (publicMode ? "public" : "private");
}
/**
* Sign in as root.
*/
public void signInAsRoot() {
signIn("root", "gtn");
}
/**
* Sign in with username and password.
*
* @param user username
* @param pass password
*/
public void signIn(String user, String pass) {
log("--Sign in as " + user + "--");
waitForElementPresent("link=Sign in");
click("link=Sign in");
waitForElementPresent("username");
type("username", user);
type("password", pass);
click("//div[@id='UIPortalLoginFormAction']/div/div/div/a");
// waitForPageToLoad(timeout);
}
/**
* Fill form field with text.
*
* @param field
* @param value
*/
public void type(String field, String value) {
HtmlElement textField = getElement(field);
if (textField instanceof HtmlInput == false)
throw new RuntimeException("Element not text input: " + textField);
((HtmlInput) textField).setValueAttribute(value);
}
/**
* Simulate clicking an element in the page.
*
* @param el expression identifying an element
*/
public void click(String el) {
HtmlElement node = getElement(el);
try {
page = node.click();
} catch (Exception ex) {
throw new RuntimeException("Click failed [" + el + "]", ex);
}
}
/**
* Wait for element to be present in the current page, for a maximum time determined by <tt>waitSeconds</tt> property.
*
* @param el element specification
*/
public void waitForElementPresent(String el) {
for (int second = 0;; second++) {
if (second >= waitTimeout) {
if (dumpPageOnFail)
dumpPage();
Assert.fail("Timeout at waitForElementPresent: " + el);
}
if (isElementPresent(el)) {
break;
}
pause(1000);
}
}
/**
* Wait for element to not be present any more in the current page, for a maximum time determined by <tt>waitSeconds</tt>
* property.
*
* @param el element specification
*/
public void waitForElementNotPresent(String el) {
for (int second = 0;; second++) {
if (second >= waitTimeout) {
if (dumpPageOnFail)
dumpPage();
Assert.fail("Timeout at waitForElementPresent: " + el);
}
if (!isElementPresent(el)) {
break;
}
pause(1000);
}
}
/**
* Check if specified element is present in current page.
*
* @param el element specification
* @return true if present
*/
public boolean isElementPresent(String el) {
HtmlElement node = getElement(el);
return node != null;
}
/**
* Get first occurence of specified element in current page.
*
* <p>
* Element specification can have one of several forms:
* <ul>
* <li>link=<em>link-text</em> - matches the first %lt;a> element with specified link-text</li>
* <li>xpath=<em>xpath-query</em> - matches the first element that the specified xpath query returns</li>
* <li>//<em>xpath-query</em> - matches the first element that the specified xpath query returns</li>
* <li><em>name-id</em> - matches the first element with id or name attribute equal to <em>name-id</em></li>
* </ul>
*
* @param el element specification
* @return element if present, null otherwise
*/
public HtmlElement getElement(String el) {
// it seems that page state is often changing at the time we call getElement()
// which causes concurrency issues and results in RuntimeExceptions
RuntimeException e = null;
for (int i = 0; i < 3; i++) {
try {
return _getElement(el);
} catch (RuntimeException ex) {
e = ex;
try {
Thread.sleep(1000);
} catch (InterruptedException ex2) {
throw new RuntimeException("Interrupted!");
}
}
}
throw e;
}
private HtmlElement _getElement(String el) {
if (el.startsWith("link=")) {
String xpath = convertFromLinkElSpec(el);
return page.getFirstByXPath(xpath);
} else if (el.startsWith("xpath=")) {
String xpath = convertFromXPathElSpec(el);
return page.getFirstByXPath(xpath);
} else if (el.startsWith("//")) {
return page.getFirstByXPath(el);
} else {
return page.getFirstByXPath(".//node()[@name='" + el + "' or @id='" + el + "']");
}
}
private String convertFromXPathElSpec(String el) {
if (el.startsWith("xpath="))
el = el.substring(6);
return el;
}
private String convertFromLinkElSpec(String el) {
if (el.startsWith("link="))
el = el.substring(5);
return ".//a[text() = '" + el + "']";
}
public String waitForTextPresent(String... text) {
for (int second = 0;; second++) {
if (second >= waitTimeout) {
if (dumpPageOnFail)
dumpPage();
Assert.fail("Timeout at waitForTextPresent: " + Arrays.asList(text));
}
String found = getTextPresent(text);
if (found != null) {
return found;
}
pause(1000);
}
}
/**
* Dump the current page as XML
*/
public void dumpPage() {
log("Page dump: " + page.asXml());
}
/**
* Wait for text to not be present any more in the current page, for a maximum time determined by <tt>waitSeconds</tt>
* property.
*
* @param text text to not be present any more
*/
public void waitForTextNotPresent(String text) {
for (int second = 0;; second++) {
if (second >= waitTimeout) {
if (dumpPageOnFail)
dumpPage();
Assert.fail("Timeout at waitForTextPresent: " + text);
}
if (isTextPresent(text) == false) {
break;
}
pause(1000);
}
}
/**
* Check if specified text is present
*
* @param text
* @return true if text is present
*/
public boolean isTextPresent(String text) {
try {
return page.asText().indexOf(text) > -1;
} catch (Exception e) {
shortPause();
return page.asText().indexOf(text) > -1;
}
}
/**
* Check if any of the passed arguments is present as text in the page. When any is encountered it is returned.
*
* @param text string items to check for presence
* @return found item or null of none of the items is present
*/
public String getTextPresent(String... text) {
String pageText;
try {
pageText = page.asText();
} catch (Exception e) {
shortPause();
pageText = page.asText();
}
for (String item : text) {
if (pageText.indexOf(item) != -1)
return item;
}
return null;
}
/**
* Pause for a few seconds - length controlled by shortPause property
*/
public void shortPause() {
pause(shortPause * 1000);
}
/**
* Pause for a specified number of millis
*
* @param millis
*/
public static void pause(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted!");
}
}
/**
* Simulate dragging source element onto a target element
*
* @param sourceEl source element specification
* @param targetEl target element specification
*/
public void dragAndDropToObject(String sourceEl, String targetEl) {
log("--Drag and drop to object--");
HtmlElement src = getElement(sourceEl);
if (src == null)
throw new RuntimeException("No source element: " + sourceEl);
HtmlElement target = getElement(targetEl);
if (target == null)
throw new RuntimeException("No target element: " + targetEl);
src.mouseDown();
src.mouseMove();
target.mouseMove();
target.mouseUp();
}
/**
* Simulate clicking Finish inside Page Editor
*/
public void finishPageEdit() {
log("--Finish Page Edit--");
waitForElementPresent("//div[@id='UIPageEditor']/div[1]/div/div/div/a[2]");
click("//div[@id='UIPageEditor']/div[1]/div/div/div/a[2]");
waitForTextNotPresent("Page Editor");
}
/**
* Add a new page with the specified name, containing one specific portlet.
*
* @param categoryTitle application registry category title that contains the portlet to use
* @param portletName portlet name in application registry
* @param pageName name of the new page - used for both navigation node, and page title
* @param portletElementToDnD - element specification pointing to the div that is dragged onto the content placeholder
* within the new page
*/
public void addNewPageUpToFinish(String categoryTitle, String portletName, String pageName, String portletElementToDnD) {
log("--Add new page up to Finish--");
waitForElementPresent("link=Add New Page");
click("link=Add New Page");
waitForElementPresent("pageName");
type("pageName", pageName);
type("pageDisplayName", pageName);
click("//table[@class='ActionContainer']/tbody/tr/td/div[2]");
waitForTextPresent("Empty Layout");
click("//table[@class='ActionContainer']/tbody/tr/td/div[2]");
if (categoryTitle != null & portletName != null & portletElementToDnD != null) {
waitForElementPresent("//div[contains(@class, 'Tab')]/a[@title='" + categoryTitle + "']");
click("//div[contains(@class, 'Tab')]/a[@title='" + categoryTitle + "']");
waitForTextPresent(portletName);
shortPause();
dragAndDropToObject(portletElementToDnD, "//div[@class='UIComponentBlock']");
shortPause();
}
}
public void goToPageManagement() {
log("--Go to Page Management--");
waitForElementPresent("link=Page Management");
click("link=Page Management");
// waitForPageToLoad(); // no need for this - we're synchronous
}
public void searchAndDeletePage(String title) {
searchPageByTitle(title, null);
deletePage(title, false, null);
}
public void setupConfirmation(final String text) {
webClient.setConfirmHandler(new ConfirmHandler() {
public boolean handleConfirm(Page page, String message) {
if (text.equals(message)) {
return true;
} else
throw new RuntimeException("Unexpected message: " + message);
}
});
}
public void resetConfirmation() {
webClient.setConfirmHandler(null);
}
private void deletePage(String title, boolean closeDialog, String verifyText) {
log("--Delete page: " + title + "--");
String delButton = "//tbody[@class='FeedBox']//tr[*/div[@title='" + title
+ "']]//img[@title='Delete Page' and contains(@onclick, 'op=Delete')]";
waitForElementPresent(delButton);
setupConfirmation("Do you want to delete this page?");
click(delButton);
resetConfirmation();
if (verifyText != null) {
waitForTextNotPresent(verifyText);
}
// if (closeDialog)
// {
// closeMessageDialog();
// }
}
public void searchPageByTitle(String title, String verifyText) {
log("--Searching page: " + title + "--");
waitForElementPresent("searchTerm");
type("searchTerm", title);
select("searchOption", "label=Title");
waitForElementPresent("xpath=//form[@id='UIPageSearch']/div[2]/a");
click("xpath=//form[@id='UIPageSearch']/div[2]/a");
shortPause();
if (verifyText != null) {
waitForTextPresent(verifyText);
}
}
private void select(String selectEl, String select) {
HtmlElement el = getElement(selectEl);
if (el instanceof HtmlSelect) {
// find SelectOption
HtmlSelect sel = (HtmlSelect) el;
HtmlOption opt = null;
if (select.startsWith("label=") || select.indexOf("=") == -1) {
String label = select.substring(6);
opt = sel.getOptionByText(label);
if (opt == null)
throw new RuntimeException("No such option (" + label + ") for Select Input " + selectEl);
page = (HtmlPage) sel.setSelectedAttribute(opt, true);
} else {
throw new RuntimeException("Unsupported options specification format: " + select);
}
} else {
throw new RuntimeException("Specified element not of type HtmlSelect (" + selectEl + "): " + el);
}
}
public void goToSiteManagement() {
log("--Go to Page Management--");
waitForElementPresent("link=Page Management");
click("link=Page Management");
}
public void editNavigation(String site) {
log("--Edit Navigation : " + site + "--");
String navLink = "//table[@class='ManagementBlock' and //tr/td/div/text() = '" + site
+ "']//a[text() = 'Edit Navigation']";
waitForElementPresent(navLink);
click(navLink);
waitForElementPresent("link=Add Node");
}
public void deleteNode(String nodeLabel) {
log("--Deleting node from navigation--");
waitForElementPresent("//a[@title='" + nodeLabel + "']");
shortPause();
contextMenuOnElement("//a[@title='" + nodeLabel + "']");
shortPause();
waitForElementPresent("//div[@id='UINavigationNodeSelector']//div[@id='NavigationNodePopupMenu']//a[@class='ItemIcon DeleteNode16x16Icon']");
setupConfirmation("Are you sure you want to delete this node?");
click("//div[@id='UINavigationNodeSelector']//div[@id='NavigationNodePopupMenu']//a[@class='ItemIcon DeleteNode16x16Icon']");
resetConfirmation();
waitForElementNotPresent("//a[@title='" + nodeLabel + "']");
waitForElementPresent("link=Save");
click("link=Save");
waitForTextNotPresent("Navigation Management");
waitForTextNotPresent(nodeLabel);
}
public void contextMenuOnElement(String element) {
String source = "selenium.doComponentExoContextMenu(\"" + element + "\")";
webClient.getJavaScriptEngine().execute(page, source, "script", 1);
}
public void leavePageEdit() {
log("-- Leave PageEdit --");
// we could have an error window popped up
String closeButton = "//div[@class='ExoMessageDecorator' and //div[@class='TabsContainer']//div[@class='SelectedTab']]//div[@class='CloseButton']";
click(closeButton);
// also we may have to abort
String abortButton = "//table[@class='ActionContainer']//div[contains(@onclick, 'action=Abort')]//a[text()='Abort']";
click(abortButton);
waitForTextNotPresent("Page Editor");
}
public void log(String msg) {
System.out.println(msg);
}
}