diff --git a/src/main/java/io/appium/java_client/pagefactory/AndroidFindAll.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindAll.java new file mode 100644 index 000000000..2a99e372c --- /dev/null +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindAll.java @@ -0,0 +1,17 @@ +package io.appium.java_client.pagefactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to mark a field on a Page/Screen Object to indicate that lookup should use a series of {@link AndroidFindBy} tags + * It will then search for all elements that match any criteria. Note that elements + * are not guaranteed to be in document order. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface AndroidFindAll { + AndroidFindBy[] value(); +} diff --git a/src/main/java/io/appium/java_client/pagefactory/AppiumAnnotations.java b/src/main/java/io/appium/java_client/pagefactory/AppiumAnnotations.java index 724435317..41b9e4218 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumAnnotations.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumAnnotations.java @@ -4,6 +4,7 @@ import io.appium.java_client.MobileBy; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -12,6 +13,7 @@ import org.openqa.selenium.By; import org.openqa.selenium.support.pagefactory.Annotations; +import org.openqa.selenium.support.pagefactory.ByAll; import org.openqa.selenium.support.pagefactory.ByChained; class AppiumAnnotations extends Annotations{ @@ -143,26 +145,34 @@ By getBy(Annotation annotation) { toUpperCase().trim(); } + private static void checkDisallowedAnnotationPairs(Annotation a1, + Annotation a2) throws IllegalArgumentException { + if (a1 != null && a2 != null) { + throw new IllegalArgumentException( + "If you use a '@" + a1.getClass().getSimpleName() + "' annotation, " + + "you must not also use a '@" + a2.getClass().getSimpleName() + "' annotation"); + } + } + private void assertValidAnnotations() { AndroidFindBy androidBy = mobileField .getAnnotation(AndroidFindBy.class); AndroidFindBys androidBys = mobileField .getAnnotation(AndroidFindBys.class); + AndroidFindAll androidFindAll = mobileField. + getAnnotation(AndroidFindAll.class); iOSFindBy iOSBy = mobileField.getAnnotation(iOSFindBy.class); iOSFindBys iOSBys = mobileField.getAnnotation(iOSFindBys.class); - - if (androidBy != null && androidBys != null) { - throw new IllegalArgumentException( - "If you use a '@AndroidFindBy' annotation, " - + "you must not also use a '@AndroidFindBys' annotation"); - } - - if (iOSBy != null && iOSBys != null) { - throw new IllegalArgumentException( - "If you use a '@iOSFindBy' annotation, " - + "you must not also use a '@iOSFindBys' annotation"); - } + iOSFindAll iOSFindAll = mobileField.getAnnotation(iOSFindAll.class); + + checkDisallowedAnnotationPairs(androidBy, androidBys); + checkDisallowedAnnotationPairs(androidBy, androidFindAll); + checkDisallowedAnnotationPairs(androidBys, androidFindAll); + + checkDisallowedAnnotationPairs(iOSBy, iOSBys); + checkDisallowedAnnotationPairs(iOSBy, iOSFindAll); + checkDisallowedAnnotationPairs(iOSBys, iOSFindAll); } private static Method[] prepareAnnotationMethods( @@ -217,14 +227,21 @@ private By getMobileBy(Annotation annotation, String valueName) { + ": There is an unknown strategy " + valueName); } - private By getChainedMobileBy(Annotation[] annotations) { + @SuppressWarnings("unchecked") + private T getComplexMobileBy(Annotation[] annotations, Class requiredByClass) { ; By[] byArray = new By[annotations.length]; for (int i = 0; i < annotations.length; i++) { byArray[i] = getMobileBy(annotations[i], getFilledValue(annotations[i])); } - return new ByChained(byArray); + try { + Constructor c = requiredByClass.getConstructor(By[].class); + Object[] values = new Object[]{byArray}; + return (T) c.newInstance(values); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override @@ -240,7 +257,12 @@ public By buildBy() { AndroidFindBys androidBys = mobileField .getAnnotation(AndroidFindBys.class); if (androidBys != null && ANDROID.toUpperCase().equals(platform)) { - return getChainedMobileBy(androidBys.value()); + return getComplexMobileBy(androidBys.value(), ByChained.class); + } + + AndroidFindAll androidFindAll = mobileField.getAnnotation(AndroidFindAll.class); + if (androidFindAll != null && ANDROID.toUpperCase().equals(platform)) { + return getComplexMobileBy(androidFindAll.value(), ByAll.class); } iOSFindBy iOSBy = mobileField.getAnnotation(iOSFindBy.class); @@ -250,8 +272,13 @@ public By buildBy() { iOSFindBys iOSBys = mobileField.getAnnotation(iOSFindBys.class); if (iOSBys != null && IOS.toUpperCase().equals(platform)) { - return getChainedMobileBy(iOSBys.value()); + return getComplexMobileBy(iOSBys.value(), ByChained.class); } + + iOSFindAll iOSFindAll = mobileField.getAnnotation(iOSFindAll.class); + if (iOSFindAll != null && IOS.toUpperCase().equals(platform)) { + return getComplexMobileBy(iOSFindAll.value(), ByAll.class); + } return super.buildBy(); } 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 5258b2f63..026ea3005 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java @@ -1,142 +1,132 @@ -package io.appium.java_client.pagefactory; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.android.AndroidElement; -import io.appium.java_client.ios.IOSElement; - -import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.support.FindAll; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.pagefactory.ElementLocator; -import org.openqa.selenium.support.pagefactory.FieldDecorator; - -/** - * Default decorator for use with PageFactory. Will decorate 1) all of the - * WebElement fields and 2) List fields that have - * {@literal @AndroidFindBy}, {@literal @AndroidFindBys}, or - * {@literal @iOSFindBy/@iOSFindBys} annotation with a proxy that locates the - * elements using the passed in ElementLocatorFactory. - * - * Please pay attention: fields of {@link WebElement}, {@link RemoteWebElement}, - * {@link MobileElement}, {@link AndroidElement} and {@link IOSElement} are allowed - * to use with this decorator - */ -public class AppiumFieldDecorator implements FieldDecorator, ResetsImplicitlyWaitTimeOut { - - private static final List> availableElementClasses = - new ArrayList>(){ - private static final long serialVersionUID = 1L; - { - add(WebElement.class); - add(RemoteWebElement.class); - add(MobileElement.class); - add(AndroidElement.class); - add(IOSElement.class); - } - - }; - - private final AppiumElementLocatorFactory factory; - - public static long DEFAULT_IMPLICITLY_WAIT_TIMEOUT = 1; - - public static TimeUnit DEFAULT_TIMEUNIT = TimeUnit.SECONDS; - - public AppiumFieldDecorator(SearchContext context, long implicitlyWaitTimeOut, TimeUnit timeUnit) { - factory = new AppiumElementLocatorFactory(context, implicitlyWaitTimeOut, timeUnit); - } - - public AppiumFieldDecorator(SearchContext context) { - factory = new AppiumElementLocatorFactory(context); - } - - public Object decorate(ClassLoader ignored, Field field) { - if (!(availableElementClasses.contains(field.getType()) || isDecoratableList(field))) { - return null; - } - - ElementLocator locator = factory.createLocator(field); - if (locator == null) { - return null; - } - - if (WebElement.class.isAssignableFrom(field.getType())) { - return proxyForLocator(field, locator); - } else if (List.class.isAssignableFrom(field.getType())) { - return proxyForListLocator(locator); - } else { - return null; - } - } - - private static boolean isAvailableElementClass(Type type){ - boolean result = false; - for (Class webElementClass: - availableElementClasses){ - if (!webElementClass.equals(type)){ - continue; - } - result = true; - break; - } - return result; - } - - private boolean isDecoratableList(Field field) { - if (!List.class.isAssignableFrom(field.getType())) { - return false; - } - - // Type erasure in Java isn't complete. Attempt to discover the generic - // type of the list. - Type genericType = field.getGenericType(); - if (!(genericType instanceof ParameterizedType)) { - return false; - } - - Type listType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; - if (field.getAnnotation(AndroidFindBy.class) == null - && field.getAnnotation(iOSFindBy.class) == null - && field.getAnnotation(AndroidFindBys.class) == null - && field.getAnnotation(iOSFindBys.class) == null - && field.getAnnotation(FindBy.class) == null - && field.getAnnotation(FindBys.class) == null - && field.getAnnotation(FindAll.class) == null){ - return false; - } - return isAvailableElementClass(listType); - } - - private Object proxyForLocator(Field field, ElementLocator locator) { - Class type = field.getType(); - if (type.equals(WebElement.class)){ - type = RemoteWebElement.class; - } - ElementInterceptor elementInterceptor = new ElementInterceptor(locator); - return ProxyFactory.getEnhancedProxy(type, - elementInterceptor); - } - - @SuppressWarnings("unchecked") - private List proxyForListLocator( - ElementLocator locator) { - ElementListInterceptor elementInterceptor = new ElementListInterceptor(locator); - return ProxyFactory.getEnhancedProxy(ArrayList.class, - elementInterceptor); - } - - @Override - public void resetImplicitlyWaitTimeOut(long timeOut, TimeUnit timeUnit) { - factory.resetImplicitlyWaitTimeOut(timeOut, timeUnit); - } -} +package io.appium.java_client.pagefactory; + +import io.appium.java_client.MobileElement; +import io.appium.java_client.android.AndroidElement; +import io.appium.java_client.ios.IOSElement; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.RemoteWebElement; +import org.openqa.selenium.support.pagefactory.ElementLocator; +import org.openqa.selenium.support.pagefactory.FieldDecorator; + +/** + * Default decorator for use with PageFactory. Will decorate 1) all of the + * WebElement fields and 2) List fields that have + * {@literal @AndroidFindBy}, {@literal @AndroidFindBys}, or + * {@literal @iOSFindBy/@iOSFindBys} annotation with a proxy that locates the + * elements using the passed in ElementLocatorFactory. + * + * Please pay attention: fields of {@link WebElement}, {@link RemoteWebElement}, + * {@link MobileElement}, {@link AndroidElement} and {@link IOSElement} are allowed + * to use with this decorator + */ +public class AppiumFieldDecorator implements FieldDecorator, ResetsImplicitlyWaitTimeOut { + + private static final List> availableElementClasses = + new ArrayList>(){ + private static final long serialVersionUID = 1L; + { + add(WebElement.class); + add(RemoteWebElement.class); + add(MobileElement.class); + add(AndroidElement.class); + add(IOSElement.class); + } + + }; + + private final AppiumElementLocatorFactory factory; + + public static long DEFAULT_IMPLICITLY_WAIT_TIMEOUT = 1; + + public static TimeUnit DEFAULT_TIMEUNIT = TimeUnit.SECONDS; + + public AppiumFieldDecorator(SearchContext context, long implicitlyWaitTimeOut, TimeUnit timeUnit) { + factory = new AppiumElementLocatorFactory(context, implicitlyWaitTimeOut, timeUnit); + } + + public AppiumFieldDecorator(SearchContext context) { + factory = new AppiumElementLocatorFactory(context); + } + + public Object decorate(ClassLoader ignored, Field field) { + if (!(availableElementClasses.contains(field.getType()) || isDecoratableList(field))) { + return null; + } + + ElementLocator locator = factory.createLocator(field); + if (locator == null) { + return null; + } + + if (WebElement.class.isAssignableFrom(field.getType())) { + return proxyForLocator(field, locator); + } else if (List.class.isAssignableFrom(field.getType())) { + return proxyForListLocator(locator); + } else { + return null; + } + } + + private static boolean isAvailableElementClass(Type type){ + boolean result = false; + for (Class webElementClass: + availableElementClasses){ + if (!webElementClass.equals(type)){ + continue; + } + result = true; + break; + } + return result; + } + + private boolean isDecoratableList(Field field) { + if (!List.class.isAssignableFrom(field.getType())) { + return false; + } + + // Type erasure in Java isn't complete. Attempt to discover the generic + // type of the list. + Type genericType = field.getGenericType(); + if (!(genericType instanceof ParameterizedType)) { + return false; + } + + Type listType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + return isAvailableElementClass(listType); + //if there is no annotation list is supposed to be found by org.openqa.selenium.support.ByIdOrName + //DefaultElementLocator has an issue :) + } + + private Object proxyForLocator(Field field, ElementLocator locator) { + Class type = field.getType(); + if (type.equals(WebElement.class)){ + type = RemoteWebElement.class; + } + ElementInterceptor elementInterceptor = new ElementInterceptor(locator); + return ProxyFactory.getEnhancedProxy(type, + elementInterceptor); + } + + @SuppressWarnings("unchecked") + private List proxyForListLocator( + ElementLocator locator) { + ElementListInterceptor elementInterceptor = new ElementListInterceptor(locator); + return ProxyFactory.getEnhancedProxy(ArrayList.class, + elementInterceptor); + } + + @Override + public void resetImplicitlyWaitTimeOut(long timeOut, TimeUnit timeUnit) { + factory.resetImplicitlyWaitTimeOut(timeOut, timeUnit); + } +} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSFindAll.java b/src/main/java/io/appium/java_client/pagefactory/iOSFindAll.java new file mode 100644 index 000000000..cb3a2c5ea --- /dev/null +++ b/src/main/java/io/appium/java_client/pagefactory/iOSFindAll.java @@ -0,0 +1,17 @@ +package io.appium.java_client.pagefactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to mark a field on a Page/Screen Object to indicate that lookup should use a series of {@link iOSFindBy} tags + * It will then search for all elements that match any criteria. Note that elements + * are not guaranteed to be in document order. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface iOSFindAll { + iOSFindBy[] value(); +} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java index 66848b2e2..1ca54b77d 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java @@ -3,6 +3,7 @@ import io.appium.java_client.MobileElement; import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.android.AndroidElement; +import io.appium.java_client.pagefactory.AndroidFindAll; import io.appium.java_client.pagefactory.AndroidFindBy; import io.appium.java_client.pagefactory.AndroidFindBys; import io.appium.java_client.pagefactory.AppiumFieldDecorator; @@ -131,7 +132,23 @@ public class AndroidPageObjectTest { @AndroidFindBy(id = "android:id/text1") }) private List androidElementViews; - + + @AndroidFindAll({ + @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/Fakecontent\")"), + @AndroidFindBy(id = "android:id/Faketext1"), + @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")"), //by this locator element is found + @AndroidFindBy(id = "android:id/FakeId") + }) + private List findAllElementViews; + + @AndroidFindAll({ + @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/Fakecontent\")"), + @AndroidFindBy(id = "android:id/Faketext1"), + @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")"), //by this locator element is found + @AndroidFindBy(id = "android:id/FakeId") + }) + private WebElement findAllElementView; + @Before public void setUp() throws Exception { File appDir = new File("src/test/java/io/appium/java_client"); @@ -284,4 +301,14 @@ public void isAndroidElementTest(){ public void areAndroidElementsTest(){ Assert.assertNotEquals(0, androidElementViews.size()); } + + @Test + public void findAllElementTest(){ + Assert.assertNotEquals(null, findAllElementView.getAttribute("text")); + } + + @Test + public void findAllElementsTest(){ + Assert.assertNotEquals(0, findAllElementViews.size()); + } } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/BrowserCompatibilityTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/BrowserCompatibilityTest.java index 10ab92c76..c96089a8f 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/BrowserCompatibilityTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/BrowserCompatibilityTest.java @@ -1,5 +1,6 @@ package io.appium.java_client.pagefactory_tests; +import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.pagefactory.AndroidFindBy; import io.appium.java_client.pagefactory.AndroidFindBys; import io.appium.java_client.pagefactory.AppiumFieldDecorator; @@ -163,6 +164,11 @@ public void setUp(Class driverClass) { @FindBys({@FindBy(className = "r"), @FindBy(tagName = "a")}) private List foundLinks; + private List ires; //this list is located by id="ires" + private WebElement gbqfb; //this element is found by id="gbqfb" + private WebDriver trap1; + private List trap2; + private void test(WebDriver driver){ try { PageFactory.initElements(new AppiumFieldDecorator(driver, IMPLICITLY_WAIT, TimeUnit.SECONDS), this); @@ -175,6 +181,10 @@ private void test(WebDriver driver){ searchTextField.sendKeys("Hello, Appium!"); searchButton.click(); Assert.assertNotEquals(0, foundLinks.size()); + Assert.assertNotEquals(0, ires.size()); + Assert.assertEquals(null, trap1); + Assert.assertEquals(null, trap2); + gbqfb.click(); } finally { driver.quit(); } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/iOSPageObjectTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/iOSPageObjectTest.java index 36ddf468b..5236a1a22 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/iOSPageObjectTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/iOSPageObjectTest.java @@ -6,6 +6,7 @@ import io.appium.java_client.pagefactory.AndroidFindBy; import io.appium.java_client.pagefactory.AndroidFindBys; import io.appium.java_client.pagefactory.AppiumFieldDecorator; +import io.appium.java_client.pagefactory.iOSFindAll; import io.appium.java_client.pagefactory.iOSFindBy; import io.appium.java_client.remote.MobileCapabilityType; @@ -96,6 +97,18 @@ public class iOSPageObjectTest { @iOSFindBy(uiAutomator = ".elements()[0]") private List iosButtons; + + @iOSFindAll({ + @iOSFindBy(xpath = "ComputeSumButton_Test"), + @iOSFindBy(name = "ComputeSumButton") //it is real locator + }) + private WebElement findAllElement; + + @iOSFindAll({ + @iOSFindBy(xpath = "ComputeSumButton_Test"), + @iOSFindBy(name = "ComputeSumButton") //it is real locator + }) + private List findAllElements; @Before public void setUp() throws Exception { @@ -230,4 +243,14 @@ public void isIOSElementTest(){ public void areIOSElements_FindByTest(){ Assert.assertNotEquals(0, iosButtons.size()); } + + @Test + public void findAllElementsTest(){ + Assert.assertNotEquals(0, findAllElements.size()); + } + + @Test + public void findAllElementTest(){ + Assert.assertNotEquals(null, findAllElement.getText()); + } }