Coverage Report - org.apache.tapestry5.test.PageTester
 
Classes in this File Line Coverage Branch Coverage Complexity
PageTester
86%
103/120
61%
23/38
0
PageTester$1
56%
15/27
50%
12/24
0
 
 1  
 // Copyright 2006, 2007, 2008, 2009 The Apache Software Foundation
 2  
 //
 3  
 // Licensed under the Apache License, Version 2.0 (the "License");
 4  
 // you may not use this file except in compliance with the License.
 5  
 // You may obtain a copy of the License at
 6  
 //
 7  
 //     http://www.apache.org/licenses/LICENSE-2.0
 8  
 //
 9  
 // Unless required by applicable law or agreed to in writing, software
 10  
 // distributed under the License is distributed on an "AS IS" BASIS,
 11  
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  
 // See the License for the specific language governing permissions and
 13  
 // limitations under the License.
 14  
 
 15  
 package org.apache.tapestry5.test;
 16  
 
 17  
 import org.apache.tapestry5.Link;
 18  
 import org.apache.tapestry5.dom.Document;
 19  
 import org.apache.tapestry5.dom.Element;
 20  
 import org.apache.tapestry5.dom.Visitor;
 21  
 import org.apache.tapestry5.internal.InternalConstants;
 22  
 import org.apache.tapestry5.internal.SingleKeySymbolProvider;
 23  
 import org.apache.tapestry5.internal.TapestryAppInitializer;
 24  
 import org.apache.tapestry5.internal.test.PageTesterContext;
 25  
 import org.apache.tapestry5.internal.test.PageTesterModule;
 26  
 import org.apache.tapestry5.internal.test.TestableRequest;
 27  
 import org.apache.tapestry5.internal.test.TestableResponse;
 28  
 import org.apache.tapestry5.ioc.Registry;
 29  
 import org.apache.tapestry5.ioc.def.ModuleDef;
 30  
 import org.apache.tapestry5.ioc.internal.util.Defense;
 31  
 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
 32  
 import org.apache.tapestry5.ioc.services.SymbolProvider;
 33  
 import org.apache.tapestry5.services.ApplicationGlobals;
 34  
 import org.apache.tapestry5.services.RequestHandler;
 35  
 import org.slf4j.Logger;
 36  
 import org.slf4j.LoggerFactory;
 37  
 
 38  
 import java.io.IOException;
 39  
 import java.util.Locale;
 40  
 import java.util.Map;
 41  
 
 42  
 /**
 43  
  * This class is used to run a Tapestry app in a single-threaded, in-process testing environment. You can ask it to
 44  
  * render a certain page and check the DOM object created. You can also ask it to click on a link element in the DOM
 45  
  * object to get the next page. Because no servlet container is required, it is very fast and you can directly debug
 46  
  * into your code in your IDE.
 47  
  */
 48  36
 public class PageTester
 49  
 {
 50  
     @SuppressWarnings({ "FieldCanBeLocal" })
 51  46
     private final Logger logger = LoggerFactory.getLogger(PageTester.class);
 52  
 
 53  
     private final Registry registry;
 54  
 
 55  
     private final TestableRequest request;
 56  
 
 57  
     private final TestableResponse response;
 58  
 
 59  
     // private final StrategyRegistry<ComponentInvoker> invokerRegistry;
 60  
 
 61  
     private final RequestHandler requestHandler;
 62  
 
 63  
     public static final String DEFAULT_CONTEXT_PATH = "src/main/webapp";
 64  
 
 65  
     private static final String DEFAULT_SUBMIT_VALUE_ATTRIBUTE = "Submit Query";
 66  
 
 67  
     /**
 68  
      * Initializes a PageTester without overriding any services and assuming that the context root is in
 69  
      * src/main/webapp.
 70  
      *
 71  
      * @see #PageTester(String, String, String, Class[])
 72  
      */
 73  
     public PageTester(String appPackage, String appName)
 74  
     {
 75  38
         this(appPackage, appName, DEFAULT_CONTEXT_PATH);
 76  38
     }
 77  
 
 78  
     /**
 79  
      * Initializes a PageTester that acts as a browser and a servlet container to test drive your Tapestry pages.
 80  
      *
 81  
      * @param appPackage    The same value you would specify using the tapestry.app-package context parameter. As this
 82  
      *                      testing environment is not run in a servlet container, you need to specify it.
 83  
      * @param appName       The same value you would specify as the filter name. It is used to form the name of the
 84  
      *                      module class for your app. If you don't have one, pass an empty string.
 85  
      * @param contextPath   The path to the context root so that Tapestry can find the templates (if they're put
 86  
      *                      there).
 87  
      * @param moduleClasses Classes of additional modules to load
 88  
      */
 89  
     public PageTester(String appPackage, String appName, String contextPath, Class... moduleClasses)
 90  46
     {
 91  46
         Defense.notBlank(appPackage, "appPackage");
 92  46
         Defense.notBlank(appName, "appName");
 93  46
         Defense.notBlank(contextPath, "contextPath");
 94  
 
 95  46
         SymbolProvider provider = new SingleKeySymbolProvider(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM, appPackage);
 96  
 
 97  46
         TapestryAppInitializer initializer = new TapestryAppInitializer(logger, provider, appName,
 98  
                                                                         PageTesterModule.TEST_MODE);
 99  
 
 100  46
         initializer.addModules(PageTesterModule.class);
 101  46
         initializer.addModules(moduleClasses);
 102  46
         initializer.addModules(provideExtraModuleDefs());
 103  
 
 104  46
         registry = initializer.createRegistry();
 105  
 
 106  46
         request = registry.getService(TestableRequest.class);
 107  46
         response = registry.getService(TestableResponse.class);
 108  
 
 109  46
         ApplicationGlobals globals = registry.getObject(ApplicationGlobals.class, null);
 110  
 
 111  46
         globals.storeContext(new PageTesterContext(contextPath));
 112  
 
 113  46
         requestHandler = registry.getService("RequestHandler", RequestHandler.class);
 114  
 
 115  46
         request.setLocale(Locale.ENGLISH);
 116  46
     }
 117  
 
 118  
     /**
 119  
      * Overridden in subclasses to provide additional module definitions beyond those normally located. This
 120  
      * implementation returns an empty array.
 121  
      */
 122  
     protected ModuleDef[] provideExtraModuleDefs()
 123  
     {
 124  46
         return new ModuleDef[0];
 125  
     }
 126  
 
 127  
 
 128  
     /**
 129  
      * Invoke this method when done using the PageTester; it shuts down the internal {@link
 130  
      * org.apache.tapestry5.ioc.Registry} used by the tester.
 131  
      */
 132  
     public void shutdown()
 133  
     {
 134  36
         registry.shutdown();
 135  36
     }
 136  
 
 137  
 
 138  
     /**
 139  
      * Returns the Registry that was created for the application.
 140  
      */
 141  
     public Registry getRegistry()
 142  
     {
 143  2
         return registry;
 144  
     }
 145  
 
 146  
     /**
 147  
      * Allows a service to be retrieved via its service interface.  Use {@link #getRegistry()} for more complicated
 148  
      * queries.
 149  
      *
 150  
      * @param serviceInterface used to select the service
 151  
      */
 152  
     public <T> T getService(Class<T> serviceInterface)
 153  
     {
 154  2
         return registry.getService(serviceInterface);
 155  
     }
 156  
 
 157  
     /**
 158  
      * Renders a page specified by its name.
 159  
      *
 160  
      * @param pageName The name of the page to be rendered.
 161  
      * @return The DOM created. Typically you will assert against it.
 162  
      */
 163  
     public Document renderPage(String pageName)
 164  
     {
 165  50
         request.clear().setPath("/" + pageName);
 166  
 
 167  
         while (true)
 168  
         {
 169  
             try
 170  
             {
 171  50
                 response.clear();
 172  
 
 173  50
                 boolean handled = requestHandler.service(request, response);
 174  
 
 175  50
                 if (!handled)
 176  
                 {
 177  0
                     throw new RuntimeException(
 178  
                             String.format("Request was not handled: '%s' may not be a valid page name.", pageName));
 179  
                 }
 180  
 
 181  50
                 Link link = response.getRedirectLink();
 182  
 
 183  50
                 if (link != null)
 184  
                 {
 185  0
                     setupRequestFromLink(link);
 186  0
                     continue;
 187  
                 }
 188  
 
 189  50
                 Document result = response.getRenderedDocument();
 190  
 
 191  50
                 if (result == null)
 192  0
                     throw new RuntimeException(
 193  
                             String.format("Render of page '%s' did not result in a Document.", pageName));
 194  
 
 195  
 
 196  50
                 return result;
 197  
             }
 198  0
             catch (IOException ex)
 199  
             {
 200  0
                 throw new RuntimeException(ex);
 201  
             }
 202  
 
 203  
         }
 204  
 
 205  
     }
 206  
 
 207  
     /**
 208  
      * Simulates a click on a link.
 209  
      *
 210  
      * @param linkElement The Link object to be "clicked" on.
 211  
      * @return The DOM created. Typically you will assert against it.
 212  
      */
 213  
 
 214  
     public Document clickLink(Element linkElement)
 215  
     {
 216  2
         Defense.notNull(linkElement, "link");
 217  
 
 218  2
         validateElementName(linkElement, "a");
 219  
 
 220  2
         String href = extractNonBlank(linkElement, "href");
 221  
 
 222  2
         setupRequestFromURI(href);
 223  
 
 224  2
         return runComponentEventRequest();
 225  
     }
 226  
 
 227  
     private String extractNonBlank(Element element, String attributeName)
 228  
     {
 229  42
         String result = element.getAttribute(attributeName);
 230  
 
 231  42
         if (InternalUtils.isBlank(result))
 232  0
             throw new RuntimeException(
 233  
                     String.format("The %s attribute of the <%s> element was blank or missing.",
 234  
                                   element.getName(), attributeName));
 235  
 
 236  42
         return result;
 237  
     }
 238  
 
 239  
     private void validateElementName(Element element, String expectedElementName)
 240  
     {
 241  6
         if (!element.getName().equalsIgnoreCase(expectedElementName))
 242  0
             throw new RuntimeException(
 243  
                     String.format("The element must be type '%s', not '%s'.", expectedElementName, element.getName()));
 244  6
     }
 245  
 
 246  
     private Document runComponentEventRequest()
 247  
     {
 248  
         while (true)
 249  
         {
 250  22
             response.clear();
 251  
 
 252  
             try
 253  
             {
 254  22
                 boolean handled = requestHandler.service(request, response);
 255  
 
 256  22
                 if (!handled)
 257  0
                     throw new RuntimeException(String.format("Request for path '%s' was not handled by Tapestry.",
 258  
                                                              request.getPath()));
 259  
 
 260  22
                 Link link = response.getRedirectLink();
 261  
 
 262  22
                 if (link != null)
 263  
                 {
 264  12
                     setupRequestFromLink(link);
 265  12
                     continue;
 266  
                 }
 267  
 
 268  10
                 Document result = response.getRenderedDocument();
 269  
 
 270  10
                 if (result == null)
 271  0
                     throw new RuntimeException(
 272  
                             String.format("Render request '%s' did not result in a Document.", request.getPath()));
 273  
 
 274  10
                 return result;
 275  
             }
 276  0
             catch (IOException ex)
 277  
             {
 278  0
                 throw new RuntimeException(ex);
 279  
             }
 280  
 
 281  
         }
 282  
 
 283  
 
 284  
     }
 285  
 
 286  
     private void setupRequestFromLink(Link link)
 287  
     {
 288  12
         setupRequestFromURI(link.toRedirectURI());
 289  12
     }
 290  
 
 291  
     private void setupRequestFromURI(String URI)
 292  
     {
 293  14
         String linkPath = stripContextFromPath(URI);
 294  
 
 295  14
         int comma = linkPath.indexOf('?');
 296  
 
 297  14
         String path = comma < 0
 298  
                       ? linkPath
 299  
                       : linkPath.substring(0, comma);
 300  
 
 301  14
         request.clear().setPath(path);
 302  
 
 303  14
         if (comma > 0)
 304  0
             decodeParametersIntoRequest(path.substring(comma + 1));
 305  14
     }
 306  
 
 307  
     private void decodeParametersIntoRequest(String queryString)
 308  
     {
 309  0
         if (InternalUtils.isNonBlank(queryString))
 310  0
             throw new RuntimeException("Have not yet implemented this method.");
 311  0
     }
 312  
 
 313  
     private String stripContextFromPath(String path)
 314  
     {
 315  22
         String contextPath = request.getContextPath();
 316  
 
 317  22
         if (contextPath.equals("")) return path;
 318  
 
 319  22
         if (!path.startsWith(contextPath))
 320  0
             throw new RuntimeException(String.format("Path '%s' does not start with context path '%s'.",
 321  
                                                      path, contextPath));
 322  
 
 323  22
         return path.substring(contextPath.length());
 324  
     }
 325  
 
 326  
     /**
 327  
      * Simulates a submission of the form specified. The caller can specify values for the form fields, which act as
 328  
      * overrides on the values stored inside the elements.
 329  
      *
 330  
      * @param form       the form to be submitted.
 331  
      * @param parameters the query parameter name/value pairs
 332  
      * @return The DOM created. Typically you will assert against it.
 333  
      */
 334  
     public Document submitForm(Element form, Map<String, String> parameters)
 335  
     {
 336  4
         Defense.notNull(form, "form");
 337  
 
 338  4
         validateElementName(form, "form");
 339  
 
 340  4
         request.clear().setPath(stripContextFromPath(extractNonBlank(form, "action")));
 341  
 
 342  4
         pushFieldValuesIntoRequest(form);
 343  
 
 344  4
         overrideParameters(parameters);
 345  
 
 346  
         // addHiddenFormFields(form);
 347  
 
 348  
         // ComponentInvocation invocation = getInvocation(form);
 349  
 
 350  4
         return runComponentEventRequest();
 351  
     }
 352  
 
 353  
     private void overrideParameters(Map<String, String> fieldValues)
 354  
     {
 355  8
         for (Map.Entry<String, String> e : fieldValues.entrySet())
 356  
         {
 357  6
             request.overrideParameter(e.getKey(), e.getValue());
 358  
         }
 359  8
     }
 360  
 
 361  
     private void pushFieldValuesIntoRequest(Element form)
 362  
     {
 363  8
         Visitor visitor = new Visitor()
 364  
         {
 365  8
             public void visit(Element element)
 366  
             {
 367  42
                 if (InternalUtils.isNonBlank(element.getAttribute("disabled")))
 368  0
                     return;
 369  
 
 370  42
                 String name = element.getName();
 371  
 
 372  42
                 if (name.equals("input"))
 373  
                 {
 374  20
                     String type = extractNonBlank(element, "type");
 375  
 
 376  20
                     if (type.equals("radio") || type.equals("checkbox"))
 377  
                     {
 378  0
                         if (InternalUtils.isBlank(element.getAttribute("checked")))
 379  0
                             return;
 380  
                     }
 381  
 
 382  
                     // Assume that, if the element is a button/submit, it wasn't clicked,
 383  
                     // and therefore, is not part of the submission.
 384  
 
 385  20
                     if (type.equals("button") || type.equals("submit"))
 386  6
                         return;
 387  
 
 388  
                     // Handle radio, checkbox, text, radio, hidden
 389  14
                     String value = element.getAttribute("value");
 390  
 
 391  14
                     if (InternalUtils.isNonBlank(value))
 392  8
                         request.loadParameter(extractNonBlank(element, "name"), value);
 393  
 
 394  14
                     return;
 395  
                 }
 396  
 
 397  22
                 if (name.equals("option"))
 398  
                 {
 399  0
                     String value = element.getAttribute("value");
 400  
 
 401  
                     // TODO: If value is blank do we use the content, or is the content only the label?
 402  
 
 403  0
                     if (InternalUtils.isNonBlank(element.getAttribute("selected")))
 404  
                     {
 405  0
                         String selectName = extractNonBlank(findAncestor(element, "select"), "name");
 406  
 
 407  0
                         request.loadParameter(selectName, value);
 408  
                     }
 409  
 
 410  0
                     return;
 411  
                 }
 412  
 
 413  22
                 if (name.equals("textarea"))
 414  
                 {
 415  0
                     String content = element.getChildMarkup();
 416  
 
 417  0
                     if (InternalUtils.isNonBlank(content))
 418  0
                         request.loadParameter(extractNonBlank(element, "name"), content);
 419  
 
 420  0
                     return;
 421  
                 }
 422  22
             }
 423  
         };
 424  
 
 425  8
         form.visit(visitor);
 426  8
     }
 427  
 
 428  
     /**
 429  
      * Simulates a submission of the form by clicking the specified submit button. The caller can specify values for the
 430  
      * form fields.
 431  
      *
 432  
      * @param submitButton the submit button to be clicked.
 433  
      * @param fieldValues  the field values keyed on field names.
 434  
      * @return The DOM created. Typically you will assert against it.
 435  
      */
 436  
     public Document clickSubmit(Element submitButton, Map<String, String> fieldValues)
 437  
     {
 438  8
         Defense.notNull(submitButton, "submitButton");
 439  
 
 440  8
         assertIsSubmit(submitButton);
 441  
 
 442  6
         Element form = getFormAncestor(submitButton);
 443  
 
 444  4
         request.clear().setPath(stripContextFromPath(extractNonBlank(form, "action")));
 445  
 
 446  4
         pushFieldValuesIntoRequest(form);
 447  
 
 448  4
         overrideParameters(fieldValues);
 449  
 
 450  4
         String value = submitButton.getAttribute("value");
 451  
 
 452  4
         if (value == null)
 453  4
             value = DEFAULT_SUBMIT_VALUE_ATTRIBUTE;
 454  
 
 455  4
         request.overrideParameter(extractNonBlank(submitButton, "name"), value);
 456  
 
 457  4
         return runComponentEventRequest();
 458  
     }
 459  
 
 460  
     private void assertIsSubmit(Element element)
 461  
     {
 462  8
         if (element.getName().equals("input"))
 463  
         {
 464  8
             String type = element.getAttribute("type");
 465  
 
 466  8
             if ("submit".equals(type)) return;
 467  
         }
 468  
 
 469  2
         throw new IllegalArgumentException("The specified element is not a submit button.");
 470  
     }
 471  
 
 472  
     private Element getFormAncestor(Element element)
 473  
     {
 474  6
         return findAncestor(element, "form");
 475  
     }
 476  
 
 477  
     private Element findAncestor(Element element, String ancestorName)
 478  
     {
 479  6
         Element e = element;
 480  
 
 481  14
         while (e != null)
 482  
         {
 483  12
             if (e.getName().equalsIgnoreCase(ancestorName))
 484  4
                 return e;
 485  
 
 486  8
             e = e.getParent();
 487  
         }
 488  
 
 489  2
         throw new RuntimeException(
 490  
                 String.format("Could not locate an ancestor element of type '%s'.", ancestorName));
 491  
 
 492  
     }
 493  
 
 494  
     /**
 495  
      * Sets the simulated browser's preferred language, i.e., the value returned from {@link
 496  
      * org.apache.tapestry5.services.Request#getLocale()}.
 497  
      *
 498  
      * @param preferedLanguage preferred language setting
 499  
      */
 500  
     public void setPreferedLanguage(Locale preferedLanguage)
 501  
     {
 502  6
         request.setLocale(preferedLanguage);
 503  6
     }
 504  
 }