Skip to content

Commit

Permalink
Make components works with injection.
Browse files Browse the repository at this point in the history
It also add hooks support of WebElement and List<WebElement>.

Close #312
  • Loading branch information
Toilal committed Sep 6, 2016
1 parent f6b0b21 commit 1f0e354
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 36 deletions.
6 changes: 5 additions & 1 deletion README.md
Expand Up @@ -585,9 +585,13 @@ public class LoginPage extends FluentPage {
}
```

Injection is recursive, and it's possible to retrieve the parent container using ```@Parent``` annotation.

### Components

A ```Component``` is a object wrapping a ```WebElement``` instance. Nothing more.
A ```Component``` is an object wrapping a ```WebElement``` instance.

A ```Component``` supports Injection like a Page Object, but all searchs are performed in the local context of the wrapped element.

Using components improves readability of both Page Objects and Tests.

Expand Down
Expand Up @@ -2,9 +2,11 @@

import org.fluentlenium.core.FluentControl;
import org.fluentlenium.core.components.ComponentInstantiator;
import org.fluentlenium.core.inject.NoInject;
import org.openqa.selenium.WebElement;

public class Component {
@NoInject
protected WebElement webElement;
protected FluentControl fluentControl;
protected ComponentInstantiator instantiator;
Expand Down
@@ -1,11 +1,16 @@
package org.fluentlenium.core.inject;

import org.fluentlenium.core.hook.HookDefinition;
import org.openqa.selenium.SearchContext;

import java.util.List;

public interface ContainerContext {
Object getContainer();

ContainerContext getParent();

SearchContext getSearchContext();

List<HookDefinition<?>> getHookDefinitions();
}
@@ -1,22 +1,25 @@
package org.fluentlenium.core.inject;

import org.fluentlenium.core.hook.HookDefinition;
import org.openqa.selenium.SearchContext;

import java.util.ArrayList;
import java.util.List;

public class DefaultContainerContext implements ContainerContext {
private final Object container;
private final ContainerContext parentContext;
private final SearchContext searchContext;
private final List<HookDefinition<?>> hookDefinitions = new ArrayList<>();

public DefaultContainerContext(Object container) {
this(container, null);
this(container, null, null);
}

public DefaultContainerContext(Object container, ContainerContext parentContext) {
public DefaultContainerContext(Object container, ContainerContext parentContext, SearchContext searchContext) {
this.container = container;
this.parentContext = parentContext;
this.searchContext = searchContext;
}

@Override
Expand All @@ -29,6 +32,11 @@ public ContainerContext getParent() {
return parentContext;
}

@Override
public SearchContext getSearchContext() {
return searchContext;
}

@Override
public List<HookDefinition<?>> getHookDefinitions() {
return hookDefinitions;
Expand Down
@@ -0,0 +1,35 @@
package org.fluentlenium.core.inject;

import org.openqa.selenium.By;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.pagefactory.ElementLocator;

import java.util.ArrayList;
import java.util.List;

public class ElementLocatorSearchContext implements SearchContext {
private final ElementLocator locator;

public ElementLocatorSearchContext(ElementLocator locator) {
this.locator = locator;
}

@Override
public List<WebElement> findElements(By by) {
List<WebElement> elements = new ArrayList<>();

List<WebElement> baseElements = locator.findElements();

for (WebElement element : baseElements) {
elements.addAll(element.findElements(by));
}

return elements;
}

@Override
public WebElement findElement(By by) {
return locator.findElement().findElement(by);
}
}
Expand Up @@ -16,8 +16,8 @@
import org.fluentlenium.core.hook.NoHook;
import org.fluentlenium.core.proxy.LocatorProxies;
import org.fluentlenium.utils.ReflectionUtils;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.events.EventFiringWebDriver;
import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory;
import org.openqa.selenium.support.pagefactory.ElementLocator;
Expand All @@ -42,6 +42,8 @@ public class FluentInjector implements FluentInjectControl {
private final ConcurrentMap<Class, Object> containerInstances = new ConcurrentHashMap<>();
private final ConcurrentMap<Object, ContainerContext> containerContexts = new ConcurrentHashMap<>();
private final ConcurrentMap<Object, ContainerAnnotationsEventsRegistry> eventsContainerSupport = new ConcurrentHashMap<>();
private final ConcurrentMap<Object, SearchContext> searchContexts = new ConcurrentHashMap<>();


private final FluentControl fluentControl;
private final ComponentsManager componentsManager;
Expand Down Expand Up @@ -86,31 +88,54 @@ public ContainerContext[] inject(Object... containers) {

@Override
public ContainerContext inject(Object container) {
inject(container, null);
inject(container, null, fluentControl.getDriver());
return containerContexts.get(container);
}

private void inject(Object container, Object parentContainer) {
initContainer(container, parentContainer);
initFluentElements(container);
initChildrenContainers(container);
private void inject(Object container, Object parentContainer, SearchContext searchContext) {
initContainer(container, parentContainer, searchContext);
initParentContainer(container, parentContainer);
initFluentElements(container, searchContext);
initChildrenContainers(container, searchContext);
}

// Default Selenium WebElement injection.
PageFactory.initElements(fluentControl.getDriver(), container);
private void injectComponent(Object componentContainer, Object parentContainer, SearchContext searchContext) {
initContainerContext(componentContainer, parentContainer, searchContext);
initParentContainer(componentContainer, parentContainer);
initFluentElements(componentContainer, searchContext);
initChildrenContainers(componentContainer, searchContext);
}

private void initParentContainer(Object container, Object parentContainer) {
for (Class cls = container.getClass(); isClassSupported(cls); cls = cls.getSuperclass()) {
for (Field field : cls.getDeclaredFields()) {
if (isParent(field)) {
try {
ReflectionUtils.set(field, container, parentContainer);
} catch (IllegalAccessException | IllegalArgumentException e) {
throw new FluentInjectException("Can't set field " + field + " with value " + parentContainer, e);
}
}
}
}
}

private void initContainer(Object container, Object parentContainer) {
initContainerContext(container, parentContainer);
private boolean isParent(Field field) {
return field.isAnnotationPresent(Parent.class);
}

private void initContainer(Object container, Object parentContainer, SearchContext searchContext) {
initContainerContext(container, parentContainer, searchContext);
if (container instanceof FluentContainer) {
((FluentContainer) container).initFluent(new ContainerFluentControl(fluentControl, containerContexts.get(container)));
}
initEventAnnotations(container);
}

private void initContainerContext(Object container, Object parentContainer) {
private void initContainerContext(Object container, Object parentContainer, SearchContext searchContext) {
ContainerContext parentContainerContext = parentContainer != null ? containerContexts.get(parentContainer) : null;

DefaultContainerContext containerContext = new DefaultContainerContext(container, parentContainerContext);
DefaultContainerContext containerContext = new DefaultContainerContext(container, parentContainerContext, searchContext);
containerContexts.put(container, containerContext);

if (parentContainerContext != null) {
Expand All @@ -136,7 +161,7 @@ private static boolean isClassSupported(Class<?> cls) {
return cls != Object.class && cls != null;
}

private void initChildrenContainers(Object container) {
private void initChildrenContainers(Object container, SearchContext searchContext) {
for (Class cls = container.getClass(); isClassSupported(cls); cls = cls.getSuperclass()) {
for (Field field : cls.getDeclaredFields()) {
if (isContainer(field)) {
Expand All @@ -150,30 +175,48 @@ private void initChildrenContainers(Object container) {
}
} else {
Object childContainer = containerInstanciator.newInstance(fieldClass, containerContexts.get(container));
initContainer(childContainer, container);
initContainer(childContainer, container, searchContext);
try {
ReflectionUtils.set(field, container, childContainer);
} catch (IllegalAccessException e) {
throw new FluentInjectException("Can't set field " + field + " with value " + childContainer, e);
}
containerInstances.putIfAbsent(fieldClass, childContainer);
inject(childContainer, container);
inject(childContainer, container, searchContext);
}
} else if (isComponent(field) && !isParent(field)) {
try {
Object o = ReflectionUtils.get(field, container);
SearchContext componentSearchContext = searchContexts.get(o);
if (componentSearchContext == null) {
componentSearchContext = fluentControl.getDriver();
}
injectComponent(o, container, componentSearchContext); // SearchContext from the component
} catch (IllegalAccessException e) {
throw new FluentInjectException("Can't get field " + field + " from container " + container, e);
}
}
}
}
}

private <T extends FluentControl> void initFluentElements(Object container) {
private <T extends FluentControl> void initFluentElements(Object container, SearchContext searchContext) {
ContainerContext containerContext = containerContexts.get(container);

for (Class cls = container.getClass(); isClassSupported(cls); cls = cls.getSuperclass()) {
for (Field field : cls.getDeclaredFields()) {
if (isSupported(container, field)) {
ArrayList<HookDefinition<?>> fieldHookDefinitions = new ArrayList<>(containerContext.getHookDefinitions());
addHookDefinitions(field.getAnnotations(), fieldHookDefinitions);
ElementLocatorFactory locatorFactory = new DefaultElementLocatorFactory(this.fluentControl.getDriver());
initFieldElements(locatorFactory, fieldHookDefinitions, container, field);
ElementLocatorFactory locatorFactory = new DefaultElementLocatorFactory(searchContext);
ElementLocator locator = locatorFactory.createLocator(field);
if (locator == null) {
continue;
}
Object fieldValue = initFieldElements(locator, fieldHookDefinitions, container, field);
if (fieldValue != null) {
searchContexts.put(fieldValue, new ElementLocatorSearchContext(locator));
}
}
}
}
Expand Down Expand Up @@ -259,7 +302,11 @@ private <T> HookDefinition<T> buildHookDefinition(Hook hookAnnotation, HookOptio


private boolean isSupported(Object container, Field field) {
return isValueNull(container, field) && !field.isAnnotationPresent(NoInject.class) && !Modifier.isFinal(field.getModifiers()) && (isListOfFluentWebElement(field) || isList(field) || isComponent(field)) || isComponentList(field);
return isValueNull(container, field) && !field.isAnnotationPresent(NoInject.class) && !Modifier.isFinal(field.getModifiers()) &&
(isListOfFluentWebElement(field) ||
isListOfComponent(field) ||
isComponent(field) ||
isComponentList(field) || isElement(field) || isListOfElement(field));
}

private static boolean isValueNull(Object container, Field field) {
Expand Down Expand Up @@ -292,9 +339,15 @@ private boolean isComponentList(Field field) {
private static boolean isListOfFluentWebElement(Field field) {
if (isList(field)) {
Class<?> genericType = getFirstGenericType(field);
if (FluentWebElement.class.isAssignableFrom(genericType)) {
return true;
}
return FluentWebElement.class.isAssignableFrom(genericType);
}
return false;
}

private boolean isListOfComponent(Field field) {
if (isList(field)) {
Class<?> genericType = getFirstGenericType(field);
return componentsManager.isComponentClass(genericType);
}
return false;
}
Expand All @@ -314,21 +367,32 @@ private static boolean isList(Field field) {
return List.class.isAssignableFrom(field.getType());
}

private Object initFieldElements(ElementLocatorFactory factory, List<HookDefinition<?>> hookDefinitions, Object container, Field field) {
ElementLocator locator = factory.createLocator(field);
if (locator == null) {
return null;
private static boolean isElement(Field field) {
return WebElement.class.isAssignableFrom(field.getType());
}

private static boolean isListOfElement(Field field) {
if (isList(field)) {
Class<?> genericType = getFirstGenericType(field);
return WebElement.class.isAssignableFrom(genericType);
}
return false;
}

private Object initFieldElements(ElementLocator locator, List<HookDefinition<?>> hookDefinitions, Object container, Field field) {
try {
if (isComponentList(field)) {
if (isComponent(field)) {
return initFieldAsComponent(locator, container, field, hookDefinitions);
} else if (isComponentList(field)) {
return initFieldAsComponentList(locator, container, field, hookDefinitions);
} else if (isComponent(field)) {
return initFieldAsElement(locator, container, field, hookDefinitions);
} else if (isListOfFluentWebElement(field)) {
return initFieldAsListOfFluentWebElement(locator, container, field, hookDefinitions);
} else if (isList(field)) {
return initFieldAsList(locator, container, field, hookDefinitions);
} else if (isListOfComponent(field)) {
return initFieldAsListOfComponent(locator, container, field, hookDefinitions);
} else if (isElement(field)) {
return initFieldAsElement(locator, container, field, hookDefinitions);
} else if (isListOfElement(field)) {
return initFieldAsListOfElement(locator, container, field, hookDefinitions);
}
} catch (IllegalAccessException e) {
throw new FluentInjectException("Unable to find an accessible constructor with an argument of type WebElement in " + field.getType(), e);
Expand All @@ -344,7 +408,7 @@ private <L extends List<T>, T> L initFieldAsComponentList(ElementLocator locator
return componentList;
}

private Object initFieldAsElement(ElementLocator locator, Object container, Field field, List<HookDefinition<?>> hookDefinitions) throws IllegalAccessException {
private Object initFieldAsComponent(ElementLocator locator, Object container, Field field, List<HookDefinition<?>> hookDefinitions) throws IllegalAccessException {
WebElement element = LocatorProxies.createWebElement(locator);
Object component = componentsManager.newComponent(field.getType(), element);

Expand All @@ -353,7 +417,7 @@ private Object initFieldAsElement(ElementLocator locator, Object container, Fiel
return component;
}

private ComponentList<?> initFieldAsList(ElementLocator locator, Object container, Field field, List<HookDefinition<?>> hookDefinitions) throws IllegalAccessException {
private ComponentList<?> initFieldAsListOfComponent(ElementLocator locator, Object container, Field field, List<HookDefinition<?>> hookDefinitions) throws IllegalAccessException {
List<WebElement> webElementList = LocatorProxies.createWebElementList(locator);
ComponentList<?> componentList = componentsManager.asComponentList(getFirstGenericType(field), webElementList);
LocatorProxies.setHooks(webElementList, hookChainBuilder, hookDefinitions);
Expand All @@ -368,4 +432,18 @@ private FluentList<? extends FluentWebElement> initFieldAsListOfFluentWebElement
ReflectionUtils.set(field, container, fluentList);
return fluentList;
}

private WebElement initFieldAsElement(ElementLocator locator, Object container, Field field, List<HookDefinition<?>> hookDefinitions) throws IllegalAccessException {
WebElement element = LocatorProxies.createWebElement(locator);
LocatorProxies.setHooks(element, hookChainBuilder, hookDefinitions);
ReflectionUtils.set(field, container, element);
return element;
}

private List<WebElement> initFieldAsListOfElement(ElementLocator locator, Object container, Field field, List<HookDefinition<?>> hookDefinitions) throws IllegalAccessException {
List<WebElement> elements = LocatorProxies.createWebElementList(locator);
LocatorProxies.setHooks(elements, hookChainBuilder, hookDefinitions);
ReflectionUtils.set(field, container, elements);
return elements;
}
}
@@ -0,0 +1,14 @@
package org.fluentlenium.core.inject;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({METHOD, CONSTRUCTOR, FIELD})
@Retention(RUNTIME)
public @interface Parent {
}

0 comments on commit 1f0e354

Please sign in to comment.