/* * Copyright 2007 the original author or authors. * * 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 jdave.wicket; import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import jdave.IContainment; import jdave.Specification; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.Page; import org.apache.wicket.markup.IMarkupResourceStreamProvider; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.border.Border; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.RepeatingView; import org.apache.wicket.model.IModel; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.StringResourceStream; import org.apache.wicket.util.tester.BaseWicketTester; import org.apache.wicket.util.tester.ITestPageSource; import org.apache.wicket.util.tester.TestPanelSource; import org.apache.wicket.util.tester.BaseWicketTester.DummyWebApplication; /** * A base class for Wicket's <code>Component</code> specifications. * * @author Joni Freeman * @author Timo Rantalaiho */ public abstract class ComponentSpecification<C extends Component, M> extends Specification<C> { protected BaseWicketTester wicket; protected C specifiedComponent; @Override public final void create() { wicket = newWicketTester(); onCreate(); } /** * Called after create(). No need to call super.onCreate(). */ protected void onCreate() { } /** * Start component for context. */ public C startComponent() { return startComponent(null); } /** * Start component for context. * * @param model The model passed to component that is used for context. */ public C startComponent(final IModel<M> model) { final Class<?> type = extractComponentType(); if (Page.class.isAssignableFrom(type)) { startPage(model); } else if (Panel.class.isAssignableFrom(type)) { startPanel(model); } else if (Border.class.isAssignableFrom(type)) { startBorder(model); } else { startComponentWithoutMarkup(model); } return specifiedComponent; } private Class<?> extractComponentType() { final ParameterizedType superclass = (ParameterizedType) getClass().getGenericSuperclass(); Class<?> type; if (superclass.getActualTypeArguments()[0] instanceof Class<?>) { type = (Class<?>) superclass.getActualTypeArguments()[0]; } else { type = (Class<?>) ((ParameterizedType) superclass.getActualTypeArguments()[0]) .getRawType(); } return type; } /** * Start component for context, using page with given markup. * * @param model The model passed to component that is used for context. * @param pageMarkup Markup (as <code>java.lang.CharSequence</code>) of * the page created to host the specified component. * @param rootComponentId Wicket id of the root component in the component * markup hierarchy given by the <code>pageMarkup</code> * parameter. This is the id of the component returned from * <code>newComponent</code> method and used as context, often * a MarkupContainer or Form. */ public C startComponent(final IModel<M> model, final CharSequence pageMarkup, final String rootComponentId) { return startComponent(model, new StringResourceStream(pageMarkup), rootComponentId); } /** * Start component for context, using page with given markup. * * @param model The model passed to component that is used for context. * @param pageMarkup Markup (as * <code>org.apache.wicket.util.resource.IResourceStream</code> * ) of the page created to host the specified component. * @param rootComponentId Wicket id of the root component in the component * markup hierarchy given by the <code>pageMarkup</code> * parameter. This is the id of the component returned from * <code>newComponent</code> method and used as context, often * a MarkupContainer or Form. */ public C startComponent(final IModel<M> model, final IResourceStream pageMarkup, final String rootComponentId) { final WebPage page = new TestPage(pageMarkup); specifiedComponent = newComponent(rootComponentId, model); page.add(specifiedComponent); wicket.startPage(page); return specifiedComponent; } /** * Start form for context, using given markup as form markup. * * @param model The model passed to <code>newComponent</code> method. * @param formMarkup Markup (as <code>java.lang.CharSequence</code>) of * the form returned from <code>newComponent</code> method, * excluding the <form> tag. */ public C startForm(final IModel<M> model, final CharSequence formMarkup) { ensureComponentIsAForm(); return startComponent(model, new StringBuilder().append( "<html><body><form wicket:id='form'>").append(formMarkup).append( "</form></body></html>").toString(), "form"); } private void ensureComponentIsAForm() { if (!Form.class.isAssignableFrom(extractComponentType())) { throw new IllegalArgumentException( "Your ComponentSpecification must be typed as <? extends Form<?>> if you want to start a form. Instead you are using " + extractComponentType().getName()); } } /** * Start component for context. * <p> * The markup file of a component is not needed. * * @param model The model passed to component that is used for context. */ public C startComponentWithoutMarkup(final IModel<M> model) { specifiedComponent = newComponent("component", model); wicket.startComponent(specifiedComponent); return specifiedComponent; } protected void startBorder(final IModel<M> model) { wicket.startPanel(new TestPanelSource() { public Panel getTestPanel(final String panelId) { final Panel panel = new Container(panelId); specifiedComponent = newComponent("component", model); panel.add(specifiedComponent); return panel; } }); } protected void startPanel(final IModel<M> model) { wicket.startPanel(new TestPanelSource() { public Panel getTestPanel(final String panelId) { specifiedComponent = newComponent(panelId, model); return (Panel) specifiedComponent; } }); } protected void startPage(final IModel<M> model) { specifiedComponent = newComponent(null, model); final TestPageSource testPageSource = new TestPageSource((Page) specifiedComponent); wicket.startPage(testPageSource); } @SuppressWarnings( { "unchecked" }) protected <X> IModel<X> cast(final IModel<?> model) { return (IModel<X>) model; } private static class TestPageSource implements ITestPageSource { private final Page page; public TestPageSource(final Page page) { this.page = page; } public Page getTestPage() { return page; } } private static class TestPage extends WebPage implements IMarkupResourceStreamProvider { private final IResourceStream markup; public TestPage(final IResourceStream markup) { this.markup = markup; } public IResourceStream getMarkupResourceStream(final MarkupContainer container, final Class<?> containerClass) { return markup; } } /** * Specify that given container contains given model objects. * <p> * This is most often used with <code>RefreshingViews</code> and * <code>ListViews</code>. * * <pre> * <blockquote><code> * ListView list = new ListView("stooges", Arrays.asList("Larry", "Moe", "Curly")) { ... }; * specify(list, containsInOrder("Larry", "Moe", "Curly"); * <code></blockquote> * </pre> * * @param actual the container of Wicket components * @param containment any containment, see: * http://www.jdave.org/documentation.html#containments */ public <T> void specify(final MarkupContainer actual, final IContainment<T> containment) { super.specify(modelObjects(actual.iterator()), containment); } /** * Select an item from a <code>RepeatingView</code>. */ @SuppressWarnings("unchecked") public <T> Item<T> itemAt(final RepeatingView view, final int index) { final Iterator<? extends Component> items = view.iterator(); for (int i = 0; i < index; i++) { items.next(); } return (Item<T>) items.next(); } /** * Select an item from a <code>ListView</code>. */ public <T> ListItem<T> itemAt(final ListView<T> view, final int index) { final Iterator<? extends ListItem<T>> items = view.iterator(); for (int i = 0; i < index; i++) { items.next(); } return items.next(); } /** * Collect model objects from given components. */ public List<?> modelObjects(final Iterator<? extends Component> components) { final List<Object> objects = new ArrayList<Object>(); while (components.hasNext()) { objects .add(((Iterator<? extends Component>) components).next() .getDefaultModelObject()); } return objects; } /** * Create a <code>WicketTester</code> for the specification. * <p> * By default, <code>WicketTester</code> is created as: * * <pre> * <blockquote><code> * return new BaseWicketTester(newApplication()); * </code></blockquote> * </pre> * * So, it is possible to overwrite <code>newApplication</code> if you just * need a different <code>Application</code> for a specification. * * @see #newApplication() */ protected BaseWicketTester newWicketTester() { return new BaseWicketTester(newApplication()); } /** * Create the application for the specification. */ protected WebApplication newApplication() { return new DummyWebApplication(); } /** * Select first component whose model object matches given Hamcrest * matcher: * * <pre> * <blockquote><code> * Item item = selectFirst(Item.class).which(is(0)).from(context); * </code></blockquote> * </pre> */ public <S extends Component> Selection<S> selectFirst(final Class<S> type) { return new Selection<S>(type); } /** * Select all components whose model objects match given Hamcrest matcher: * * <pre> * <blockquote><code> * List<Label> labels = selectAll(Label.class).which(is(Person.class)).from(context); * </code></blockquote> * </pre> */ public <S extends Component> MultiSelection<S> selectAll(final Class<S> type) { return new MultiSelection<S>(type); } /** * Select first component whose Wicket id is given String: * * <pre> * <blockquote><code> * Label itemName = selectFirst(Label.class, "name").from(context); * </code></blockquote> * </pre> */ public <S extends Component> Selection<S> selectFirst(final Class<S> type, final String wicketId) { return new Selection<S>(type, wicketId); } /** * Select all components whose ids are given Wicket id: * * <pre> * <blockquote><code> * List<Label> prices = selectAll(Label.class, "price").from(context); * </code></blockquote> * </pre> */ public <S extends Component> MultiSelection<S> selectAll(final Class<S> type, final String wicketId) { return new MultiSelection<S>(type, wicketId); } /** * Create a new instance of a Wicket component to be specified. <p The * component must get given id. If the component is a <code>Page</code>, * the id is null. * * @param id The id of a component, null if the component is a * <code>Page</code>, * @param model A model for the component which was passed in * <code>startComponent</code> method. * @see #startComponent(IModel) */ protected abstract C newComponent(String id, IModel<M> model); }