// Copyright � 2002-2007 Canoo Engineering AG, Switzerland.
package com.canoo.webtest.steps.request;
import com.canoo.webtest.engine.StepFailedException;
import com.canoo.webtest.util.ConversionUtil;
import com.canoo.webtest.util.HtmlConstants;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.html.ClickableElement;
import com.gargoylesoftware.htmlunit.html.HtmlButton;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlImageInput;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.xml.xpath.XPathException;
/**
* Provides the ability to click on a submit button.
*
* @author unknown
* @author Marc Guillemot
* @author Paul King
* @author Denis N. Antonioli
* @webtest.step category="Core"
* name="clickButton"
* alias="clickbutton"
* description="This step is used to locate a form button and then click it."
*/
public class ClickButton extends AbstractIdOrLabelTarget {
private static final Logger LOG = Logger.getLogger(ClickButton.class);
private static final Set INPUT_BUTTONS_TYPES = new HashSet();
private String fName;
private String fFieldIndex;
private String fX;
private String fY;
static {
INPUT_BUTTONS_TYPES.add(HtmlConstants.SUBMIT);
INPUT_BUTTONS_TYPES.add(HtmlConstants.IMAGE);
INPUT_BUTTONS_TYPES.add(HtmlConstants.BUTTON);
INPUT_BUTTONS_TYPES.add(HtmlConstants.RESET);
}
/**
* @webtest.parameter required="yes/no"
* description="The NAME attribute for the button of interest.
* Name has lower precedence than <em>htmlId</em>."
*/
public void setName(String name) {
fName = name;
}
public String getName() {
return fName;
}
/**
* Sets the index of the button to click (starting with 0) within the buttons
* identified with the other criteria.
*
* @param index the new value
* @webtest.parameter required="no"
* default="0"
* description="The index (starting with 0) of the button to click within the buttons having the specified label and/or name. Useful for instance to distinguish two buttons having the same name."
*/
public void setFieldIndex(final String index) {
fFieldIndex = index;
}
public String getFieldIndex() {
return fFieldIndex;
}
/**
* @webtest.parameter required="no"
* description="Optional X coordinate of click within an image button. If set, Y coordinate must also be set."
*/
public void setX(String clickPositionX) {
fX = clickPositionX;
}
public String getX() {
return fX;
}
/**
* @webtest.parameter required="no"
* description="Optional Y coordinate of click within an image button. If set, X coordinate must also be set."
*/
public void setY(String clickPositionY) {
fY = clickPositionY;
}
public String getY() {
return fY;
}
/**
* @deprecated use setFieldIndex instead
*/
public void setIndex(final int index) {
LOG.warn("setIndex is deprecated - use setFieldIndex instead");
setFieldIndex(Integer.toString(index));
}
/**
* Finds the button in the page according to the properties set on this step
*
* @param page the page to search in
* @return the button, <code>null</code> if not found
*/
protected ClickableElement findClickableElementByAttribute(final HtmlPage page) {
ClickableElement button = null;
// look for the button in the current form
if (getContext().getCurrentForm() != null) {
LOG.debug("Looking for button in current form");
button = findButton(getContext().getCurrentForm());
}
// if not found look at the other forms
if (button == null) {
button = findButtonAllForms(page);
}
return button;
}
protected Page findTarget() throws XPathException, IOException, SAXException {
final ClickableElement button = findClickableElement(getContext().getCurrentHtmlResponse(this));
if (button == null) {
throw buildNoButtonFoundException();
}
LOG.info("-> findTarget(by " + button.getTagName() + "): name="
+ button.getAttribute("name") + " value="
+ button.getAttribute("value"));
if (isImageButton()) {
LOG.info("-> findTarget(by " + button.getTagName() + "): name="
+ button.getAttribute("name") + " value="
+ button.getAttribute("value"));
return ((HtmlInput) button).click(Integer.parseInt(getX()), Integer.parseInt(getY()));
}
return button.click();
}
/**
* Builds an exception with helpfull information
* @return the exception
*/
private StepFailedException buildNoButtonFoundException() {
final StepFailedException e = new StepFailedException("No button found", this);
final HtmlForm currentForm = getContext().getCurrentForm();
final StringBuffer msg = new StringBuffer();
if (currentForm != null) {
msg.append("In current form:\n");
msg.append(getButtonsDescription(currentForm));
}
final Iterator formsIterator = getContext().getCurrentHtmlResponse(this).getForms().iterator();
while (formsIterator.hasNext()) {
final HtmlForm form = (HtmlForm) formsIterator.next();
if (form != currentForm)
{
if (msg.length() != 0)
msg.append("\n\n");
msg.append("In " + form + ":\n");
msg.append(getButtonsDescription(form));
}
}
e.addDetail("available buttons", msg.toString());
return e;
}
private String getButtonsDescription(final HtmlForm _form) {
final List buttons = new ArrayList();
for (final Iterator iter=_form.getAllHtmlChildElements().iterator(); iter.hasNext(); ) {
final HtmlElement element = (HtmlElement) iter.next();
if ((element instanceof HtmlInput)
&& isInputButtonType((HtmlInput) element)) {
buttons.add(element);
}
else if (element instanceof HtmlButton) {
buttons.add(element);
}
}
if (buttons.isEmpty())
return "none";
else
return buttons.toString();
}
protected String getLogMessageForTarget() {
return "by clickButton with name: " + getName();
}
protected void verifyParameters() {
super.verifyParameters();
nullResponseCheck();
paramCheck((StringUtils.isEmpty(getX()) && !StringUtils.isEmpty(getY())) || (!StringUtils.isEmpty(getX()) && StringUtils.isEmpty(getY())),
"X and Y values must be set for click button support!");
optionalIntegerParamCheck(getFieldIndex(), "fieldIndex", true);
optionalIntegerParamCheck(getX(), "x", false);
optionalIntegerParamCheck(getY(), "y", false);
}
/**
* Checks that the element is of the desired html type and has the right name and label (if needed)
*
* @param elt the button to check
* @return the button if ok, <code>null</code> otherwise
*/
ClickableElement checkFoundElement(final HtmlElement elt) throws StepFailedException {
// check that it is a "button"
if (!isButton(elt)) {
throw new StepFailedException("Selected element is a " + elt.getTagName() + " tag and not a button", this);
}
if (hasMatchingNameOrDontCare(elt) && hasMatchingLabelOrDontCare(elt)) {
LOG.debug("Button passes test with label and name");
return (ClickableElement) elt;
}
LOG.debug("Test with name and label fails for html button: " + elt);
return null;
}
/**
* Looks for the first button (that may be an input of type submit, image or button or a "normal" button)
* in the given form corresponding to the criterias
*
* @param form the form in which the button should be searched
* @return the button, <code>null</code> if not found
*/
ClickableElement findButton(final HtmlForm form) {
LOG.debug("Looking for inputs of type submit, image or button in " + form);
ClickableElement button = findInputButton(form);
if (button != null) {
return button;
}
LOG.debug("Looking for \"normal\" button in " + form);
return findNormalButton(form);
}
private ClickableElement findButtonAllForms(final HtmlPage currentResp) {
LOG.debug("Looking for button in all forms contained in the document");
List forms = currentResp.getForms();
if (forms.size() == 0) {
LOG.warn("No forms found - page probably non-compliant - searching page anyway");
return searchButton(currentResp);
}
for (final Iterator iter = forms.iterator(); iter.hasNext();) {
ClickableElement button = findButton((HtmlForm) iter.next());
if (button != null) {
return button;
}
}
return null;
}
private static boolean isButton(HtmlElement elt) {
if (elt instanceof HtmlButton) {
LOG.debug("It's a button, that's ok");
return true;
}
if (elt instanceof HtmlInput && isInputButtonType((HtmlInput) elt)) {
LOG.debug("It's an " + elt.getAttribute(HtmlConstants.TYPE) + " input, that's ok");
return true;
}
LOG.debug("Html element is not a button");
return false;
}
private static boolean isInputButtonType(final HtmlInput input) {
return INPUT_BUTTONS_TYPES.contains(input.getTypeAttribute().toLowerCase());
}
private ClickableElement findInputButton(final HtmlForm form) {
final Collection inputButtons = form.getHtmlElementsByTagName(HtmlConstants.INPUT);
return findInputButton(inputButtons.iterator());
}
private ClickableElement searchButton(final HtmlPage page) {
final Collection buttons = new ArrayList();
final Iterator childElements = page.getAllHtmlChildElements().iterator();
while (childElements.hasNext()) {
final HtmlElement elt = (HtmlElement) childElements.next();
if (isButton(elt)) {
buttons.add(elt);
}
}
ClickableElement button = findInputButton(buttons.iterator());
if (button == null) {
button = findNormalButton(buttons.iterator());
}
return button;
}
private ClickableElement findInputButton(final Iterator candidateIterator) {
int indexFound = 0; // should index be across both button types? currently not
while (candidateIterator.hasNext()) {
final HtmlElement curElement = (HtmlElement) candidateIterator.next();
if (!(curElement instanceof HtmlInput)) {
continue;
}
final HtmlInput curInput = (HtmlInput) curElement;
if (!isInputButtonType(curInput)) {
continue; // not a "button"
}
LOG.debug("Examining button: " + curInput);
if (hasMatchingNameOrDontCare(curInput) && hasMatchingLabelOrDontCare(curInput)) {
if (indexFound == ConversionUtil.convertToInt(getFieldIndex(), 0)) {
LOG.debug(curInput.getTypeAttribute() + " button found: " + curInput);
return curInput;
}
++indexFound;
}
}
return null;
}
private ClickableElement findNormalButton(final HtmlForm form) {
return findNormalButton(form.getAllHtmlChildElements().iterator());
}
private ClickableElement findNormalButton(final Iterator candidateIterator) {
int indexFound = 0; // should index be across both button types? currently not
while (candidateIterator.hasNext()) {
final HtmlElement curElement = (HtmlElement) candidateIterator.next();
if (!(curElement instanceof HtmlButton)) {
continue;
}
final HtmlButton curButton = (HtmlButton) curElement;
LOG.debug("Examining button: " + curButton);
if (hasMatchingNameOrDontCare(curButton) && hasMatchingLabelOrDontCare(curButton)) {
if (indexFound == ConversionUtil.convertToInt(getFieldIndex(), 0)) {
LOG.debug("Normal button found: " + curButton);
return curButton;
}
++indexFound;
}
}
return null;
}
private boolean hasMatchingNameOrDontCare(final HtmlElement curButton) {
if (curButton instanceof HtmlInput) {
return hasMatchingNameOrDontCare((HtmlInput) curButton);
}
if (curButton instanceof HtmlButton) {
return hasMatchingNameOrDontCare((HtmlButton) curButton);
}
throw new IllegalArgumentException("Button is neither a HtmlInput nor a HtmlButton: " + curButton);
}
private boolean hasMatchingNameOrDontCare(final HtmlInput curButton) {
return getName() == null || getName().equals(curButton.getNameAttribute());
}
private boolean hasMatchingNameOrDontCare(final HtmlButton curButton) {
return getName() == null || getName().equals(curButton.getNameAttribute());
}
private boolean hasMatchingLabelOrDontCare(final HtmlElement curButton) {
if (getLabel() == null)
return true;
else if (curButton instanceof HtmlImageInput)
{
return getLabel().equals(((HtmlImageInput) curButton).getAltAttribute());
}
else
{
return getLabel().equals(curButton.asText());
}
}
protected boolean isImageButton() {
return !StringUtils.isEmpty(getX()) && !StringUtils.isEmpty(getY());
}
/**
* Called by Ant to set the text nested between opening and closing tags.
* @param text the text to set
* @webtest.nested.parameter
* required="no"
* description="Alternative way to set the 'label' attribute."
*/
public void addText(final String text) {
setLabel(getProject().replaceProperties(text));
}
// override to specify doc specific for clickButton
/**
* @param xpath The xpath to set.
* @webtest.parameter required="no"
* description="The xpath of the button to click."
*/
public void setXpath(final String xpath) {
super.setXpath(xpath);
}
/**
* @param newLabel The Label to set.
* @webtest.parameter required="no"
* description="The label of the button to click."
*/
public void setLabel(final String newLabel) {
super.setLabel(newLabel);
}
/**
* @param htmlId The HtmlId to set.
* @webtest.parameter required="no"
* description="The html id of the button to click."
*/
public void setHtmlId(final String htmlId) {
super.setHtmlId(htmlId);
}
}