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