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("ondblclick") { 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, "ondblclick"); 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}