package net.thucydides.core.steps; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.thucydides.core.guice.Injectors; import net.thucydides.core.pages.Pages; import net.thucydides.core.steps.di.DependencyInjectorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.google.common.collect.ImmutableSet.copyOf; /** * Produces an instance of a set of requirement steps for use in the acceptance tests. * Requirement steps navigate through pages using a WebDriver driver. */ public class StepFactory { private final Pages pages; private final Map<Class<?>, Object> index = new HashMap<Class<?>, Object>(); private static final Logger LOGGER = LoggerFactory.getLogger(StepFactory.class); private final DependencyInjectorService dependencyInjectorService; private boolean throwExceptionImmediately = false; /** * Create a new step factory. * All web-testing step factories need a Pages object, which is passed to ScenarioSteps objects when they * are created. */ public StepFactory(final Pages pages) { this.pages = pages; this.dependencyInjectorService = Injectors.getInjector().getInstance(DependencyInjectorService.class); } /** * Create a new step factory without webdriver support. * This is to be used for non-webtest acceptance tests. */ public StepFactory() { this(null); } private static final Class<?>[] CONSTRUCTOR_ARG_TYPES = {Pages.class}; /** * Returns a new ScenarioSteps instance, of the specified type. * This is actually a proxy that allows reporting and screenshots to * be performed at each step. * @param scenarioStepsClass the scenario step class * @param <T> the scenario step class type * @return the instrumented step library */ public <T> T getStepLibraryFor(final Class<T> scenarioStepsClass) { if (isStepLibraryInstantiatedFor(scenarioStepsClass)) { return getManagedStepLibraryFor(scenarioStepsClass); } else { return instantiateNewStepLibraryFor(scenarioStepsClass); } } public <T> T getNewStepLibraryFor(final Class<T> scenarioStepsClass) { if (isStepLibraryInstantiatedFor(scenarioStepsClass)) { return getManagedStepLibraryFor(scenarioStepsClass); } else { return instantiateNewStepLibraryFor(scenarioStepsClass); } } public <T> T getUniqueStepLibraryFor(final Class<T> scenarioStepsClass) { return instantiateUniqueStepLibraryFor(scenarioStepsClass); } public void reset() { index.clear(); } private boolean isStepLibraryInstantiatedFor(Class<?> scenarioStepsClass) { return index.containsKey(scenarioStepsClass); } @SuppressWarnings("unchecked") private <T> T getManagedStepLibraryFor(Class<T> scenarioStepsClass) { return (T) index.get(scenarioStepsClass); } /** * Create a new instance of a class containing test steps. * This method will instrument the class appropriately and inject any nested step libraries or * other dependencies. */ public <T> T instantiateNewStepLibraryFor(Class<T> scenarioStepsClass) { StepInterceptor stepInterceptor = new StepInterceptor(scenarioStepsClass); stepInterceptor.setThowsExceptionImmediately(throwExceptionImmediately); return instantiateNewStepLibraryFor(scenarioStepsClass, stepInterceptor); } /** * Create a new instance of a class containing test steps using custom interceptors. */ public <T> T instantiateNewStepLibraryFor(Class<T> scenarioStepsClass, MethodInterceptor interceptor) { T steps = createProxyStepLibrary(scenarioStepsClass, interceptor); indexStepLibrary(scenarioStepsClass, steps); instantiateAnyNestedStepLibrariesIn(steps, scenarioStepsClass); injectOtherDependenciesInto(steps); return steps; } private <T> void injectOtherDependenciesInto(T steps) { List<DependencyInjector> dependencyInjectors = dependencyInjectorService.findDependencyInjectors(); dependencyInjectors.addAll(getDefaultDependencyInjectors()); for(DependencyInjector dependencyInjector : dependencyInjectors) { dependencyInjector.injectDependenciesInto(steps); } } private List<DependencyInjector> getDefaultDependencyInjectors() { return ImmutableList.of((DependencyInjector)new PageObjectDependencyInjector(pages)); } private <T> T instantiateUniqueStepLibraryFor(Class<T> scenarioStepsClass) { StepInterceptor stepInterceptor = new StepInterceptor(scenarioStepsClass); stepInterceptor.setThowsExceptionImmediately(throwExceptionImmediately); T steps = createProxyStepLibrary(scenarioStepsClass, stepInterceptor); instantiateAnyNestedStepLibrariesIn(steps, scenarioStepsClass); injectOtherDependenciesInto(steps); return steps; } @SuppressWarnings("unchecked") private <T> T createProxyStepLibrary(Class<T> scenarioStepsClass, MethodInterceptor interceptor) { Enhancer e = new Enhancer(); e.setSuperclass(scenarioStepsClass); e.setCallback(interceptor); if (isWebdriverStepClass(scenarioStepsClass)) { return webEnabledStepLibrary(scenarioStepsClass, e); } else { return (T) e.create(); } } private <T> T webEnabledStepLibrary(final Class<T> scenarioStepsClass, final Enhancer e) { if (hasAPagesConstructor(scenarioStepsClass)) { Object[] arguments = new Object[1]; arguments[0] = pages; return (T) e.create(CONSTRUCTOR_ARG_TYPES, arguments); } else { T newStepLibrary = (T) e.create(); return injectPagesInto(scenarioStepsClass, newStepLibrary); } } private <T> T injectPagesInto(final Class<T> stepLibraryClass, T newStepLibrary) { if (ScenarioSteps.class.isAssignableFrom(stepLibraryClass)) { ((ScenarioSteps) newStepLibrary).setPages(pages); } else if (hasAPagesField(stepLibraryClass)) { ImmutableSet<Field> fields = copyOf(stepLibraryClass.getDeclaredFields()); Field pagesField = Iterables.find(fields, ofTypePages()); pagesField.setAccessible(true); try { pagesField.set(newStepLibrary, pages); } catch (IllegalAccessException e) { LOGGER.error("Could not instantiate pages field for step library {}", newStepLibrary); } } return newStepLibrary; } private <T> boolean isWebdriverStepClass(final Class<T> stepLibraryClass) { return (isAScenarioStepClass(stepLibraryClass) || hasAPagesConstructor(stepLibraryClass) || hasAPagesField(stepLibraryClass)); } private <T> boolean hasAPagesConstructor(final Class<T> stepLibraryClass) { ImmutableSet<Constructor<?>> constructors = copyOf(stepLibraryClass.getDeclaredConstructors()); return Iterables.any(constructors, withASinglePagesParameter()); } private <T> boolean hasAPagesField(final Class<T> stepLibraryClass) { ImmutableSet<Field> fields = copyOf(stepLibraryClass.getDeclaredFields()); return Iterables.any(fields, ofTypePages()); } private Predicate<Constructor> withASinglePagesParameter() { return new Predicate<Constructor>() { public boolean apply(Constructor constructor) { return ((constructor.getParameterTypes().length == 1) && (constructor.getParameterTypes()[0] == Pages.class)); } }; } private Predicate<Field> ofTypePages() { return new Predicate<Field>() { public boolean apply(Field field) { return (field.getType() == Pages.class); } }; } private <T> boolean isAScenarioStepClass(final Class<T> stepLibraryClass) { return ScenarioSteps.class.isAssignableFrom(stepLibraryClass); } private <T> void indexStepLibrary(Class<T> scenarioStepsClass, T steps) { index.put(scenarioStepsClass, steps); } private <T> void instantiateAnyNestedStepLibrariesIn(final T steps, final Class<T> scenarioStepsClass) { StepAnnotations.injectNestedScenarioStepsInto(steps, this, scenarioStepsClass); } public StepFactory thatThrowsExcpetionsImmediately() { throwExceptionImmediately = true; return this; } }