001: /*
002: * $Id: FormTester.java 475147 2006-11-15 07:57:36Z ivaynberg $ $Revision: 475147 $
003: * $Date: 2006-11-15 08:57:36 +0100 (Wed, 15 Nov 2006) $
004: *
005: * ==============================================================================
006: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
007: * use this file except in compliance with the License. You may obtain a copy of
008: * the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015: * License for the specific language governing permissions and limitations under
016: * the License.
017: */
018: package wicket.util.tester;
019:
020: import java.lang.reflect.InvocationTargetException;
021: import java.lang.reflect.Method;
022: import java.util.Arrays;
023: import java.util.HashMap;
024: import java.util.HashSet;
025: import java.util.List;
026: import java.util.Map;
027:
028: import junit.framework.Assert;
029: import wicket.Component;
030: import wicket.WicketRuntimeException;
031: import wicket.Component.IVisitor;
032: import wicket.markup.html.form.AbstractTextComponent;
033: import wicket.markup.html.form.Check;
034: import wicket.markup.html.form.CheckGroup;
035: import wicket.markup.html.form.DropDownChoice;
036: import wicket.markup.html.form.Form;
037: import wicket.markup.html.form.FormComponent;
038: import wicket.markup.html.form.IChoiceRenderer;
039: import wicket.markup.html.form.ListMultipleChoice;
040: import wicket.markup.html.form.Radio;
041: import wicket.markup.html.form.RadioChoice;
042: import wicket.markup.html.form.RadioGroup;
043: import wicket.util.string.Strings;
044:
045: /**
046: * A helper for testing validaiton and submission of Form component.
047: *
048: * @author Ingram Chen
049: */
050: public class FormTester {
051: /**
052: * An instance of FormTester can only be used once. Create a new instance of
053: * each test
054: */
055: private boolean closed = false;
056:
057: /** form component to be test */
058: private Form workingForm;
059:
060: /** wicketTester that create FormTester */
061: private final WicketTester wicketTester;
062:
063: /** path to form component */
064: private final String path;
065:
066: private ChoiceSelectorFactory choiceSelectorFactory = new ChoiceSelectorFactory();
067:
068: /**
069: * @see WicketTester#newFormTester(String)
070: *
071: * @param path
072: * path to form component
073: * @param workingForm
074: * form component to be test
075: * @param wicketTester
076: * wicketTester that create FormTester
077: * @param fillBlankString
078: * specify whether filling child Text Components with blank
079: * String
080: */
081: protected FormTester(final String path, final Form workingForm,
082: final WicketTester wicketTester,
083: final boolean fillBlankString) {
084: this .path = path;
085: this .workingForm = workingForm;
086: this .wicketTester = wicketTester;
087: this .wicketTester.setupRequestAndResponse();
088:
089: // fill blank String for Text Component.
090: workingForm.visitFormComponents(new FormComponent.IVisitor() {
091: public void formComponent(FormComponent formComponent) {
092: // do nothing for invisible component
093: if (!formComponent.isVisibleInHierarchy()) {
094: return;
095: }
096:
097: // if component is text field and do not have exist value, fill
098: // blank String if required
099: if (formComponent instanceof AbstractTextComponent) {
100: if (fillBlankString
101: && Strings
102: .isEmpty(formComponent.getValue())) {
103: setFormComponentValue(formComponent, "");
104: return;
105: }
106: }
107: }
108:
109: });
110: }
111:
112: /**
113: * simulate filling a field of a Form.
114: *
115: * @param formComponentId
116: * relative path (from form) to formComponent
117: * @param value
118: * field value of form.
119: */
120: public void setValue(final String formComponentId,
121: final String value) {
122: checkClosed();
123:
124: FormComponent formComponent = (FormComponent) workingForm
125: .get(formComponentId);
126: setFormComponentValue(formComponent, value);
127: }
128:
129: /**
130: * submit the form. note that submit() can be executed only once.
131: */
132: public void submit() {
133: checkClosed();
134: try {
135: wicketTester.getServletRequest().setRequestToComponent(
136: workingForm);
137: wicketTester.processRequestCycle();
138: } finally {
139: closed = true;
140: }
141: }
142:
143: /**
144: * FormTester must only be used once. Create a new instance of FormTester
145: * for each test.
146: */
147: private void checkClosed() {
148: if (closed) {
149: throw new IllegalStateException("'" + path
150: + "' already sumbitted. Note that FormTester "
151: + "is allowed to submit only once");
152: }
153: }
154:
155: /**
156: * A convenient method to submit form with alternative button.
157: *
158: * Note that if the button associates with a model, it's better to use
159: * setValue() instead:
160: *
161: * <pre>
162: * formTester.setValue("to:my:button", "value on the button");
163: * formTester.submit();
164: * </pre>
165: *
166: * @param buttonComponentId
167: * relative path (from form) to the button
168: */
169: public void submit(String buttonComponentId) {
170: setValue(buttonComponentId, "marked");
171: submit();
172: }
173:
174: /**
175: * simulate selecting an option of a Form Component. Support RadioGroup,
176: * CheckGroup, and AbstractChoice family currently. The behavior is similar
177: * to interacting on the browser: For single choice, such as Radio or
178: * DropDownList, the selection will toggle each other. For multiple choice,
179: * such as Checkbox or ListMultipleChoice, the selection will cumulate.
180: *
181: * @param formComponentId
182: * relative path (from form) to selectable formComponent
183: * @param index
184: * index of selectable option, start from 0
185: */
186: public void select(String formComponentId, int index) {
187: checkClosed();
188: ChoiceSelector choiceSelector = choiceSelectorFactory
189: .create((FormComponent) workingForm
190: .get(formComponentId));
191: choiceSelector.doSelect(index);
192: }
193:
194: /**
195: * A convenient method to select multiple options for the form component.
196: * The method only support multiple selectable form component.
197: *
198: * @see #select(String, int)
199: *
200: * @param formComponentId
201: * relative path (from form) to selectable formComponent
202: * @param indexes
203: * index of selectable option, start from 0
204: */
205: public void selectMultiple(String formComponentId, int[] indexes) {
206: checkClosed();
207:
208: ChoiceSelector choiceSelector = choiceSelectorFactory
209: .createForMultiple((FormComponent) workingForm
210: .get(formComponentId));
211:
212: for (int i = 0; i < indexes.length; i++) {
213: choiceSelector.doSelect(indexes[i]);
214: }
215: }
216:
217: /**
218: * set formComponent's value into request parameter, this method overwrites
219: * exist parameters.
220: *
221: * @param formComponent
222: * @param value
223: */
224: private void setFormComponentValue(FormComponent formComponent,
225: String value) {
226: wicketTester.getServletRequest().setParameter(
227: formComponent.getInputName(), value);
228: }
229:
230: /**
231: * add additional formComponent's value into request parameter, this method
232: * retain exist parameters but remove any duplicated parameters.
233: *
234: * @param formComponent
235: * @param value
236: */
237: private void addFormComponentValue(FormComponent formComponent,
238: String value) {
239: if (parameterExist(formComponent)) {
240: String[] values = wicketTester.getServletRequest()
241: .getParameterValues(formComponent.getInputName());
242: // remove duplicated
243: HashSet all = new HashSet(Arrays.asList(values));
244: all.add(value);
245: Map newParameters = new HashMap();
246: newParameters.put(formComponent.getInputName(), all
247: .toArray(new String[all.size()]));
248: wicketTester.getServletRequest().setParameters(
249: newParameters);
250: } else {
251: setFormComponentValue(formComponent, value);
252: }
253: }
254:
255: /**
256: *
257: * @param formComponent
258: * @return Boolean
259: */
260: private boolean parameterExist(FormComponent formComponent) {
261: String parameter = wicketTester.getServletRequest()
262: .getParameter(formComponent.getInputName());
263: return parameter != null && parameter.trim().length() > 0;
264: }
265:
266: /**
267: * A Factory to create appropriate ChoiceSelector based on type of
268: * formComponent
269: */
270: private class ChoiceSelectorFactory {
271: /**
272: *
273: */
274: private final class SingleChoiceSelector extends ChoiceSelector {
275: /**
276: * Construct.
277: *
278: * @param formComponent
279: */
280: protected SingleChoiceSelector(FormComponent formComponent) {
281: super (formComponent);
282: }
283:
284: /**
285: *
286: * @see wicket.util.tester.FormTester.ChoiceSelector#assignValueToFormComponent(wicket.markup.html.form.FormComponent,
287: * java.lang.String)
288: */
289: protected void assignValueToFormComponent(
290: FormComponent formComponent, String value) {
291: // single selectable should overwrite already selected option
292: setFormComponentValue(formComponent, value);
293: }
294: }
295:
296: /**
297: *
298: */
299: private final class MultipleChoiceSelector extends
300: ChoiceSelector {
301: /**
302: * Construct.
303: *
304: * @param formComponent
305: */
306: protected MultipleChoiceSelector(FormComponent formComponent) {
307: super (formComponent);
308: if (!allowMultipleChoice(formComponent)) {
309: Assert.fail("Component:'" + formComponent.getPath()
310: + "' Not support multiple selection.");
311: }
312: }
313:
314: /**
315: *
316: * @see wicket.util.tester.FormTester.ChoiceSelector#assignValueToFormComponent(wicket.markup.html.form.FormComponent,
317: * java.lang.String)
318: */
319: protected void assignValueToFormComponent(
320: FormComponent formComponent, String value) {
321: // multiple selectable should retain selected option
322: addFormComponentValue(formComponent, value);
323: }
324: }
325:
326: /**
327: *
328: * @param formComponent
329: * @return ChoiceSelector
330: */
331: protected ChoiceSelector create(FormComponent formComponent) {
332: if (formComponent instanceof RadioGroup
333: || formComponent instanceof DropDownChoice
334: || formComponent instanceof RadioChoice) {
335: return new SingleChoiceSelector(formComponent);
336: } else if (allowMultipleChoice(formComponent)) {
337: return new MultipleChoiceSelector(formComponent);
338: } else {
339: Assert.fail("Selecting on the component:'"
340: + formComponent.getPath()
341: + "' is not supported.");
342: return null;
343: }
344: }
345:
346: /**
347: *
348: * @param formComponent
349: * @return boolean
350: */
351: private boolean allowMultipleChoice(FormComponent formComponent) {
352: return formComponent instanceof CheckGroup
353: || formComponent instanceof ListMultipleChoice;
354: }
355:
356: /**
357: *
358: * @param formComponent
359: * @return ChoiceSelector
360: */
361: protected ChoiceSelector createForMultiple(
362: FormComponent formComponent) {
363: return new MultipleChoiceSelector(formComponent);
364: }
365: }
366:
367: /**
368: * A selector template for selecting seletable form component via index of
369: * option, support RadioGroup, CheckGroup, and AbstractChoice family.
370: *
371: */
372: protected abstract class ChoiceSelector {
373: private final FormComponent formComponent;
374:
375: /**
376: * Construct.
377: *
378: * @param formComponent
379: */
380: protected ChoiceSelector(FormComponent formComponent) {
381: this .formComponent = formComponent;
382: }
383:
384: /**
385: *
386: * @param index
387: */
388: protected final void doSelect(final int index) {
389: if (formComponent instanceof RadioGroup) {
390: Radio foundRadio = (Radio) formComponent.visitChildren(
391: Radio.class, new SearchOptionByIndexVisitor(
392: index));
393: if (foundRadio == null) {
394: Assert.fail("RadioGroup " + formComponent.getPath()
395: + " does not has index:" + index);
396: }
397: assignValueToFormComponent(formComponent, foundRadio
398: .getValue());
399: } else if (formComponent instanceof CheckGroup) {
400: Check foundCheck = (Check) formComponent.visitChildren(
401: Check.class, new SearchOptionByIndexVisitor(
402: index));
403: if (foundCheck == null) {
404: Assert.fail("CheckGroup " + formComponent.getPath()
405: + " does not have index:" + index);
406: }
407:
408: assignValueToFormComponent(formComponent, String
409: .valueOf(foundCheck.getValue()));
410: } else {
411: String idValue = selectAbstractChoice(formComponent,
412: index);
413: if (idValue == null) {
414: Assert.fail(formComponent.getPath()
415: + " is not selectable component.");
416: } else {
417: assignValueToFormComponent(formComponent, idValue);
418: }
419: }
420: }
421:
422: /**
423: * implement whether toggle or cumulate selection
424: *
425: * @param formComponent
426: * @param value
427: */
428: protected abstract void assignValueToFormComponent(
429: FormComponent formComponent, String value);
430:
431: /**
432: *
433: * @param formComponent
434: * @param index
435: * @return xxx
436: */
437: private String selectAbstractChoice(
438: FormComponent formComponent, final int index) {
439: try {
440: Method getChoicesMethod = formComponent.getClass()
441: .getMethod("getChoices", (Class[]) null);
442: getChoicesMethod.setAccessible(true);
443: List choices = (List) getChoicesMethod.invoke(
444: formComponent, (Object[]) null);
445:
446: Method getChoiceRendererMethod = formComponent
447: .getClass().getMethod("getChoiceRenderer",
448: (Class[]) null);
449: getChoiceRendererMethod.setAccessible(true);
450: IChoiceRenderer choiceRenderer = (IChoiceRenderer) getChoiceRendererMethod
451: .invoke(formComponent, (Object[]) null);
452:
453: return choiceRenderer.getIdValue(choices.get(index),
454: index);
455: } catch (SecurityException e) {
456: throw new WicketRuntimeException(
457: "unexpect select failure", e);
458: } catch (NoSuchMethodException e) {
459: // component without getChoices() or getChoiceRenderer() is not
460: // selectable
461: return null;
462: } catch (IllegalAccessException e) {
463: throw new WicketRuntimeException(
464: "unexpect select failure", e);
465: } catch (InvocationTargetException e) {
466: throw new WicketRuntimeException(
467: "unexpect select failure", e);
468: }
469: }
470:
471: /**
472: * ???
473: */
474: private final class SearchOptionByIndexVisitor implements
475: IVisitor {
476: private final int index;
477:
478: int count = 0;
479:
480: private SearchOptionByIndexVisitor(int index) {
481: super ();
482: this .index = index;
483: }
484:
485: /**
486: * @see wicket.Component.IVisitor#component(wicket.Component)
487: */
488: public Object component(Component component) {
489: if (count == index) {
490: return component;
491: } else {
492: count++;
493: return CONTINUE_TRAVERSAL;
494: }
495: }
496: }
497: }
498: }
|