blob: 59d4096e695d1c228af21825ff8b81558edd230a [file] [log] [blame]
// Copyright 2006, 2007 The Apache Software Foundation
//
// 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.tapestry.test.pagelevel;
import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import org.apache.tapestry.dom.Document;
import org.apache.tapestry.dom.Element;
import org.apache.tapestry.dom.Node;
import org.apache.tapestry.internal.InternalConstants;
import org.apache.tapestry.internal.SingleKeySymbolProvider;
import org.apache.tapestry.internal.TapestryAppInitializer;
import org.apache.tapestry.internal.services.ActionLinkTarget;
import org.apache.tapestry.internal.services.ComponentInvocation;
import org.apache.tapestry.internal.services.LocalizationSetter;
import org.apache.tapestry.internal.services.PageLinkTarget;
import org.apache.tapestry.ioc.Registry;
import org.apache.tapestry.ioc.internal.NullAnnotationProvider;
import org.apache.tapestry.ioc.internal.util.Defense;
import org.apache.tapestry.ioc.services.SymbolProvider;
import org.apache.tapestry.ioc.util.StrategyRegistry;
import org.apache.tapestry.services.ApplicationGlobals;
/**
* This class is used to run a Tapestry app in an 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.
*/
public class PageTester implements ComponentInvoker
{
private Registry _registry;
private ComponentInvocationMapForPageTester _invocationMap = new ComponentInvocationMapForPageTester();
private FormParameterLookupForPageTester _formParameterLookup = new FormParameterLookupForPageTester();
private CookiesForPageTester _cookies;
// For the moment, a page tester instance works in a single session.
private SessionHolderForPageTester _sessionHolder = new SessionHolderForPageTester();
private StrategyRegistry<ComponentInvoker> _invokerRegistry;
private Locale _preferedLanguage;
private LocalizationSetter _localizationSetter;
public static final String DEFAULT_CONTEXT_PATH = "src/main/webapp";
private final String _contextPath;
/**
* Initializes a PageTester without overriding any services and assuming that the context root
* is in src/main/webapp.
*
* @see #PageTester(String, String, String, Map)
*/
public PageTester(String appPackage, String appName)
{
this(appPackage, appName, DEFAULT_CONTEXT_PATH);
}
/**
* @see #PageTester(String, String, String, Map)
*/
@SuppressWarnings("unchecked")
public PageTester(String appPackage, String appName, String contextPath)
{
this(appPackage, appName, contextPath, Collections.EMPTY_MAP);
}
/**
* 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 builder 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 serviceOverrides
* The mock implementation (value) for some services (key).
*/
public PageTester(String appPackage, String appName, String contextPath,
Map<String, Object> serviceOverrides)
{
_preferedLanguage = Locale.ENGLISH;
_contextPath = contextPath;
_cookies = new CookiesForPageTester();
SymbolProvider provider = new SingleKeySymbolProvider(
InternalConstants.TAPESTRY_APP_PACKAGE_PARAM, appPackage);
_registry = new TapestryAppInitializer(provider, appName, "test",
addDefaultOverrides(serviceOverrides)).getRegistry();
_localizationSetter = _registry.getService("LocalizationSetter", LocalizationSetter.class);
ApplicationGlobals globals = _registry.getObject(
ApplicationGlobals.class,
new NullAnnotationProvider());
globals.store(new ContextForPageTester(_contextPath));
buildInvokersRegistry();
}
private void buildInvokersRegistry()
{
Map<Class, ComponentInvoker> map = newMap();
map.put(PageLinkTarget.class, new PageLinkInvoker(_registry));
map.put(ActionLinkTarget.class, new ActionLinkInvoker(_registry, this, _invocationMap));
_invokerRegistry = new StrategyRegistry<ComponentInvoker>(ComponentInvoker.class, map);
}
private Map<String, Object> addDefaultOverrides(Map<String, Object> serviceOverrides)
{
Map<String, Object> modifiedOverrides = newMap(serviceOverrides);
addDefaultOverride(modifiedOverrides, "ContextPathSource", new FooContextPathSource());
addDefaultOverride(modifiedOverrides, "URLEncoder", new NoOpURLEncoder());
addDefaultOverride(modifiedOverrides, "ComponentInvocationMap", _invocationMap);
addDefaultOverride(modifiedOverrides, "FormParameterLookup", _formParameterLookup);
addDefaultOverride(modifiedOverrides, "SessionHolder", _sessionHolder);
addDefaultOverride(modifiedOverrides, "CookieSource", _cookies);
addDefaultOverride(modifiedOverrides, "CookieSink", _cookies);
return modifiedOverrides;
}
private void addDefaultOverride(Map<String, Object> serviceOverrides, String serviceId,
Object overridingImpl)
{
if (!serviceOverrides.containsKey(serviceId))
{
serviceOverrides.put(serviceId, overridingImpl);
}
}
/** You should call it after use */
public void shutdown()
{
if (_registry != null)
{
_registry.shutdown();
}
}
/**
* 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)
{
return invoke(new ComponentInvocation(new PageLinkTarget(pageName), new String[0], null));
}
/**
* Simulates a click on a link.
*
* @param link
* The Link object to be "clicked" on.
* @return The DOM created. Typically you will assert against it.
*/
public Document clickLink(Element link)
{
Defense.notNull(link, "link");
ComponentInvocation invocation = getInvocation(link);
return invoke(invocation);
}
private ComponentInvocation getInvocation(Element element)
{
ComponentInvocation invocation = _invocationMap.get(element);
if (invocation == null) { throw new IllegalArgumentException(
"No component invocation object is associated with the Element"); }
return invocation;
}
public Document invoke(ComponentInvocation invocation)
{
// It is critical to clear the map before invoking an invocation (render a page or click a
// link).
_invocationMap.clear();
setThreadLocale();
ComponentInvoker invoker = _invokerRegistry.getByInstance(invocation.getTarget());
return invoker.invoke(invocation);
}
private void setThreadLocale()
{
_localizationSetter.setThreadLocale(_preferedLanguage);
}
/**
* Simulates a submission of the form specified. The caller can specify values for the form
* fields.
*
* @param form
* the form to be submitted.
* @param fieldValues
* the field values keyed on field names.
* @return The DOM created. Typically you will assert against it.
*/
public Document submitForm(Element form, Map<String, String> fieldValues)
{
Defense.notNull(form, "form");
_formParameterLookup.clear();
_formParameterLookup.addFieldValues(fieldValues);
addHiddenFormFields(form);
ComponentInvocation invocation = getInvocation(form);
return invoke(invocation);
}
/**
* 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)
{
final String DEFAULT_SUBMIT_VALUE_ATTRIBUTE = "Submit Query";
Defense.notNull(submitButton, "submitButton");
assertIsSubmit(submitButton);
Element form = getFormAncestor(submitButton);
String value = submitButton.getAttribute("value");
if (value == null)
{
value = DEFAULT_SUBMIT_VALUE_ATTRIBUTE;
}
fieldValues.put(submitButton.getAttribute("name"), value);
return submitForm(form, fieldValues);
}
private void assertIsSubmit(Element element)
{
if (element.getName().equals("input"))
{
String type = element.getAttribute("type");
if (type != null && type.equals("submit")) { return; }
}
throw new IllegalArgumentException("The specified element is not a submit button");
}
private Element getFormAncestor(Element element)
{
while (true)
{
if (element == null) { throw new IllegalArgumentException(
"The given element is not contained in a form"); }
if (element.getName().equalsIgnoreCase("form")) { return element; }
element = element.getParent();
}
}
private void addHiddenFormFields(Element element)
{
if (isHiddenFormField(element))
{
_formParameterLookup.addFieldValue(element.getAttribute("name"), element
.getAttribute("value"));
}
for (Node child : element.getChildren())
{
if (child instanceof Element)
{
addHiddenFormFields((Element) child);
}
}
}
private boolean isHiddenFormField(Element element)
{
return element.getName().equals("input") && "hidden".equals(element.getAttribute("type"));
}
public void setPreferedLanguage(Locale preferedLanguage)
{
_preferedLanguage = preferedLanguage;
}
}