001: // Copyright 2006, 2007 The Apache Software Foundation
002: //
003: // Licensed under the Apache License, Version 2.0 (the "License");
004: // you may not use this file except in compliance with the License.
005: // You may obtain a copy of the License at
006: //
007: // http://www.apache.org/licenses/LICENSE-2.0
008: //
009: // Unless required by applicable law or agreed to in writing, software
010: // distributed under the License is distributed on an "AS IS" BASIS,
011: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: // See the License for the specific language governing permissions and
013: // limitations under the License.
014:
015: package org.apache.tapestry.test;
016:
017: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
018: import static org.apache.tapestry.ioc.internal.util.Defense.notNull;
019:
020: import java.util.Locale;
021: import java.util.Map;
022:
023: import org.apache.tapestry.dom.Document;
024: import org.apache.tapestry.dom.Element;
025: import org.apache.tapestry.dom.Node;
026: import org.apache.tapestry.internal.InternalConstants;
027: import org.apache.tapestry.internal.SingleKeySymbolProvider;
028: import org.apache.tapestry.internal.TapestryAppInitializer;
029: import org.apache.tapestry.internal.services.ActionLinkTarget;
030: import org.apache.tapestry.internal.services.ComponentInvocation;
031: import org.apache.tapestry.internal.services.ComponentInvocationMap;
032: import org.apache.tapestry.internal.services.LocalizationSetter;
033: import org.apache.tapestry.internal.services.PageLinkTarget;
034: import org.apache.tapestry.internal.test.ActionLinkInvoker;
035: import org.apache.tapestry.internal.test.ComponentInvoker;
036: import org.apache.tapestry.internal.test.PageLinkInvoker;
037: import org.apache.tapestry.internal.test.PageTesterContext;
038: import org.apache.tapestry.internal.test.PageTesterModule;
039: import org.apache.tapestry.internal.test.TestableRequest;
040: import org.apache.tapestry.ioc.Registry;
041: import org.apache.tapestry.ioc.services.SymbolProvider;
042: import org.apache.tapestry.ioc.util.StrategyRegistry;
043: import org.apache.tapestry.services.ApplicationGlobals;
044:
045: /**
046: * This class is used to run a Tapestry app in an in-process testing environment. You can ask it to
047: * render a certain page and check the DOM object created. You can also ask it to click on a link
048: * element in the DOM object to get the next page. Because no servlet container is required, it is
049: * very fast and you can directly debug into your code in your IDE.
050: */
051: public class PageTester implements ComponentInvoker {
052: private final Registry _registry;
053:
054: private final ComponentInvocationMap _invocationMap;
055:
056: private final TestableRequest _request;
057:
058: private final StrategyRegistry<ComponentInvoker> _invokerRegistry;
059:
060: private Locale _preferedLanguage;
061:
062: private final LocalizationSetter _localizationSetter;
063:
064: public static final String DEFAULT_CONTEXT_PATH = "src/main/webapp";
065:
066: private final String _contextPath;
067:
068: private static final String DEFAULT_SUBMIT_VALUE_ATTRIBUTE = "Submit Query";
069:
070: /**
071: * Initializes a PageTester without overriding any services and assuming that the context root
072: * is in src/main/webapp.
073: *
074: * @see #PageTester(String, String, String, Map)
075: */
076: public PageTester(String appPackage, String appName) {
077: this (appPackage, appName, DEFAULT_CONTEXT_PATH);
078: }
079:
080: /**
081: * Initializes a PageTester that acts as a browser and a servlet container to test drive your
082: * Tapestry pages.
083: *
084: * @param appPackage
085: * The same value you would specify using the tapestry.app-package context parameter.
086: * As this testing environment is not run in a servlet container, you need to specify
087: * it.
088: * @param appName
089: * The same value you would specify as the filter name. It is used to form the name
090: * of the module builder for your app. If you don't have one, pass an empty string.
091: * @param contextPath
092: * The path to the context root so that Tapestry can find the templates (if they're
093: * put there).
094: */
095: public PageTester(String appPackage, String appName,
096: String contextPath) {
097: _preferedLanguage = Locale.ENGLISH;
098: _contextPath = contextPath;
099:
100: SymbolProvider provider = new SingleKeySymbolProvider(
101: InternalConstants.TAPESTRY_APP_PACKAGE_PARAM,
102: appPackage);
103:
104: TapestryAppInitializer initializer = new TapestryAppInitializer(
105: provider, appName, PageTesterModule.TEST_MODE);
106:
107: initializer.addModules(PageTesterModule.class);
108:
109: _registry = initializer.getRegistry();
110:
111: _request = _registry.getObject(TestableRequest.class, null);
112:
113: _localizationSetter = _registry.getService(
114: "LocalizationSetter", LocalizationSetter.class);
115:
116: _invocationMap = _registry.getObject(
117: ComponentInvocationMap.class, null);
118:
119: ApplicationGlobals globals = _registry.getObject(
120: ApplicationGlobals.class, null);
121:
122: globals.store(new PageTesterContext(_contextPath));
123:
124: Map<Class, ComponentInvoker> map = newMap();
125: map.put(PageLinkTarget.class, new PageLinkInvoker(_registry));
126: map.put(ActionLinkTarget.class, new ActionLinkInvoker(
127: _registry, this , _invocationMap));
128:
129: _invokerRegistry = new StrategyRegistry<ComponentInvoker>(
130: ComponentInvoker.class, map);
131: }
132:
133: /** You should call it after use */
134: public void shutdown() {
135: _registry.shutdown();
136: }
137:
138: /**
139: * Renders a page specified by its name.
140: *
141: * @param pageName
142: * The name of the page to be rendered.
143: * @return The DOM created. Typically you will assert against it.
144: */
145: public Document renderPage(String pageName) {
146: return invoke(new ComponentInvocation(new PageLinkTarget(
147: pageName), new String[0], null));
148: }
149:
150: /**
151: * Simulates a click on a link.
152: *
153: * @param link
154: * The Link object to be "clicked" on.
155: * @return The DOM created. Typically you will assert against it.
156: */
157: public Document clickLink(Element link) {
158: notNull(link, "link");
159:
160: ComponentInvocation invocation = getInvocation(link);
161:
162: return invoke(invocation);
163: }
164:
165: private ComponentInvocation getInvocation(Element element) {
166: ComponentInvocation invocation = _invocationMap.get(element);
167:
168: if (invocation == null)
169: throw new IllegalArgumentException(
170: "No component invocation object is associated with the Element.");
171:
172: return invocation;
173: }
174:
175: public Document invoke(ComponentInvocation invocation) {
176: // It is critical to clear the map before invoking an invocation (render a page or click a
177: // link).
178: _invocationMap.clear();
179:
180: setThreadLocale();
181:
182: ComponentInvoker invoker = _invokerRegistry
183: .getByInstance(invocation.getTarget());
184:
185: return invoker.invoke(invocation);
186: }
187:
188: private void setThreadLocale() {
189: _localizationSetter.setThreadLocale(_preferedLanguage);
190: }
191:
192: /**
193: * Simulates a submission of the form specified. The caller can specify values for the form
194: * fields.
195: *
196: * @param form
197: * the form to be submitted.
198: * @param parameters
199: * the query parameter name/value pairs
200: * @return The DOM created. Typically you will assert against it.
201: */
202: public Document submitForm(Element form,
203: Map<String, String> parameters) {
204: notNull(form, "form");
205:
206: _request.clear();
207:
208: _request.loadParameters(parameters);
209:
210: addHiddenFormFields(form);
211:
212: ComponentInvocation invocation = getInvocation(form);
213:
214: return invoke(invocation);
215: }
216:
217: /**
218: * Simulates a submission of the form by clicking the specified submit button. The caller can
219: * specify values for the form fields.
220: *
221: * @param submitButton
222: * the submit button to be clicked.
223: * @param fieldValues
224: * the field values keyed on field names.
225: * @return The DOM created. Typically you will assert against it.
226: */
227: public Document clickSubmit(Element submitButton,
228: Map<String, String> fieldValues) {
229: notNull(submitButton, "submitButton");
230:
231: assertIsSubmit(submitButton);
232:
233: Element form = getFormAncestor(submitButton);
234: String value = submitButton.getAttribute("value");
235:
236: if (value == null)
237: value = DEFAULT_SUBMIT_VALUE_ATTRIBUTE;
238:
239: fieldValues.put(submitButton.getAttribute("name"), value);
240:
241: return submitForm(form, fieldValues);
242: }
243:
244: private void assertIsSubmit(Element element) {
245: if (element.getName().equals("input")) {
246: String type = element.getAttribute("type");
247:
248: if ("submit".equals(type))
249: return;
250: }
251:
252: throw new IllegalArgumentException(
253: "The specified element is not a submit button.");
254: }
255:
256: private Element getFormAncestor(Element element) {
257: while (true) {
258: if (element == null)
259: throw new IllegalArgumentException(
260: "The given element is not contained by a form.");
261:
262: if (element.getName().equalsIgnoreCase("form"))
263: return element;
264:
265: element = element.getParent();
266: }
267: }
268:
269: private void addHiddenFormFields(Element element) {
270: if (isHiddenFormField(element))
271: _request.loadParameter(element.getAttribute("name"),
272: element.getAttribute("value"));
273:
274: for (Node child : element.getChildren()) {
275: if (child instanceof Element) {
276: addHiddenFormFields((Element) child);
277: }
278: }
279: }
280:
281: private boolean isHiddenFormField(Element element) {
282: return element.getName().equalsIgnoreCase("input")
283: && "hidden".equalsIgnoreCase(element
284: .getAttribute("type"));
285: }
286:
287: public void setPreferedLanguage(Locale preferedLanguage) {
288: _preferedLanguage = preferedLanguage;
289: }
290: }
|