| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package org.apache.tapestry5.test; |
| |
| import org.apache.tapestry5.Link; |
| import org.apache.tapestry5.dom.Document; |
| import org.apache.tapestry5.dom.Element; |
| import org.apache.tapestry5.dom.Visitor; |
| import org.apache.tapestry5.internal.InternalConstants; |
| import org.apache.tapestry5.internal.SingleKeySymbolProvider; |
| import org.apache.tapestry5.internal.TapestryAppInitializer; |
| import org.apache.tapestry5.internal.test.PageTesterContext; |
| import org.apache.tapestry5.internal.test.PageTesterModule; |
| import org.apache.tapestry5.internal.test.TestableRequest; |
| import org.apache.tapestry5.internal.test.TestableResponse; |
| import org.apache.tapestry5.ioc.Registry; |
| import org.apache.tapestry5.ioc.def.ModuleDef; |
| import org.apache.tapestry5.ioc.internal.util.InternalUtils; |
| import org.apache.tapestry5.ioc.services.SymbolProvider; |
| import org.apache.tapestry5.services.ApplicationGlobals; |
| import org.apache.tapestry5.services.RequestHandler; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /** |
| * This class is used to run a Tapestry app in a single-threaded, in-process testing environment. |
| * You can ask it to render a certain page and check the DOM object created. You can also ask it to click on a link |
| * element in the DOM object to get the next page. Because no servlet container is required, it is very fast and you |
| * can directly debug into your code in your IDE. |
| * |
| * When using the PageTester in your tests, you should add the {@code org.apache.tapestry:tapestry-test-constants} |
| * module as a dependency. |
| */ |
| @SuppressWarnings("all") |
| public class PageTester |
| { |
| |
| private final Registry registry; |
| |
| private final TestableRequest request; |
| |
| private final TestableResponse response; |
| |
| private final RequestHandler requestHandler; |
| |
| public static final String DEFAULT_CONTEXT_PATH = "src/main/webapp"; |
| |
| private static final String DEFAULT_SUBMIT_VALUE_ATTRIBUTE = "Submit Query"; |
| |
| /** |
| * Initializes a PageTester without overriding any services and assuming that the context root |
| * is in |
| * src/main/webapp. |
| * |
| * @see #PageTester(String, String, String, Class[]) |
| */ |
| public PageTester(String appPackage, String appName) |
| { |
| this(appPackage, appName, DEFAULT_CONTEXT_PATH); |
| } |
| |
| /** |
| * Initializes a PageTester that acts as a browser and a servlet container to test drive your |
| * Tapestry pages. |
| * |
| * @param appPackage |
| * The same value you would specify using the tapestry.app-package context parameter. |
| * As this |
| * testing environment is not run in a servlet container, you need to specify it. |
| * @param appName |
| * The same value you would specify as the filter name. It is used to form the name |
| * of the |
| * module class for your app. If you don't have one, pass an empty string. |
| * @param contextPath |
| * The path to the context root so that Tapestry can find the templates (if they're |
| * put |
| * there). |
| * @param moduleClasses |
| * Classes of additional modules to load |
| */ |
| public PageTester(String appPackage, String appName, String contextPath, Class... moduleClasses) |
| { |
| assert InternalUtils.isNonBlank(appPackage); |
| assert appName != null; |
| assert InternalUtils.isNonBlank(contextPath); |
| |
| SymbolProvider provider = new SingleKeySymbolProvider(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM, appPackage); |
| |
| TapestryAppInitializer initializer = new TapestryAppInitializer(LoggerFactory.getLogger(PageTester.class), provider, appName, |
| null); |
| |
| initializer.addModules(PageTesterModule.class); |
| initializer.addModules(moduleClasses); |
| initializer.addModules(provideExtraModuleDefs()); |
| |
| registry = initializer.createRegistry(); |
| |
| request = registry.getService(TestableRequest.class); |
| response = registry.getService(TestableResponse.class); |
| |
| ApplicationGlobals globals = registry.getObject(ApplicationGlobals.class, null); |
| |
| globals.storeContext(new PageTesterContext(contextPath)); |
| |
| registry.performRegistryStartup(); |
| |
| requestHandler = registry.getService("RequestHandler", RequestHandler.class); |
| |
| request.setLocale(Locale.ENGLISH); |
| initializer.announceStartup(); |
| } |
| |
| /** |
| * Overridden in subclasses to provide additional module definitions beyond those normally |
| * located. This |
| * implementation returns an empty array. |
| */ |
| protected ModuleDef[] provideExtraModuleDefs() |
| { |
| return new ModuleDef[0]; |
| } |
| |
| /** |
| * Invoke this method when done using the PageTester; it shuts down the internal |
| * {@link org.apache.tapestry5.ioc.Registry} used by the tester. |
| */ |
| public void shutdown() |
| { |
| registry.cleanupThread(); |
| |
| registry.shutdown(); |
| } |
| |
| /** |
| * Returns the Registry that was created for the application. |
| */ |
| public Registry getRegistry() |
| { |
| return registry; |
| } |
| |
| /** |
| * Allows a service to be retrieved via its service interface. Use {@link #getRegistry()} for |
| * more complicated |
| * queries. |
| * |
| * @param serviceInterface |
| * used to select the service |
| */ |
| public <T> T getService(Class<T> serviceInterface) |
| { |
| return registry.getService(serviceInterface); |
| } |
| |
| /** |
| * Renders a page specified by its name. |
| * |
| * @param pageName |
| * The name of the page to be rendered. |
| * @return The DOM created. Typically you will assert against it. |
| */ |
| public Document renderPage(String pageName) |
| { |
| |
| renderPageAndReturnResponse(pageName); |
| |
| Document result = response.getRenderedDocument(); |
| |
| if (result == null) |
| throw new RuntimeException(String.format("Render of page '%s' did not result in a Document.", |
| pageName)); |
| |
| return result; |
| |
| } |
| |
| /** |
| * Renders a page specified by its name and returns the response. |
| * |
| * @param pageName |
| * The name of the page to be rendered. |
| * @return The response object to assert against |
| * @since 5.2.3 |
| */ |
| public TestableResponse renderPageAndReturnResponse(String pageName) |
| { |
| request.clear().setPath("/" + pageName); |
| |
| while (true) |
| { |
| try |
| { |
| response.clear(); |
| |
| boolean handled = requestHandler.service(request, response); |
| |
| if (!handled) |
| { |
| throw new RuntimeException(String.format( |
| "Request was not handled: '%s' may not be a valid page name.", pageName)); |
| } |
| |
| Link link = response.getRedirectLink(); |
| |
| if (link != null) |
| { |
| setupRequestFromLink(link); |
| continue; |
| } |
| |
| return response; |
| |
| } catch (IOException ex) |
| { |
| throw new RuntimeException(ex); |
| } finally |
| { |
| registry.cleanupThread(); |
| } |
| } |
| |
| } |
| |
| /** |
| * Simulates a click on a link. |
| * |
| * @param linkElement |
| * The Link object to be "clicked" on. |
| * @return The DOM created. Typically you will assert against it. |
| */ |
| public Document clickLink(Element linkElement) |
| { |
| clickLinkAndReturnResponse(linkElement); |
| |
| return getDocumentFromResponse(); |
| } |
| |
| /** |
| * Simulates a click on a link. |
| * |
| * @param linkElement |
| * The Link object to be "clicked" on. |
| * @return The response object to assert against |
| * @since 5.2.3 |
| */ |
| public TestableResponse clickLinkAndReturnResponse(Element linkElement) |
| { |
| assert linkElement != null; |
| |
| validateElementName(linkElement, "a"); |
| |
| String href = extractNonBlank(linkElement, "href"); |
| |
| setupRequestFromURI(href); |
| |
| return runComponentEventRequest(); |
| } |
| |
| private String extractNonBlank(Element element, String attributeName) |
| { |
| String result = element.getAttribute(attributeName); |
| |
| if (InternalUtils.isBlank(result)) |
| throw new RuntimeException(String.format("The %s attribute of the <%s> element was blank or missing.", |
| attributeName, element.getName())); |
| |
| return result; |
| } |
| |
| private void validateElementName(Element element, String expectedElementName) |
| { |
| if (!element.getName().equalsIgnoreCase(expectedElementName)) |
| throw new RuntimeException(String.format("The element must be type '%s', not '%s'.", expectedElementName, |
| element.getName())); |
| } |
| |
| private Document getDocumentFromResponse() |
| { |
| Document result = response.getRenderedDocument(); |
| |
| if (result == null) |
| throw new RuntimeException(String.format("Render request '%s' did not result in a Document.", request.getPath())); |
| |
| return result; |
| } |
| |
| private TestableResponse runComponentEventRequest() |
| { |
| while (true) |
| { |
| response.clear(); |
| |
| try |
| { |
| boolean handled = requestHandler.service(request, response); |
| |
| if (!handled) |
| throw new RuntimeException(String.format("Request for path '%s' was not handled by Tapestry.", |
| request.getPath())); |
| |
| Link link = response.getRedirectLink(); |
| |
| if (link != null) |
| { |
| setupRequestFromLink(link); |
| continue; |
| } |
| |
| return response; |
| } catch (IOException ex) |
| { |
| throw new RuntimeException(ex); |
| } finally |
| { |
| registry.cleanupThread(); |
| } |
| } |
| |
| } |
| |
| private void setupRequestFromLink(Link link) |
| { |
| setupRequestFromURI(link.toRedirectURI()); |
| } |
| |
| public void setupRequestFromURI(String URI) |
| { |
| String linkPath = stripContextFromPath(URI); |
| |
| int comma = linkPath.indexOf('?'); |
| |
| String path = comma < 0 ? linkPath : linkPath.substring(0, comma); |
| |
| request.clear().setPath(path); |
| |
| if (comma > 0) |
| decodeParametersIntoRequest(linkPath.substring(comma + 1)); |
| } |
| |
| private void decodeParametersIntoRequest(String queryString) |
| { |
| if (InternalUtils.isBlank(queryString)) |
| return; |
| |
| for (String term : queryString.split("&")) |
| { |
| int eqx = term.indexOf("="); |
| |
| String key = term.substring(0, eqx).trim(); |
| String value = term.substring(eqx + 1).trim(); |
| |
| request.loadParameter(key, value); |
| } |
| } |
| |
| private String stripContextFromPath(String path) |
| { |
| String contextPath = request.getContextPath(); |
| |
| if (contextPath.equals("")) |
| return path; |
| |
| if (!path.startsWith(contextPath)) |
| throw new RuntimeException(String.format("Path '%s' does not start with context path '%s'.", path, |
| contextPath)); |
| |
| return path.substring(contextPath.length()); |
| } |
| |
| /** |
| * Simulates a submission of the form specified. The caller can specify values for the form |
| * fields, which act as |
| * overrides on the values stored inside the elements. |
| * |
| * @param form |
| * the form to be submitted. |
| * @param parameters |
| * the query parameter name/value pairs |
| * @return The DOM created. Typically you will assert against it. |
| */ |
| public Document submitForm(Element form, Map<String, String> parameters) |
| { |
| submitFormAndReturnResponse(form, parameters); |
| |
| return getDocumentFromResponse(); |
| } |
| |
| /** |
| * Simulates a submission of the form specified. The caller can specify values for the form |
| * fields, which act as |
| * overrides on the values stored inside the elements. |
| * |
| * @param form |
| * the form to be submitted. |
| * @param parameters |
| * the query parameter name/value pairs |
| * @return The response object to assert against. |
| * @since 5.2.3 |
| */ |
| public TestableResponse submitFormAndReturnResponse(Element form, Map<String, String> parameters) |
| { |
| assert form != null; |
| |
| validateElementName(form, "form"); |
| |
| request.clear().setPath(stripContextFromPath(extractNonBlank(form, "action"))); |
| |
| pushFieldValuesIntoRequest(form); |
| |
| overrideParameters(parameters); |
| |
| // addHiddenFormFields(form); |
| |
| // ComponentInvocation invocation = getInvocation(form); |
| |
| return runComponentEventRequest(); |
| } |
| |
| private void overrideParameters(Map<String, String> fieldValues) |
| { |
| for (Map.Entry<String, String> e : fieldValues.entrySet()) |
| { |
| request.overrideParameter(e.getKey(), e.getValue()); |
| } |
| } |
| |
| private void pushFieldValuesIntoRequest(Element form) |
| { |
| Visitor visitor = new Visitor() |
| { |
| public void visit(Element element) |
| { |
| if (InternalUtils.isNonBlank(element.getAttribute("disabled"))) |
| return; |
| |
| String name = element.getName(); |
| |
| if (name.equals("input")) |
| { |
| String type = extractNonBlank(element, "type"); |
| |
| if (type.equals("radio") || type.equals("checkbox")) |
| { |
| if (InternalUtils.isBlank(element.getAttribute("checked"))) |
| return; |
| } |
| |
| // Assume that, if the element is a button/submit, it wasn't clicked, |
| // and therefore, is not part of the submission. |
| |
| if (type.equals("button") || type.equals("submit")) |
| return; |
| |
| // Handle radio, checkbox, text, radio, hidden |
| String value = element.getAttribute("value"); |
| |
| if (InternalUtils.isNonBlank(value)) |
| request.loadParameter(extractNonBlank(element, "name"), value); |
| |
| return; |
| } |
| |
| if (name.equals("option")) |
| { |
| String value = element.getAttribute("value"); |
| |
| // TODO: If value is blank do we use the content, or is the content only the |
| // label? |
| |
| if (InternalUtils.isNonBlank(element.getAttribute("selected"))) |
| { |
| String selectName = extractNonBlank(findAncestor(element, "select"), "name"); |
| |
| request.loadParameter(selectName, value); |
| } |
| |
| return; |
| } |
| |
| if (name.equals("textarea")) |
| { |
| String content = element.getChildMarkup(); |
| |
| if (InternalUtils.isNonBlank(content)) |
| request.loadParameter(extractNonBlank(element, "name"), content); |
| |
| return; |
| } |
| } |
| }; |
| |
| form.visit(visitor); |
| } |
| |
| /** |
| * Simulates a submission of the form by clicking the specified submit button. The caller can |
| * specify values for the |
| * form fields. |
| * |
| * @param submitButton |
| * the submit button to be clicked. |
| * @param fieldValues |
| * the field values keyed on field names. |
| * @return The DOM created. Typically you will assert against it. |
| */ |
| public Document clickSubmit(Element submitButton, Map<String, String> fieldValues) |
| { |
| clickSubmitAndReturnResponse(submitButton, fieldValues); |
| |
| return getDocumentFromResponse(); |
| } |
| |
| /** |
| * Simulates a submission of the form by clicking the specified submit button. The caller can |
| * specify values for the |
| * form fields. |
| * |
| * @param submitButton |
| * the submit button to be clicked. |
| * @param fieldValues |
| * the field values keyed on field names. |
| * @return The response object to assert against. |
| * @since 5.2.3 |
| */ |
| public TestableResponse clickSubmitAndReturnResponse(Element submitButton, Map<String, String> fieldValues) |
| { |
| assert submitButton != null; |
| |
| assertIsSubmit(submitButton); |
| |
| Element form = getFormAncestor(submitButton); |
| |
| request.clear().setPath(stripContextFromPath(extractNonBlank(form, "action"))); |
| |
| pushFieldValuesIntoRequest(form); |
| |
| overrideParameters(fieldValues); |
| |
| String value = submitButton.getAttribute("value"); |
| |
| if (value == null) |
| value = DEFAULT_SUBMIT_VALUE_ATTRIBUTE; |
| |
| request.overrideParameter(extractNonBlank(submitButton, "name"), value); |
| |
| return runComponentEventRequest(); |
| } |
| |
| private void assertIsSubmit(Element element) |
| { |
| if (element.getName().equals("input")) |
| { |
| String type = element.getAttribute("type"); |
| |
| if ("submit".equals(type)) |
| return; |
| } |
| |
| throw new IllegalArgumentException("The specified element is not a submit button."); |
| } |
| |
| private Element getFormAncestor(Element element) |
| { |
| return findAncestor(element, "form"); |
| } |
| |
| private Element findAncestor(Element element, String ancestorName) |
| { |
| Element e = element; |
| |
| while (e != null) |
| { |
| if (e.getName().equalsIgnoreCase(ancestorName)) |
| return e; |
| |
| e = e.getContainer(); |
| } |
| |
| throw new RuntimeException(String.format("Could not locate an ancestor element of type '%s'.", ancestorName)); |
| |
| } |
| |
| /** |
| * Sets the simulated browser's preferred language, i.e., the value returned from |
| * {@link org.apache.tapestry5.services.Request#getLocale()}. |
| * |
| * @param preferedLanguage |
| * preferred language setting |
| */ |
| public void setPreferedLanguage(Locale preferedLanguage) |
| { |
| request.setLocale(preferedLanguage); |
| } |
| } |