Skip to content
Merged
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 @@ -684,7 +684,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 @@ -700,6 +700,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. The interface must be public. Creation succeeds when this library's class loader can see that interface, or when the interface's class loader can see this library. A package-private or protected interface, and loaders that cannot see either way, fail with `IllegalArgumentException` instead of a linkage error. 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 @@ -49,6 +49,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 @@ -335,6 +336,43 @@ private static Class<? extends Widget> resolveWidgetClass(
return null;
}

/**
* Widget class for a field, including an {@code @OverrideWidget} interface.
*
* @param field the page object field
* @return the widget class or interface, or null
*/
@Nullable
static Class<?> declaredWidgetType(Field field) {
Class<?> pageClass = PageFactory.getCurrentPageClass();
return declaredWidgetType(
field,
ElementFieldDecorator.resolveSingleFieldClasses(field, pageClass),
ElementFieldDecorator.resolveListClasses(field, pageClass)
);
}

@Nullable
private static Class<?> declaredWidgetType(
Field field,
List<Class<?>> fieldClasses,
List<Class<?>> listElementClasses
) {
Class<? extends Widget> widgetClass = resolveWidgetClass(field, fieldClasses, listElementClasses);
if (widgetClass != null) {
return widgetClass;
}
List<Class<?>> candidates = List.class.isAssignableFrom(field.getType())
? listElementClasses
: fieldClasses;
for (Class<?> candidate : candidates) {
if (isOverriddenWidgetInterface(candidate, field)) {
return candidate;
}
}
return null;
}

/**
* Decorated page object {@code field}.
*
Expand All @@ -349,7 +387,7 @@ public Object decorate(ClassLoader ignored, Field field) {
Class<?> pageClass = PageFactory.getCurrentPageClass();
List<Class<?>> fieldClasses = ElementFieldDecorator.resolveSingleFieldClasses(field, pageClass);
List<Class<?>> listElementClasses = ElementFieldDecorator.resolveListClasses(field, pageClass);
Class<? extends Widget> widgetClass = resolveWidgetClass(field, fieldClasses, listElementClasses);
Class<?> widgetClass = declaredWidgetType(field, fieldClasses, listElementClasses);
Object result = decorateWidget(field, widgetClass, pageClass);
if (result == null) {
result = decorateBy(field, pageClass);
Expand Down Expand Up @@ -445,17 +483,21 @@ private static By unwrapSingleBy(@Nullable By by) {
}

@Nullable
@SuppressWarnings("unchecked")
private Object decorateWidget(
Field field,
@Nullable Class<? extends Widget> widgetType,
@Nullable Class<?> widgetType,
@Nullable Class<?> pageClass
) {
if (widgetType == null) {
return null;
}
boolean isAlist = List.class.isAssignableFrom(field.getType());
Class<? extends Widget> locatorType = widgetType.isInterface()
? null
: (Class<? extends Widget>) widgetType;

CacheableLocator locator = widgetLocatorFactory.createLocator(field, pageClass, widgetType);
CacheableLocator locator = widgetLocatorFactory.createLocator(field, pageClass, locatorType);
Map<ContentType, Constructor<? extends Widget>> map = OverrideWidgetReader.read(widgetType, field, platform);

if (isAlist) {
Expand All @@ -472,22 +514,43 @@ private Object decorateWidget(
);
}

Constructor<? extends Widget> constructor = WidgetConstructorUtil.findConvenientConstructor(widgetType);
WidgetInterceptor interceptor = new WidgetInterceptor(
locator,
webDriverReference,
null,
map,
duration,
defaultLocatorGroupStrategy
);
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, webDriverReference)},
new WidgetInterceptor(
locator,
webDriverReference,
null,
map,
duration,
defaultLocatorGroupStrategy
)
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.
*
* @param type the field type, or the list element type
* @param field the annotated field
* @return whether the type should be decorated as an interface widget
*/
static boolean isOverriddenWidgetInterface(Class<?> type, Field field) {
return type.isInterface()
&& !WebElement.class.isAssignableFrom(type)
&& field.isAnnotationPresent(OverrideWidget.class);
}

private static WebElement proxyForAnElement(
ElementLocator locator,
WeakReference<WebDriver> webDriverReference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@

/**
* 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 a public interface
* implemented by those classes. A package-private or protected interface is rejected with
* {@link IllegalArgumentException}. A public interface is accepted when this library's
* class loader can see it, or when its class loader can see this library. Otherwise
* initialization throws {@link IllegalArgumentException}.
* 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 +37,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 +56,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 @@ -59,23 +59,23 @@ private By getByFromDeclaredClass(WhatIsNeeded whatIsNeeded) {

By result = null;
try {
Class<? extends Widget> declaredClass = resolveWidgetClassForField(field);
Class<?> declaredClass = resolveWidgetClassForField(field);
if (declaredClass == null) {
declaredClass = AppiumFieldDecorator.declaredWidgetType(field);
}
if (declaredClass == null) {
return null;
}

Class<?> convenientClass;

if (whatIsNeeded.equals(WhatIsNeeded.DEFAULT_OR_HTML)) {
convenientClass =
getDefaultOrHTMLWidgetClass(declaredClass, field);
convenientClass = getDefaultOrHTMLWidgetClass(declaredClass, field);
} else {
convenientClass =
getMobileNativeWidgetClass(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 @@ -36,7 +36,6 @@

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

/**
* Proxy interceptor class for widgets.
Expand All @@ -59,6 +58,8 @@ public class WidgetInterceptor extends InterceptorOfASingleElement {
private final Duration duration;
private final LocatorGroupStrategy defaultLocatorGroupStrategy;
private WeakReference<WebElement> cachedElementReference;
// Interface list proxies do not store the element. Hold it until the widget, which keeps it, exists.
private WebElement elementUntilWidgetCreated;

/**
* Creates a new interceptor.
Expand Down Expand Up @@ -96,6 +97,7 @@ public WidgetInterceptor(
) {
super(locator, driverReference);
this.cachedElementReference = cachedElementReference;
this.elementUntilWidgetCreated = cachedElementReference == null ? null : cachedElementReference.get();
this.instantiationMap = instantiationMap;
this.duration = duration;
this.defaultLocatorGroupStrategy = defaultLocatorGroupStrategy;
Expand All @@ -120,7 +122,14 @@ private boolean isCached(ContentType type) {
}

private void rejectAbstractWidget(ContentType type) throws InstantiationException {
Class<? extends Widget> clazz = instantiationMap.get(type).getDeclaringClass();
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())) {
throw new InstantiationException(
String.format("%s is abstract so it cannot be instantiated", clazz.getName())
Expand All @@ -142,6 +151,7 @@ private void bind(ContentType type, WebElement root) throws Throwable {
);
cachedElementReference = new WeakReference<>(root);
cachedInstances.put(type, widget);
elementUntilWidgetCreated = null;
}

private static Object invoke(Widget widget, Method method, Object[] args) throws Throwable {
Expand All @@ -155,9 +165,14 @@ private static Object invoke(Widget widget, Method method, Object[] args) throws

@Override
public Object call(Object obj, Method method, Object[] args, Callable<?> original) throws Throwable {
WebElement element = ofNullable(cachedElementReference).map(WeakReference::get).orElse(null);
WebElement element = referencedElement();
return locator == null && element != null
? getObject(element, method, args)
: super.call(obj, method, args, original);
}

private WebElement referencedElement() {
WebElement cached = cachedElementReference == null ? null : cachedElementReference.get();
return cached != null ? cached : elementUntilWidgetCreated;
}
}
Loading
Loading