// Copyright � 2002-2005 Canoo Engineering AG, Switzerland. package com.canoo.webtest.steps.form; import java.io.IOException; import java.util.Iterator; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.xml.sax.SAXException; import com.canoo.webtest.engine.IStringVerifier; import com.canoo.webtest.engine.StepFailedException; import com.canoo.webtest.extension.StoreElementAttribute; import com.canoo.webtest.steps.AbstractBrowserAction; import com.canoo.webtest.steps.Step; import com.canoo.webtest.util.ConversionUtil; import com.gargoylesoftware.htmlunit.html.HtmlElement; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlLabel; import com.gargoylesoftware.htmlunit.html.HtmlPage; /** * Abstract class for steps which update form fields. * This class handles the attributes name/formName, htmlId or xpath. * * @author Marc Guillemot * @author Paul King * @author Denis N. Antonioli */ public abstract class AbstractSetFieldStep extends AbstractBrowserAction { private static final Logger LOG = Logger.getLogger(AbstractSetFieldStep.class); public static final String MESSAGE_ARGUMENT_MISSING = "One of 'forLabel', 'htmlId', 'name', or 'xpath' must be set!"; public static final String MESSAGE_ARGUMENT_REDUNDANT = "Only one of 'forLabel', 'htmlId', 'name', and 'xpath' should be set!"; private String fName; private String fXPath; private String fFormName; private String fFieldIndex; private String fHtmlId; private String fForLabel; /** * Set the name. * @param name * @webtest.parameter required="yes/no" * description="The name of the input field of interest. * One of <em>forLabel</em>, <em>htmlId</em>, <em>name</em>, or <em>xpath</em> is required." */ public void setName(final String name) { fName = name; } public String getName() { return fName; } /** * Set the text of the label associated with the field to set. * @param text the label text * @webtest.parameter required="yes/no" * description="The text of the label field associated with the input field of interest. * One of <em>forLabel</em>, <em>htmlId</em>, <em>name</em>, or <em>xpath</em> is required." */ public void setForLabel(final String text) { fForLabel = text; } public String getForLabel() { return fForLabel; } public String getXpath() { return fXPath; } /** * Set the xpath. * * @param xpath * @webtest.parameter required="yes/no" * description="The xpath of the input field of interest. * One of <em>forLabel</em>, <em>htmlId</em>, <em>name</em>, or <em>xpath</em> is required." */ public void setXpath(final String xpath) { fXPath = xpath; } /** * Set the form name. * @param formName * @webtest.parameter required="no" * default="the last form selected using 'selectForm', otherwise searches all forms" * description="The name of the form containing the field of interest. Ignored if <em>htmlId</em> is used." */ public void setFormName(final String formName) { fFormName = formName; } public String getFormName() { return fFormName; } public String getHtmlId() { return fHtmlId; } /** * Set the html id. * * @param htmlId * @webtest.parameter required="yes/no" * description="The id of the input field of interest. * One of <em>forLabel</em>, <em>htmlId</em>, <em>name</em>, or <em>xpath</em> is required." */ public void setHtmlId(final String htmlId) { fHtmlId = htmlId; } /** * Set the index. * * @param index * @webtest.parameter required="no" * default="the first field found that matches criteria" * description="The index of the field of interest (starting at 0) if more than one field matches criteria. Ignored if <em>htmlId</em> or <em>xpath</em> is used." */ public void setFieldIndex(final String index) { fFieldIndex = index; } public String getFieldIndex() { return fFieldIndex; } public void doExecute() throws SAXException, IOException { if (fName != null) { final HtmlForm form = findForm(); if (form == null) { throw new StepFailedException("No suitable form found having field named \"" + getName() + "\"", this); } LOG.debug("Found matching form " + form); setField(selectField(trimFields(findFields(form)), getFieldIndex(), this)); } else if (getForLabel() != null) { setField(findFieldByLabel(getContext().getCurrentHtmlResponse(this), getForLabel())); } else { // htmlId, xpath setField(StoreElementAttribute.findElement(getContext().getCurrentHtmlResponse(this), getHtmlId(), getXpath(), LOG, this)); } } /** * Retrieves the (first) field associated with the label containing the provided text * @param page the page to search in * @param labelText the text of the label * @return the associated form field * @throws StepFailedException if no field is found */ HtmlElement findFieldByLabel(final HtmlPage page, final String labelText) { LOG.debug("Searching label tag with text: " + labelText); final List labels = page.getDocumentElement().getHtmlElementsByTagName("label"); LOG.debug(labels.size() + " found in the page"); final IStringVerifier verifier = getVerifier(false); for (final Iterator iter=labels.iterator(); iter.hasNext();) { final HtmlLabel label = (HtmlLabel) iter.next(); if (verifier.verifyStrings(labelText, label.asText())) { LOG.debug("Found label with matching text, examining the associated field: " + label); final HtmlElement target = label.getReferencedElement(); if (keepField(target)) { LOG.debug("Found field: " + target); return target; } else { LOG.debug("Target doesn't match: " + target); } } } throw new StepFailedException("No label found with text \"" + labelText + "\"", this); } /** * Sets a field according to the step. * It is up to the step's implementation to decide how to set the step. * * @param field The field to set. */ protected abstract void setField(HtmlElement field) throws IOException; /** * Finds the relevant form. * * @return The found form. */ protected abstract HtmlForm findForm(); /** * Finds all possible input fields. This is a generic implementation, sub-classes may want to take advantage * of more specific functions. * * @param form The form to search. * @return A list of candidate fields. */ protected List findFields(final HtmlForm form) { return form.getInputsByName(fName); } /** * Apply {@link #keepField(com.gargoylesoftware.htmlunit.html.HtmlElement)} to trim the list of fields found. * * @param fields All fields found. * @return A list of candidate fields. */ protected List trimFields(final List fields) { for (final Iterator iter = fields.iterator(); iter.hasNext();) { final HtmlElement elt = (HtmlElement) iter.next(); LOG.debug("Considering element " + elt); if (!keepField(elt)) { iter.remove(); } } LOG.debug("Found " + fields.size() + " field(s)"); return fields; } /** * Called by {@link #findFields(com.gargoylesoftware.htmlunit.html.HtmlForm)} to filter out elements * with the correct name but not matching some other selection criteria. * @param elt One of the elements with the correct name. * @return True if the element is accepted. */ protected boolean keepField(HtmlElement elt) { return true; } /** * Finds the desired field by selecting either a specific field designated by * indexStr or the first one if indexStr is left blank * * @param fieldList A list of {@link HtmlElement fields}. * @param indexStr The index of the desired field. * @param step The calling step, for exception. * @return The selected field */ public static HtmlElement selectField(final List fieldList, final String indexStr, final Step step) { if (fieldList.isEmpty()) { throw new StepFailedException("No suitable field(s) found", step); } int numFieldsFound = fieldList.size(); int index; if (StringUtils.isEmpty(indexStr)) { LOG.info("Found " + numFieldsFound + " suitable fields, considering only the first one"); index = 0; } else { index = ConversionUtil.convertToInt(indexStr, 0); if (index < 0 || index >= numFieldsFound) { throw new StepFailedException("Can't set field with index '" + index + "', valid range is 0.." + (numFieldsFound - 1), step); } } return (HtmlElement) fieldList.get(index); } protected void verifyParameters() { super.verifyParameters(); nullResponseCheck(); int count = 0; if (fXPath != null) { count++; } if (fName != null) { count++; } if (fHtmlId != null) { count++; } if (getForLabel() != null) { count++; } paramCheck(count == 0, MESSAGE_ARGUMENT_MISSING); paramCheck(count > 1, MESSAGE_ARGUMENT_REDUNDANT); if (fName == null) { paramCheck(fFieldIndex != null, "The attribute 'fieldIndex' is only valid with the attribute 'name'."); } else { optionalIntegerParamCheck(fFieldIndex, "fieldIndex", false); } } }