diff --git a/docs/Page-objects.md b/docs/Page-objects.md index d1664e082..3e8195a0d 100644 --- a/docs/Page-objects.md +++ b/docs/Page-objects.md @@ -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 @@ -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 diff --git a/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java b/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java index 3d8e9b42b..04f9a491b 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java @@ -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; @@ -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 widgetType; + Class widgetType; boolean isAlist = false; if (List.class.isAssignableFrom(type)) { isAlist = true; @@ -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) listType; + widgetType = listClass; } else { return null; } } else { - //noinspection unchecked - widgetType = (Class) field.getType(); + widgetType = field.getType(); } CacheableLocator locator = widgetLocatorFactory.createLocator(field); @@ -251,15 +252,32 @@ private Object decorateWidget(Field field) { ); } - Constructor constructor = WidgetConstructorUtil.findConvenientConstructor(widgetType); + WidgetInterceptor interceptor = new WidgetInterceptor(locator, webDriverReference, null, map, duration); + if (widgetType.isInterface()) { + return getEnhancedInterfaceProxy(widgetType, interceptor); + } + + //noinspection unchecked + Class widgetClass = (Class) widgetType; + Constructor 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); diff --git a/src/main/java/io/appium/java_client/pagefactory/OverrideWidget.java b/src/main/java/io/appium/java_client/pagefactory/OverrideWidget.java index 53bdee8f0..1569a03f9 100644 --- a/src/main/java/io/appium/java_client/pagefactory/OverrideWidget.java +++ b/src/main/java/io/appium/java_client/pagefactory/OverrideWidget.java @@ -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: @@ -33,8 +34,8 @@ @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} */ @@ -42,8 +43,8 @@ /** * 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} */ @@ -52,8 +53,8 @@ /** * 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} */ @@ -61,8 +62,8 @@ /** * 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} */ diff --git a/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java b/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java index 09984729c..79e6cb28e 100644 --- a/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java +++ b/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java @@ -40,15 +40,14 @@ class OverrideWidgetReader { private OverrideWidgetReader() { } - @SuppressWarnings("unchecked") - private static Class getConvenientClass(Class declaredClass, - AnnotatedElement annotatedElement, String method) { - Class 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) OverrideWidget.class + (Class) OverrideWidget.class .getDeclaredMethod(method).invoke(overrideWidget)) .equals(EMPTY)) { convenientClass = declaredClass; @@ -68,13 +67,12 @@ private static Class getConvenientClass(Class getDefaultOrHTMLWidgetClass( - Class declaredClass, AnnotatedElement annotatedElement) { + static Class getDefaultOrHTMLWidgetClass(Class declaredClass, AnnotatedElement annotatedElement) { return getConvenientClass(declaredClass, annotatedElement, HTML); } - static Class getMobileNativeWidgetClass(Class 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)) { @@ -92,22 +90,30 @@ static Class getMobileNativeWidgetClass(Class findConstructor(Class clazz) { + if (clazz == null || clazz.isInterface() || !Widget.class.isAssignableFrom(clazz)) { + return null; + } + return findConvenientConstructor((Class) clazz); + } + private static Constructor getConstructorOfADefaultOrHTMLWidget( - Class declaredClass, AnnotatedElement annotatedElement) { - Class clazz = - getDefaultOrHTMLWidgetClass(declaredClass, annotatedElement); - return findConvenientConstructor(clazz); + Class declaredClass, AnnotatedElement annotatedElement) { + return findConstructor(getDefaultOrHTMLWidgetClass(declaredClass, annotatedElement)); } private static Constructor getConstructorOfAMobileNativeWidgets( - Class declaredClass, AnnotatedElement annotatedElement, String platform) { - Class clazz = - getMobileNativeWidgetClass(declaredClass, annotatedElement, platform); - return findConvenientConstructor(clazz); + Class declaredClass, AnnotatedElement annotatedElement, String platform) { + return findConstructor(getMobileNativeWidgetClass(declaredClass, annotatedElement, platform)); } protected static Map> read( - Class declaredClass, AnnotatedElement annotatedElement, String platform) { + Class declaredClass, AnnotatedElement annotatedElement, String platform) { Map> result = new HashMap<>(); result.put(ContentType.HTML_OR_DEFAULT, getConstructorOfADefaultOrHTMLWidget(declaredClass, annotatedElement)); diff --git a/src/main/java/io/appium/java_client/pagefactory/WidgetByBuilder.java b/src/main/java/io/appium/java_client/pagefactory/WidgetByBuilder.java index 749632702..1336ccf34 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetByBuilder.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetByBuilder.java @@ -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; @@ -72,15 +71,12 @@ private By getByFromDeclaredClass(WhatIsNeeded whatIsNeeded) { Class convenientClass; if (whatIsNeeded.equals(WhatIsNeeded.DEFAULT_OR_HTML)) { - convenientClass = - getDefaultOrHTMLWidgetClass((Class) declaredClass, field); + convenientClass = getDefaultOrHTMLWidgetClass(declaredClass, field); } else { - convenientClass = - getMobileNativeWidgetClass((Class) 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(); diff --git a/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java index 4a26839f9..245b03ffb 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java @@ -77,6 +77,12 @@ protected Object getObject(WebElement element, Method method, Object[] args) thr cachedElementReference = new WeakReference<>(element); Constructor 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 clazz = constructor.getDeclaringClass(); if (Modifier.isAbstract(clazz.getModifiers())) { diff --git a/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java index 243ac8084..e5c0a8b89 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java @@ -34,6 +34,7 @@ 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; @@ -41,8 +42,8 @@ /** Proxy interceptor class for lists of widgets. */ public class WidgetListInterceptor extends InterceptorOfAListOfElements { private final Map> instantiationMap; - private final List cachedWidgets = new ArrayList<>(); - private final Class declaredType; + private final List cachedWidgets = new ArrayList<>(); + private final Class declaredType; private final Duration duration; private final WeakReference driver; private final List> cachedElementReferences = new ArrayList<>(); @@ -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 driver, Map> instantiationMap, - Class declaredType, + Class declaredType, Duration duration ) { super(locator); @@ -84,14 +85,15 @@ protected Object getObject(List 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 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); } } @@ -101,4 +103,14 @@ protected Object getObject(List 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) declaredType, params, new Object[] {element}, interceptor); + } } diff --git a/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java b/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java index b0ee40c7e..1a0fa87c3 100644 --- a/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java +++ b/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java @@ -16,18 +16,28 @@ package io.appium.java_client.pagefactory.utils; +import io.appium.java_client.proxy.HasMethodCallListeners; +import io.appium.java_client.proxy.Interceptor; import io.appium.java_client.proxy.MethodCallListener; +import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatcher; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.WeakHashMap; import static io.appium.java_client.proxy.Helpers.OBJECT_METHOD_NAMES; import static io.appium.java_client.proxy.Helpers.createProxy; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.not; @@ -39,6 +49,8 @@ public final class ProxyFactory { private static final Set NON_PROXYABLE_METHODS = setWithout( OBJECT_METHOD_NAMES, "toString", "equals", "hashCode" ); + private static final Map, Class> CACHED_INTERFACE_PROXIES = + Collections.synchronizedMap(new WeakHashMap<>()); @SafeVarargs private static Set setWithout(@SuppressWarnings("SameParameterValue") Set source, T... items) { @@ -97,4 +109,50 @@ public static T getEnhancedProxy( extraMatcher ); } + + /** + * Creates a proxy that implements {@code iface} and forwards its public methods to {@code listener}. + * Used when an {@code @OverrideWidget} field is declared as an interface. + * + * @param iface an interface type + * @param listener receives the proxied calls + * @return a proxy that implements {@code iface} + */ + public static Object getEnhancedInterfaceProxy(Class iface, MethodCallListener listener) { + if (iface == null || !iface.isInterface()) { + throw new IllegalArgumentException("Expected an interface, got " + iface); + } + if (listener == null) { + throw new IllegalArgumentException("Listener must not be null"); + } + Class proxyClass = CACHED_INTERFACE_PROXIES.computeIfAbsent(iface, ProxyFactory::loadInterfaceProxy); + try { + Object result = proxyClass.getDeclaredConstructor().newInstance(); + ((HasMethodCallListeners) result).setMethodCallListeners(new MethodCallListener[] {listener}); + return result; + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(String.format("Unable to create a proxy of %s", iface.getName()), e); + } + } + + private static Class loadInterfaceProxy(Class iface) { + ClassLoader loader = iface.getClassLoader() == null + ? ProxyFactory.class.getClassLoader() + : iface.getClassLoader(); + ElementMatcher methods = isPublic().and(not( + namedOneOf(NON_PROXYABLE_METHODS.toArray(new String[0])) + )); + //noinspection resource + return new ByteBuddy() + .subclass(Object.class) + .implement(iface) + .method(methods) + .intercept(MethodDelegation.to(Interceptor.class)) + .defineField("methodCallListeners", MethodCallListener[].class, Visibility.PRIVATE) + .implement(HasMethodCallListeners.class) + .intercept(FieldAccessor.ofBeanProperty()) + .make() + .load(loader, ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); + } } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/OverriddenWidgetInterfaceTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/OverriddenWidgetInterfaceTest.java new file mode 100644 index 000000000..366ad5ec0 --- /dev/null +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/OverriddenWidgetInterfaceTest.java @@ -0,0 +1,183 @@ +package io.appium.java_client.pagefactory_tests.widget.tests.combined; + +import io.appium.java_client.pagefactory.AndroidFindBy; +import io.appium.java_client.pagefactory.AppiumFieldDecorator; +import io.appium.java_client.pagefactory.OverrideWidget; +import io.appium.java_client.pagefactory.Widget; +import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; +import io.appium.java_client.support.FindBy; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import java.util.List; +import java.util.stream.Stream; + +import static io.appium.java_client.support.PageFactory.initElements; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OverriddenWidgetInterfaceTest { + + static Stream drivers() { + return Stream.of( + Arguments.of(new AbstractStubWebDriver.StubAndroidDriver(), "android"), + Arguments.of(new AbstractStubWebDriver.StubIOSXCUITDriver(), "ios"), + Arguments.of(new AbstractStubWebDriver.StubBrowserDriver(), "html"), + Arguments.of(new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), "html") + ); + } + + @ParameterizedTest + @MethodSource("drivers") + void delegatesInterfaceMethodToPlatformWidget(WebDriver driver, String expected) { + var page = new Page(); + initElements(new AppiumFieldDecorator(driver), page); + + assertEquals(expected, page.widget.platformAction()); + assertEquals(2, page.widgets.size()); + page.widgets.forEach(widget -> assertEquals(expected, widget.platformAction())); + } + + @Test + void usesLocatorDeclaredOnThePlatformWidgetClass() { + var page = new ClassLocatedPage(); + initElements(new AppiumFieldDecorator(new AbstractStubWebDriver.StubAndroidDriver()), page); + + assertEquals("android", page.widget.platformAction()); + assertTrue(page.widget.toString().contains("android-root")); + assertEquals(2, page.widgets.size()); + page.widgets.forEach(widget -> assertEquals("android", widget.platformAction())); + } + + @Test + void allowsPartialOverrideForTheCurrentContent() { + var page = new IosOnlyPage(); + initElements(new AppiumFieldDecorator(new AbstractStubWebDriver.StubIOSXCUITDriver()), page); + + assertEquals("ios", page.widget.platformAction()); + } + + @Test + void missingContentOverrideFailsWhenThatContentIsUsed() { + var page = new IosOnlyPage(); + initElements(new AppiumFieldDecorator(new AbstractStubWebDriver.StubAndroidDriver()), page); + + InstantiationException error = assertThrows(InstantiationException.class, () -> page.widget.platformAction()); + assertTrue(error.getMessage().contains("NATIVE_MOBILE_SPECIFIC")); + } + + @Test + void rejectsOverrideClassThatDoesNotImplementTheInterface() { + var page = new MismatchedPage(); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> initElements(new AppiumFieldDecorator(new AbstractStubWebDriver.StubAndroidDriver()), page) + ); + assertTrue(error.getCause().getMessage().contains("is not assignable from")); + } + + @Test + void leavesUnannotatedInterfaceFieldUnset() { + var page = new PlainPage(); + initElements(new AppiumFieldDecorator(new AbstractStubWebDriver.StubAndroidDriver()), page); + + assertNull(page.untouched); + } + + static class Page { + @OverrideWidget(html = HtmlWidget.class, + androidUIAutomator = AndroidWidget.class, + iOSXCUITAutomation = IosWidget.class) + @FindBy(id = "widget") + private ActionWidget widget; + + @OverrideWidget(html = HtmlWidget.class, + androidUIAutomator = AndroidWidget.class, + iOSXCUITAutomation = IosWidget.class) + @FindBy(id = "widget") + private List widgets; + } + + static class ClassLocatedPage { + @OverrideWidget(html = HtmlWidget.class, + androidUIAutomator = AndroidWidget.class, + iOSXCUITAutomation = IosWidget.class) + private ActionWidget widget; + + @OverrideWidget(html = HtmlWidget.class, + androidUIAutomator = AndroidWidget.class, + iOSXCUITAutomation = IosWidget.class) + private List widgets; + } + + static class IosOnlyPage { + @OverrideWidget(iOSXCUITAutomation = IosWidget.class) + @FindBy(id = "widget") + private ActionWidget widget; + } + + static class MismatchedPage { + @OverrideWidget(androidUIAutomator = UnrelatedWidget.class, + iOSXCUITAutomation = IosWidget.class, + html = HtmlWidget.class) + @FindBy(id = "widget") + private ActionWidget widget; + } + + static class PlainPage { + private ActionWidget untouched; + } + + public interface ActionWidget { + String platformAction(); + } + + @AndroidFindBy(uiAutomator = "android-root") + public static class AndroidWidget extends Widget implements ActionWidget { + protected AndroidWidget(WebElement element) { + super(element); + } + + @Override + public String platformAction() { + return "android"; + } + } + + @iOSXCUITFindBy(iOSNsPredicate = "ios-root") + public static class IosWidget extends Widget implements ActionWidget { + protected IosWidget(WebElement element) { + super(element); + } + + @Override + public String platformAction() { + return "ios"; + } + } + + public static class HtmlWidget extends Widget implements ActionWidget { + protected HtmlWidget(WebElement element) { + super(element); + } + + @Override + public String platformAction() { + return "html"; + } + } + + public static class UnrelatedWidget extends Widget { + protected UnrelatedWidget(WebElement element) { + super(element); + } + } +}