Index: FormTester.java
===================================================================
--- FormTester.java	(revision 5381)
+++ FormTester.java	(working copy)
@@ -1,6 +1,6 @@
 /*
- * $Id$ $Revision:
- * 1.1 $ $Date$
+ * $Id$ $Revision$
+ * $Date$
  * 
  * ==============================================================================
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
@@ -17,8 +17,30 @@
  */
 package wicket.util.tester;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.Assert;
+import wicket.Component;
+import wicket.WicketRuntimeException;
+import wicket.Component.IVisitor;
+import wicket.markup.html.form.AbstractTextComponent;
+import wicket.markup.html.form.Check;
+import wicket.markup.html.form.CheckGroup;
+import wicket.markup.html.form.DropDownChoice;
 import wicket.markup.html.form.Form;
 import wicket.markup.html.form.FormComponent;
+import wicket.markup.html.form.IChoiceRenderer;
+import wicket.markup.html.form.ListMultipleChoice;
+import wicket.markup.html.form.Radio;
+import wicket.markup.html.form.RadioChoice;
+import wicket.markup.html.form.RadioGroup;
+import wicket.util.string.Strings;
 
 /**
  * A helper for testing validaiton and submission of Form component.
@@ -27,7 +49,10 @@
  */
 public class FormTester
 {
-	/** An instance of FormTester can only be used once. Create a new instance of each test */
+	/**
+	 * An instance of FormTester can only be used once. Create a new instance of
+	 * each test
+	 */
 	private boolean closed = false;
 
 	/** form component to be test */
@@ -49,31 +74,41 @@
 	 * @param wicketTester
 	 *            wicketTester that create FormTester
 	 * @param fillBlankString
-	 *            specify whether fill all child FormComponents with blank
+	 *            specify whether filling child Text Components with blank
 	 *            String
 	 */
-	FormTester(final String path, final Form workingForm, final WicketTester wicketTester,
-			boolean fillBlankString)
+	protected FormTester(final String path, final Form workingForm,
+			final WicketTester wicketTester, final boolean fillBlankString)
 	{
 		this.path = path;
 		this.workingForm = workingForm;
 		this.wicketTester = wicketTester;
 		this.wicketTester.setupRequestAndResponse();
 
-		if (fillBlankString)
+		// fill blank String for Text Component.
+		workingForm.visitFormComponents(new FormComponent.IVisitor()
 		{
-			workingForm.visitFormComponents(new FormComponent.IVisitor()
+			public void formComponent(FormComponent formComponent)
 			{
-				public void formComponent(FormComponent formComponent)
+				// do nothing for invisible component
+				if (!formComponent.isVisibleInHierarchy())
 				{
-					if (formComponent.isEnabled())
+					return;
+				}
+
+				// if component is text field and do not have exist value, fill
+				// blank String if required
+				if (formComponent instanceof AbstractTextComponent)
+				{
+					if (fillBlankString && Strings.isEmpty(formComponent.getValue()))
 					{
-						setValue(formComponent.getInputName(), "");
+						setFormComponentValue(formComponent, "");
+						return;
 					}
 				}
-			});
-		}
+			}
 
+		});
 	}
 
 	/**
@@ -89,7 +124,7 @@
 		checkClosed();
 
 		FormComponent formComponent = (FormComponent)workingForm.get(formComponentId);
-		wicketTester.getServletRequest().setParameter(formComponent.getInputName(), value);
+		setFormComponentValue(formComponent, value);
 	}
 
 	/**
@@ -122,4 +157,316 @@
 					+ "is allowed to submit only once");
 		}
 	}
+
+	/**
+	 * A convenient method to submit form with alternative button.
+	 * 
+	 * Note that if the button associates with a model, it's better to use
+	 * setValue() instead:
+	 * 
+	 * <pre>
+	 * formTester.setValue(&quot;to:my:button&quot;, &quot;value on the button&quot;);
+	 * formTester.submit();
+	 * </pre>
+	 * 
+	 * @param buttonComponentId
+	 *            relative path (from form) to the button
+	 */
+	public void submit(String buttonComponentId)
+	{
+		setValue(buttonComponentId, "marked");
+		submit();
+	}
+
+	private ChoiceSelectorFactory choiceSelectorFactory = new ChoiceSelectorFactory();
+
+	/**
+	 * simulate selecting an option of a Form Component. Support RadioGroup,
+	 * CheckGroup, and AbstractChoice family currently. The behavior is similar
+	 * to interacting on the browser: For single choice, such as Radio or
+	 * DropDownList, the selection will toggle each other. For multiple choice,
+	 * such as Checkbox or ListMultipleChoice, the selection will cumulate.
+	 * 
+	 * @param formComponentId
+	 *            relative path (from form) to selectable formComponent
+	 * @param index
+	 *            index of selectable option, start from 0
+	 */
+	public void select(String formComponentId, int index)
+	{
+		checkClosed();
+		ChoiceSelector choiceSelector = choiceSelectorFactory.create((FormComponent)workingForm
+				.get(formComponentId));
+		choiceSelector.doSelect(index);
+	}
+
+	/**
+	 * A convenient method to select multiple options for the form component.
+	 * The method only support multiple selectable form component.
+	 * 
+	 * @see #select(String, int)
+	 * 
+	 * @param formComponentId
+	 *            relative path (from form) to selectable formComponent
+	 * @param indexes
+	 *            index of selectable option, start from 0
+	 */
+	public void selectMultiple(String formComponentId, int[] indexes)
+	{
+		checkClosed();
+
+		ChoiceSelector choiceSelector = choiceSelectorFactory
+				.createForMultiple((FormComponent)workingForm.get(formComponentId));
+
+		for (int i = 0; i < indexes.length; i++)
+		{
+			choiceSelector.doSelect(indexes[i]);
+		}
+	}
+
+	/**
+	 * set formComponent's value into request parameter, this method overwrites
+	 * exist parameters.
+	 * 
+	 * @param formComponent
+	 * @param value
+	 */
+	private void setFormComponentValue(FormComponent formComponent, String value)
+	{
+		wicketTester.getServletRequest().setParameter(formComponent.getInputName(), value);
+	}
+
+	/**
+	 * add additional formComponent's value into request parameter, this method
+	 * retain exist parameters but remove any duplicated parameters.
+	 * 
+	 * @param formComponent
+	 * @param value
+	 */
+	private void addFormComponentValue(FormComponent formComponent, String value)
+	{
+		if (parameterExist(formComponent))
+		{
+			String[] values = wicketTester.getServletRequest().getParameterValues(
+					formComponent.getInputName());
+			// remove duplicated
+			HashSet all = new HashSet(Arrays.asList(values));
+			all.add(value);
+			Map newParameters = new HashMap();
+			newParameters.put(formComponent.getInputName(), all.toArray(new String[all.size()]));
+			wicketTester.getServletRequest().setParameters(newParameters);
+		}
+		else
+		{
+			setFormComponentValue(formComponent, value);
+		}
+	}
+
+	private boolean parameterExist(FormComponent formComponent)
+	{
+		String parameter = wicketTester.getServletRequest().getParameter(
+				formComponent.getInputName());
+		return parameter != null && parameter.trim().length() > 0;
+	}
+
+
+	/**
+	 * A Factory to create appropriate ChoiceSelector based on type of
+	 * formComponent
+	 */
+	private class ChoiceSelectorFactory
+	{
+		private final class SingleChoiceSelector extends ChoiceSelector
+		{
+			protected SingleChoiceSelector(FormComponent formComponent)
+			{
+				super(formComponent);
+			}
+
+			protected void assignValueToFormComponent(FormComponent formComponent, String value)
+			{
+				// single selectable should overwrite already selected option
+				setFormComponentValue(formComponent, value);
+			}
+		}
+		private final class MultipleChoiceSelector extends ChoiceSelector
+		{
+			protected MultipleChoiceSelector(FormComponent formComponent)
+			{
+				super(formComponent);
+				if (!allowMultipleChoice(formComponent))
+				{
+					Assert.fail("Component:'" + formComponent.getPath()
+							+ "' Not support multiple selection.");
+				}
+			}
+
+			protected void assignValueToFormComponent(FormComponent formComponent, String value)
+			{
+				// multiple selectable should retain selected option
+				addFormComponentValue(formComponent, value);
+			}
+		}
+
+		protected ChoiceSelector create(FormComponent formComponent)
+		{
+			if (formComponent instanceof RadioGroup || formComponent instanceof DropDownChoice
+					|| formComponent instanceof RadioChoice)
+			{
+				return new SingleChoiceSelector(formComponent);
+			}
+			else if (allowMultipleChoice(formComponent))
+			{
+				return new MultipleChoiceSelector(formComponent);
+			}
+			else
+			{
+				Assert.fail("Selecting on the component:'" + formComponent.getPath()
+						+ "' is not supported.");
+				return null;
+			}
+		}
+
+		private boolean allowMultipleChoice(FormComponent formComponent)
+		{
+			return formComponent instanceof CheckGroup
+					|| formComponent instanceof ListMultipleChoice;
+		}
+
+		protected ChoiceSelector createForMultiple(FormComponent formComponent)
+		{
+			return new MultipleChoiceSelector(formComponent);
+		}
+	}
+
+
+	/**
+	 * A selector template for selecting seletable form component via index of
+	 * option, support RadioGroup, CheckGroup, and AbstractChoice family.
+	 * 
+	 */
+	protected abstract class ChoiceSelector
+	{
+		private final FormComponent formComponent;
+
+		protected ChoiceSelector(FormComponent formComponent)
+		{
+			this.formComponent = formComponent;
+		}
+
+		protected final void doSelect(final int index)
+		{
+			if (formComponent instanceof RadioGroup)
+			{
+				Radio foundRadio = (Radio)formComponent.visitChildren(Radio.class,
+						new SearchOptionByIndexVisitor(index));
+				if (foundRadio == null)
+				{
+					Assert.fail("RadioGroup " + formComponent.getPath() + " does not has index:"
+							+ index);
+				}
+				assignValueToFormComponent(formComponent, foundRadio.getPath());
+			}
+			else if (formComponent instanceof CheckGroup)
+			{
+				Check foundCheck = (Check)formComponent.visitChildren(Check.class,
+						new SearchOptionByIndexVisitor(index));
+				if (foundCheck == null)
+				{
+					Assert.fail("CheckGroup " + formComponent.getPath() + " does not has index:"
+							+ index);
+				}
+				assignValueToFormComponent(formComponent, foundCheck.getPath());
+			}
+			else
+			{
+				String idValue = selectAbstractChoice(formComponent, index);
+				if (idValue == null)
+				{
+					Assert.fail(formComponent.getPath() + " is not selectable component.");
+				}
+				else
+				{
+					assignValueToFormComponent(formComponent, idValue);
+				}
+			}
+		}
+
+		/**
+		 * implement whether toggle or cumulate selection
+		 * 
+		 * @param formComponent
+		 * @param value
+		 */
+		protected abstract void assignValueToFormComponent(FormComponent formComponent, String value);
+
+		private String selectAbstractChoice(FormComponent formComponent, final int index)
+		{
+			try
+			{
+				Method getChoicesMethod = formComponent.getClass().getMethod("getChoices",
+						(Class[])null);
+				getChoicesMethod.setAccessible(true);
+				List choices = (List)getChoicesMethod.invoke(formComponent, (Object[])null);
+
+				Method getChoiceRendererMethod = formComponent.getClass().getMethod(
+						"getChoiceRenderer", (Class[])null);
+				getChoiceRendererMethod.setAccessible(true);
+				IChoiceRenderer choiceRenderer = (IChoiceRenderer)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);
+			}
+		}
+
+		private final class SearchOptionByIndexVisitor implements IVisitor
+		{
+			private final int index;
+
+			int count = 0;
+
+			private SearchOptionByIndexVisitor(int index)
+			{
+				super();
+				this.index = index;
+			}
+
+			/**
+			 * @see wicket.Component.IVisitor#component(wicket.Component)
+			 */
+			public Object component(Component component)
+			{
+				if (count == index)
+				{
+					return component;
+				}
+				else
+				{
+					count++;
+					return CONTINUE_TRAVERSAL;
+				}
+			}
+		}
+
+	}
+
+
 }
\ No newline at end of file
