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