Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/Page-objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ and then

This use case has some restrictions;

- All classes which are declared by the OverrideWidget annotation should be subclasses of the class declared by field
- All classes which are declared by the OverrideWidget annotation should be subclasses of the class declared by the field, or implementations of the interface declared by the field

- All classes which are declared by the OverrideWidget should not be abstract. If a declared class is overridden partially like

Expand All @@ -681,6 +681,20 @@ This use case has some restrictions;
//below is the other field/method declaration
```

The field may be an interface implemented by those classes. Each class named by `@OverrideWidget` must extend `Widget` and implement the interface. A content type that is left as the default cannot be instantiated, so name a class for every content type the field is used with:

```java
public interface UsersWidget {
void doWork();
}

@FindBy(id = "users")
@OverrideWidget(html = HtmlUsersWidget.class,
androidUIAutomator = AndroidUsersWidget.class,
iOSXCUITAutomation = IosUsersWidget.class)
UsersWidget widget;
```

- for now it is not possible to

```java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.util.Map;

import static io.appium.java_client.internal.Preconditions.checkNotNull;
import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedInterfaceProxy;
import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedProxy;
import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.unpackObjectFromSearchContext;
import static io.appium.java_client.remote.options.SupportsAutomationNameOption.AUTOMATION_NAME_OPTION;
Expand Down Expand Up @@ -207,11 +208,12 @@ public Object decorate(ClassLoader ignored, Field field) {
@Nullable
private Object decorateWidget(Field field) {
Class<?> type = field.getType();
if (!Widget.class.isAssignableFrom(type) && !List.class.isAssignableFrom(type)) {
if (!Widget.class.isAssignableFrom(type) && !List.class.isAssignableFrom(type)
&& !isOverriddenWidgetInterface(type, field)) {
return null;
}

Class<? extends Widget> widgetType;
Class<?> widgetType;
boolean isAlist = false;
if (List.class.isAssignableFrom(type)) {
isAlist = true;
Expand All @@ -227,18 +229,17 @@ private Object decorateWidget(Field field) {
}

if (listType instanceof Class) {
if (!Widget.class.isAssignableFrom((Class<?>) listType)) {
Class<?> listClass = (Class<?>) listType;
if (!Widget.class.isAssignableFrom(listClass) && !isOverriddenWidgetInterface(listClass, field)) {
return null;
}
//noinspection unchecked
widgetType = (Class<? extends Widget>) listType;
widgetType = listClass;
} else {
return null;
}

} else {
//noinspection unchecked
widgetType = (Class<? extends Widget>) field.getType();
widgetType = field.getType();
}

CacheableLocator locator = widgetLocatorFactory.createLocator(field);
Expand All @@ -251,15 +252,32 @@ private Object decorateWidget(Field field) {
);
}

Constructor<? extends Widget> constructor = WidgetConstructorUtil.findConvenientConstructor(widgetType);
WidgetInterceptor interceptor = new WidgetInterceptor(locator, webDriverReference, null, map, duration);
if (widgetType.isInterface()) {
return getEnhancedInterfaceProxy(widgetType, interceptor);
}

//noinspection unchecked
Class<? extends Widget> widgetClass = (Class<? extends Widget>) widgetType;
Constructor<? extends Widget> constructor = WidgetConstructorUtil.findConvenientConstructor(widgetClass);
return getEnhancedProxy(
widgetType,
widgetClass,
new Class[]{constructor.getParameterTypes()[0]},
new Object[]{proxyForAnElement(locator)},
new WidgetInterceptor(locator, webDriverReference, null, map, duration)
interceptor
);
}

/**
* True when {@code type} is a widget contract rather than a {@link WebElement}.
* {@code @OverrideWidget} on an element field keeps the normal element proxy.
*/
private static boolean isOverriddenWidgetInterface(Class<?> type, Field field) {
return type.isInterface()
&& !WebElement.class.isAssignableFrom(type)
&& field.isAnnotationPresent(OverrideWidget.class);
}

private WebElement proxyForAnElement(ElementLocator locator) {
ElementInterceptor elementInterceptor = new ElementInterceptor(locator, webDriverReference);
return getEnhancedProxy(AppiumWebElement.class, elementInterceptor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@

/**
* This annotation should mark an abstract/certain Widget object class
* or field that declares some abstract/certain Widget object class.
* or field that declares some abstract/certain Widget object class or an interface
* implemented by those classes.
* io.appium.java_client.pagefactory.Widget is the Appium-specific extension of the
* Page Object design pattern.
* About the Page Object design pattern please read these documents:
Expand All @@ -33,17 +34,17 @@
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface OverrideWidget {
/**
* This property is designed for HTML (browser or web view) content.
* A declared class should not be abstract. Declared class also should be a subclass
* of an annotated class/class which is declared by an annotated field.
* A declared class should not be abstract. It must be a subclass of the field type,
* or an implementation of the field type when that type is an interface.
*
* @return a class which extends {@link io.appium.java_client.pagefactory.Widget}
*/
Class<? extends Widget> html() default Widget.class;

/**
* This property is designed for Android native content.
* A declared class should not be abstract. Declared class also should be a subclass
* of an annotated class/class which is declared by an annotated field.
* A declared class should not be abstract. It must be a subclass of the field type,
* or an implementation of the field type when that type is an interface.
*
* @return a class which extends {@link io.appium.java_client.pagefactory.Widget}
*/
Expand All @@ -52,17 +53,17 @@
/**
* This property is designed for iOS native content when
* {@link io.appium.java_client.remote.AutomationName#IOS_XCUI_TEST} automation is used.
* A declared class should not be abstract. Declared class also should be a subclass
* of an annotated class/class which is declared by an annotated field.
* A declared class should not be abstract. It must be a subclass of the field type,
* or an implementation of the field type when that type is an interface.
*
* @return a class which extends {@link io.appium.java_client.pagefactory.Widget}
*/
Class<? extends Widget> iOSXCUITAutomation() default Widget.class;

/**
* This property is designed for Windows native content.
* A declared class should not be abstract. Declared class also should be a subclass
* of an annotated class/class which is declared by an annotated field.
* A declared class should not be abstract. It must be a subclass of the field type,
* or an implementation of the field type when that type is an interface.
*
* @return a class which extends {@link io.appium.java_client.pagefactory.Widget}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,14 @@ class OverrideWidgetReader {
private OverrideWidgetReader() {
}

@SuppressWarnings("unchecked")
private static Class<? extends Widget> getConvenientClass(Class<? extends Widget> declaredClass,
AnnotatedElement annotatedElement, String method) {
Class<? extends Widget> convenientClass;
private static Class<?> getConvenientClass(Class<?> declaredClass,
AnnotatedElement annotatedElement, String method) {
Class<?> convenientClass;
OverrideWidget overrideWidget = annotatedElement.getAnnotation(OverrideWidget.class);

try {
if (overrideWidget == null || (convenientClass =
(Class<? extends Widget>) OverrideWidget.class
(Class<?>) OverrideWidget.class
.getDeclaredMethod(method).invoke(overrideWidget))
.equals(EMPTY)) {
convenientClass = declaredClass;
Expand All @@ -68,13 +67,12 @@ private static Class<? extends Widget> getConvenientClass(Class<? extends Widget

}

static Class<? extends Widget> getDefaultOrHTMLWidgetClass(
Class<? extends Widget> declaredClass, AnnotatedElement annotatedElement) {
static Class<?> getDefaultOrHTMLWidgetClass(Class<?> declaredClass, AnnotatedElement annotatedElement) {
return getConvenientClass(declaredClass, annotatedElement, HTML);
}

static Class<? extends Widget> getMobileNativeWidgetClass(Class<? extends Widget> declaredClass,
AnnotatedElement annotatedElement, String platform) {
static Class<?> getMobileNativeWidgetClass(Class<?> declaredClass,
AnnotatedElement annotatedElement, String platform) {
String transformedPlatform = String.valueOf(platform).toUpperCase(ROOT).trim();

if (ANDROID.equalsIgnoreCase(transformedPlatform)) {
Expand All @@ -92,22 +90,30 @@ static Class<? extends Widget> getMobileNativeWidgetClass(Class<? extends Widget
return getDefaultOrHTMLWidgetClass(declaredClass, annotatedElement);
}

/**
* Interfaces have no widget constructor. A missing platform override stays unresolved
* until that content type is used, matching a partial {@code @OverrideWidget}.
*/
@SuppressWarnings("unchecked")
private static Constructor<? extends Widget> findConstructor(Class<?> clazz) {
if (clazz == null || clazz.isInterface() || !Widget.class.isAssignableFrom(clazz)) {
return null;
}
return findConvenientConstructor((Class<? extends Widget>) clazz);
}

private static Constructor<? extends Widget> getConstructorOfADefaultOrHTMLWidget(
Class<? extends Widget> declaredClass, AnnotatedElement annotatedElement) {
Class<? extends Widget> clazz =
getDefaultOrHTMLWidgetClass(declaredClass, annotatedElement);
return findConvenientConstructor(clazz);
Class<?> declaredClass, AnnotatedElement annotatedElement) {
return findConstructor(getDefaultOrHTMLWidgetClass(declaredClass, annotatedElement));
}

private static Constructor<? extends Widget> getConstructorOfAMobileNativeWidgets(
Class<? extends Widget> declaredClass, AnnotatedElement annotatedElement, String platform) {
Class<? extends Widget> clazz =
getMobileNativeWidgetClass(declaredClass, annotatedElement, platform);
return findConvenientConstructor(clazz);
Class<?> declaredClass, AnnotatedElement annotatedElement, String platform) {
return findConstructor(getMobileNativeWidgetClass(declaredClass, annotatedElement, platform));
}

protected static Map<ContentType, Constructor<? extends Widget>> read(
Class<? extends Widget> declaredClass, AnnotatedElement annotatedElement, String platform) {
Class<?> declaredClass, AnnotatedElement annotatedElement, String platform) {
Map<ContentType, Constructor<? extends Widget>> result = new HashMap<>();
result.put(ContentType.HTML_OR_DEFAULT,
getConstructorOfADefaultOrHTMLWidget(declaredClass, annotatedElement));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ private static Class<?> getClassFromAListField(Field field) {
return (Class<?>) listType;
}

@SuppressWarnings("unchecked")
private By getByFromDeclaredClass(WhatIsNeeded whatIsNeeded) {
AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated();
Field field = (Field) annotatedElement;
Expand All @@ -72,15 +71,12 @@ private By getByFromDeclaredClass(WhatIsNeeded whatIsNeeded) {
Class<?> convenientClass;

if (whatIsNeeded.equals(WhatIsNeeded.DEFAULT_OR_HTML)) {
convenientClass =
getDefaultOrHTMLWidgetClass((Class<? extends Widget>) declaredClass, field);
convenientClass = getDefaultOrHTMLWidgetClass(declaredClass, field);
} else {
convenientClass =
getMobileNativeWidgetClass((Class<? extends Widget>) declaredClass, field,
platform);
convenientClass = getMobileNativeWidgetClass(declaredClass, field, platform);
}

while (result == null && !convenientClass.equals(Object.class)) {
while (result == null && convenientClass != null && !convenientClass.equals(Object.class)) {
setAnnotated(convenientClass);
if (whatIsNeeded.equals(WhatIsNeeded.DEFAULT_OR_HTML)) {
result = super.buildDefaultBy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ protected Object getObject(WebElement element, Method method, Object[] args) thr
cachedElementReference = new WeakReference<>(element);

Constructor<? extends Widget> constructor = instantiationMap.get(type);
if (constructor == null) {
throw new InstantiationException(String.format(
"No concrete @OverrideWidget class is declared for %s. "
+ "A field typed as an interface needs a widget class for this content.",
type));
}
Class<? extends Widget> clazz = constructor.getDeclaringClass();

if (Modifier.isAbstract(clazz.getModifiers())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@
import java.util.stream.Collectors;

import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException;
import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedInterfaceProxy;
import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedProxy;
import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.getCurrentContentType;
import static java.util.Optional.ofNullable;

/** Proxy interceptor class for lists of widgets. */
public class WidgetListInterceptor extends InterceptorOfAListOfElements {
private final Map<ContentType, Constructor<? extends Widget>> instantiationMap;
private final List<Widget> cachedWidgets = new ArrayList<>();
private final Class<? extends Widget> declaredType;
private final List<Object> cachedWidgets = new ArrayList<>();
private final Class<?> declaredType;
private final Duration duration;
private final WeakReference<WebDriver> driver;
private final List<WeakReference<WebElement>> cachedElementReferences = new ArrayList<>();
Expand All @@ -53,14 +54,14 @@ public class WidgetListInterceptor extends InterceptorOfAListOfElements {
* @param locator the locator of the widget elements
* @param driver the reference to the driver
* @param instantiationMap the widget constructors mapped by the content type
* @param declaredType the declared widget type
* @param declaredType the declared widget class, or an interface implemented by the override classes
* @param duration the timeout of waiting for an element presence
*/
public WidgetListInterceptor(
@Nullable CacheableLocator locator,
WeakReference<WebDriver> driver,
Map<ContentType, Constructor<? extends Widget>> instantiationMap,
Class<? extends Widget> declaredType,
Class<?> declaredType,
Duration duration
) {
super(locator);
Expand All @@ -84,14 +85,15 @@ protected Object getObject(List<WebElement> elements, Method method, Object[] ar
ContentType type = null;
for (WebElement element : elements) {
type = ofNullable(type).orElseGet(() -> getCurrentContentType(element));
Class<?>[] params = new Class<?>[] {instantiationMap.get(type).getParameterTypes()[0]};
WeakReference<WebElement> elementReference = new WeakReference<>(element);
cachedWidgets.add(
getEnhancedProxy(
declaredType, params, new Object[] {element},
new WidgetInterceptor(null, driver, elementReference, instantiationMap, duration)
)
);
WidgetInterceptor interceptor = new WidgetInterceptor(
null, driver, elementReference, instantiationMap, duration);
if (declaredType.isInterface()) {
cachedWidgets.add(getEnhancedInterfaceProxy(declaredType, interceptor));
} else {
Class<?>[] params = new Class<?>[] {instantiationMap.get(type).getParameterTypes()[0]};
cachedWidgets.add(proxyWidget(declaredType, params, element, interceptor));
}
cachedElementReferences.add(elementReference);
}
}
Expand All @@ -101,4 +103,14 @@ protected Object getObject(List<WebElement> elements, Method method, Object[] ar
throw extractReadableException(t);
}
}

@SuppressWarnings("unchecked")
private static Widget proxyWidget(
Class<?> declaredType,
Class<?>[] params,
WebElement element,
WidgetInterceptor interceptor
) {
return getEnhancedProxy((Class<Widget>) declaredType, params, new Object[] {element}, interceptor);
}
}
Loading