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;
020import static org.junit.jupiter.api.Assertions.fail;
021
022import java.io.IOException;
023import java.io.Serializable;
024import java.lang.reflect.Constructor;
025import java.lang.reflect.Method;
026import java.nio.charset.Charset;
027import java.text.ParseException;
028import java.time.Duration;
029import java.util.ArrayList;
030import java.util.Enumeration;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Locale;
034import java.util.Map;
035import java.util.Set;
036import java.util.UUID;
037import java.util.regex.Pattern;
038
039import javax.servlet.FilterConfig;
040import javax.servlet.ServletContext;
041import javax.servlet.http.Cookie;
042import javax.servlet.http.HttpSession;
043
044import org.apache.wicket.Application;
045import org.apache.wicket.Component;
046import org.apache.wicket.IPageManagerProvider;
047import org.apache.wicket.IPageRendererProvider;
048import org.apache.wicket.IRequestCycleProvider;
049import org.apache.wicket.IRequestListener;
050import org.apache.wicket.MarkupContainer;
051import org.apache.wicket.Page;
052import org.apache.wicket.Session;
053import org.apache.wicket.ThreadContext;
054import org.apache.wicket.WicketRuntimeException;
055import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
056import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
057import org.apache.wicket.ajax.AjaxEventBehavior;
058import org.apache.wicket.ajax.form.AjaxFormSubmitBehavior;
059import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
060import org.apache.wicket.ajax.markup.html.AjaxLink;
061import org.apache.wicket.ajax.markup.html.IAjaxLink;
062import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
063import org.apache.wicket.behavior.AbstractAjaxBehavior;
064import org.apache.wicket.behavior.Behavior;
065import org.apache.wicket.core.request.handler.BookmarkableListenerRequestHandler;
066import org.apache.wicket.core.request.handler.BookmarkablePageRequestHandler;
067import org.apache.wicket.core.request.handler.IPageProvider;
068import org.apache.wicket.core.request.handler.ListenerRequestHandler;
069import org.apache.wicket.core.request.handler.PageAndComponentProvider;
070import org.apache.wicket.core.request.handler.PageProvider;
071import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
072import org.apache.wicket.feedback.ExactLevelFeedbackMessageFilter;
073import org.apache.wicket.feedback.FeedbackCollector;
074import org.apache.wicket.feedback.FeedbackMessage;
075import org.apache.wicket.feedback.IFeedbackMessageFilter;
076import org.apache.wicket.markup.ContainerInfo;
077import org.apache.wicket.markup.IMarkupFragment;
078import org.apache.wicket.markup.Markup;
079import org.apache.wicket.markup.MarkupParser;
080import org.apache.wicket.markup.MarkupResourceStream;
081import org.apache.wicket.markup.html.WebPage;
082import org.apache.wicket.markup.html.basic.Label;
083import org.apache.wicket.markup.html.form.Form;
084import org.apache.wicket.markup.html.form.FormComponent;
085import org.apache.wicket.markup.html.form.SubmitLink;
086import org.apache.wicket.markup.html.internal.Enclosure;
087import org.apache.wicket.markup.html.link.AbstractLink;
088import org.apache.wicket.markup.html.link.BookmarkablePageLink;
089import org.apache.wicket.markup.html.link.ExternalLink;
090import org.apache.wicket.markup.html.link.Link;
091import org.apache.wicket.markup.html.link.ResourceLink;
092import org.apache.wicket.markup.parser.XmlPullParser;
093import org.apache.wicket.markup.parser.XmlTag;
094import org.apache.wicket.mock.MockApplication;
095import org.apache.wicket.mock.MockPageManager;
096import org.apache.wicket.mock.MockRequestParameters;
097import org.apache.wicket.model.PropertyModel;
098import org.apache.wicket.page.IPageManager;
099import org.apache.wicket.protocol.http.AjaxEnclosureListener;
100import org.apache.wicket.protocol.http.IMetaDataBufferingWebResponse;
101import org.apache.wicket.protocol.http.WebApplication;
102import org.apache.wicket.protocol.http.WicketFilter;
103import org.apache.wicket.protocol.http.mock.CookieCollection;
104import org.apache.wicket.protocol.http.mock.MockHttpServletRequest;
105import org.apache.wicket.protocol.http.mock.MockHttpServletResponse;
106import org.apache.wicket.protocol.http.mock.MockHttpSession;
107import org.apache.wicket.protocol.http.mock.MockServletContext;
108import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
109import org.apache.wicket.protocol.http.servlet.ServletWebResponse;
110import org.apache.wicket.request.IExceptionMapper;
111import org.apache.wicket.request.IRequestHandler;
112import org.apache.wicket.request.IRequestMapper;
113import org.apache.wicket.request.Request;
114import org.apache.wicket.request.Response;
115import org.apache.wicket.request.Url;
116import org.apache.wicket.request.cycle.RequestCycle;
117import org.apache.wicket.request.cycle.RequestCycleContext;
118import org.apache.wicket.request.handler.render.PageRenderer;
119import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
120import org.apache.wicket.request.http.WebRequest;
121import org.apache.wicket.request.http.WebResponse;
122import org.apache.wicket.request.mapper.IRequestMapperDelegate;
123import org.apache.wicket.request.mapper.parameter.PageParameters;
124import org.apache.wicket.request.resource.IResource;
125import org.apache.wicket.request.resource.ResourceReference;
126import org.apache.wicket.settings.RequestCycleSettings.RenderStrategy;
127import org.apache.wicket.util.lang.Args;
128import org.apache.wicket.util.lang.Classes;
129import org.apache.wicket.util.lang.Generics;
130import org.apache.wicket.util.resource.StringResourceStream;
131import org.apache.wicket.util.string.Strings;
132import org.apache.wicket.util.visit.IVisit;
133import org.apache.wicket.util.visit.IVisitor;
134import org.slf4j.Logger;
135import org.slf4j.LoggerFactory;
136
137/**
138 * A helper class to ease unit testing of Wicket applications without the need for a servlet
139 * container. See javadoc of <code>WicketTester</code> for example usage. This class can be used as
140 * is, but JUnit users should use derived class <code>WicketTester</code>.
141 *
142 * @see WicketTester
143 *
144 * @author Ingram Chen
145 * @author Juergen Donnerstag
146 * @author Frank Bille
147 * @author Igor Vaynberg
148 *
149 * @since 1.2.6
150 */
151@SuppressWarnings("serial")
152public class BaseWicketTester
153{
154        /** log. */
155        private static final Logger log = LoggerFactory.getLogger(BaseWicketTester.class);
156
157        private final ServletContext servletContext;
158        private final WebApplication application;
159        private final List<MockHttpServletRequest> previousRequests = Generics.newArrayList();
160        private final List<MockHttpServletResponse> previousResponses = Generics.newArrayList();
161        private MockHttpSession httpSession;
162        private boolean followRedirects = true;
163        private int redirectCount;
164        private MockHttpServletRequest lastRequest;
165        private MockHttpServletResponse lastResponse;
166        /** current request and response */
167        private MockHttpServletRequest request;
168        private MockHttpServletResponse response;
169
170        /** current session */
171        private Session session;
172
173        /** current request cycle */
174        private RequestCycle requestCycle;
175
176        private Page lastRenderedPage;
177
178        private boolean exposeExceptions = true;
179
180        private boolean useRequestUrlAsBase = true;
181
182        private IRequestHandler forcedHandler;
183
184        private IFeedbackMessageFilter originalFeedbackMessageCleanupFilter;
185
186        private ComponentInPage componentInPage;
187
188        // User may provide request header value any time. They get applied (and reset) upon next
189        // invocation of processRequest()
190        private Map<String, String> preHeader;
191
192        /**
193         * Creates <code>WicketTester</code> and automatically create a <code>WebApplication</code>, but
194         * the tester will have no home page.
195         */
196        public BaseWicketTester()
197        {
198                this(new MockApplication());
199        }
200
201        /**
202         * Creates <code>WicketTester</code> and automatically creates a <code>WebApplication</code>.
203         *
204         * @param <C>
205         * @param homePage
206         *            a home page <code>Class</code>
207         */
208        public <C extends Page> BaseWicketTester(final Class<C> homePage)
209        {
210                this(new MockApplication()
211                {
212                        @Override
213                        public Class<? extends Page> getHomePage()
214                        {
215                                return homePage;
216                        }
217                });
218        }
219
220        /**
221         * Creates a <code>WicketTester</code>.
222         *
223         * @param application
224         *            a <code>WicketTester</code> <code>WebApplication</code> object
225         */
226        public BaseWicketTester(final WebApplication application)
227        {
228                this(application, (MockServletContext)null);
229        }
230
231        /**
232         * Creates a <code>WicketTester</code>.
233         *
234         * @param application
235         *            a <code>WicketTester</code> <code>WebApplication</code> object
236         * @param servletContextBasePath
237         *            the absolute path on disk to the web application's contents (e.g. war root) - may
238         *            be <code>null</code>
239         */
240        public BaseWicketTester(final WebApplication application, String servletContextBasePath)
241        {
242                this(application, new MockServletContext(application, servletContextBasePath));
243        }
244
245        /**
246         * Creates a <code>WicketTester</code>.
247         *
248         * @param application
249         *            a <code>WicketTester</code> <code>WebApplication</code> object
250         * @param servletCtx
251         *            the servlet context used as backend
252         */
253        public BaseWicketTester(final WebApplication application, final ServletContext servletCtx)
254        {
255                this(application, servletCtx, true);
256        }
257
258        /**
259         * Creates a <code>WicketTester</code>.
260         *
261         * @param application
262         *            a <code>WicketTester</code> <code>WebApplication</code> object
263         * @param init
264         *            force the application to be initialized (default = true)
265         */
266        public BaseWicketTester(final WebApplication application, boolean init)
267        {
268                this(application, null, init);
269        }
270
271        /**
272         * Creates a <code>WicketTester</code>.
273         *
274         * @param application
275         *            a <code>WicketTester</code> <code>WebApplication</code> object
276         * @param servletCtx
277         *            the servlet context used as backend
278         * @param init
279         *            force the application to be initialized (default = true)
280         */
281        public BaseWicketTester(final WebApplication application, final ServletContext servletCtx,
282                boolean init)
283        {
284                if (!init)
285                {
286                        Args.notNull(application, "application");
287                }
288
289                servletContext = servletCtx != null ? servletCtx
290                        // If it's provided from the container it's not necessary to mock.
291                        : !init && application.getServletContext() != null ? application.getServletContext()
292                                : new MockServletContext(application, null);
293
294                // If using Arquillian and it's configured in a web.xml it'll be provided. If not, mock it.
295                if (application.getWicketFilter() == null)
296                {
297                        final FilterConfig filterConfig = new TestFilterConfig();
298                        WicketFilter filter = new WicketFilter()
299                        {
300                                @Override
301                                public FilterConfig getFilterConfig()
302                                {
303                                        return filterConfig;
304                                }
305                        };
306                        application.setWicketFilter(filter);
307                }
308
309                httpSession = new MockHttpSession(servletContext);
310
311                ThreadContext.detach();
312
313                this.application = application;
314
315                ThreadContext.setApplication(application);
316
317                if (init)
318                {
319                        if (application.getName() == null)
320                        {
321                                application.setName("WicketTesterApplication-" + UUID.randomUUID());
322                        }
323
324                        application.setServletContext(servletContext);
325                        // initialize the application
326                        application.initApplication();
327                }
328
329                // We don't expect any changes during testing. In addition we avoid creating
330                // ModificationWatcher threads tests.
331                application.getResourceSettings().setResourcePollFrequency(getResourcePollFrequency());
332
333                // reconfigure application for the test environment
334                application.setPageRendererProvider(
335                        new LastPageRecordingPageRendererProvider(application.getPageRendererProvider()));
336                application.setRequestCycleProvider(
337                        new TestRequestCycleProvider(application.getRequestCycleProvider()));
338
339                // set a feedback message filter that will not remove any messages
340                originalFeedbackMessageCleanupFilter = application.getApplicationSettings()
341                        .getFeedbackMessageCleanupFilter();
342                application.getApplicationSettings()
343                        .setFeedbackMessageCleanupFilter(IFeedbackMessageFilter.NONE);
344                IPageManagerProvider pageManagerProvider = newTestPageManagerProvider();
345                if (pageManagerProvider != null)
346                {
347                        application.setPageManagerProvider(pageManagerProvider);
348                }
349
350                // create a new session when the old one is invalidated
351                application.getSessionStore().registerUnboundListener(sessionId -> newSession());
352
353                // prepare session
354                setupNextRequestCycle();
355        }
356
357        /**
358         * By default Modification Watcher is disabled by default for the tests.
359         *
360         * @return the duration between two checks for changes in the resources
361         */
362        protected Duration getResourcePollFrequency()
363        {
364                return null;
365        }
366
367        /**
368         *
369         * @return page manager provider
370         */
371        protected IPageManagerProvider newTestPageManagerProvider()
372        {
373                return new TestPageManagerProvider();
374        }
375
376        /**
377         * @return last rendered page
378         */
379        public Page getLastRenderedPage()
380        {
381                return lastRenderedPage;
382        }
383
384        private void setupNextRequestCycle()
385        {
386                request = new MockHttpServletRequest(application, httpSession, servletContext,
387                        servletRequestLocale());
388                request.setURL(request.getContextPath() + request.getServletPath() + "/");
389
390                // assign protocol://host:port to next request unless the last request was ajax
391                final boolean assignBaseLocation = lastRequest != null &&
392                        lastRequest.getHeader("Wicket-Ajax") == null;
393
394                // resume request processing with scheme://host:port from last request
395                if (assignBaseLocation)
396                {
397                        request.setScheme(lastRequest.getScheme());
398                        request.setSecure(lastRequest.isSecure());
399                        request.setServerName(lastRequest.getServerName());
400                        request.setServerPort(lastRequest.getServerPort());
401                }
402
403                response = new MockHttpServletResponse(request);
404
405                // Preserve response cookies in redirects
406                // XXX: is this really needed ? Browsers wont do that, but some
407                // Wicket tests assert that a cookie is in the response,
408                // even after redirects (see
409                // org.apache.wicket.util.cookies.SetCookieAndRedirectTest.statefulPage())
410                // They should assert that the cookie is in the next *request*
411                if (lastResponse != null)
412                {
413                        List<Cookie> lastResponseCookies = lastResponse.getCookies();
414                        if (lastResponse.isRedirect())
415                        {
416                                CookieCollection responseCookies = new CookieCollection();
417
418                                // if the last request is a redirect, all cookies from last response should appear
419                                // in current response
420                                // this call will filter duplicates
421                                responseCookies.addAll(lastResponseCookies);
422                                for (Cookie cookie : responseCookies.allAsList())
423                                {
424                                        response.addCookie(cookie);
425                                }
426
427                                // copy all request cookies from last request to the new request because of redirect
428                                // handling this way, the cookie will be send to the next requested page
429                                if (lastRequest != null)
430                                {
431                                        CookieCollection requestCookies = new CookieCollection();
432                                        // this call will filter duplicates
433                                        requestCookies.addAll(lastRequest.getCookies());
434                                        request.addCookies(requestCookies.asList());
435                                }
436                        }
437                        else
438                        {
439                                // if the last response is not a redirect
440                                // - copy last request cookies to collection
441                                // - copy last response cookies to collection
442                                // - set only the not expired cookies to the next request
443                                CookieCollection cookies = new CookieCollection();
444                                if (lastRequest != null)
445                                {
446                                        // this call will filter duplicates
447                                        cookies.addAll(lastRequest.getCookies());
448                                }
449                                // this call will filter duplicates
450                                cookies.addAll(lastResponseCookies);
451                                request.addCookies(cookies.asList());
452                        }
453                }
454
455                ServletWebRequest servletWebRequest = newServletWebRequest();
456                requestCycle = application.createRequestCycle(servletWebRequest,
457                        newServletWebResponse(servletWebRequest));
458                ThreadContext.setRequestCycle(requestCycle);
459
460                if (session == null)
461                {
462                        newSession();
463                }
464        }
465
466        protected Locale servletRequestLocale()
467        {
468                return Locale.getDefault();
469        }
470
471        /**
472         * Cleans up feedback messages. This usually happens on detach, but is disabled in unit testing
473         * so feedback messages can be examined.
474         */
475        public void cleanupFeedbackMessages()
476        {
477                cleanupFeedbackMessages(originalFeedbackMessageCleanupFilter);
478        }
479
480        /**
481         * Removes all feedback messages
482         */
483        public void clearFeedbackMessages()
484        {
485                cleanupFeedbackMessages(IFeedbackMessageFilter.ALL);
486        }
487
488        /**
489         * Cleans up feedback messages given the specified filter.
490         *
491         * @param filter
492         *            filter used to cleanup messages, accepted messages will be removed
493         */
494        protected void cleanupFeedbackMessages(IFeedbackMessageFilter filter)
495        {
496                
497                IVisitor<Component, Void> clearer = new IVisitor<Component, Void>()
498                {
499                        @Override
500                        public void component(Component component, IVisit<Void> visit)
501                        {
502                                if (component.hasFeedbackMessage()) {
503                                        component.getFeedbackMessages().clear(filter);
504                                }
505                        }
506                };
507                clearer.component(getLastRenderedPage(), null);
508                getLastRenderedPage().visitChildren(clearer);
509                
510                getSession().getFeedbackMessages().clear(filter);
511        }
512
513        /**
514         * @param servletWebRequest
515         * @return servlet web response
516         */
517        protected Response newServletWebResponse(final ServletWebRequest servletWebRequest)
518        {
519                return new WicketTesterServletWebResponse(servletWebRequest, response);
520        }
521
522        /**
523         * @return the configured in the user's application web request
524         */
525        private ServletWebRequest newServletWebRequest()
526        {
527                return (ServletWebRequest)application.newWebRequest(request, request.getFilterPrefix());
528        }
529
530        /**
531         *
532         */
533        private void newSession()
534        {
535                ThreadContext.setSession(null);
536
537                // the following will create a new session and put it in the thread context
538                session = Session.get();
539        }
540
541        /**
542         * @return request object
543         */
544        public MockHttpServletRequest getRequest()
545        {
546                return request;
547        }
548
549        /**
550         * @param request
551         */
552        public void setRequest(final MockHttpServletRequest request)
553        {
554                this.request = request;
555                applyRequest();
556        }
557
558        /**
559         * @param response
560         */
561        public void setLastResponse(final MockHttpServletResponse response)
562        {
563                this.lastResponse = response;
564        }
565
566        /**
567         * @return session
568         */
569        public Session getSession()
570        {
571                return session;
572        }
573
574        /**
575         * Returns {@link HttpSession} for this environment
576         *
577         * @return session
578         */
579        public MockHttpSession getHttpSession()
580        {
581                return httpSession;
582        }
583
584        /**
585         * Returns the {@link Application} for this environment.
586         *
587         * @return application
588         */
589        public WebApplication getApplication()
590        {
591                return application;
592        }
593
594        /**
595         * Returns the {@link ServletContext} for this environment
596         *
597         * @return servlet context
598         */
599        public ServletContext getServletContext()
600        {
601                return servletContext;
602        }
603
604        /**
605         * Destroys the tester. Restores {@link ThreadContext} to state before instance of
606         * {@link WicketTester} was created.
607         */
608        public void destroy()
609        {
610                try
611                {
612                        ThreadContext.setApplication(application);
613                        application.internalDestroy();
614                }
615                finally
616                {
617                        ThreadContext.detach();
618                }
619        }
620
621        /**
622         * @return true, if process was executed successfully
623         */
624        public boolean processRequest()
625        {
626                return processRequest(null, null);
627        }
628
629        /**
630         * Processes the request in mocked Wicket environment.
631         *
632         * @param request
633         *            request to process
634         * @return true, if process was executed successfully
635         */
636        public boolean processRequest(final MockHttpServletRequest request)
637        {
638                return processRequest(request, null);
639        }
640
641        /**
642         * Processes the request in mocked Wicket environment.
643         *
644         * @param request
645         *            request to process
646         * @param forcedRequestHandler
647         *            optional parameter to override parsing the request URL and force
648         *            {@link IRequestHandler}
649         * @return true, if process was executed successfully
650         */
651        public boolean processRequest(final MockHttpServletRequest request,
652                final IRequestHandler forcedRequestHandler)
653        {
654                return processRequest(request, forcedRequestHandler, false);
655        }
656
657        /**
658         * @param forcedRequestHandler
659         * @return true, if process was executed successfully
660         */
661        public boolean processRequest(final IRequestHandler forcedRequestHandler)
662        {
663                return processRequest(null, forcedRequestHandler, false);
664        }
665
666        /**
667         * Process the request. This is a fairly central function and is almost always invoked for
668         * executing the request.
669         * <p>
670         * You may subclass processRequest it, to monitor or change any pre-configured value. Request
671         * headers can be configured more easily by calling {@link #addRequestHeader(String, String)}.
672         *
673         * @param forcedRequest
674         *            Can be null.
675         * @param forcedRequestHandler
676         *            Can be null.
677         * @param redirect
678         * @return true, if process was executed successfully
679         */
680        protected boolean processRequest(final MockHttpServletRequest forcedRequest,
681                final IRequestHandler forcedRequestHandler, final boolean redirect)
682        {
683                if (forcedRequest != null)
684                {
685                        request = forcedRequest;
686                }
687
688                forcedHandler = forcedRequestHandler;
689
690                if (!redirect && getRequest().getHeader("Wicket-Ajax") == null)
691                {
692                        lastRenderedPage = null;
693                }
694
695                // Add or replace any system provided header entry with the user provided.
696                if ((request != null) && (preHeader != null))
697                {
698                        for (Map.Entry<String, String> entry : preHeader.entrySet())
699                        {
700                                if (Strings.isEmpty(entry.getKey()) == false)
701                                {
702                                        request.setHeader(entry.getKey(), entry.getValue());
703                                }
704                        }
705
706                        // Reset the user provided headers
707                        preHeader = null;
708                }
709
710                applyRequest();
711                requestCycle.scheduleRequestHandlerAfterCurrent(null);
712
713                try
714                {
715                        if (!requestCycle.processRequestAndDetach())
716                        {
717                                return false;
718                        }
719                }
720                finally
721                {
722                        recordRequestResponse();
723                        setupNextRequestCycle();
724                }
725
726                try
727                {
728                        if (isFollowRedirects() && lastResponse.isRedirect())
729                        {
730                                if (redirectCount++ >= 100)
731                                {
732                                        throw new AssertionError("Possible infinite redirect detected. Bailing out.");
733                                }
734
735                                Url newUrl = Url.parse(lastResponse.getRedirectLocation(),
736                                        Charset.forName(request.getCharacterEncoding()));
737
738                                if (isExternalRedirect(lastRequest.getUrl(), newUrl))
739                                {
740                                        // we can't handle external redirects here
741                                        // just bail out here and let the user's test code
742                                        // check #assertRedirectUrl
743                                        return true;
744                                }
745
746                                if (newUrl.isFull() || newUrl.isContextAbsolute())
747                                {
748                                        request.setUrl(newUrl);
749
750                                        final String protocol = newUrl.getProtocol();
751
752                                        if (protocol != null)
753                                        {
754                                                request.setScheme(protocol);
755                                        }
756
757                                        request.setSecure("https".equals(protocol));
758
759                                        if (newUrl.getHost() != null)
760                                        {
761                                                request.setServerName(newUrl.getHost());
762                                        }
763                                        if (newUrl.getPort() != null)
764                                        {
765                                                request.setServerPort(newUrl.getPort());
766                                        }
767                                }
768                                else
769                                {
770                                        // append redirect URL to current URL (what browser would do)
771                                        Url mergedURL = new Url(lastRequest.getUrl().getSegments(),
772                                                newUrl.getQueryParameters());
773                                        mergedURL.concatSegments(newUrl.getSegments());
774
775                                        request.setUrl(mergedURL);
776                                }
777
778                                processRequest(null, null, true);
779
780                                --redirectCount;
781                        }
782
783                        return true;
784                }
785                finally
786                {
787                        redirectCount = 0;
788                }
789        }
790
791        /**
792         * Determine whether a given response contains a redirect leading to an external site (which
793         * cannot be replicated in WicketTester). This is done by comparing the previous request's
794         * hostname with the hostname given in the redirect.
795         *
796         * @param requestUrl
797         *            request...
798         * @param newUrl
799         *            ...and the redirect generated in its response
800         * @return true if there is a redirect and it is external, false otherwise
801         */
802        private boolean isExternalRedirect(Url requestUrl, Url newUrl)
803        {
804                String originalHost = requestUrl.getHost();
805                String redirectHost = newUrl.getHost();
806                Integer originalPort = requestUrl.getPort();
807                Integer newPort = newUrl.getPort();
808
809                if (originalHost.equals(redirectHost))
810                {
811                        return false; // identical or both null
812                }
813                else if (redirectHost == null)
814                {
815                        return false; // no new host
816                }
817                else if (originalPort.equals(newPort) == false)
818                {
819                        return true;
820                }
821                else
822                {
823                        return !(redirectHost.equals(originalHost));
824                }
825        }
826
827        /**
828         * Allows to set Request header value any time. They'll be applied (add/modify) on process
829         * execution {@link #processRequest(MockHttpServletRequest, IRequestHandler, boolean)}. They are
830         * reset immediately after and thus are not re-used for a sequence of requests.
831         * <p>
832         * Deletion (not replace) of pre-configured header value can be achieved by subclassing
833         * {@link #processRequest(MockHttpServletRequest, IRequestHandler, boolean)} and modifying the
834         * request header directly.
835         *
836         * @param key
837         * @param value
838         */
839        public final void addRequestHeader(final String key, final String value)
840        {
841                Args.notEmpty(key, "key");
842
843                if (preHeader == null)
844                {
845                        preHeader = Generics.newHashMap();
846                }
847
848                preHeader.put(key, value);
849        }
850
851        private void recordRequestResponse()
852        {
853                lastRequest = request;
854                setLastResponse(response);
855
856                previousRequests.add(request);
857                previousResponses.add(response);
858        }
859
860        /**
861         * Renders the page specified by given {@link IPageProvider}. After render the page instance can
862         * be retrieved using {@link #getLastRenderedPage()} and the rendered document will be available
863         * in {@link #getLastResponse()}.
864         *
865         * Depending on {@link RenderStrategy} invoking this method can mean that a redirect will happen
866         * before the actual render.
867         *
868         * @param pageProvider
869         * @return last rendered page
870         */
871        public Page startPage(final IPageProvider pageProvider)
872        {
873                // should be null for Pages
874                componentInPage = null;
875
876                // prepare request
877                request.setURL(request.getContextPath() + request.getServletPath() + "/");
878                IRequestHandler handler = new RenderPageRequestHandler(pageProvider);
879
880                // process request
881                processRequest(request, handler);
882
883                // The page rendered
884                return getLastRenderedPage();
885        }
886
887        /**
888         * Renders the page.
889         *
890         * @see #startPage(IPageProvider)
891         *
892         * @param page
893         * @return Page
894         */
895        @SuppressWarnings("unchecked")
896        public <T extends Page> T startPage(final T page)
897        {
898                return (T)startPage(new PageProvider(page));
899        }
900
901        /**
902         * Simulates a request to a mounted {@link IResource}
903         *
904         * @param resource
905         *            the resource to test
906         * @return the used {@link ResourceReference} for the simulation
907         */
908        public ResourceReference startResource(final IResource resource)
909        {
910                return startResourceReference(new ResourceReference("testResourceReference")
911                {
912                        private static final long serialVersionUID = 1L;
913
914                        @Override
915                        public IResource getResource()
916                        {
917                                return resource;
918                        }
919                });
920        }
921
922        /**
923         * Simulates a request to a mounted {@link ResourceReference}
924         *
925         * @param reference
926         *            the resource reference to test
927         * @return the tested resource reference
928         */
929        public ResourceReference startResourceReference(final ResourceReference reference)
930        {
931                return startResourceReference(reference, null);
932        }
933
934        /**
935         * Simulates a request to a mounted {@link ResourceReference}
936         *
937         * @param reference
938         *            the resource reference to test
939         * @param pageParameters
940         *            the parameters passed to the resource reference
941         * @return the tested resource reference
942         */
943        public ResourceReference startResourceReference(final ResourceReference reference,
944                final PageParameters pageParameters)
945        {
946                // prepare request
947                request.setURL(request.getContextPath() + request.getServletPath() + "/");
948                IRequestHandler handler = new ResourceReferenceRequestHandler(reference, pageParameters);
949
950                // execute request
951                processRequest(request, handler);
952
953                // the reference processed
954                return reference;
955        }
956
957        /**
958         * @return last response or <code>null</code> if no response has been produced yet.
959         */
960        public MockHttpServletResponse getLastResponse()
961        {
962                return lastResponse;
963        }
964
965        /**
966         * The last response as a string when a page is tested via {@code startPage()} methods.
967         * <p>
968         * In case the processed component was not a {@link Page} then the automatically created page
969         * markup gets removed. If you need the whole returned markup in this case use
970         * {@link #getLastResponse()}{@link MockHttpServletResponse#getDocument() .getDocument()}
971         * </p>
972         *
973         * @return last response as String.
974         */
975        public String getLastResponseAsString()
976        {
977                String response = lastResponse.getDocument();
978
979                // null, if a Page was rendered last
980                if (componentInPage == null)
981                {
982                        return response;
983                }
984
985                // remove the markup for the auto-generated page. leave just component's markup
986                int end = response.lastIndexOf("</body>");
987                if (end > -1)
988                {
989                        int start = response.indexOf("<body>") + "<body>".length();
990                        response = response.substring(start, end);
991                }
992
993                return response;
994        }
995
996        /**
997         * This method tries to parse the last response to return the encoded base URL and will throw an
998         * exception if there none was encoded.
999         *
1000         * @return Wicket-Ajax-BaseURL set on last response by {@link AbstractDefaultAjaxBehavior}
1001         * @throws IOException
1002         * @throws ParseException
1003         */
1004        public String getWicketAjaxBaseUrlEncodedInLastResponse()
1005                throws IOException, ParseException
1006        {
1007                XmlPullParser parser = new XmlPullParser();
1008                parser.parse(getLastResponseAsString());
1009                XmlTag tag;
1010                while ((tag = parser.nextTag()) != null)
1011                {
1012                        if (tag.isOpen() && tag.getName().equals("script") &&
1013                                "wicket-ajax-base-url".equals(tag.getAttribute("id")))
1014                        {
1015                                parser.next();
1016                                return parser.getString().toString().split("\\\"")[1];
1017                        }
1018                }
1019
1020                fail("Last response has no AJAX base URL set by AbstractDefaultAjaxBehavior.");
1021                return null;
1022        }
1023
1024        /**
1025         * @return list of prior requests
1026         */
1027        public List<MockHttpServletRequest> getPreviousRequests()
1028        {
1029                return previousRequests;
1030        }
1031
1032        /**
1033         * @return list of prior responses
1034         */
1035        public List<MockHttpServletResponse> getPreviousResponses()
1036        {
1037                return previousResponses;
1038        }
1039
1040        /**
1041         * Sets whether responses with redirects will be followed automatically.
1042         *
1043         * @param followRedirects
1044         */
1045        public void setFollowRedirects(boolean followRedirects)
1046        {
1047                this.followRedirects = followRedirects;
1048        }
1049
1050        /**
1051         * @return <code>true</code> if redirect responses will be followed automatically,
1052         *         <code>false</code> otherwise.
1053         */
1054        public boolean isFollowRedirects()
1055        {
1056                return followRedirects;
1057        }
1058
1059        /**
1060         * Encodes the {@link IRequestHandler} to {@link Url}. It should be safe to call this method
1061         * outside request thread as log as no registered {@link IRequestMapper} requires a
1062         * {@link RequestCycle}.
1063         *
1064         * @param handler
1065         * @return {@link Url} for handler.
1066         */
1067        public Url urlFor(final IRequestHandler handler)
1068        {
1069                Url url = application.getRootRequestMapper().mapHandler(handler);
1070                return transform(url);
1071        }
1072
1073        /**
1074         * @param link
1075         * @return url for Link
1076         */
1077        public String urlFor(Link<?> link)
1078        {
1079                Args.notNull(link, "link");
1080
1081                Url url = Url.parse(link.urlForListener(new PageParameters()).toString());
1082                return transform(url).toString();
1083        }
1084
1085        /**
1086         * Simulates processing URL that invokes an {@link IRequestListener} on a component.
1087         *
1088         * After the listener is invoked the page containing the component will be rendered (with an
1089         * optional redirect - depending on {@link RenderStrategy}).
1090         *
1091         * @param component
1092         */
1093        public void executeListener(final Component component)
1094        {
1095                Args.notNull(component, "component");
1096
1097                // there are two ways to do this. RequestCycle could be forced to call the handler
1098                // directly but constructing and parsing the URL increases the chance of triggering bugs
1099                Page page = component.getPage();
1100                PageAndComponentProvider pageAndComponentProvider = new PageAndComponentProvider(page,
1101                        component);
1102
1103                IRequestHandler handler = null;
1104                if (page.isPageStateless() || (page.isBookmarkable() && page.wasCreatedBookmarkable()))
1105                {
1106                        handler = new BookmarkableListenerRequestHandler(pageAndComponentProvider);
1107                }
1108                else
1109                {
1110                        handler = new ListenerRequestHandler(pageAndComponentProvider);
1111                }
1112
1113                Url url = urlFor(handler);
1114                request.setUrl(url);
1115
1116                // Process the request
1117                processRequest(request, null);
1118        }
1119
1120        /**
1121         * Simulates invoking an {@link IRequestListener} on a component. As opposed to the
1122         * {@link #executeListener(Component)} method, current request/response objects will be used
1123         *
1124         * After the listener is invoked the page containing the component will be rendered (with an
1125         * optional redirect - depending on {@link RenderStrategy}).
1126         *
1127         * @param component
1128         */
1129        public void invokeListener(final Component component)
1130        {
1131                Args.notNull(component, "component");
1132
1133                // there are two ways to do this. RequestCycle could be forced to call the handler
1134                // directly but constructing and parsing the URL increases the chance of triggering bugs
1135                IRequestHandler handler = new ListenerRequestHandler(
1136                        new PageAndComponentProvider(component.getPage(), component));
1137
1138                processRequest(handler);
1139        }
1140
1141        /**
1142         * Simulates invoking an {@link IRequestListener} on a component. As opposed to the
1143         * {@link #executeListener(Component)} method, current request/response objects will be used
1144         *
1145         * After the listener is invoked the page containing the component will be rendered (with an
1146         * optional redirect - depending on {@link RenderStrategy}).
1147         *
1148         * @param component
1149         * @param behavior
1150         */
1151        public void invokeListener(Component component, final Behavior behavior)
1152        {
1153                Args.notNull(component, "component");
1154                Args.notNull(behavior, "behavior");
1155
1156                // there are two ways to do this. RequestCycle could be forced to call the handler
1157                // directly but constructing and parsing the URL increases the chance of triggering bugs
1158                IRequestHandler handler = new ListenerRequestHandler(
1159                        new PageAndComponentProvider(component.getPage(), component),
1160                        component.getBehaviorId(behavior));
1161
1162                processRequest(handler);
1163        }
1164
1165        /**
1166         * Builds and processes a request suitable for executing an <code>AbstractAjaxBehavior</code>.
1167         *
1168         * @param behavior
1169         *            an <code>AbstractAjaxBehavior</code> to execute
1170         */
1171        public void executeBehavior(final AbstractAjaxBehavior behavior)
1172        {
1173                Args.notNull(behavior, "behavior");
1174
1175                Url url = Url.parse(behavior.getCallbackUrl().toString(),
1176                        Charset.forName(request.getCharacterEncoding()));
1177                transform(url);
1178                request.setUrl(url);
1179                request.addHeader(WebRequest.HEADER_ORIGIN, createOriginHeader());
1180                request.addHeader(WebRequest.HEADER_AJAX_BASE_URL, url.toString());
1181                request.addHeader(WebRequest.HEADER_AJAX, "true");
1182
1183                if (behavior instanceof AjaxFormSubmitBehavior)
1184                {
1185                        AjaxFormSubmitBehavior formSubmitBehavior = (AjaxFormSubmitBehavior)behavior;
1186                        Form<?> form = formSubmitBehavior.getForm();
1187                        getRequest().setUseMultiPartContentType(form.isMultiPart());
1188                        serializeFormToRequest(form);
1189
1190                        // mark behavior's component as the form submitter,
1191                        String name = Form.getRootFormRelativeId(
1192                                new PropertyModel<Component>(behavior, "component").getObject());
1193                        if (!request.getPostParameters().getParameterNames().contains(name))
1194                        {
1195                                request.getPostParameters().setParameterValue(name, "marked");
1196                        }
1197                }
1198
1199                processRequest();
1200        }
1201
1202        /**
1203         * Build value to Origin header based on RequestCycle Url
1204         *
1205         * @return Origin header
1206         */
1207        protected String createOriginHeader()
1208        {
1209                Url url = RequestCycle.get().getRequest().getUrl();
1210                return url.getProtocol() + "://" + url.getHost() + ":" + url.getPort();
1211        }
1212
1213        /**
1214         *
1215         * @param link
1216         * @return Url
1217         */
1218        public Url urlFor(final AjaxLink<?> link)
1219        {
1220                AbstractAjaxBehavior behavior = WicketTesterHelper.findAjaxEventBehavior(link, "click");
1221                Url url = Url.parse(behavior.getCallbackUrl().toString(),
1222                        Charset.forName(request.getCharacterEncoding()));
1223                return transform(url);
1224        }
1225
1226        /**
1227         *
1228         * @param url
1229         */
1230        public void executeAjaxUrl(final Url url)
1231        {
1232                Args.notNull(url, "url");
1233
1234                transform(url);
1235                request.setUrl(url);
1236                request.addHeader("Wicket-Ajax-BaseURL", url.toString());
1237                request.addHeader("Wicket-Ajax", "true");
1238
1239                processRequest();
1240        }
1241
1242        /**
1243         * Renders a <code>Page</code> from its default constructor.
1244         *
1245         * @param <C>
1246         * @param pageClass
1247         *            a test <code>Page</code> class with default constructor
1248         * @return the rendered <code>Page</code>
1249         */
1250        public final <C extends Page> C startPage(final Class<C> pageClass)
1251        {
1252                return startPage(pageClass, null);
1253        }
1254
1255        /**
1256         * Renders a <code>Page</code> from its default constructor.
1257         *
1258         * @param <C>
1259         * @param pageClass
1260         *            a test <code>Page</code> class with default constructor
1261         * @param parameters
1262         *            the parameters to use for the class.
1263         * @return the rendered <code>Page</code>
1264         */
1265        @SuppressWarnings("unchecked")
1266        public final <C extends Page> C startPage(final Class<C> pageClass,
1267                final PageParameters parameters)
1268        {
1269                Args.notNull(pageClass, "pageClass");
1270
1271                // must be null for Pages
1272                componentInPage = null;
1273
1274                // prepare the request
1275                request.setUrl(application.getRootRequestMapper().mapHandler(
1276                        new BookmarkablePageRequestHandler(new PageProvider(pageClass, parameters))));
1277
1278                // process the request
1279                processRequest();
1280
1281                // The last rendered page
1282                return (C)getLastRenderedPage();
1283        }
1284
1285        /**
1286         * Creates a {@link FormTester} for the <code>Form</code> at a given path, and fills all child
1287         * {@link org.apache.wicket.markup.html.form.FormComponent}s with blank <code>String</code>s.
1288         *
1289         * @param path
1290         *            path to <code>FormComponent</code>
1291         * @return a <code>FormTester</code> instance for testing the <code>Form</code>
1292         * @see #newFormTester(String, boolean)
1293         */
1294        public FormTester newFormTester(final String path)
1295        {
1296                return newFormTester(path, true);
1297        }
1298
1299        /**
1300         * Creates a {@link FormTester} for the <code>Form</code> at a given path.
1301         *
1302         * @param path
1303         *            path to <code>FormComponent</code>
1304         * @param fillBlankString
1305         *            specifies whether to fill all child <code>FormComponent</code>s with blank
1306         *            <code>String</code>s
1307         * @return a <code>FormTester</code> instance for testing the <code>Form</code>
1308         * @see FormTester
1309         */
1310        public FormTester newFormTester(final String path, final boolean fillBlankString)
1311        {
1312                return new FormTester(path, (Form<?>)getComponentFromLastRenderedPage(path), this,
1313                        fillBlankString);
1314        }
1315
1316        /**
1317         * Process a component. A web page will be automatically created with the markup created in
1318         * {@link #createPageMarkup(String)}.
1319         * <p>
1320         * <strong>Note</strong>: the instantiated component will have an auto-generated id. To reach
1321         * any of its children use their relative path to the component itself. For example if the
1322         * started component has a child a Link component with id "link" then after starting the
1323         * component you can click it with: <code>tester.clickLink("link")</code>
1324         * </p>
1325         *
1326         * @param <C>
1327         *            the type of the component
1328         * @param componentClass
1329         *            the class of the component to be tested
1330         * @return The component processed
1331         * @see #startComponentInPage(org.apache.wicket.Component)
1332         */
1333        public final <C extends Component> C startComponentInPage(final Class<C> componentClass)
1334        {
1335                return startComponentInPage(componentClass, null);
1336        }
1337
1338        /**
1339         * Process a component. A web page will be automatically created with the {@code pageMarkup}
1340         * provided. In case pageMarkup is null, the markup will be automatically created with
1341         * {@link #createPageMarkup(String)}.
1342         * <p>
1343         * <strong>Note</strong>: the instantiated component will have an auto-generated id. To reach
1344         * any of its children use their relative path to the component itself. For example if the
1345         * started component has a child a Link component with id "link" then after starting the
1346         * component you can click it with: <code>tester.clickLink("link")</code>
1347         * </p>
1348         *
1349         * @param <C>
1350         *            the type of the component
1351         *
1352         * @param componentClass
1353         *            the class of the component to be tested
1354         * @param pageMarkup
1355         *            the markup for the Page that will be automatically created. May be {@code null}.
1356         * @return The component processed
1357         */
1358        public final <C extends Component> C startComponentInPage(final Class<C> componentClass,
1359                final IMarkupFragment pageMarkup)
1360        {
1361                Args.notNull(componentClass, "componentClass");
1362
1363                // Create the component instance from the class
1364                C comp = null;
1365                try
1366                {
1367                        Constructor<C> c = componentClass.getConstructor(String.class);
1368                        comp = c.newInstance(ComponentInPage.ID);
1369                }
1370                catch (Exception e)
1371                {
1372                        log.error(e.getMessage(), e);
1373                        fail(String.format("Cannot instantiate component with type '%s' because of '%s'",
1374                                componentClass.getName(), e.getMessage()));
1375                }
1376
1377                // process the component
1378                C started = startComponentInPage(comp, pageMarkup);
1379                
1380                componentInPage.isInstantiated = true;
1381                
1382                return started;
1383        }
1384
1385        /**
1386         * Process a component. A web page will be automatically created with markup created by the
1387         * {@link #createPageMarkup(String)}.
1388         * <p>
1389         * <strong>Note</strong>: the component id is set by the user. To reach any of its children use
1390         * this id + their relative path to the component itself. For example if the started component
1391         * has id <em>compId</em> and a Link child component component with id "link" then after
1392         * starting the component you can click it with: <code>tester.clickLink("compId:link")</code>
1393         * </p>
1394         *
1395         * @param <C>
1396         *            the type of the component
1397         * @param component
1398         *            the component to be tested
1399         * @return The component processed
1400         * @see #startComponentInPage(Class)
1401         */
1402        public final <C extends Component> C startComponentInPage(final C component)
1403        {
1404                return startComponentInPage(component, null);
1405        }
1406
1407        /**
1408         * Process a component. A web page will be automatically created with the {@code pageMarkup}
1409         * provided. In case {@code pageMarkup} is null, the markup will be automatically created with
1410         * {@link #createPageMarkup(String)}.
1411         * <p>
1412         * <strong>Note</strong>: the component id is set by the user. To reach any of its children use
1413         * this id + their relative path to the component itself. For example if the started component
1414         * has id <em>compId</em> and a Link child component component with id "link" then after
1415         * starting the component you can click it with: <code>tester.clickLink("compId:link")</code>
1416         * </p>
1417         *
1418         * @param <C>
1419         *            the type of the component
1420         * @param component
1421         *            the component to be tested
1422         * @param pageMarkup
1423         *            the markup for the Page that will be automatically created. May be {@code null}.
1424         * @return The component processed
1425         */
1426        public final <C extends Component> C startComponentInPage(final C component,
1427                IMarkupFragment pageMarkup)
1428        {
1429                Args.notNull(component, "component");
1430
1431                // Create a page object and assign the markup
1432                Page page = createPage();
1433                if (page == null)
1434                {
1435                        fail("The automatically created page should not be null.");
1436                }
1437
1438                // Automatically create the page markup if not provided
1439                if (pageMarkup == null)
1440                {
1441                        String markup = createPageMarkup(component.getId());
1442                        if (markup == null)
1443                        {
1444                                fail("The markup for the automatically created page should not be null.");
1445                        }
1446
1447                        try
1448                        {
1449                                // set a ContainerInfo to be able to use HtmlHeaderContainer so header contribution
1450                                // still work. WICKET-3700
1451                                ContainerInfo containerInfo = new ContainerInfo(page);
1452                                MarkupResourceStream markupResourceStream = new MarkupResourceStream(
1453                                        new StringResourceStream(markup), containerInfo, page.getClass());
1454
1455                                MarkupParser markupParser = getApplication().getMarkupSettings()
1456                                        .getMarkupFactory()
1457                                        .newMarkupParser(markupResourceStream);
1458                                pageMarkup = markupParser.parse();
1459                        }
1460                        catch (Exception e)
1461                        {
1462                                String errorMessage = "Error while parsing the markup for the autogenerated page: " +
1463                                        e.getMessage();
1464                                log.error(errorMessage, e);
1465                                fail(errorMessage);
1466                        }
1467                }
1468
1469                if (page instanceof StartComponentInPage)
1470                {
1471                        ((StartComponentInPage)page).setPageMarkup(pageMarkup);
1472                }
1473                else
1474                {
1475                        page.setMarkup(pageMarkup);
1476                }
1477
1478                // Add the child component
1479                page.add(component);
1480
1481                // Process the page
1482                startPage(page);
1483
1484                componentInPage = new ComponentInPage();
1485                componentInPage.component = component;
1486                
1487                return component;
1488        }
1489
1490        /**
1491         * Creates the markup that will be used for the automatically created {@link Page} that will be
1492         * used to test a component with {@link #startComponentInPage(Class, IMarkupFragment)}
1493         *
1494         * @param componentId
1495         *            the id of the component to be tested
1496         * @return the markup for the {@link Page} as {@link String}. Cannot be {@code null}.
1497         */
1498        protected String createPageMarkup(final String componentId)
1499        {
1500                return "<html><head></head><body><span wicket:id='" + componentId +
1501                        "'></span></body></html>";
1502        }
1503
1504        /**
1505         * Creates a {@link Page} to test a component with
1506         * {@link #startComponentInPage(Component, IMarkupFragment)}
1507         *
1508         * @return a {@link Page} which will contain the component under test as a single child
1509         */
1510        protected Page createPage()
1511        {
1512                return new StartComponentInPage();
1513        }
1514
1515        public Component getComponentFromLastRenderedPage(String path,
1516                        boolean wantVisibleInHierarchy)
1517        {
1518                return getComponentFromLastRenderedPage(path, wantVisibleInHierarchy, true);
1519        }
1520
1521        /**
1522         * Gets the component with the given path from last rendered page. This method fails in case the
1523         * component couldn't be found.
1524         *
1525         * @param path
1526         *            Path to component
1527         * @param wantVisibleInHierarchy
1528         *            if true component needs to be visible in hierarchy else {@code null} is returned
1529         * @return The component at the path
1530         * @see org.apache.wicket.MarkupContainer#get(String)
1531         */
1532        public Component getComponentFromLastRenderedPage(String path,
1533                boolean wantVisibleInHierarchy, boolean failOnAbsent)
1534        {
1535                if (componentInPage != null && componentInPage.isInstantiated)
1536                {
1537                        String componentIdPageId = componentInPage.component.getId() + ':';
1538                        if (path.startsWith(componentIdPageId) == false)
1539                        {
1540                                path = componentIdPageId + path;
1541                        }
1542                }
1543
1544                Component component = getLastRenderedPage().get(path);
1545                if (component == null)
1546                {
1547                        if (failOnAbsent)
1548                        {
1549                                fail("path: '" + path + "' does not exist for page: " +
1550                                                Classes.simpleName(getLastRenderedPage().getClass()));
1551                        }
1552                        return null;
1553                }
1554
1555                if (!wantVisibleInHierarchy || component.isVisibleInHierarchy())
1556                {
1557                        return component;
1558                }
1559
1560                // Not found or not visible
1561                return null;
1562        }
1563
1564        /**
1565         * Gets the component with the given path from last rendered page. This method fails in case the
1566         * component couldn't be found, and it will return null if the component was found, but is not
1567         * visible.
1568         *
1569         * @param path
1570         *            Path to component
1571         * @return The component at the path
1572         * @see org.apache.wicket.MarkupContainer#get(String)
1573         */
1574        public Component getComponentFromLastRenderedPage(String path)
1575        {
1576                return getComponentFromLastRenderedPage(path, true);
1577        }
1578
1579        /**
1580         * assert the text of <code>Label</code> component.
1581         *
1582         * @param path
1583         *            path to <code>Label</code> component
1584         * @param expectedLabelText
1585         *            expected label text
1586         * @return a <code>Result</code>
1587         */
1588        public Result hasLabel(String path, String expectedLabelText)
1589        {
1590                Label label = (Label)getComponentFromLastRenderedPage(path);
1591                return isEqual(expectedLabelText, label.getDefaultModelObjectAsString());
1592        }
1593
1594        /**
1595         * assert component class
1596         *
1597         * @param <C>
1598         *
1599         * @param path
1600         *            path to component
1601         * @param expectedComponentClass
1602         *            expected component class
1603         * @return a <code>Result</code>
1604         */
1605        public <C extends Component> Result isComponent(String path, Class<C> expectedComponentClass)
1606        {
1607                Component component = assertExists(path);
1608
1609                return isTrue(
1610                        "component '" + Classes.name(component.getClass()) + "' is not of type: " +
1611                                Classes.name(expectedComponentClass),
1612                        expectedComponentClass.isAssignableFrom(component.getClass()));
1613        }
1614
1615        /**
1616         * assert component visible.
1617         *
1618         * @param path
1619         *            path to component
1620         * @return a <code>Result</code>
1621         */
1622        public Result isVisible(final String path)
1623        {
1624                final Result result;
1625
1626                Component component = getComponentFromLastRenderedPage(path, false);
1627                if (component == null)
1628                {
1629                        result = Result.fail("path: '" + path + "' does not exist for page: " +
1630                                Classes.simpleName(getLastRenderedPage().getClass()));
1631                }
1632                else
1633                {
1634                        result = isTrue("component '" + path + "' is not visible",
1635                                component.isVisibleInHierarchy());
1636                }
1637
1638                return result;
1639        }
1640
1641        /**
1642         * assert component invisible.
1643         *
1644         * @param path
1645         *            path to component
1646         * @return a <code>Result</code>
1647         */
1648        public Result isInvisible(final String path)
1649        {
1650                final Result result;
1651
1652                Component component = getComponentFromLastRenderedPage(path, false);
1653                if (component == null)
1654                {
1655                        result = Result.fail("path: '" + path + "' does not exist for page: " +
1656                                Classes.simpleName(getLastRenderedPage().getClass()));
1657                }
1658                else
1659                {
1660                        result = isFalse("component '" + path + "' is visible",
1661                                component.isVisibleInHierarchy());
1662                }
1663
1664                return result;
1665        }
1666
1667        /**
1668         * assert component enabled.
1669         *
1670         * @param path
1671         *            path to component
1672         * @return a <code>Result</code>
1673         */
1674        public Result isEnabled(final String path)
1675        {
1676                Component component = assertExists(path);
1677
1678                return isTrue("component '" + path + "' is disabled", component.isEnabledInHierarchy());
1679        }
1680
1681        /**
1682         * assert component disabled.
1683         *
1684         * @param path
1685         *            path to component
1686         * @return a <code>Result</code>
1687         */
1688        public Result isDisabled(final String path)
1689        {
1690                Component component = assertExists(path);
1691
1692                return isFalse("component '" + path + "' is enabled", component.isEnabledInHierarchy());
1693        }
1694
1695        public Component assertExists(final String path)
1696        {
1697                Component component = getComponentFromLastRenderedPage(path);
1698                if (component == null)
1699                {
1700                        fail("path: '" + path + "' does not exist for page: " +
1701                                Classes.simpleName(getLastRenderedPage().getClass()));
1702                        return null;
1703                }
1704                return component;
1705        }
1706
1707        public void assertNotExists(final String path)
1708        {
1709                Component component = getComponentFromLastRenderedPage(path, true, false);
1710                if (component != null)
1711                {
1712                        fail("path: '" + path + "' does exists for page: " +
1713                                        Classes.simpleName(getLastRenderedPage().getClass()));
1714                }
1715        }
1716
1717        private FormComponent<?> assertFormComponent(final String path)
1718        {
1719                Component component = assertExists(path);
1720
1721                if (component instanceof FormComponent<?> == false)
1722                {
1723                        fail("path: '" + path + "' is not a form component");
1724                        return null;
1725                }
1726                return (FormComponent<?>)component;
1727        }
1728
1729        /**
1730         * assert component required.
1731         *
1732         * @param path
1733         *            path to component
1734         * @return a <code>Result</code>
1735         */
1736        public Result isRequired(String path)
1737        {
1738                FormComponent<?> formComponent = assertFormComponent(path);
1739
1740                return isRequired(formComponent);
1741        }
1742
1743        /**
1744         * assert component required.
1745         *
1746         * @param component
1747         *            a form component
1748         * @return a <code>Result</code>
1749         */
1750        public Result isRequired(FormComponent<?> component)
1751        {
1752                return isTrue("component '" + component + "' is not required", component.isRequired());
1753        }
1754
1755        /**
1756         * Asserts that a component is not required.
1757         *
1758         * @param path
1759         *            path to component
1760         * @return a <code>Result</code>
1761         */
1762        public Result isNotRequired(String path)
1763        {
1764                FormComponent<?> formComponent = assertFormComponent(path);
1765
1766                return isNotRequired(formComponent);
1767        }
1768
1769        /**
1770         * Asserts that a component is not required.
1771         *
1772         * @param component
1773         *            a form component
1774         * @return a <code>Result</code>
1775         */
1776        public Result isNotRequired(FormComponent<?> component)
1777        {
1778                return isFalse("component '" + component + "' is required", component.isRequired());
1779        }
1780
1781        /**
1782         * assert the content of last rendered page contains(matches) regex pattern.
1783         *
1784         * @param pattern
1785         *            reqex pattern to match
1786         * @return a <code>Result</code>
1787         */
1788        public Result ifContains(String pattern)
1789        {
1790                return isTrue("pattern '" + pattern + "' not found in:\n" + getLastResponseAsString(),
1791                        getLastResponseAsString().matches("(?s).*" + pattern + ".*"));
1792        }
1793
1794        /**
1795         * assert the content of last rendered page contains(matches) regex pattern.
1796         *
1797         * @param pattern
1798         *            reqex pattern to match
1799         * @return a <code>Result</code>
1800         */
1801        public Result ifContainsNot(String pattern)
1802        {
1803                return isFalse("pattern '" + pattern + "' found",
1804                        getLastResponseAsString().matches("(?s).*" + pattern + ".*"));
1805        }
1806
1807        /**
1808         * Click the {@link Link} in the last rendered Page.
1809         * <p>
1810         * Simulate that AJAX is enabled.
1811         *
1812         * @see WicketTester#clickLink(String, boolean)
1813         * @param path
1814         *            Click the <code>Link</code> in the last rendered Page.
1815         */
1816        public void clickLink(String path)
1817        {
1818                clickLink(path, true);
1819        }
1820
1821        /**
1822         * Click the {@link Link} in the last rendered Page.
1823         * <p>
1824         * This method also works for {@link AjaxLink}, {@link AjaxFallbackLink} and
1825         * {@link AjaxSubmitLink}.
1826         * <p>
1827         * On AjaxLinks and AjaxFallbackLinks the onClick method is invoked with a valid
1828         * AjaxRequestTarget. In that way you can test the flow of your application when using AJAX.
1829         * <p>
1830         * When clicking an AjaxSubmitLink the form, which the AjaxSubmitLink is attached to is first
1831         * submitted, and then the onSubmit method on AjaxSubmitLink is invoked. If you have changed
1832         * some values in the form during your test, these will also be submitted. This should not be
1833         * used as a replacement for the {@link FormTester} to test your forms. It should be used to
1834         * test that the code in your onSubmit method in AjaxSubmitLink actually works.
1835         * <p>
1836         * This method is also able to simulate that AJAX (javascript) is disabled on the client. This
1837         * is done by setting the isAjax parameter to false. If you have an AjaxFallbackLink you can
1838         * then check that it doesn't fail when invoked as a normal link.
1839         *
1840         * @param path
1841         *            path to <code>Link</code> component
1842         * @param isAjax
1843         *            Whether to simulate that AJAX (javascript) is enabled or not. If it's false then
1844         *            AjaxLink and AjaxSubmitLink will fail, since it wouldn't work in real life.
1845         *            AjaxFallbackLink will be invoked with null as the AjaxRequestTarget parameter.
1846         */
1847        public void clickLink(String path, boolean isAjax)
1848        {
1849                Component linkComponent = getComponentFromLastRenderedPage(path);
1850
1851                checkUsability(linkComponent, true);
1852
1853                // if the link is an AjaxLink, we process it differently
1854                // than a normal link
1855                if (linkComponent instanceof AjaxLink)
1856                {
1857                        // If it's not ajax we fail
1858                        if (isAjax == false)
1859                        {
1860                                fail("Link " + path + "is an AjaxLink and will " +
1861                                        "not be invoked when AJAX (javascript) is disabled.");
1862                        }
1863
1864                        List<AjaxEventBehavior> behaviors = WicketTesterHelper
1865                                .findAjaxEventBehaviors(linkComponent, "click");
1866                        for (AjaxEventBehavior behavior : behaviors)
1867                        {
1868                                executeBehavior(behavior);
1869                        }
1870                }
1871                // if the link is an AjaxSubmitLink, we need to find the form
1872                // from it using reflection so we know what to submit.
1873                else if (linkComponent instanceof AjaxSubmitLink)
1874                {
1875                        // If it's not ajax we fail
1876                        if (isAjax == false)
1877                        {
1878                                fail("Link " + path + " is an AjaxSubmitLink and " +
1879                                        "will not be invoked when AJAX (javascript) is disabled.");
1880                        }
1881
1882                        AjaxSubmitLink link = (AjaxSubmitLink)linkComponent;
1883
1884                        String pageRelativePath = link.getInputName();
1885                        request.getPostParameters().setParameterValue(pageRelativePath, "x");
1886
1887                        submitAjaxFormSubmitBehavior(link,
1888                                (AjaxFormSubmitBehavior)WicketTesterHelper.findAjaxEventBehavior(link, "click"));
1889                }
1890                // if the link is an IAjaxLink, use it (do check if AJAX is expected)
1891                else if (isAjax &&
1892                        (linkComponent instanceof IAjaxLink || linkComponent instanceof AjaxFallbackLink))
1893                {
1894                        List<AjaxEventBehavior> behaviors = WicketTesterHelper
1895                                .findAjaxEventBehaviors(linkComponent, "click");
1896                        for (AjaxEventBehavior behavior : behaviors)
1897                        {
1898                                executeBehavior(behavior);
1899                        }
1900                }
1901                /*
1902                 * If the link is a submitlink then we pretend to have clicked it
1903                 */
1904                else if (linkComponent instanceof SubmitLink)
1905                {
1906                        SubmitLink submitLink = (SubmitLink)linkComponent;
1907
1908                        String pageRelativePath = submitLink.getInputName();
1909                        request.getPostParameters().setParameterValue(pageRelativePath, "x");
1910
1911                        serializeFormToRequest(submitLink.getForm());
1912                        submitForm(submitLink.getForm().getPageRelativePath());
1913                }
1914                else if (linkComponent instanceof ExternalLink)
1915                {
1916                        ExternalLink externalLink = (ExternalLink)linkComponent;
1917                        String href = externalLink.getDefaultModelObjectAsString();
1918                        try
1919                        {
1920                                getResponse().sendRedirect(href);
1921                                recordRequestResponse();
1922                                setupNextRequestCycle();
1923                        }
1924                        catch (IOException iox)
1925                        {
1926                                throw new WicketRuntimeException("An error occurred while redirecting to: " + href,
1927                                        iox);
1928                        }
1929                }
1930                // if the link is a normal link (or ResourceLink)
1931                else if (linkComponent instanceof AbstractLink)
1932                {
1933                        AbstractLink link = (AbstractLink)linkComponent;
1934
1935                        /*
1936                         * If the link is a bookmarkable link, then we need to transfer the parameters to the
1937                         * next request.
1938                         */
1939                        if (link instanceof BookmarkablePageLink)
1940                        {
1941                                BookmarkablePageLink<?> bookmarkablePageLink = (BookmarkablePageLink<?>)link;
1942                                try
1943                                {
1944                                        Method getParametersMethod = BookmarkablePageLink.class
1945                                                .getDeclaredMethod("getPageParameters", (Class<?>[])null);
1946                                        getParametersMethod.setAccessible(true);
1947
1948                                        PageParameters parameters = (PageParameters)getParametersMethod
1949                                                .invoke(bookmarkablePageLink, (Object[])null);
1950
1951                                        startPage(bookmarkablePageLink.getPageClass(), parameters);
1952                                }
1953                                catch (Exception e)
1954                                {
1955                                        throw new WicketRuntimeException("Internal error in WicketTester. " +
1956                                                "Please report this in Wicket's Issue Tracker.", e);
1957                                }
1958                        }
1959                        else if (link instanceof ResourceLink)
1960                        {
1961                                try
1962                                {
1963                                        Method getURL = ResourceLink.class.getDeclaredMethod("getURL");
1964                                        getURL.setAccessible(true);
1965                                        CharSequence url = (CharSequence)getURL.invoke(link);
1966                                        executeUrl(url.toString());
1967                                }
1968                                catch (Exception x)
1969                                {
1970                                        throw new RuntimeException("An error occurred while clicking on a ResourceLink",
1971                                                x);
1972                                }
1973                        }
1974                        else
1975                        {
1976                                executeListener(link);
1977                        }
1978                }
1979                // The link requires AJAX
1980                else if (linkComponent instanceof IAjaxLink && isAjax == false)
1981                {
1982                        fail("Link " + path + "is an IAjaxLink and will " +
1983                                "not be invoked when AJAX (javascript) is disabled.");
1984                }
1985                else
1986                {
1987                        fail("Link " + path + " is not an instance of AbstractLink or IAjaxLink");
1988                }
1989        }
1990
1991        /**
1992         * Submit the given form in the last rendered {@link Page}
1993         * <p>
1994         * <strong>Note</strong>: Form request parameters have to be set explicitly.
1995         *
1996         * @param form
1997         *            path to component
1998         */
1999        public void submitForm(Form<?> form)
2000        {
2001                submitForm(form.getPageRelativePath());
2002        }
2003
2004        /**
2005         * Submits the {@link Form} in the last rendered {@link Page}.
2006         * <p>
2007         * <strong>Note</strong>: Form request parameters have to be set explicitely.
2008         *
2009         * @param path
2010         *            path to component
2011         */
2012        public void submitForm(String path)
2013        {
2014                Form<?> form = (Form<?>)getComponentFromLastRenderedPage(path);
2015                Url url = Url.parse(form.getRootForm().urlForListener(new PageParameters()).toString(),
2016                        Charset.forName(request.getCharacterEncoding()));
2017
2018                // make url absolute
2019                transform(url);
2020
2021                request.setUrl(url);
2022                processRequest();
2023        }
2024
2025        /**
2026         * make url suitable for wicket tester use. usually this involves stripping any leading ..
2027         * segments to make the url absolute
2028         *
2029         * @param url
2030         * @return Url
2031         */
2032        private Url transform(final Url url)
2033        {
2034                while (url.getSegments().size() > 0 &&
2035                        (url.getSegments().get(0).equals("..") || url.getSegments().get(0).equals(".")))
2036                {
2037                        url.getSegments().remove(0);
2038                }
2039                return url;
2040        }
2041
2042        /**
2043         * Asserts the last rendered <code>Page</code> class.
2044         *
2045         * @param <C>
2046         * @param expectedRenderedPageClass
2047         *            expected class of last rendered page
2048         * @return a <code>Result</code>
2049         */
2050        public <C extends Page> Result isRenderedPage(Class<C> expectedRenderedPageClass)
2051        {
2052                Args.notNull(expectedRenderedPageClass, "expectedRenderedPageClass");
2053
2054                Page page = getLastRenderedPage();
2055                if (page == null)
2056                {
2057                        return Result.fail("page was null");
2058                }
2059                if (!expectedRenderedPageClass.isAssignableFrom(page.getClass()))
2060                {
2061                        return Result.fail(String.format("classes not the same, expected '%s', current '%s'",
2062                                expectedRenderedPageClass, page.getClass()));
2063                }
2064                return Result.pass();
2065        }
2066
2067        /**
2068         * Asserts last rendered <code>Page</code> against an expected HTML document.
2069         * <p>
2070         * Use <code>-Dwicket.replace.expected.results=true</code> to automatically replace the expected
2071         * output file.
2072         * </p>
2073         *
2074         * @param pageClass
2075         *            used to load the <code>File</code> (relative to <code>clazz</code> package)
2076         * @param filename
2077         *            expected output <code>File</code> name
2078         * @throws Exception
2079         */
2080        public void assertResultPage(final Class<?> pageClass, final String filename) throws Exception
2081        {
2082                // Validate the document
2083                String document = getLastResponseAsString();
2084                DiffUtil.validatePage(document, pageClass, filename, true);
2085        }
2086
2087        /**
2088         * Asserts last rendered <code>Page</code> against an expected HTML document as a
2089         * <code>String</code>.
2090         *
2091         * @param expectedDocument
2092         *            expected output
2093         * @return a <code>Result</code>
2094     */
2095        public Result isResultPage(final String expectedDocument) {
2096                // Validate the document
2097                String document = getLastResponseAsString();
2098                return isTrue("expected rendered page equals", document.equals(expectedDocument));
2099        }
2100
2101        /**
2102         * Asserts no error-level feedback messages.
2103         *
2104         * @return a <code>Result</code>
2105         * @see #hasNoFeedbackMessage(int)
2106         */
2107        public Result hasNoErrorMessage()
2108        {
2109                return hasNoFeedbackMessage(FeedbackMessage.ERROR);
2110        }
2111
2112        /**
2113         * Asserts no info-level feedback messages.
2114         *
2115         * @return a <code>Result</code>
2116         * @see #hasNoFeedbackMessage(int)
2117         */
2118        public Result hasNoInfoMessage()
2119        {
2120                return hasNoFeedbackMessage(FeedbackMessage.INFO);
2121        }
2122
2123        /**
2124         * Asserts there are no feedback messages with the given level.
2125         *
2126         * @param level
2127         *            the level of the feedback message
2128         * @return a <code>Result</code>
2129         */
2130        public Result hasNoFeedbackMessage(int level)
2131        {
2132                List<Serializable> messages = getMessages(level);
2133                return isTrue(String.format("expected no %s message, but contains\n%s",
2134                        new FeedbackMessage(null, "", level).getLevelAsString().toLowerCase(Locale.ROOT),
2135                        WicketTesterHelper.asLined(messages)), messages.isEmpty());
2136        }
2137
2138        /**
2139         * Retrieves <code>FeedbackMessages</code>.
2140         *
2141         * @param level
2142         *            level of feedback message, for example:
2143         *            <code>FeedbackMessage.DEBUG or FeedbackMessage.INFO.. etc</code>
2144         * @return <code>List</code> of messages (as <code>String</code>s)
2145         * @see FeedbackMessage
2146         */
2147        public List<Serializable> getMessages(final int level)
2148        {
2149                List<FeedbackMessage> messages = getFeedbackMessages(
2150                        new ExactLevelFeedbackMessageFilter(level));
2151
2152                List<Serializable> actualMessages = Generics.newArrayList();
2153                for (FeedbackMessage message : messages)
2154                {
2155                        actualMessages.add(message.getMessage());
2156                }
2157                return actualMessages;
2158        }
2159
2160        /**
2161         * Retrieves <code>FeedbackMessages</code>.
2162         *
2163         * @param filter
2164         *            A filter that decides which messages to collect
2165         * @return <code>List</code> of messages (as <code>String</code>s)
2166         * @see FeedbackMessage
2167         */
2168        public List<FeedbackMessage> getFeedbackMessages(final IFeedbackMessageFilter filter)
2169        {
2170                return new FeedbackCollector(getLastRenderedPage(), true).collect(filter);
2171        }
2172
2173        /**
2174         * Dumps the source of last rendered <code>Page</code>.
2175         */
2176        public void dumpPage()
2177        {
2178                log.info(getLastResponseAsString());
2179        }
2180
2181        /**
2182         * Dumps the <code>Component</code> trees.
2183         */
2184        public void debugComponentTrees()
2185        {
2186                debugComponentTrees("");
2187        }
2188
2189        /**
2190         * Dumps the <code>Component</code> trees to log. Show only the <code>Component</code>s whose
2191         * paths contain the filter <code>String</code>.
2192         *
2193         * @param filter
2194         *            a filter <code>String</code>
2195         */
2196        public void debugComponentTrees(String filter)
2197        {
2198                log.info("debugging ----------------------------------------------");
2199                for (WicketTesterHelper.ComponentData obj : WicketTesterHelper
2200                        .getComponentData(getLastRenderedPage()))
2201                {
2202                        if (obj.path.matches(".*" + filter + ".*"))
2203                        {
2204                                log.info("[{}{}] path\t" + obj.path + " \t" + obj.type + " \t[" + obj.value + "]",
2205                                        obj.isEnabled ? "E" : "-", obj.isVisible ? "V" : "-");
2206                        }
2207                }
2208        }
2209
2210        /**
2211         * Tests that a <code>Component</code> has been added to a <code>AjaxRequestTarget</code>, using
2212         * {@link org.apache.wicket.ajax.AjaxRequestTarget#add(org.apache.wicket.Component...)}. This
2213         * method actually tests that a <code>Component</code> is on the Ajax response sent back to the
2214         * client.
2215         * <p>
2216         * PLEASE NOTE! This method doesn't actually insert the <code>Component</code> in the client DOM
2217         * tree, using JavaScript. But it shouldn't be needed because you have to trust that the Wicket
2218         * Ajax JavaScript just works.
2219         *
2220         * @param component
2221         *            the <code>Component</code> to test
2222         * @return a <code>Result</code>
2223         */
2224        public Result isComponentOnAjaxResponse(final Component component)
2225        {
2226                String failMessage = "A component which is null could not have been added to the AJAX response";
2227                notNull(failMessage, component);
2228
2229                Result result;
2230
2231                // test that the component renders the placeholder tag if it's not visible
2232                String componentInfo = component.toString();
2233                if (!component.isVisible())
2234                {
2235                        failMessage = "A component which is invisible and doesn't render a placeholder tag" +
2236                                " will not be rendered at all and thus won't be accessible for subsequent AJAX interaction. " +
2237                                componentInfo;
2238                        result = isTrue(failMessage, component.getOutputMarkupPlaceholderTag());
2239                        if (result.wasFailed())
2240                        {
2241                                return result;
2242                        }
2243                }
2244
2245                // Get the AJAX response
2246                String ajaxResponse = getLastResponseAsString();
2247
2248                // Test that the previous response was actually a AJAX response
2249                failMessage = "The previous response was not an AJAX response. " +
2250                        "You need to execute an AJAX event, using #clickLink() or " +
2251                        "#executeAjaxEvent(), before using this assertion method";
2252                boolean isAjaxResponse = Pattern
2253                        .compile("^<\\?xml version=\"1.0\" encoding=\".*?\"\\?><ajax-response>")
2254                        .matcher(ajaxResponse)
2255                        .find();
2256                result = isTrue(failMessage, isAjaxResponse);
2257                if (result.wasFailed())
2258                {
2259                        return result;
2260                }
2261
2262                // See if the component has a markup id
2263                String markupId = component.getMarkupId();
2264
2265                failMessage = "The component doesn't have a markup id, " +
2266                        "which means that it can't have been added to the AJAX response. " + componentInfo;
2267                result = isTrue(failMessage, !Strings.isEmpty(markupId));
2268                if (result.wasFailed())
2269                {
2270                        return result;
2271                }
2272
2273                // Look for that the component is on the response, using the markup id
2274                boolean isComponentInAjaxResponse = ajaxResponse
2275                        .matches("(?s).*<component id=\"" + markupId + "\"[^>]*?>.*");
2276                failMessage = "Component wasn't found in the AJAX response. " + componentInfo;
2277                result = isTrue(failMessage, isComponentInAjaxResponse);
2278
2279                if (!result.wasFailed())
2280                {
2281                        return result;
2282                }
2283
2284                // Check if the component has been included as part of an enclosure render
2285                Enclosure enclosure = getLastRenderedPage().visitChildren(Enclosure.class,
2286                        (Enclosure enc, IVisit<Enclosure> visit) -> {
2287                                if (AjaxEnclosureListener.isControllerOfEnclosure(component, enc))
2288                                {
2289                                        visit.stop(enc);
2290                                }
2291                        });
2292
2293                if (enclosure == null)
2294                {
2295                        return result;
2296                }
2297
2298                failMessage = "Component's enclosure was not found in the AJAX response. " + enclosure;
2299                boolean isEnclosureInAjaxResponse = !isComponentOnAjaxResponse(enclosure).wasFailed();
2300                return isTrue(failMessage, isEnclosureInAjaxResponse);
2301
2302        }
2303
2304        /**
2305         * Simulates the firing of an Ajax event.
2306         *
2307         * @see #executeAjaxEvent(Component, String)
2308         *
2309         * @since 1.2.3
2310         * @param componentPath
2311         *            the <code>Component</code> path
2312         * @param event
2313         *            the event which we simulate being fired. If <code>event</code> is
2314         *            <code>null</code>, the test will fail.
2315         */
2316        public void executeAjaxEvent(final String componentPath, final String event)
2317        {
2318                Component component = getComponentFromLastRenderedPage(componentPath);
2319                executeAjaxEvent(component, event);
2320        }
2321
2322        /**
2323         * Simulates the firing of all ajax timer behaviors on the page
2324         *
2325         * @param page
2326         *            the page which timers will be executed
2327         */
2328        public void executeAllTimerBehaviors(final MarkupContainer page)
2329        {
2330                // execute all timer behaviors for the page itself
2331                internalExecuteAllTimerBehaviors(page);
2332
2333                // and for all its children
2334                page.visitChildren(Component.class, new IVisitor<Component, Void>()
2335                {
2336                        @Override
2337                        public void component(final Component component, final IVisit<Void> visit)
2338                        {
2339                                internalExecuteAllTimerBehaviors(component);
2340                        }
2341                });
2342        }
2343
2344        private void internalExecuteAllTimerBehaviors(final Component component)
2345        {
2346                List<AbstractAjaxTimerBehavior> behaviors = component
2347                        .getBehaviors(AbstractAjaxTimerBehavior.class);
2348                for (AbstractAjaxTimerBehavior timer : behaviors)
2349                {
2350                        checkUsability(component, true);
2351
2352                        if (!timer.isStopped())
2353                        {
2354                                if (log.isDebugEnabled())
2355                                {
2356                                        log.debug("Triggering AjaxSelfUpdatingTimerBehavior: {}",
2357                                                component.getClassRelativePath());
2358                                }
2359
2360                                executeBehavior(timer);
2361                        }
2362                }
2363        }
2364
2365        /**
2366         * Simulates the firing of an Ajax event. You add an Ajax event to a <code>Component</code> by
2367         * using:
2368         *
2369         * <pre>
2370         *     ...
2371         *     component.add(new AjaxEventBehavior(&quot;ondblclick&quot;) {
2372         *         public void onEvent(AjaxRequestTarget) {}
2373         *     });
2374         *     ...
2375         * </pre>
2376         *
2377         * You can then test that the code inside <code>onEvent</code> actually does what it's supposed
2378         * to, using the <code>WicketTester</code>:
2379         *
2380         * <pre>
2381         *     ...
2382         *     tester.executeAjaxEvent(component, &quot;ondblclick&quot;);
2383         *     // Test that the code inside onEvent is correct.
2384         *     ...
2385         * </pre>
2386         *
2387         * This also works with <code>AjaxFormSubmitBehavior</code>, where it will "submit" the
2388         * <code>Form</code> before executing the command.
2389         * <p>
2390         * PLEASE NOTE! This method doesn't actually insert the <code>Component</code> in the client DOM
2391         * tree, using JavaScript.
2392         *
2393         * @param component
2394         *            the <code>Component</code> that has the <code>AjaxEventBehavior</code> we want to
2395         *            test. If the <code>Component</code> is <code>null</code>, the test will fail.
2396         * @param event
2397         *            the event to simulate being fired. If <code>event</code> is <code>null</code>, the
2398         *            test will fail.
2399         */
2400        public void executeAjaxEvent(final Component component, final String event)
2401        {
2402                Args.notNull(component, "component");
2403                Args.notNull(event, "event");
2404
2405                checkUsability(component, true);
2406
2407                List<AjaxEventBehavior> ajaxEventBehaviors = WicketTesterHelper
2408                        .findAjaxEventBehaviors(component, event);
2409                for (AjaxEventBehavior ajaxEventBehavior : ajaxEventBehaviors)
2410                {
2411                        executeBehavior(ajaxEventBehavior);
2412                }
2413        }
2414
2415        /**
2416         * Retrieves a <code>TagTester</code> based on a <code>wicket:id</code>. If more
2417         * <code>Component</code>s exist with the same <code>wicket:id</code> in the markup, only the
2418         * first one is returned.
2419         *
2420         * @param wicketId
2421         *            the <code>wicket:id</code> to search for
2422         * @return the <code>TagTester</code> for the tag which has the given <code>wicket:id</code>
2423         */
2424        public TagTester getTagByWicketId(String wicketId)
2425        {
2426                return TagTester.createTagByAttribute(getLastResponseAsString(), "wicket:id", wicketId);
2427        }
2428
2429        /**
2430         * Modified version of BaseWicketTester#getTagByWicketId(String) that returns all matching tags
2431         * instead of just the first.
2432         *
2433         * @param wicketId
2434         * @return List of Tags
2435         */
2436        public List<TagTester> getTagsByWicketId(String wicketId)
2437        {
2438                return TagTester.createTagsByAttribute(getLastResponseAsString(), "wicket:id", wicketId,
2439                        false);
2440        }
2441
2442        /**
2443         * Retrieves a <code>TagTester</code> based on an DOM id. If more <code>Component</code>s exist
2444         * with the same id in the markup, only the first one is returned.
2445         *
2446         * @param id
2447         *            the DOM id to search for.
2448         * @return the <code>TagTester</code> for the tag which has the given DOM id
2449         */
2450        public TagTester getTagById(String id)
2451        {
2452                return TagTester.createTagByAttribute(getLastResponseAsString(), "id", id);
2453        }
2454
2455        /**
2456         * Helper method for all the places where an Ajax call should submit an associated
2457         * <code>Form</code>.
2458         *
2459         * @param component
2460         *            The component the behavior is attached to
2461         * @param behavior
2462         *            The <code>AjaxFormSubmitBehavior</code> with the <code>Form</code> to "submit"
2463         */
2464        private void submitAjaxFormSubmitBehavior(final Component component,
2465                AjaxFormSubmitBehavior behavior)
2466        {
2467                // The form that needs to be "submitted".
2468                Form<?> form = behavior.getForm();
2469                assertNotNull(form, "No form attached to the submitlink.");
2470
2471                checkUsability(form, true);
2472                serializeFormToRequest(form);
2473                executeBehavior(behavior);
2474        }
2475
2476        /**
2477         * Puts all not already scheduled (e.g. via {@link FormTester#setValue(String, String)}) form
2478         * component values in the post parameters for the next form submit
2479         *
2480         * @param form
2481         *            the {@link Form} which components should be submitted
2482         */
2483        private void serializeFormToRequest(final Form<?> form)
2484        {
2485                final MockRequestParameters postParameters = request.getPostParameters();
2486                final Set<String> currentParameterNamesSet = postParameters.getParameterNames();
2487
2488                form.visitFormComponents(new IVisitor<FormComponent<?>, Void>()
2489                {
2490                        @Override
2491                        public void component(final FormComponent<?> formComponent, final IVisit<Void> visit)
2492                        {
2493                                final String inputName = formComponent.getInputName();
2494                                if (!currentParameterNamesSet.contains(inputName))
2495                                {
2496                                        String[] values = FormTester.getInputValue(formComponent);
2497                                        for (String value : values)
2498                                        {
2499                                                postParameters.addParameterValue(inputName, value);
2500                                        }
2501                                }
2502                        }
2503                });
2504        }
2505
2506        /**
2507         * Retrieves the content type from the response header.
2508         *
2509         * @return the content type from the response header
2510         */
2511        public String getContentTypeFromResponseHeader()
2512        {
2513                String contentType = getLastResponse().getContentType();
2514                assertNotNull("No Content-Type header found", contentType);
2515                return contentType;
2516        }
2517
2518        /**
2519         * Retrieves the content length from the response header.
2520         *
2521         * @return the content length from the response header
2522         */
2523        public int getContentLengthFromResponseHeader()
2524        {
2525                String contentLength = getLastResponse().getHeader("Content-Length");
2526                assertNotNull("No Content-Length header found", contentLength);
2527                return Integer.parseInt(contentLength);
2528        }
2529
2530        /**
2531         * Retrieves the last-modified value from the response header.
2532         *
2533         * @return the last-modified value from the response header
2534         */
2535        public String getLastModifiedFromResponseHeader()
2536        {
2537                return getLastResponse().getHeader("Last-Modified");
2538        }
2539
2540        /**
2541         * Retrieves the content disposition from the response header.
2542         *
2543         * @return the content disposition from the response header
2544         */
2545        public String getContentDispositionFromResponseHeader()
2546        {
2547                return getLastResponse().getHeader("Content-Disposition");
2548        }
2549
2550        /**
2551         * Rebuilds {@link ServletWebRequest} used by wicket from the mock request used to build
2552         * requests. Sometimes this method is useful when changes need to be checked without processing
2553         * a request.
2554         */
2555        public void applyRequest()
2556        {
2557                Request req = newServletWebRequest();
2558                requestCycle.setRequest(req);
2559                if (useRequestUrlAsBase)
2560                {
2561                        requestCycle.getUrlRenderer().setBaseUrl(req.getUrl());
2562                }
2563        }
2564
2565        /**
2566         *
2567         * @param message
2568         * @param condition
2569         * @return fail with message if false
2570         */
2571        private Result isTrue(String message, boolean condition)
2572        {
2573                if (condition)
2574                {
2575                        return Result.pass();
2576                }
2577                return Result.fail(message);
2578        }
2579
2580        /**
2581         *
2582         * @param message
2583         * @param condition
2584         * @return fail with message if true
2585         */
2586        private Result isFalse(String message, boolean condition)
2587        {
2588                if (!condition)
2589                {
2590                        return Result.pass();
2591                }
2592                return Result.fail(message);
2593        }
2594
2595        /**
2596         *
2597         * @param expected
2598         * @param actual
2599         * @return fail with message if not equal
2600         */
2601        protected final Result isEqual(Object expected, Object actual)
2602        {
2603                if (expected == null && actual == null)
2604                {
2605                        return Result.pass();
2606                }
2607                if (expected != null && expected.equals(actual))
2608                {
2609                        return Result.pass();
2610                }
2611                String message = "expected:<" + expected + "> but was:<" + actual + ">";
2612                return Result.fail(message);
2613        }
2614
2615        /**
2616         *
2617         * @param message
2618         * @param object
2619         */
2620        private void notNull(String message, Object object)
2621        {
2622                if (object == null)
2623                {
2624                        fail(message);
2625                }
2626        }
2627
2628        /**
2629         * Checks whether a component is visible and/or enabled before usage
2630         *
2631         * @param component
2632         * @param throwException
2633         * @return result
2634         */
2635        protected Result checkUsability(final Component component, boolean throwException)
2636        {
2637                Result res = Result.pass();
2638
2639                if (component.isVisibleInHierarchy() == false)
2640                {
2641                        res = Result.fail(
2642                                "The component is currently not visible in the hierarchy and thus you can not be used." +
2643                                        " Component: " + component);
2644                }
2645
2646                if (component.isEnabledInHierarchy() == false)
2647                {
2648                        res = Result.fail(
2649                                "The component is currently not enabled in the hierarchy and thus you can not be used." +
2650                                        " Component: " + component);
2651                }
2652
2653                if (throwException && res.wasFailed())
2654                {
2655                        throw new AssertionError(res.getMessage());
2656                }
2657                return res;
2658        }
2659
2660        /**
2661         * @return request cycle
2662         */
2663        public RequestCycle getRequestCycle()
2664        {
2665                return requestCycle;
2666        }
2667
2668        /**
2669         * @return servlet response
2670         */
2671        public MockHttpServletResponse getResponse()
2672        {
2673                return response;
2674        }
2675
2676        /**
2677         * @return last request
2678         */
2679        public MockHttpServletRequest getLastRequest()
2680        {
2681                return lastRequest;
2682        }
2683
2684        /**
2685         * @return true, if exceptions are exposed
2686         */
2687        public boolean isExposeExceptions()
2688        {
2689                return exposeExceptions;
2690        }
2691
2692        /**
2693         * @param exposeExceptions
2694         */
2695        public void setExposeExceptions(boolean exposeExceptions)
2696        {
2697                this.exposeExceptions = exposeExceptions;
2698        }
2699
2700        /**
2701         * @return useRequestUrlAsBase
2702         */
2703        public boolean isUseRequestUrlAsBase()
2704        {
2705                return useRequestUrlAsBase;
2706        }
2707
2708        /**
2709         * @param setBaseUrl
2710         */
2711        public void setUseRequestUrlAsBase(boolean setBaseUrl)
2712        {
2713                useRequestUrlAsBase = setBaseUrl;
2714        }
2715
2716        /**
2717         * Starts a page, a shared resource or a {@link IRequestListener} depending on what the
2718         * {@link IRequestMapper}s resolve for the passed url.
2719         *
2720         * @param _url
2721         *            the url to resolve and execute
2722         */
2723        public void executeUrl(final String _url)
2724        {
2725                Url url = Url.parse(_url, Charset.forName(request.getCharacterEncoding()));
2726                transform(url);
2727                getRequest().setUrl(url);
2728                processRequest();
2729        }
2730
2731        /**
2732         * A page that is used as the automatically created page for
2733         * {@link BaseWicketTester#startComponentInPage(Class)} and the other variations.
2734         * <p>
2735         * This page caches the generated markup so that it is available even after
2736         * {@link Component#detach()} where the {@link Component#markup component's markup cache} is
2737         * cleared.
2738         */
2739        public static class StartComponentInPage extends WebPage
2740        {
2741                private transient IMarkupFragment pageMarkup = null;
2742
2743                /**
2744                 * Construct.
2745                 */
2746                public StartComponentInPage()
2747                {
2748                        setStatelessHint(false);
2749                }
2750
2751                @Override
2752                public IMarkupFragment getMarkup()
2753                {
2754                        IMarkupFragment calculatedMarkup = null;
2755                        if (pageMarkup == null)
2756                        {
2757                                IMarkupFragment markup = super.getMarkup();
2758                                if (markup != null && markup != Markup.NO_MARKUP)
2759                                {
2760                                        calculatedMarkup = markup;
2761                                        pageMarkup = markup;
2762                                }
2763                        }
2764                        else
2765                        {
2766                                calculatedMarkup = pageMarkup;
2767                        }
2768
2769                        return calculatedMarkup;
2770                }
2771
2772                /**
2773                 * @param markup
2774                 */
2775                public void setPageMarkup(IMarkupFragment markup)
2776                {
2777                        setMarkup(markup);
2778                        pageMarkup = markup;
2779                }
2780        }
2781
2782        private static class TestPageManagerProvider implements IPageManagerProvider
2783        {
2784                @Override
2785                public IPageManager get()
2786                {
2787                        return new MockPageManager();
2788                }
2789        }
2790
2791        private static class WicketTesterServletWebResponse extends ServletWebResponse
2792                implements
2793                        IMetaDataBufferingWebResponse
2794        {
2795                private List<Cookie> cookies = new ArrayList<Cookie>();
2796
2797                private WicketTesterServletWebResponse(ServletWebRequest request,
2798                        MockHttpServletResponse response)
2799                {
2800                        super(request, response);
2801                }
2802
2803                @Override
2804                public void addCookie(Cookie cookie)
2805                {
2806                        super.addCookie(cookie);
2807                        cookies.add(cookie);
2808                }
2809
2810                @Override
2811                public void writeMetaData(WebResponse webResponse)
2812                {
2813                        for (Cookie cookie : cookies)
2814                        {
2815                                webResponse.addCookie(cookie);
2816                        }
2817                }
2818
2819                @Override
2820                public void sendRedirect(String url)
2821                {
2822                        super.sendRedirect(url);
2823                        try
2824                        {
2825                                getContainerResponse().sendRedirect(url);
2826                        }
2827                        catch (IOException e)
2828                        {
2829                                throw new RuntimeException(e);
2830                        }
2831                }
2832        }
2833
2834        private class LastPageRecordingPageRendererProvider implements IPageRendererProvider
2835        {
2836                private final IPageRendererProvider delegate;
2837
2838                private Page lastPage;
2839
2840                private LastPageRecordingPageRendererProvider(IPageRendererProvider delegate)
2841                {
2842                        this.delegate = delegate;
2843                }
2844
2845                @Override
2846                public PageRenderer apply(final RenderPageRequestHandler handler)
2847                {
2848                        return new PageRenderer(handler)
2849                        {
2850                                @Override
2851                                public void respond(RequestCycle requestCycle)
2852                                {
2853                                        delegate.apply(handler).respond(requestCycle);
2854
2855                                        // WICKET-5424 record page after wrapped renderer has responded
2856                                        if (handler.getPageProvider().hasPageInstance())
2857                                        {
2858                                                Page renderedPage = (Page)handler.getPageProvider().getPageInstance();
2859                                                if (componentInPage != null && lastPage != null && renderedPage != null &&
2860                                                        lastPage.getPageClass() != renderedPage.getPageClass())
2861                                                {
2862                                                        // WICKET-3913: reset startComponent if a new page
2863                                                        // type is rendered
2864                                                        componentInPage = null;
2865                                                }
2866                                                lastRenderedPage = lastPage = renderedPage;
2867                                        }
2868                                        else
2869                                        {
2870                                                lastRenderedPage = null;
2871                                        }
2872                                }
2873                        };
2874                }
2875        }
2876
2877        private class TestExceptionMapper implements IExceptionMapper
2878        {
2879                private final IExceptionMapper delegate;
2880
2881                private TestExceptionMapper(IExceptionMapper delegate)
2882                {
2883                        this.delegate = delegate;
2884                }
2885
2886                @Override
2887                public IRequestHandler map(Exception e)
2888                {
2889                        if (exposeExceptions)
2890                        {
2891                                if (e instanceof RuntimeException)
2892                                {
2893                                        throw (RuntimeException)e;
2894                                }
2895                                else
2896                                {
2897                                        throw new WicketRuntimeException(e);
2898                                }
2899                        }
2900                        else
2901                        {
2902                                return delegate.map(e);
2903                        }
2904                }
2905        }
2906
2907        private class TestRequestCycleProvider implements IRequestCycleProvider
2908        {
2909                private final IRequestCycleProvider delegate;
2910
2911                private TestRequestCycleProvider(IRequestCycleProvider delegate)
2912                {
2913                        this.delegate = delegate;
2914                }
2915
2916                @Override
2917                public RequestCycle apply(RequestCycleContext context)
2918                {
2919                        context.setRequestMapper(new TestRequestMapper(context.getRequestMapper()));
2920                        forcedHandler = null;
2921                        context.setExceptionMapper(new TestExceptionMapper(context.getExceptionMapper()));
2922                        return delegate.apply(context);
2923                }
2924        }
2925
2926        private class TestRequestMapper implements IRequestMapperDelegate
2927        {
2928                private final IRequestMapper delegate;
2929
2930                private TestRequestMapper(IRequestMapper delegate)
2931                {
2932                        this.delegate = delegate;
2933                }
2934
2935                @Override
2936                public IRequestMapper getDelegateMapper()
2937                {
2938                        return delegate;
2939                }
2940
2941                @Override
2942                public int getCompatibilityScore(Request request)
2943                {
2944                        return delegate.getCompatibilityScore(request);
2945                }
2946
2947                @Override
2948                public Url mapHandler(IRequestHandler requestHandler)
2949                {
2950                        return delegate.mapHandler(requestHandler);
2951                }
2952
2953                @Override
2954                public IRequestHandler mapRequest(Request request)
2955                {
2956                        if (forcedHandler != null)
2957                        {
2958                                IRequestHandler handler = forcedHandler;
2959                                forcedHandler = null;
2960                                return handler;
2961                        }
2962                        else
2963                        {
2964                                return delegate.mapRequest(request);
2965                        }
2966                }
2967        }
2968
2969        public class TestFilterConfig implements FilterConfig
2970        {
2971                private final Map<String, String> initParameters = new HashMap<String, String>();
2972
2973                private TestFilterConfig()
2974                {
2975                        initParameters.put(WicketFilter.FILTER_MAPPING_PARAM, "/servlet/*");
2976                }
2977
2978                @Override
2979                public String getFilterName()
2980                {
2981                        return getClass().getName();
2982                }
2983
2984                @Override
2985                public ServletContext getServletContext()
2986                {
2987                        return servletContext;
2988                }
2989
2990                @Override
2991                public String getInitParameter(String s)
2992                {
2993                        return initParameters.get(s);
2994                }
2995
2996                @Override
2997                public Enumeration<String> getInitParameterNames()
2998                {
2999                        throw new UnsupportedOperationException("Not implemented");
3000                }
3001        }
3002}