// Copyright � 2002-2007 Canoo Engineering AG, Switzerland.
package com.canoo.webtest.steps.form;
import java.util.Iterator;
import java.util.List;
import org.apache.log4j.Logger;
import com.canoo.webtest.engine.IStringVerifier;
import com.canoo.webtest.engine.StepExecutionException;
import com.canoo.webtest.engine.StepFailedException;
import com.canoo.webtest.steps.request.TargetHelper;
import com.canoo.webtest.util.ConversionUtil;
import com.canoo.webtest.util.FormUtil;
import com.canoo.webtest.util.HtmlConstants;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlOption;
import com.gargoylesoftware.htmlunit.html.HtmlSelect;
/**
* Selects one or many elements of a select field ( <select name="foo">...
* </select>)
*
* @author Marc Guillemot
* @webtest.step category="Core"
* name="setSelectField"
* alias="new_setselectfield,setselectfield"
* description="Provides the ability to update select fields in <key>HTML</key> forms."
*/
public class SetSelectField extends AbstractSetNamedFieldStep {
private static final Logger LOG = Logger.getLogger(SetSelectField.class);
static final String MESSAGE_MISSING_OPTION_IDENTIFIER = "Either \"value\", \"text\" or \"optionIndex\" is required to identify option in select identified by \"htmlId\" or \"xpath\"!";
static final String AT_MOST_ONE_VALUE_TEXT_OPTIONINDEX = "At most one of \"value\", \"text\" or \"optionIndex\" can be set!";
private boolean fIsMultiSelect, fIsRegex;
private String fMultiSelect;
private String fRegex;
private String fText;
private String fOptionIndex;
private String fUserName;
private String fPassword;
private String fSavePrefix;
private String fSaveResponse;
private final TargetHelper fTargetHelper = new TargetHelper(this);
/**
* Gets the value of the regex attribute. The value is only available after the parameter has been validated.
* @return <code>true</code> if {@link #getText()} is a regular expression
*/
public boolean isRegex() {
return fIsRegex;
}
/**
* Gets the value of the multiSelect attribute. The value is only available after the parameter has been validated.
* @return <code>true</code> if multiple selection is allowed
*/
public boolean isMultiSelect() {
return fIsMultiSelect;
}
/**
* @webtest.parameter required="false"
* default="false"
* description="Specifies whether multiple selections are allowed. Unless set to true, every setselect overrides the value of preceding calls."
*/
public void setMultiselect(final String multiSelect) {
fMultiSelect = multiSelect;
}
public String getMultiselect() {
return fMultiSelect;
}
/**
* @webtest.parameter required="no"
* default="false"
* description="Specifies whether the option text represents a regular expression."
*/
public void setRegex(final String regex) {
fRegex = regex;
}
public String getRegex() {
return fRegex;
}
/**
* @webtest.parameter required="yes/no"
* description="The text of the option to select (i.e. the text nested in the option tag. One of <em>text</em>, <em>value</em> or <em>optionIndex</em> is required."
*/
public void setText(final String text) {
fText = text;
}
public String getText() {
return fText;
}
/**
* @param index The new index value
* @webtest.parameter required="yes/no"
* description="The index of the option to select (i.e. the position of the option in the select starting with 0). One of <em>text</em>, <em>value</em> or <em>optionIndex</em> is required."
*/
public void setOptionIndex(final String index) {
fOptionIndex = index; // option index (do we eventually need both of these?)
}
public String getOptionIndex() {
return fOptionIndex;
}
/**
* @param userName
* @webtest.parameter required="no"
* description="A username that can be provided for pages that require basic authentication. Only needed if setting the select field invokes <key>javascript</key> and causes the page to move to a secure page. Required if <em>password</em> is specified."
*/
public void setUserName(String userName) {
fUserName = userName;
}
public String getUserName() {
return fUserName;
}
/**
* @param password
* @webtest.parameter required="no"
* description="A password that can be provided for pages that require basic authentication. Required if <em>userName</em> is specified."
*/
public void setPassword(String password) {
fPassword = password;
}
public String getPassword() {
return fPassword;
}
/**
* @webtest.parameter required="no"
* default="the 'savePrefix' parameter as specified in <config>."
* description="A name prefix can be specified for making a permanent copy of any received responses. Only needed if setting the select field invokes <key>javascript</key> which causes the browser to move to another page."
*/
public void setSavePrefix(String prefix) {
fSavePrefix = prefix;
}
public String getSavePrefix() {
return fSavePrefix;
}
/**
* @webtest.parameter required="no"
* description="Whether to make a permanent copy of any received responses. Overrides the default value set in the <config> element. Only needed if setting the select field invokes <key>javascript</key> which causes the browser to move to another page."
*/
public void setSaveResponse(String response) {
fSaveResponse = response;
}
public String getSaveResponse() {
return fSaveResponse;
}
public String getSave() {
return null;
}
/**
* @param index
* @deprecated use setOptionIndex instead
*/
public void setIndex(final String index) {
LOG.warn("setIndex is deprecated - use setOptionIndex instead");
setOptionIndex(index);
}
/**
* Set the value
*
* @param value
* @webtest.parameter required="yes/no"
* description="The value of the option to select (i.e. the value of the \"value\" attribute of a select element). One of <em>text</em>, <em>value</em> or <em>optionIndex</em> is required."
*/
public void setValue(final String value) {
super.setValue(value);
}
protected HtmlForm findForm() {
return FormUtil.findFormForField(getContext(), getFormName(), HtmlConstants.SELECT, null, getName(), this);
}
/**
* Finds all relevant fields with the given name in the form.
*
* @param form The form to search
* @return The list of fields with the given name
*/
protected List findFields(final HtmlForm form) {
return form.getSelectsByName(getName());
}
protected void setField(final HtmlElement elt) {
final HtmlSelect select;
final HtmlOption option;
if (elt instanceof HtmlOption)
{
option = (HtmlOption) elt;
select = (HtmlSelect) option.getEnclosingElement(HtmlConstants.SELECT); // TODO: can be simplified with next htmlunit build
}
else if (elt instanceof HtmlSelect)
{
select = (HtmlSelect) elt;
// if htmlId or xpath specified, we know now first that text, value or optionIndex is needed
if (getText() == null && getOptionIndex() == null && getValue() == null)
throw new StepExecutionException(MESSAGE_MISSING_OPTION_IDENTIFIER, this);
option = findMatchingOption(select);
}
else
{
throw new StepFailedException("Found " + elt.getTagName() +
" when looking for select", this);
}
if (select.isMultipleSelectEnabled() && !fIsMultiSelect) {
deselectOtherOptions(select, option);
}
updateOption(select, option);
}
void updateOption(final HtmlSelect select, final HtmlOption option) {
if (option == null) {
throw new StepFailedException("No option found matching criteria in select " + select);
}
fTargetHelper.setUsername(getUserName());
fTargetHelper.setPassword(getPassword());
maybeTarget(option.getPage(), select, option);
}
protected void maybeTarget(final Page page, final HtmlSelect select, final HtmlOption option) {
LOG.debug("Selected option: " + option);
select.setSelectedAttribute(option, true);
}
private static void deselectOtherOptions(final HtmlSelect select, final HtmlOption option) {
for (final Iterator iter = select.getOptions().iterator(); iter.hasNext();) {
final HtmlOption curOpt = (HtmlOption) iter.next();
if (curOpt != option && curOpt.isSelected()) {
curOpt.setSelected(false);
}
}
}
/**
* Selects the option specified by value, text or index and returns it.<p>
*
* @param select the <select> containing the option
* @return the selected option
*/
HtmlOption findMatchingOption(final HtmlSelect select) throws StepExecutionException {
LOG.debug("Searching for the right option in " + select);
if (getValue() != null) {
LOG.debug("Searching option with value: " + getValue());
try {
return select.getOptionByValue(getValue());
}
catch (final ElementNotFoundException enfe) {
LOG.debug(enfe.getMessage());
return null;
}
}
else if (getText() != null) {
LOG.debug("Searching option with text: " + getText());
return getOptionForText(select, getText());
}
else
{
LOG.debug("Searching option with index: " + getOptionIndex());
return (HtmlOption) select.getOptions().get(ConversionUtil.convertToInt(getOptionIndex(), 0));
}
}
/**
* Search in the select the first option element that has the given
* text <p/>
*
* @param select the select in which to search
* @param text The text representing a particular value
* @return The option element corresponding to the specified text
*/
private HtmlOption getOptionForText(final HtmlSelect select, final String text) {
final IStringVerifier verifier = getVerifier(fIsRegex);
for (final Iterator iter = select.getOptions().iterator(); iter.hasNext();) {
final HtmlOption option = (HtmlOption) iter.next();
LOG.debug("Examining option: " + option);
if (verifier.verifyStrings(text, option.asText())) {
LOG.debug("Found option by text: " + option);
return option;
}
}
throw new StepFailedException("No option element found with text \"" + text + "\"", this);
}
protected void verifyParameters() {
super.verifyParameters();
int iNbNotNull = 0;
if (!isValueNull()) {
++iNbNotNull;
}
if (getText() != null) {
++iNbNotNull;
}
if (getOptionIndex() != null) {
integerParamCheck(getOptionIndex(), "optionIndex", false);
++iNbNotNull;
}
final boolean bXPathOrId = getXpath() != null || getHtmlId() != null;
paramCheck(iNbNotNull > 1, AT_MOST_ONE_VALUE_TEXT_OPTIONINDEX);
paramCheck(!bXPathOrId && iNbNotNull == 0,
"One of \"xpath\", \"htmlId\", \"value\", \"text\" or \"optionIndex\" is required!");
fIsMultiSelect = ConversionUtil.convertToBoolean(getMultiselect(), false);
fIsRegex = ConversionUtil.convertToBoolean(getRegex(), false);
}
}