/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.wicket.util.tester;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
import org.apache.wicket.markup.html.form.AbstractSingleSelectChoice;
import org.apache.wicket.markup.html.form.AbstractTextComponent;
import org.apache.wicket.markup.html.form.Check;
import org.apache.wicket.markup.html.form.CheckGroup;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.markup.html.form.IFormSubmittingComponent;
import org.apache.wicket.markup.html.form.ListMultipleChoice;
import org.apache.wicket.markup.html.form.Radio;
import org.apache.wicket.markup.html.form.RadioGroup;
import org.apache.wicket.markup.html.form.FormComponentUpdatingBehavior;
import org.apache.wicket.markup.html.form.upload.FileUploadField;
import org.apache.wicket.markup.html.form.upload.MultiFileUploadField;
import org.apache.wicket.protocol.http.mock.MockHttpServletRequest;
import org.apache.wicket.util.file.File;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.StringValue;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;
import org.junit.Assert;
/**
* A helper class for testing validation and submission of <code>FormComponent</code>s.
*
* @author Ingram Chen
* @author Frank Bille (frankbille)
* @since 1.2.6
*/
public class FormTester
{
/**
* An auto incrementing index used as a suffix for MultiFileUploadField's inputName
*/
private int multiFileUploadIndex = 0;
/**
* A selector template for selecting selectable <code>FormComponent</code>s with an index of
* option -- supports <code>RadioGroup</code>, <code>CheckGroup</code>, and
* <code>AbstractChoice</code> family.
*/
protected abstract class ChoiceSelector
{
/**
* TODO need Javadoc from author.
*/
private final class SearchOptionByIndexVisitor implements IVisitor<Component, Component>
{
int count = 0;
private final int index;
private SearchOptionByIndexVisitor(int index)
{
super();
this.index = index;
}
/**
* @see org.apache.wicket.util.visit.IVisitor#component(Object,
* org.apache.wicket.util.visit.IVisit)
*/
@Override
public void component(final Component component, final IVisit<Component> visit)
{
if (count == index)
{
visit.stop(component);
}
else
{
count++;
}
}
}
private final FormComponent<?> formComponent;
/**
* Constructor.
*
* @param formComponent
* a <code>FormComponent</code>
*/
protected ChoiceSelector(FormComponent<?> formComponent)
{
this.formComponent = formComponent;
}
/**
* Implements whether toggle or accumulate the selection.
*
* @param formComponent
* a <code>FormComponent</code>
* @param value
* a <code>String</code> value
*/
protected abstract void assignValueToFormComponent(FormComponent<?> formComponent,
String value);
public String getChoiceValueForIndex(int index)
{
if (formComponent instanceof RadioGroup)
{
Radio<?> foundRadio = (Radio<?>)formComponent.visitChildren(Radio.class,
new SearchOptionByIndexVisitor(index));
if (foundRadio == null)
{
fail("RadioGroup " + formComponent.getPath() + " does not have index:" + index);
return null;
}
return foundRadio.getValue();
}
else if (formComponent instanceof CheckGroup)
{
Check<?> foundCheck = (Check<?>)formComponent.visitChildren(Check.class,
new SearchOptionByIndexVisitor(index));
if (foundCheck == null)
{
fail("CheckGroup " + formComponent.getPath() + " does not have index:" + index);
return null;
}
return foundCheck.getValue();
}
else
{
String idValue = selectAbstractChoice(formComponent, index);
if (idValue == null)
{
fail(formComponent.getPath() + " is not a selectable Component.");
return null;
}
else
{
return idValue;
}
}
}
/**
* Selects a given index in a selectable <code>FormComponent</code>.
*
* @param index
*/
protected final void doSelect(final int index)
{
String value = getChoiceValueForIndex(index);
assignValueToFormComponent(formComponent, value);
}
/**
* Selects a given index in a selectable <code>FormComponent</code>.
*
* @param formComponent
* a <code>FormComponent</code>
* @param index
* the index to select
* @return the id value at the selected index
*/
@SuppressWarnings("unchecked")
private String selectAbstractChoice(final FormComponent<?> formComponent, final int index)
{
try
{
Method getChoicesMethod = formComponent.getClass().getMethod("getChoices",
(Class<?>[])null);
getChoicesMethod.setAccessible(true);
List<Object> choices = (List<Object>)getChoicesMethod.invoke(formComponent,
(Object[])null);
Method getChoiceRendererMethod = formComponent.getClass().getMethod(
"getChoiceRenderer", (Class<?>[])null);
getChoiceRendererMethod.setAccessible(true);
IChoiceRenderer<Object> choiceRenderer = (IChoiceRenderer<Object>)getChoiceRendererMethod.invoke(
formComponent, (Object[])null);
return choiceRenderer.getIdValue(choices.get(index), index);
}
catch (SecurityException e)
{
throw new WicketRuntimeException("unexpect select failure", e);
}
catch (NoSuchMethodException e)
{
// component without getChoices() or getChoiceRenderer() is not
// selectable
return null;
}
catch (IllegalAccessException e)
{
throw new WicketRuntimeException("unexpect select failure", e);
}
catch (InvocationTargetException e)
{
throw new WicketRuntimeException("unexpect select failure", e);
}
}
}
/**
* A factory that creates an appropriate <code>ChoiceSelector</code> based on type of
* <code>FormComponent</code>.
*/
private class ChoiceSelectorFactory
{
/**
* <code>MultipleChoiceSelector</code> class.
*/
private final class MultipleChoiceSelector extends ChoiceSelector
{
/**
* Constructor.
*
* @param formComponent
* a <code>FormComponent</code>
*/
protected MultipleChoiceSelector(FormComponent<?> formComponent)
{
super(formComponent);
if (!allowMultipleChoice(formComponent))
{
fail("Component:'" + formComponent.getPath() +
"' Does not support multiple selection.");
}
}
@Override
protected void assignValueToFormComponent(FormComponent<?> formComponent, String value)
{
// multiple selectable should retain selected option
addFormComponentValue(formComponent, value);
}
}
/**
* <code>SingleChoiceSelector</code> class.
*/
private final class SingleChoiceSelector extends ChoiceSelector
{
/**
* Constructor.
*
* @param formComponent
* a <code>FormComponent</code>
*/
protected SingleChoiceSelector(FormComponent<?> formComponent)
{
super(formComponent);
}
@Override
protected void assignValueToFormComponent(FormComponent<?> formComponent, String value)
{
// single selectable should overwrite already selected option
setFormComponentValue(formComponent, value);
}
}
/**
* Creates a <code>ChoiceSelector</code>.
*
* @param formComponent
* a <code>FormComponent</code>
* @return ChoiceSelector a <code>ChoiceSelector</code>
*/
protected ChoiceSelector create(FormComponent<?> formComponent)
{
if (formComponent == null)
{
fail("Trying to select on null component.");
return null;
}
else if (formComponent instanceof RadioGroup ||
formComponent instanceof AbstractSingleSelectChoice)
{
return new SingleChoiceSelector(formComponent);
}
else if (allowMultipleChoice(formComponent))
{
return new MultipleChoiceSelector(formComponent);
}
else
{
fail("Selecting on the component:'" + formComponent.getPath() +
"' is not supported.");
return null;
}
}
/**
* Creates a <code>MultipleChoiceSelector</code>.
*
* @param formComponent
* a <code>FormComponent</code>
* @return ChoiceSelector a <code>ChoiceSelector</code>
*/
protected ChoiceSelector createForMultiple(FormComponent<?> formComponent)
{
return new MultipleChoiceSelector(formComponent);
}
/**
* Tests if a given <code>FormComponent</code> allows multiple choice.
*
* @param formComponent
* a <code>FormComponent</code>
* @return <code>true</code> if the given FormComponent allows multiple choice
*/
private boolean allowMultipleChoice(FormComponent<?> formComponent)
{
return formComponent instanceof CheckGroup ||
formComponent instanceof ListMultipleChoice;
}
}
private final ChoiceSelectorFactory choiceSelectorFactory = new ChoiceSelectorFactory();
/**
* An instance of <code>FormTester</code> can only be used once. Create a new instance of each
* test.
*/
private boolean closed = false;
/** path to <code>FormComponent</code> */
private final String path;
/** <code>BaseWicketTester</code> that create <code>FormTester</code> */
private final BaseWicketTester tester;
/** <code>FormComponent</code> to be tested */
private final Form<?> workingForm;
private boolean clearFeedbackMessagesBeforeSubmit = true;
/**
* @see WicketTester#newFormTester(String)
*
* @param path
* path to <code>FormComponent</code>
* @param workingForm
* <code>FormComponent</code> to be tested
* @param wicketTester
* <code>WicketTester</code> that creates <code>FormTester</code>
* @param fillBlankString
* specifies whether to fill child <code>TextComponent</code>s with blank
* <code>String</code>s
*/
protected FormTester(final String path, final Form<?> workingForm,
final BaseWicketTester wicketTester, final boolean fillBlankString)
{
this.path = path;
this.workingForm = workingForm;
tester = wicketTester;
// fill blank String for Text Component.
workingForm.visitFormComponents(new IVisitor<FormComponent<?>, Void>()
{
@Override
public void component(final FormComponent<?> formComponent, final IVisit<Void> visit)
{
// do nothing for invisible or disabled component -- the browser would not send any
// parameter for a disabled component
if (!(formComponent.isVisibleInHierarchy() && formComponent.isEnabledInHierarchy()))
{
return;
}
String[] values = getInputValue(formComponent);
if (formComponent instanceof AbstractTextComponent<?>)
{
if (values.length == 0 && fillBlankString)
{
setFormComponentValue(formComponent, "");
}
}
for (String value : values)
{
addFormComponentValue(formComponent, value);
}
}
});
workingForm.detach();
}
/**
* Gets request parameter values for the form component that represents its current model value
*
* @param formComponent
* @return array containing parameter values
*/
public static String[] getInputValue(FormComponent<?> formComponent)
{
// the browser sends parameters for visible and enabled components only
if (formComponent.isVisibleInHierarchy() && formComponent.isEnabledInHierarchy())
{
if (formComponent instanceof IFormSubmittingComponent)
{
// buttons have to be submitted explicitely
}
else if (formComponent instanceof AbstractTextComponent)
{
return new String[] { getFormComponentValue(formComponent) };
}
else
{
// TODO is it safe to assume that all other components' values can be split?
String value = getFormComponentValue(formComponent);
if (!Strings.isEmpty(value))
{
return value.split(FormComponent.VALUE_SEPARATOR);
}
}
}
return new String[] { };
}
private static String getFormComponentValue(final FormComponent<?> formComponent)
{
boolean oldEscape = formComponent.getEscapeModelStrings();
formComponent.setEscapeModelStrings(false);
String val = formComponent.getValue();
formComponent.setEscapeModelStrings(oldEscape);
return val;
}
/**
* Retrieves the current <code>Form</code> object.
*
* @return the working <code>Form</code>
*/
public Form<?> getForm()
{
return workingForm;
}
/**
* Gets the value for an <code>AbstractTextComponent</code> with the provided id.
*
* @param id
* <code>Component</code> id
* @return the value of the text component
*/
public String getTextComponentValue(final String id)
{
Component c = getForm().get(id);
if (c instanceof AbstractTextComponent)
{
return ((AbstractTextComponent<?>)c).getValue();
}
return null;
}
/**
* Simulates selecting an option of a <code>FormComponent</code>. Supports
* <code>RadioGroup</code>, <code>CheckGroup</code>, and <code>AbstractChoice</code> family
* currently. The behavior is similar to interacting on the browser: For a single choice, such
* as <code>Radio</code> or <code>DropDownList</code>, the selection will toggle each other. For
* multiple choice, such as <code>Checkbox</code> or <code>ListMultipleChoice</code>, the
* selection will accumulate.
*
* @param formComponentId
* relative path (from <code>Form</code>) to the selectable
* <code>FormComponent</code>
* @param index
* index of the selectable option, starting from 0
* @return This
*/
public FormTester select(final String formComponentId, int index)
{
checkClosed();
FormComponent<?> component = (FormComponent<?>)workingForm.get(formComponentId);
ChoiceSelector choiceSelector = choiceSelectorFactory.create(component);
choiceSelector.doSelect(index);
for (FormComponentUpdatingBehavior updater : component.getBehaviors(FormComponentUpdatingBehavior.class)) {
tester.invokeListener(component, updater);
}
return this;
}
/**
* A convenience method to select multiple options for the <code>FormComponent</code>. The
* method only support multiple selectable <code>FormComponent</code>s.
*
* @see #select(String, int)
*
* @param formComponentId
* relative path (from <code>Form</code>) to the selectable
* <code>FormComponent</code>
* @param indexes
* index of the selectable option, starting from 0
* @return This
*/
public FormTester selectMultiple(String formComponentId, int[] indexes)
{
return selectMultiple(formComponentId, indexes, false);
}
/**
* A convenience method to select multiple options for the <code>FormComponent</code>. The
* method only support multiple selectable <code>FormComponent</code>s.
*
* @see #select(String, int)
*
* @param formComponentId
* relative path (from <code>Form</code>) to the selectable
* <code>FormComponent</code>
* @param indexes
* index of the selectable option, starting from 0
* @param replace
* If true, than all previous selects are first reset, thus existing selects are
* replaced. If false, than the new indexes will be added.
* @return This
*/
public FormTester selectMultiple(String formComponentId, int[] indexes, final boolean replace)
{
checkClosed();
if (replace)
{
// Reset first
setValue(formComponentId, "");
}
ChoiceSelector choiceSelector = choiceSelectorFactory.createForMultiple((FormComponent<?>)workingForm.get(formComponentId));
for (int index : indexes)
{
choiceSelector.doSelect(index);
}
return this;
}
/**
* Simulates filling in a field on a <code>Form</code>.
*
* @param formComponentId
* relative path (from <code>Form</code>) to the selectable
* <code>FormComponent</code> or <code>IFormSubmittingComponent</code>
* @param value
* the field value
* @return This
*/
public FormTester setValue(final String formComponentId, final String value)
{
Component component = workingForm.get(formComponentId);
Assert.assertNotNull("Unable to set value. Couldn't find component with name: " +
formComponentId, component);
return setValue(component, value);
}
/**
* Simulates filling in a field on a <code>Form</code>.
*
* @param formComponent
* relative path (from <code>Form</code>) to the selectable
* <code>FormComponent</code> or <code>IFormSubmittingComponent</code>
* @param value
* the field value
* @return This
*/
public FormTester setValue(final Component formComponent, final String value)
{
Args.notNull(formComponent, "formComponent");
checkClosed();
if (formComponent instanceof IFormSubmittingComponent)
{
setFormSubmittingComponentValue((IFormSubmittingComponent)formComponent, value);
}
else if (formComponent instanceof FormComponent)
{
setFormComponentValue((FormComponent<?>)formComponent, value);
}
else
{
fail("Component with id: " + formComponent.getId() + " is not a FormComponent");
}
return this;
}
/**
* @param checkBoxId
* @param value
* @return This
*/
public FormTester setValue(String checkBoxId, boolean value)
{
return setValue(checkBoxId, Boolean.toString(value));
}
/**
* Sets the <code>File</code> on a {@link FileUploadField}.
*
* @param formComponentId
* relative path (from <code>Form</code>) to the selectable
* <code>FormComponent</code>. The <code>FormComponent</code> must be of a type
* <code>FileUploadField</code>.
* @param file
* the <code>File</code> to upload or {@code null} for an empty input
* @param contentType
* the content type of the file. Must be a valid mime type.
* @return This
*/
public FormTester setFile(final String formComponentId, final File file,
final String contentType)
{
checkClosed();
FormComponent<?> formComponent = (FormComponent<?>)workingForm.get(formComponentId);
MockHttpServletRequest servletRequest = tester.getRequest();
if (formComponent instanceof FileUploadField)
{
servletRequest.addFile(formComponent.getInputName(), file, contentType);
}
else if (formComponent instanceof MultiFileUploadField)
{
String inputName = formComponent.getInputName() + MultiFileUploadField.MAGIC_SEPARATOR + multiFileUploadIndex++;
servletRequest.addFile(inputName, file, contentType);
}
else
{
fail("'" + formComponentId + "' is not " +
"a FileUploadField. You can only attach a file to form " +
"component of this type.");
}
return this;
}
/**
* Submits the <code>Form</code>. Note that <code>submit</code> can be executed only once.
*
* @return This
*/
public FormTester submit()
{
checkClosed();
try
{
if (clearFeedbackMessagesBeforeSubmit)
{
tester.clearFeedbackMessages();
}
tester.getRequest().setUseMultiPartContentType(workingForm.isMultiPart());
tester.submitForm(path);
}
finally
{
closed = true;
}
return this;
}
public boolean isClearFeedbackMessagesBeforeSubmit()
{
return clearFeedbackMessagesBeforeSubmit;
}
public FormTester setClearFeedbackMessagesBeforeSubmit(boolean clearFeedbackMessagesBeforeSubmit)
{
this.clearFeedbackMessagesBeforeSubmit = clearFeedbackMessagesBeforeSubmit;
return this;
}
/**
* A convenience method for submitting the <code>Form</code> with an alternate button.
* <p>
* Note that if the button is associated with a model, it's better to use the
* <code>setValue</code> method instead:
*
* <pre>
* formTester.setValue("to:my:button", "value on the button");
* formTester.submit();
* </pre>
*
* @param buttonComponentId
* relative path (from <code>Form</code>) to the button
* @return This
*/
public FormTester submit(final String buttonComponentId)
{
Component submitter = getForm().get(buttonComponentId);
if (submitter == null)
{
fail("Cannot submit the form because there is no submitting component with id: " + buttonComponentId);
}
return submit(submitter);
}
/**
* A convenience method for submitting the <code>Form</code> with an alternate button.
* <p>
* Note that if the button is associated with a model, it's better to use the
* <code>setValue</code> method instead:
*
* <pre>
* formTester.setValue(myButton, "value on the button");
* formTester.submit();
* </pre>
*
* @param buttonComponent
* relative path (from <code>Form</code>) to the button
* @return This
*/
public FormTester submit(final Component buttonComponent)
{
Args.notNull(buttonComponent, "buttonComponent");
setValue(buttonComponent, "marked");
if (buttonComponent instanceof AjaxButton || buttonComponent instanceof AjaxSubmitLink)
{
if (clearFeedbackMessagesBeforeSubmit)
{
tester.clearFeedbackMessages();
}
tester.getRequest().setUseMultiPartContentType(workingForm.isMultiPart());
tester.executeAjaxEvent(buttonComponent, "click");
return this;
}
else
{
return submit();
}
}
/**
* A convenience method to submit the Form via a SubmitLink which may inside or outside of the
* Form.
*
* @param path
* The path to the SubmitLink
* @param pageRelative
* if true, than the 'path' to the SubmitLink is relative to the page. Thus the link
* can be outside the form. If false, the path is relative to the form and thus the
* link is inside the form.
* @return This
*/
public FormTester submitLink(String path, final boolean pageRelative)
{
if (pageRelative)
{
tester.clickLink(path, false);
}
else
{
path = this.path + ":" + path;
tester.clickLink(path, false);
}
return this;
}
/**
* Adds an additional <code>FormComponent</code>'s value into request parameter -- this method
* retains existing parameters but removes any duplicate parameters.
*
* @param formComponent
* a <code>FormComponent</code>
* @param value
* a value to add
* @return This
*/
private FormTester addFormComponentValue(FormComponent<?> formComponent, String value)
{
if (parameterExist(formComponent))
{
List<StringValue> values = tester.getRequest()
.getPostParameters()
.getParameterValues(formComponent.getInputName());
// remove duplicated
HashSet<String> all = new HashSet<String>();
for (StringValue val : values)
{
all.add(val.toString());
}
all.add(value);
values = new ArrayList<StringValue>();
for (String val : all)
{
values.add(StringValue.valueOf(val));
}
tester.getRequest()
.getPostParameters()
.setParameterValues(formComponent.getInputName(), values);
}
else
{
setFormComponentValue(formComponent, value);
}
return this;
}
/**
* <code>FormTester</code> must only be used once. Create a new instance of
* <code>FormTester</code> for each test.
*/
private void checkClosed()
{
if (closed)
{
fail("'" + path + "' already submitted. Note that FormTester " +
"is allowed to submit only once");
}
}
/**
* Returns <code>true</code> if the parameter exists in the <code>FormComponent</code>.
*
* @param formComponent
* a <code>FormComponent</code>
* @return <code>true</code> if the parameter exists in the <code>FormComponent</code>
*/
private boolean parameterExist(final FormComponent<?> formComponent)
{
String parameter = tester.getRequest()
.getPostParameters()
.getParameterValue(formComponent.getInputName())
.toString();
return parameter != null && parameter.trim().length() > 0;
}
/**
* Set formComponent's value into request parameter, this method overwrites existing parameters.
*
* @param formComponent
* a <code>FormComponent</code>
* @param value
* a value to add
*/
private void setFormComponentValue(final FormComponent<?> formComponent, final String value)
{
tester.getRequest()
.getPostParameters()
.setParameterValue(formComponent.getInputName(), value);
}
/**
* Set component's value into request parameter, this method overwrites existing parameters.
*
* @param component
* an {@link IFormSubmittingComponent}
* @param value
* a value to add
*/
private void setFormSubmittingComponentValue(IFormSubmittingComponent component, String value)
{
tester.getRequest().getPostParameters().setParameterValue(component.getInputName(), value);
}
/**
*
* @param message
*/
private void fail(String message)
{
throw new WicketRuntimeException(message);
}
}