001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.util.tester;
018
019import static org.junit.jupiter.api.Assertions.assertNotNull;
020
021import java.lang.reflect.InvocationTargetException;
022import java.lang.reflect.Method;
023import java.util.ArrayList;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Objects;
027
028import org.apache.wicket.Component;
029import org.apache.wicket.WicketRuntimeException;
030import org.apache.wicket.ajax.markup.html.form.AjaxButton;
031import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
032import org.apache.wicket.markup.html.form.AbstractSingleSelectChoice;
033import org.apache.wicket.markup.html.form.AbstractTextComponent;
034import org.apache.wicket.markup.html.form.Check;
035import org.apache.wicket.markup.html.form.CheckGroup;
036import org.apache.wicket.markup.html.form.Form;
037import org.apache.wicket.markup.html.form.FormComponent;
038import org.apache.wicket.markup.html.form.FormComponentUpdatingBehavior;
039import org.apache.wicket.markup.html.form.IChoiceRenderer;
040import org.apache.wicket.markup.html.form.IFormSubmittingComponent;
041import org.apache.wicket.markup.html.form.ListMultipleChoice;
042import org.apache.wicket.markup.html.form.Radio;
043import org.apache.wicket.markup.html.form.RadioGroup;
044import org.apache.wicket.markup.html.form.upload.FileUploadField;
045import org.apache.wicket.markup.html.form.upload.MultiFileUploadField;
046import org.apache.wicket.protocol.http.mock.MockHttpServletRequest;
047import org.apache.wicket.util.file.File;
048import org.apache.wicket.util.lang.Args;
049import org.apache.wicket.util.string.StringValue;
050import org.apache.wicket.util.string.Strings;
051import org.apache.wicket.util.visit.IVisit;
052import org.apache.wicket.util.visit.IVisitor;
053
054/**
055 * A helper class for testing validation and submission of <code>FormComponent</code>s.
056 *
057 * @author Ingram Chen
058 * @author Frank Bille (frankbille)
059 * @since 1.2.6
060 */
061public class FormTester
062{
063        private static final String NO_FORM_FOR_PATH = "No Form componet found for path '%s'. Check if path value is correct and "
064                + "if form component is visible and active.";
065
066    /**
067         * An auto incrementing index used as a suffix for MultiFileUploadField's inputName
068         */
069        private int multiFileUploadIndex = 0;
070
071        /**
072         * A selector template for selecting selectable <code>FormComponent</code>s with an index of
073         * option -- supports <code>RadioGroup</code>, <code>CheckGroup</code>, and
074         * <code>AbstractChoice</code> family.
075         */
076        protected abstract class ChoiceSelector
077        {
078                /**
079                 * TODO need Javadoc from author.
080                 */
081                private final class SearchOptionByIndexVisitor implements IVisitor<Component, Component>
082                {
083                        int count = 0;
084
085                        private final int index;
086
087                        private SearchOptionByIndexVisitor(int index)
088                        {
089                                super();
090                                this.index = index;
091                        }
092
093                        /**
094                         * @see org.apache.wicket.util.visit.IVisitor#component(Object,
095                         *      org.apache.wicket.util.visit.IVisit)
096                         */
097                        @Override
098                        public void component(final Component component, final IVisit<Component> visit)
099                        {
100                                if (count == index)
101                                {
102                                        visit.stop(component);
103                                }
104                                else
105                                {
106                                        count++;
107                                }
108                        }
109                }
110
111                private final FormComponent<?> formComponent;
112
113                /**
114                 * Constructor.
115                 *
116                 * @param formComponent
117                 *            a <code>FormComponent</code>
118                 */
119                protected ChoiceSelector(FormComponent<?> formComponent)
120                {
121                        this.formComponent = formComponent;
122                }
123
124                /**
125                 * Implements whether toggle or accumulate the selection.
126                 *
127                 * @param formComponent
128                 *            a <code>FormComponent</code>
129                 * @param value
130                 *            a <code>String</code> value
131                 */
132                protected abstract void assignValueToFormComponent(FormComponent<?> formComponent,
133                        String value);
134
135                public String getChoiceValueForIndex(int index)
136                {
137                        if (formComponent instanceof RadioGroup)
138                        {
139                                Radio<?> foundRadio = (Radio<?>)formComponent.visitChildren(Radio.class,
140                                        new SearchOptionByIndexVisitor(index));
141                                if (foundRadio == null)
142                                {
143                                        fail("RadioGroup " + formComponent.getPath() + " does not have index:" + index);
144                                        return null;
145                                }
146                                return foundRadio.getValue();
147                        }
148                        else if (formComponent instanceof CheckGroup)
149                        {
150                                Check<?> foundCheck = (Check<?>)formComponent.visitChildren(Check.class,
151                                        new SearchOptionByIndexVisitor(index));
152                                if (foundCheck == null)
153                                {
154                                        fail("CheckGroup " + formComponent.getPath() + " does not have index:" + index);
155                                        return null;
156                                }
157
158                                return foundCheck.getValue();
159                        }
160                        else
161                        {
162                                String idValue = selectAbstractChoice(formComponent, index);
163                                if (idValue == null)
164                                {
165                                        fail(formComponent.getPath() + " is not a selectable Component.");
166                                        return null;
167                                }
168                                else
169                                {
170                                        return idValue;
171                                }
172                        }
173
174                }
175
176                /**
177                 * Selects a given index in a selectable <code>FormComponent</code>.
178                 *
179                 * @param index
180                 */
181                protected final void doSelect(final int index)
182                {
183                        String value = getChoiceValueForIndex(index);
184                        assignValueToFormComponent(formComponent, value);
185                }
186
187                /**
188                 * Selects a given index in a selectable <code>FormComponent</code>.
189                 *
190                 * @param formComponent
191                 *            a <code>FormComponent</code>
192                 * @param index
193                 *            the index to select
194                 * @return the id value at the selected index
195                 */
196                @SuppressWarnings("unchecked")
197                private String selectAbstractChoice(final FormComponent<?> formComponent, final int index)
198                {
199                        try
200                        {
201                                Method getChoicesMethod = formComponent.getClass().getMethod("getChoices",
202                                        (Class<?>[])null);
203                                getChoicesMethod.setAccessible(true);
204                                List<Object> choices = (List<Object>)getChoicesMethod.invoke(formComponent,
205                                        (Object[])null);
206
207                                Method getChoiceRendererMethod = formComponent.getClass().getMethod(
208                                        "getChoiceRenderer", (Class<?>[])null);
209                                getChoiceRendererMethod.setAccessible(true);
210                                IChoiceRenderer<Object> choiceRenderer = (IChoiceRenderer<Object>)getChoiceRendererMethod.invoke(
211                                        formComponent, (Object[])null);
212
213                                return choiceRenderer.getIdValue(choices.get(index), index);
214                        }
215                        catch (SecurityException e)
216                        {
217                                throw new WicketRuntimeException("unexpect select failure", e);
218                        }
219                        catch (NoSuchMethodException e)
220                        {
221                                // component without getChoices() or getChoiceRenderer() is not
222                                // selectable
223                                return null;
224                        }
225                        catch (IllegalAccessException e)
226                        {
227                                throw new WicketRuntimeException("unexpect select failure", e);
228                        }
229                        catch (InvocationTargetException e)
230                        {
231                                throw new WicketRuntimeException("unexpect select failure", e);
232                        }
233                }
234        }
235
236        /**
237         * A factory that creates an appropriate <code>ChoiceSelector</code> based on type of
238         * <code>FormComponent</code>.
239         */
240        private class ChoiceSelectorFactory
241        {
242                /**
243                 * <code>MultipleChoiceSelector</code> class.
244                 */
245                private final class MultipleChoiceSelector extends ChoiceSelector
246                {
247                        /**
248                         * Constructor.
249                         *
250                         * @param formComponent
251                         *            a <code>FormComponent</code>
252                         */
253                        protected MultipleChoiceSelector(FormComponent<?> formComponent)
254                        {
255                                super(formComponent);
256                                if (!allowMultipleChoice(formComponent))
257                                {
258                                        fail("Component:'" + formComponent.getPath() +
259                                                "' Does not support multiple selection.");
260                                }
261                        }
262
263                        @Override
264                        protected void assignValueToFormComponent(FormComponent<?> formComponent, String value)
265                        {
266                                // multiple selectable should retain selected option
267                                addFormComponentValue(formComponent, value);
268                        }
269                }
270
271                /**
272                 * <code>SingleChoiceSelector</code> class.
273                 */
274                private final class SingleChoiceSelector extends ChoiceSelector
275                {
276                        /**
277                         * Constructor.
278                         *
279                         * @param formComponent
280                         *            a <code>FormComponent</code>
281                         */
282                        protected SingleChoiceSelector(FormComponent<?> formComponent)
283                        {
284                                super(formComponent);
285                        }
286
287                        @Override
288                        protected void assignValueToFormComponent(FormComponent<?> formComponent, String value)
289                        {
290                                // single selectable should overwrite already selected option
291                                setFormComponentValue(formComponent, value);
292                        }
293                }
294
295                /**
296                 * Creates a <code>ChoiceSelector</code>.
297                 *
298                 * @param formComponent
299                 *            a <code>FormComponent</code>
300                 * @return ChoiceSelector a <code>ChoiceSelector</code>
301                 */
302                protected ChoiceSelector create(FormComponent<?> formComponent)
303                {
304                        if (formComponent == null)
305                        {
306                                fail("Trying to select on null component.");
307                                return null;
308                        }
309                        else if (formComponent instanceof RadioGroup ||
310                                formComponent instanceof AbstractSingleSelectChoice)
311                        {
312                                return new SingleChoiceSelector(formComponent);
313                        }
314                        else if (allowMultipleChoice(formComponent))
315                        {
316                                return new MultipleChoiceSelector(formComponent);
317                        }
318                        else
319                        {
320                                fail("Selecting on the component:'" + formComponent.getPath() +
321                                        "' is not supported.");
322                                return null;
323                        }
324                }
325
326                /**
327                 * Creates a <code>MultipleChoiceSelector</code>.
328                 *
329                 * @param formComponent
330                 *            a <code>FormComponent</code>
331                 * @return ChoiceSelector a <code>ChoiceSelector</code>
332                 */
333                protected ChoiceSelector createForMultiple(FormComponent<?> formComponent)
334                {
335                        return new MultipleChoiceSelector(formComponent);
336                }
337
338                /**
339                 * Tests if a given <code>FormComponent</code> allows multiple choice.
340                 *
341                 * @param formComponent
342                 *            a <code>FormComponent</code>
343                 * @return <code>true</code> if the given FormComponent allows multiple choice
344                 */
345                private boolean allowMultipleChoice(FormComponent<?> formComponent)
346                {
347                        return formComponent instanceof CheckGroup ||
348                                formComponent instanceof ListMultipleChoice;
349                }
350        }
351
352        private final ChoiceSelectorFactory choiceSelectorFactory = new ChoiceSelectorFactory();
353
354        /**
355         * An instance of <code>FormTester</code> can only be used once. Create a new instance of each
356         * test.
357         */
358        private boolean closed = false;
359
360        /** path to <code>FormComponent</code> */
361        private final String path;
362
363        /** <code>BaseWicketTester</code> that create <code>FormTester</code> */
364        private final BaseWicketTester tester;
365
366        /** <code>FormComponent</code> to be tested */
367        private final Form<?> workingForm;
368
369        private boolean clearFeedbackMessagesBeforeSubmit = true;
370
371        /**
372         * @see WicketTester#newFormTester(String)
373         *
374         * @param path
375         *            path to <code>FormComponent</code>
376         * @param workingForm
377         *            <code>FormComponent</code> to be tested
378         * @param wicketTester
379         *            <code>WicketTester</code> that creates <code>FormTester</code>
380         * @param fillBlankString
381         *            specifies whether to fill child <code>TextComponent</code>s with blank
382         *            <code>String</code>s
383         */
384        protected FormTester(final String path, final Form<?> workingForm,
385                final BaseWicketTester wicketTester, final boolean fillBlankString)
386        {
387                this.workingForm = Objects.requireNonNull(workingForm, String.format(NO_FORM_FOR_PATH, path));
388                this.tester = wicketTester;
389            this.path = path;
390
391                // fill blank String for Text Component.
392                workingForm.visitFormComponents(new IVisitor<FormComponent<?>, Void>()
393                {
394                        @Override
395                        public void component(final FormComponent<?> formComponent, final IVisit<Void> visit)
396                        {
397                                // do nothing for invisible or disabled component -- the browser would not send any
398                                // parameter for a disabled component
399                                if (!(formComponent.isVisibleInHierarchy() && formComponent.isEnabledInHierarchy()))
400                                {
401                                        return;
402                                }
403
404                                String[] values = getInputValue(formComponent);
405                                if (formComponent instanceof AbstractTextComponent<?>)
406                                {
407                                        if (values.length == 0 && fillBlankString)
408                                        {
409                                                setFormComponentValue(formComponent, "");
410                                        }
411                                }
412                                for (String value : values)
413                                {
414                                        addFormComponentValue(formComponent, value);
415                                }
416                        }
417                });
418                workingForm.detach();
419        }
420
421        /**
422         * Gets request parameter values for the form component that represents its current model value
423         *
424         * @param formComponent
425         * @return array containing parameter values
426         */
427        public static String[] getInputValue(FormComponent<?> formComponent)
428        {
429                // the browser sends parameters for visible and enabled components only
430                if (formComponent.isVisibleInHierarchy() && formComponent.isEnabledInHierarchy())
431                {
432                        if (formComponent instanceof IFormSubmittingComponent)
433                        {
434                                // buttons have to be submitted explicitely
435                        }
436                        else if (formComponent instanceof AbstractTextComponent)
437                        {
438                                return new String[] { getFormComponentValue(formComponent) };
439                        }
440                        else
441                        {
442                                // TODO is it safe to assume that all other components' values can be split?
443                                String value = getFormComponentValue(formComponent);
444                                if (!Strings.isEmpty(value))
445                                {
446                                        return value.split(FormComponent.VALUE_SEPARATOR);
447                                }
448                        }
449                }
450                return new String[] { };
451        }
452
453
454        private static String getFormComponentValue(final FormComponent<?> formComponent)
455        {
456                boolean oldEscape = formComponent.getEscapeModelStrings();
457                formComponent.setEscapeModelStrings(false);
458                String val = formComponent.getValue();
459                formComponent.setEscapeModelStrings(oldEscape);
460                return val;
461        }
462
463        /**
464         * Retrieves the current <code>Form</code> object.
465         *
466         * @return the working <code>Form</code>
467         */
468        public Form<?> getForm()
469        {
470                return workingForm;
471        }
472
473        /**
474         * Gets the value for an <code>AbstractTextComponent</code> with the provided id.
475         *
476         * @param id
477         *            <code>Component</code> id
478         * @return the value of the text component
479         */
480        public String getTextComponentValue(final String id)
481        {
482                Component c = getForm().get(id);
483                if (c instanceof AbstractTextComponent)
484                {
485                        return ((AbstractTextComponent<?>)c).getValue();
486                }
487                return null;
488        }
489
490        /**
491         * Simulates selecting an option of a <code>FormComponent</code>. Supports
492         * <code>RadioGroup</code>, <code>CheckGroup</code>, and <code>AbstractChoice</code> family
493         * currently. The behavior is similar to interacting on the browser: For a single choice, such
494         * as <code>Radio</code> or <code>DropDownList</code>, the selection will toggle each other. For
495         * multiple choice, such as <code>Checkbox</code> or <code>ListMultipleChoice</code>, the
496         * selection will accumulate.
497         *
498         * @param formComponentId
499         *            relative path (from <code>Form</code>) to the selectable
500         *            <code>FormComponent</code>
501         * @param index
502         *            index of the selectable option, starting from 0
503         * @return This
504         */
505        public FormTester select(final String formComponentId, int index)
506        {
507                checkClosed();
508                FormComponent<?> component = (FormComponent<?>)workingForm.get(formComponentId);
509
510                ChoiceSelector choiceSelector = choiceSelectorFactory.create(component);
511                choiceSelector.doSelect(index);
512
513                for (FormComponentUpdatingBehavior updater : component.getBehaviors(FormComponentUpdatingBehavior.class)) {
514                        tester.invokeListener(component, updater);
515                }
516
517                return this;
518        }
519
520        /**
521         * A convenience method to select multiple options for the <code>FormComponent</code>. The
522         * method only support multiple selectable <code>FormComponent</code>s.
523         *
524         * @see #select(String, int)
525         *
526         * @param formComponentId
527         *            relative path (from <code>Form</code>) to the selectable
528         *            <code>FormComponent</code>
529         * @param indexes
530         *            index of the selectable option, starting from 0
531         * @return This
532         */
533        public FormTester selectMultiple(String formComponentId, int[] indexes)
534        {
535                return selectMultiple(formComponentId, indexes, false);
536        }
537
538        /**
539         * A convenience method to select multiple options for the <code>FormComponent</code>. The
540         * method only support multiple selectable <code>FormComponent</code>s.
541         *
542         * @see #select(String, int)
543         *
544         * @param formComponentId
545         *            relative path (from <code>Form</code>) to the selectable
546         *            <code>FormComponent</code>
547         * @param indexes
548         *            index of the selectable option, starting from 0
549         * @param replace
550         *            If true, than all previous selects are first reset, thus existing selects are
551         *            replaced. If false, than the new indexes will be added.
552         * @return This
553         */
554        public FormTester selectMultiple(String formComponentId, int[] indexes, final boolean replace)
555        {
556                checkClosed();
557
558                if (replace)
559                {
560                        // Reset first
561                        setValue(formComponentId, "");
562                }
563
564                ChoiceSelector choiceSelector = choiceSelectorFactory.createForMultiple((FormComponent<?>)workingForm.get(formComponentId));
565
566                for (int index : indexes)
567                {
568                        choiceSelector.doSelect(index);
569                }
570
571                return this;
572        }
573
574        /**
575         * Simulates filling in a field on a <code>Form</code>.
576         *
577         * @param formComponentId
578         *            relative path (from <code>Form</code>) to the selectable
579         *            <code>FormComponent</code> or <code>IFormSubmittingComponent</code>
580         * @param value
581         *            the field value
582         * @return This
583         */
584        public FormTester setValue(final String formComponentId, final String value)
585        {
586                Component component = workingForm.get(formComponentId);
587                assertNotNull(component, "Unable to set value. Couldn't find component with name: " +
588                        formComponentId);
589                return setValue(component, value);
590        }
591
592        /**
593         * Simulates filling in a field on a <code>Form</code>.
594         *
595         * @param formComponent
596         *            relative path (from <code>Form</code>) to the selectable
597         *            <code>FormComponent</code> or <code>IFormSubmittingComponent</code>
598         * @param value
599         *            the field value
600         * @return This
601         */
602        public FormTester setValue(final Component formComponent, final String value)
603        {
604                Args.notNull(formComponent, "formComponent");
605
606                checkClosed();
607
608                if (formComponent instanceof IFormSubmittingComponent)
609                {
610                        setFormSubmittingComponentValue((IFormSubmittingComponent)formComponent, value);
611                }
612                else if (formComponent instanceof FormComponent)
613                {
614                        setFormComponentValue((FormComponent<?>)formComponent, value);
615                }
616                else
617                {
618                        fail("Component with id: " + formComponent.getId() + " is not a FormComponent");
619                }
620
621                return this;
622        }
623
624        /**
625         * @param checkBoxId
626         * @param value
627         * @return This
628         */
629        public FormTester setValue(String checkBoxId, boolean value)
630        {
631                return setValue(checkBoxId, Boolean.toString(value));
632        }
633
634        /**
635         * Sets the <code>File</code> on a {@link FileUploadField}.
636         *
637         * @param formComponentId
638         *            relative path (from <code>Form</code>) to the selectable
639         *            <code>FormComponent</code>. The <code>FormComponent</code> must be of a type
640         *            <code>FileUploadField</code>.
641         * @param file
642         *            the <code>File</code> to upload or {@code null} for an empty input
643         * @param contentType
644         *            the content type of the file. Must be a valid mime type.
645         * @return This
646         */
647        public FormTester setFile(final String formComponentId, final File file,
648                final String contentType)
649        {
650                checkClosed();
651
652                FormComponent<?> formComponent = (FormComponent<?>)workingForm.get(formComponentId);
653
654                MockHttpServletRequest servletRequest = tester.getRequest();
655
656                if (formComponent instanceof FileUploadField)
657                {
658                        servletRequest.addFile(formComponent.getInputName(), file, contentType);
659                }
660                else if (formComponent instanceof MultiFileUploadField)
661                {
662                        String inputName = formComponent.getInputName() + MultiFileUploadField.MAGIC_SEPARATOR + multiFileUploadIndex++;
663                        servletRequest.addFile(inputName, file, contentType);
664                }
665                else
666                {
667                        fail("'" + formComponentId + "' is not " +
668                                "a FileUploadField. You can only attach a file to form " +
669                                "component of this type.");
670                }
671
672                return this;
673        }
674
675        /**
676         * Submits the <code>Form</code>. Note that <code>submit</code> can be executed only once.
677         *
678         * @return This
679         */
680        public FormTester submit()
681        {
682                checkClosed();
683                try
684                {
685                        if (clearFeedbackMessagesBeforeSubmit)
686                        {
687                                tester.clearFeedbackMessages();
688                        }
689                        tester.getRequest().setUseMultiPartContentType(workingForm.isMultiPart());
690                        tester.submitForm(path);
691                }
692                finally
693                {
694                        closed = true;
695                }
696
697                return this;
698        }
699
700        public boolean isClearFeedbackMessagesBeforeSubmit()
701        {
702                return clearFeedbackMessagesBeforeSubmit;
703        }
704
705        public FormTester setClearFeedbackMessagesBeforeSubmit(boolean clearFeedbackMessagesBeforeSubmit)
706        {
707                this.clearFeedbackMessagesBeforeSubmit = clearFeedbackMessagesBeforeSubmit;
708                return this;
709        }
710
711        /**
712         * A convenience method for submitting the <code>Form</code> with an alternate button.
713         * <p>
714         * Note that if the button is associated with a model, it's better to use the
715         * <code>setValue</code> method instead:
716         *
717         * <pre>
718         * formTester.setValue(&quot;to:my:button&quot;, &quot;value on the button&quot;);
719         * formTester.submit();
720         * </pre>
721         *
722         * @param buttonComponentId
723         *            relative path (from <code>Form</code>) to the button
724         * @return This
725         */
726        public FormTester submit(final String buttonComponentId)
727        {
728                Component submitter = getForm().get(buttonComponentId);
729                if (submitter == null)
730                {
731                        fail("Cannot submit the form because there is no submitting component with id: " + buttonComponentId);
732                }
733
734                return submit(submitter);
735        }
736
737        /**
738         * A convenience method for submitting the <code>Form</code> with an alternate button.
739         * <p>
740         * Note that if the button is associated with a model, it's better to use the
741         * <code>setValue</code> method instead:
742         *
743         * <pre>
744         * formTester.setValue(myButton, &quot;value on the button&quot;);
745         * formTester.submit();
746         * </pre>
747         *
748         * @param buttonComponent
749         *            relative path (from <code>Form</code>) to the button
750         * @return This
751         */
752        public FormTester submit(final Component buttonComponent)
753        {
754                Args.notNull(buttonComponent, "buttonComponent");
755
756                setValue(buttonComponent, "marked");
757
758                if (buttonComponent instanceof AjaxButton || buttonComponent instanceof AjaxSubmitLink)
759                {
760                        if (clearFeedbackMessagesBeforeSubmit)
761                        {
762                                tester.clearFeedbackMessages();
763                        }
764                        tester.getRequest().setUseMultiPartContentType(workingForm.isMultiPart());
765                        tester.executeAjaxEvent(buttonComponent, "click");
766                        return this;
767                }
768                else
769                {
770                        return submit();
771                }
772        }
773
774        /**
775         * A convenience method to submit the Form via a SubmitLink which may inside or outside of the
776         * Form.
777         *
778         * @param path
779         *            The path to the SubmitLink
780         * @param pageRelative
781         *            if true, than the 'path' to the SubmitLink is relative to the page. Thus the link
782         *            can be outside the form. If false, the path is relative to the form and thus the
783         *            link is inside the form.
784         * @return This
785         */
786        public FormTester submitLink(String path, final boolean pageRelative)
787        {
788                if (pageRelative)
789                {
790                        tester.clickLink(path, false);
791                }
792                else
793                {
794                        path = this.path + ":" + path;
795                        tester.clickLink(path, false);
796                }
797                return this;
798        }
799
800        /**
801         * Adds an additional <code>FormComponent</code>'s value into request parameter -- this method
802         * retains existing parameters but removes any duplicate parameters.
803         *
804         * @param formComponent
805         *            a <code>FormComponent</code>
806         * @param value
807         *            a value to add
808         * @return This
809         */
810        private FormTester addFormComponentValue(FormComponent<?> formComponent, String value)
811        {
812                if (parameterExist(formComponent))
813                {
814                        List<StringValue> values = tester.getRequest()
815                                .getPostParameters()
816                                .getParameterValues(formComponent.getInputName());
817                        // remove duplicated
818
819                        HashSet<String> all = new HashSet<String>();
820                        for (StringValue val : values)
821                        {
822                                all.add(val.toString());
823                        }
824                        all.add(value);
825
826                        values = new ArrayList<StringValue>();
827                        for (String val : all)
828                        {
829                                values.add(StringValue.valueOf(val));
830                        }
831                        tester.getRequest()
832                                .getPostParameters()
833                                .setParameterValues(formComponent.getInputName(), values);
834                }
835                else
836                {
837                        setFormComponentValue(formComponent, value);
838                }
839
840                return this;
841        }
842
843        /**
844         * <code>FormTester</code> must only be used once. Create a new instance of
845         * <code>FormTester</code> for each test.
846         */
847        private void checkClosed()
848        {
849                if (closed)
850                {
851                        fail("'" + path + "' already submitted. Note that FormTester " +
852                                "is allowed to submit only once");
853                }
854        }
855
856        /**
857         * Returns <code>true</code> if the parameter exists in the <code>FormComponent</code>.
858         *
859         * @param formComponent
860         *            a <code>FormComponent</code>
861         * @return <code>true</code> if the parameter exists in the <code>FormComponent</code>
862         */
863        private boolean parameterExist(final FormComponent<?> formComponent)
864        {
865                String parameter = tester.getRequest()
866                        .getPostParameters()
867                        .getParameterValue(formComponent.getInputName())
868                        .toString();
869
870                return parameter != null && parameter.trim().length() > 0;
871        }
872
873        /**
874         * Set formComponent's value into request parameter, this method overwrites existing parameters.
875         *
876         * @param formComponent
877         *            a <code>FormComponent</code>
878         * @param value
879         *            a value to add
880         */
881        private void setFormComponentValue(final FormComponent<?> formComponent, final String value)
882        {
883                tester.getRequest()
884                        .getPostParameters()
885                        .setParameterValue(formComponent.getInputName(), value);
886        }
887
888        /**
889         * Set component's value into request parameter, this method overwrites existing parameters.
890         *
891         * @param component
892         *            an {@link IFormSubmittingComponent}
893         * @param value
894         *            a value to add
895         */
896        private void setFormSubmittingComponentValue(IFormSubmittingComponent component, String value)
897        {
898                tester.getRequest().getPostParameters().setParameterValue(component.getInputName(), value);
899        }
900
901        /**
902         *
903         * @param message
904         */
905        private void fail(String message)
906        {
907                throw new WicketRuntimeException(message);
908        }
909}