diff --git a/build.gradle b/build.gradle index 6e6a8069d..83a54f4c2 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,6 @@ dependencies { if (seleniumVersion.toString().contains('SNAPSHOT')) { api "org.seleniumhq.selenium:selenium-api:${seleniumVersion}" api "org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}" - api "org.seleniumhq.selenium:selenium-support:${seleniumVersion}" } else { api('org.seleniumhq.selenium:selenium-api') { version { @@ -81,12 +80,6 @@ dependencies { prefer "${seleniumVersion}" } } - api('org.seleniumhq.selenium:selenium-support') { - version { - strictly "[${seleniumVersion}, 5.0)" - prefer "${seleniumVersion}" - } - } } implementation 'com.google.code.gson:gson:2.14.0' implementation "org.slf4j:slf4j-api:${slf4jVersion}" @@ -226,6 +219,7 @@ testing { implementation 'org.junit.jupiter:junit-jupiter:5.14.4' runtimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'org.hamcrest:hamcrest:3.0' + implementation "org.seleniumhq.selenium:selenium-support:${seleniumVersion}" runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}" } targets.configureEach { diff --git a/config/checkstyle/appium-style.xml b/config/checkstyle/appium-style.xml index b7473e937..6521c6a64 100755 --- a/config/checkstyle/appium-style.xml +++ b/config/checkstyle/appium-style.xml @@ -32,6 +32,9 @@ + + + diff --git a/config/checkstyle/import-control.xml b/config/checkstyle/import-control.xml new file mode 100644 index 000000000..0c1b1ef6a --- /dev/null +++ b/config/checkstyle/import-control.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 0587e646e..4d26058d9 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -2,4 +2,5 @@ + diff --git a/docs/Page-objects.md b/docs/Page-objects.md index f3e1c9627..d1664e082 100644 --- a/docs/Page-objects.md +++ b/docs/Page-objects.md @@ -1,10 +1,19 @@ Appium Java client has facilities which components to [Page Object](https://github.com/SeleniumHQ/selenium/wiki/PageObjects) design pattern and [Selenium PageFactory](https://github.com/SeleniumHQ/selenium/wiki/PageFactory). +> **Migrating from Selenium's page factory**: `@FindBy`, `@FindBys`, `@FindAll`, `@CacheLookup`, `PageFactory` and +> the related classes are now provided by the `io.appium.java_client.support` package (same names and attributes as +> `org.openqa.selenium.support`), because Appium Java Client does not depend on `selenium-support` anymore. +> Replace the `org.openqa.selenium.support` imports with `io.appium.java_client.support` ones. +> Page objects that still use Selenium's `@FindBy`, `@FindBys`, `@FindAll` and `@CacheLookup` keep working with +> `AppiumFieldDecorator` as long as `selenium-support` is on your classpath. +> `PageFactory.initElements(driver, page)` shortcuts that rely on Selenium's own decorator are not provided, +> use `PageFactory.initElements(new AppiumFieldDecorator(driver), page)` instead. + # WebElement/list of WebElement field can be populated by default: ```java import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; +import io.appium.java_client.support.FindBy; ... @FindBy(someStrategy) //for browser or web view html UI @@ -62,7 +71,7 @@ List someElements; ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBy; +import io.appium.java_client.support.FindBy; //the fully cross platform example @FindBy(someStrategy) //for browser or web view html UI @@ -84,8 +93,8 @@ List someElements; ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.FindBy; @FindBys({@FindBy(someStrategy1), @FindBy(someStrategy2)}) @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2) @@ -103,8 +112,8 @@ or ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.FindBy; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.CHAIN; @@ -126,8 +135,8 @@ List someElements; ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindByAll; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.FindAll; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; @@ -149,8 +158,8 @@ List someElements; ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindByAll; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.FindAll; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.CHAIN; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; @@ -173,8 +182,8 @@ or ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindByAll; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.FindAll; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; @@ -203,8 +212,8 @@ If the using of _xpath_ is not convenient for some reasons so there are possible ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.FindBy; //it is necessary to define priorities at this case. The lower number means the higher priority. //The default value is 0 (the highest priority) @@ -229,8 +238,8 @@ List someElements; ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.FindBy; //it is not necessary to define priorities at this case. But it can manage the searching. //The lower number means the higher priority. @@ -261,7 +270,7 @@ Object fields are populated as below: - ```java import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.PageFactory; +import io.appium.java_client.support.PageFactory; PageFactory.initElements(new AppiumFieldDecorator(searchContext /*searchContext is a WebDriver or WebElement @@ -273,7 +282,7 @@ PageFactory.initElements(new AppiumFieldDecorator(searchContext - ```java import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.PageFactory; +import io.appium.java_client.support.PageFactory; import java.util.concurrent.TimeUnit; PageFactory.initElements(new AppiumFieldDecorator(searchContext, @@ -288,7 +297,7 @@ PageFactory.initElements(new AppiumFieldDecorator(searchContext, - ```java import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.PageFactory; +import io.appium.java_client.support.PageFactory; import java.util.concurrent.TimeUnit; PageFactory.initElements(new AppiumFieldDecorator(searchContext, diff --git a/docs/transitive-dependencies-management.md b/docs/transitive-dependencies-management.md index dc148d816..919d18691 100644 --- a/docs/transitive-dependencies-management.md +++ b/docs/transitive-dependencies-management.md @@ -1,3 +1,7 @@ +> **Note**: Appium Java Client does not depend on `selenium-support` anymore. If your code uses its classes +> (`WebDriverWait`, `ExpectedConditions`, `Select`, `EventFiringDecorator`, etc.), declare it in your own +> build the same way as the other Selenium dependencies below. + # Maven Maven downloads dependency of [the latest version](https://cwiki.apache.org/confluence/display/MAVENOLD/Dependency+Mediation+and+Conflict+Resolution#DependencyMediationandConflictResolution-DependencyVersionRanges) @@ -21,10 +25,6 @@ In order to pin Selenium dependencies they should be declared in `pom.xml` in th org.seleniumhq.selenium selenium-remote-driver - - org.seleniumhq.selenium - selenium-support - @@ -37,11 +37,6 @@ In order to pin Selenium dependencies they should be declared in `pom.xml` in th selenium-remote-driver A.B.C - - org.seleniumhq.selenium - selenium-support - A.B.C - ``` @@ -63,6 +58,5 @@ dependencies { implementation('io.appium:java-client:X.Y.Z') implementation('org.seleniumhq.selenium:selenium-api:A.B.C') implementation('org.seleniumhq.selenium:selenium-remote-driver:A.B.C') - implementation('org.seleniumhq.selenium:selenium-support:A.B.C') } ``` diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java index 0db6f2647..45e55d4a8 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java @@ -1,7 +1,6 @@ package io.appium.java_client.android; import io.appium.java_client.functions.AppiumFunction; -import io.appium.java_client.functions.ExpectedCondition; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -104,7 +103,7 @@ public void complexWaitingTestWithPreCondition() { }; AppiumFunction> compositeFunction = waitingForContext - .andThen((ExpectedCondition>) input -> { + .andThen((AppiumFunction>) input -> { List result = searchingFunction.apply(input); if (result != null) { calls.add(true); diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java index 1e0bff096..10cf3889a 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java @@ -20,13 +20,13 @@ import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.remote.MobileBrowserType; import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.PageFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; import java.util.HashMap; import java.util.Map; diff --git a/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java b/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java index 68e89ddb6..e669c20c7 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java @@ -23,6 +23,9 @@ import io.appium.java_client.pagefactory.AndroidFindBys; import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.HowToUseLocators; +import io.appium.java_client.support.CacheLookup; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.PageFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -30,9 +33,6 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.WrapsDriver; import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.support.CacheLookup; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; import java.util.ArrayList; import java.util.HashSet; diff --git a/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java b/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java index 824261c52..6e561ee46 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java @@ -22,6 +22,9 @@ import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.remote.MobileBrowserType; import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.PageFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -29,9 +32,6 @@ import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.PageFactory; import java.util.List; diff --git a/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java b/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java index 7d89bd331..58d569736 100644 --- a/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java @@ -20,12 +20,12 @@ import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.HowToUseLocators; import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import io.appium.java_client.support.PageFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.PageFactory; import java.util.List; diff --git a/src/main/java/io/appium/java_client/AppiumFluentWait.java b/src/main/java/io/appium/java_client/AppiumFluentWait.java index 1496eea3c..1bbc5d6a1 100644 --- a/src/main/java/io/appium/java_client/AppiumFluentWait.java +++ b/src/main/java/io/appium/java_client/AppiumFluentWait.java @@ -17,6 +17,8 @@ package io.appium.java_client; import com.google.common.base.Throwables; +import io.appium.java_client.support.ui.FluentWait; +import io.appium.java_client.support.ui.Sleeper; import lombok.AccessLevel; import lombok.Getter; import org.jspecify.annotations.NonNull; @@ -24,8 +26,6 @@ import org.jspecify.annotations.Nullable; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.support.ui.FluentWait; -import org.openqa.selenium.support.ui.Sleeper; import java.time.Clock; import java.time.Duration; diff --git a/src/main/java/io/appium/java_client/functions/AppiumFunction.java b/src/main/java/io/appium/java_client/functions/AppiumFunction.java index e23dcb298..da2ec634a 100644 --- a/src/main/java/io/appium/java_client/functions/AppiumFunction.java +++ b/src/main/java/io/appium/java_client/functions/AppiumFunction.java @@ -24,7 +24,7 @@ /** * This is extended version of {@link com.google.common.base.Function}. It is combined * with {@link java.util.function.Function}. It was designed in order to provide compatibility - * with the {@link org.openqa.selenium.support.ui.Wait}. + * with the {@link io.appium.java_client.support.ui.Wait}. * * @param The input type * @param The return type diff --git a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java index aa245d971..f0ee7b7a9 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java @@ -28,7 +28,7 @@ /** * Used to mark a field on a Page Object to indicate an alternative mechanism for locating the * element or a list of elements. Used in conjunction with - * {@link org.openqa.selenium.support.PageFactory} + * {@link io.appium.java_client.support.PageFactory} * this allows users to quickly and easily create PageObjects. * using Android UI selectors, accessibility, id, name, class name, tag and xpath */ diff --git a/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java index 9e148a2c7..fb81a8332 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java @@ -18,6 +18,7 @@ import io.appium.java_client.pagefactory.bys.ContentMappedBy; import io.appium.java_client.pagefactory.locator.CacheableLocator; +import io.appium.java_client.support.ui.FluentWait; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; @@ -25,7 +26,6 @@ import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.FluentWait; import java.lang.ref.WeakReference; import java.time.Duration; 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 792932cd4..2ffa915d4 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java @@ -19,6 +19,10 @@ import io.appium.java_client.internal.CapabilityHelpers; import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.locator.CacheableLocator; +import io.appium.java_client.support.pagefactory.DefaultFieldDecorator; +import io.appium.java_client.support.pagefactory.ElementLocator; +import io.appium.java_client.support.pagefactory.ElementLocatorFactory; +import io.appium.java_client.support.pagefactory.FieldDecorator; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.openqa.selenium.HasCapabilities; @@ -27,10 +31,6 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.support.pagefactory.DefaultFieldDecorator; -import org.openqa.selenium.support.pagefactory.ElementLocator; -import org.openqa.selenium.support.pagefactory.ElementLocatorFactory; -import org.openqa.selenium.support.pagefactory.FieldDecorator; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; diff --git a/src/main/java/io/appium/java_client/pagefactory/DefaultElementByBuilder.java b/src/main/java/io/appium/java_client/pagefactory/DefaultElementByBuilder.java index ab4e29274..2b967317b 100644 --- a/src/main/java/io/appium/java_client/pagefactory/DefaultElementByBuilder.java +++ b/src/main/java/io/appium/java_client/pagefactory/DefaultElementByBuilder.java @@ -21,13 +21,13 @@ import io.appium.java_client.pagefactory.bys.builder.AppiumByBuilder; import io.appium.java_client.pagefactory.bys.builder.ByChained; import io.appium.java_client.pagefactory.bys.builder.HowToUseSelectors; +import io.appium.java_client.support.ByIdOrName; +import io.appium.java_client.support.CacheLookup; +import io.appium.java_client.support.FindAll; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.pagefactory.ByAll; import org.openqa.selenium.By; -import org.openqa.selenium.support.ByIdOrName; -import org.openqa.selenium.support.CacheLookup; -import org.openqa.selenium.support.FindAll; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.pagefactory.ByAll; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; @@ -82,10 +82,10 @@ private static By buildMobileBy(LocatorGroupStrategy locatorGroupStrategy, By[] @Override protected void assertValidAnnotations() { AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated(); - FindBy findBy = annotatedElement.getAnnotation(FindBy.class); - FindBys findBys = annotatedElement.getAnnotation(FindBys.class); + FindBy findBy = SeleniumAnnotationsCompat.find(annotatedElement, FindBy.class); + FindBys findBys = SeleniumAnnotationsCompat.find(annotatedElement, FindBys.class); checkDisallowedAnnotationPairs(findBy, findBys); - FindAll findAll = annotatedElement.getAnnotation(FindAll.class); + FindAll findAll = SeleniumAnnotationsCompat.find(annotatedElement, FindAll.class); checkDisallowedAnnotationPairs(findBy, findAll); checkDisallowedAnnotationPairs(findBys, findAll); } @@ -94,20 +94,20 @@ protected void assertValidAnnotations() { protected By buildDefaultBy() { AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated(); By defaultBy = null; - FindBy findBy = annotatedElement.getAnnotation(FindBy.class); + FindBy findBy = SeleniumAnnotationsCompat.find(annotatedElement, FindBy.class); if (findBy != null) { defaultBy = new FindBy.FindByBuilder().buildIt(findBy, (Field) annotatedElement); } if (defaultBy == null) { - FindBys findBys = annotatedElement.getAnnotation(FindBys.class); + FindBys findBys = SeleniumAnnotationsCompat.find(annotatedElement, FindBys.class); if (findBys != null) { defaultBy = new FindBys.FindByBuilder().buildIt(findBys, (Field) annotatedElement); } } if (defaultBy == null) { - FindAll findAll = annotatedElement.getAnnotation(FindAll.class); + FindAll findAll = SeleniumAnnotationsCompat.find(annotatedElement, FindAll.class); if (findAll != null) { defaultBy = new FindAll.FindByBuilder().buildIt(findAll, (Field) annotatedElement); } @@ -181,7 +181,7 @@ protected By buildMobileNativeBy() { @Override public boolean isLookupCached() { AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated(); - return annotatedElement.getAnnotation(CacheLookup.class) != null; + return SeleniumAnnotationsCompat.isPresent(annotatedElement, CacheLookup.class); } private By returnMappedBy(By byDefault, By nativeAppBy) { diff --git a/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java index 82b61990b..cc1efa4a9 100644 --- a/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java @@ -17,9 +17,9 @@ package io.appium.java_client.pagefactory; import io.appium.java_client.pagefactory.interceptors.InterceptorOfASingleElement; +import io.appium.java_client.support.pagefactory.ElementLocator; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.pagefactory.ElementLocator; import java.lang.ref.WeakReference; import java.lang.reflect.Method; diff --git a/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java index 77e68a329..c6c56d4a0 100644 --- a/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java @@ -17,8 +17,8 @@ package io.appium.java_client.pagefactory; import io.appium.java_client.pagefactory.interceptors.InterceptorOfAListOfElements; +import io.appium.java_client.support.pagefactory.ElementLocator; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.pagefactory.ElementLocator; import java.lang.reflect.Method; import java.util.List; diff --git a/src/main/java/io/appium/java_client/pagefactory/SeleniumAnnotationsCompat.java b/src/main/java/io/appium/java_client/pagefactory/SeleniumAnnotationsCompat.java new file mode 100644 index 000000000..e03c2e148 --- /dev/null +++ b/src/main/java/io/appium/java_client/pagefactory/SeleniumAnnotationsCompat.java @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.pagefactory; + +import org.jspecify.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.Arrays; + +/** + * Lets page objects that still use Selenium's {@code @FindBy}, {@code @FindBys}, {@code @FindAll} + * and {@code @CacheLookup} keep working when selenium-support is on the classpath. + * The Selenium annotations are matched by name and read reflectively, so there is no compile-time + * dependency on Selenium's support package. + */ +final class SeleniumAnnotationsCompat { + private static final String SELENIUM_SUPPORT_PACKAGE = "org.openqa.selenium.support."; + + private SeleniumAnnotationsCompat() { + } + + /** + * Finds the given annotation on the element, falling back to its Selenium counterpart. + * + * @param element the annotated element + * @param type the Appium annotation type + * @param the annotation type + * @return the Appium annotation, or a view of the Selenium one, or null if none is present + */ + @Nullable + static A find(AnnotatedElement element, Class type) { + A annotation = element.getAnnotation(type); + if (annotation != null) { + return annotation; + } + return Arrays.stream(element.getAnnotations()) + .filter(a -> isSeleniumCounterpart(a, type)) + .findFirst() + .map(a -> adapt(a, type)) + .orElse(null); + } + + static boolean isPresent(AnnotatedElement element, Class type) { + return find(element, type) != null; + } + + private static boolean isSeleniumCounterpart(Annotation annotation, Class type) { + return annotation.annotationType().getName().equals(SELENIUM_SUPPORT_PACKAGE + type.getSimpleName()); + } + + private static A adapt(Annotation source, Class target) { + InvocationHandler handler = (proxy, method, args) -> { + switch (method.getName()) { + case "annotationType": + return target; + case "toString": + return "Adapted " + source; + case "hashCode": + return source.hashCode(); + case "equals": + return proxy == args[0]; + default: + return convert(readAttribute(source, method.getName()), method.getReturnType()); + } + }; + return target.cast(Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target}, handler)); + } + + private static Object readAttribute(Annotation source, String name) throws Throwable { + try { + return source.annotationType().getMethod(name).invoke(source); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Object convert(Object value, Class targetType) { + if (targetType.isEnum()) { + return Enum.valueOf((Class) targetType, ((Enum) value).name()); + } + if (targetType.isArray() && targetType.getComponentType().isAnnotation()) { + int length = Array.getLength(value); + Object result = Array.newInstance(targetType.getComponentType(), length); + for (int i = 0; i < length; i++) { + Array.set(result, i, adapt((Annotation) Array.get(value, i), + (Class) targetType.getComponentType())); + } + return result; + } + return value; + } +} 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 46d946628..57061dd7b 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java @@ -19,10 +19,10 @@ import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.interceptors.InterceptorOfASingleElement; import io.appium.java_client.pagefactory.locator.CacheableLocator; +import io.appium.java_client.support.PageFactory; import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.PageFactory; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/AppiumByBuilder.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/AppiumByBuilder.java index 73f6717aa..649899cbd 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/AppiumByBuilder.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/AppiumByBuilder.java @@ -16,10 +16,10 @@ package io.appium.java_client.pagefactory.bys.builder; +import io.appium.java_client.support.pagefactory.AbstractAnnotations; +import io.appium.java_client.support.pagefactory.ByAll; import org.jspecify.annotations.Nullable; import org.openqa.selenium.By; -import org.openqa.selenium.support.pagefactory.AbstractAnnotations; -import org.openqa.selenium.support.pagefactory.ByAll; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java index b92d2eb10..94ff5f0d7 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java @@ -16,19 +16,19 @@ package io.appium.java_client.pagefactory.bys.builder; +import io.appium.java_client.support.ui.FluentWait; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.FluentWait; import java.util.Optional; import java.util.function.Function; import static java.util.Objects.requireNonNull; -public class ByChained extends org.openqa.selenium.support.pagefactory.ByChained { +public class ByChained extends io.appium.java_client.support.pagefactory.ByChained { private final By[] bys; diff --git a/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java index 3f8bd4fdf..884184ff9 100644 --- a/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java +++ b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java @@ -17,9 +17,9 @@ package io.appium.java_client.pagefactory.interceptors; import io.appium.java_client.proxy.MethodCallListener; +import io.appium.java_client.support.pagefactory.ElementLocator; import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.pagefactory.ElementLocator; import java.lang.reflect.Method; import java.util.ArrayList; diff --git a/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfASingleElement.java b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfASingleElement.java index 968ff824d..a35f2a38a 100644 --- a/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfASingleElement.java +++ b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfASingleElement.java @@ -17,12 +17,12 @@ package io.appium.java_client.pagefactory.interceptors; import io.appium.java_client.proxy.MethodCallListener; +import io.appium.java_client.support.pagefactory.ElementLocator; import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.WrapsDriver; import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.support.pagefactory.ElementLocator; import java.lang.ref.WeakReference; import java.lang.reflect.Method; diff --git a/src/main/java/io/appium/java_client/pagefactory/locator/CacheableElementLocatorFactory.java b/src/main/java/io/appium/java_client/pagefactory/locator/CacheableElementLocatorFactory.java index 3fd540bbc..688f504c5 100644 --- a/src/main/java/io/appium/java_client/pagefactory/locator/CacheableElementLocatorFactory.java +++ b/src/main/java/io/appium/java_client/pagefactory/locator/CacheableElementLocatorFactory.java @@ -16,7 +16,7 @@ package io.appium.java_client.pagefactory.locator; -import org.openqa.selenium.support.pagefactory.ElementLocatorFactory; +import io.appium.java_client.support.pagefactory.ElementLocatorFactory; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; diff --git a/src/main/java/io/appium/java_client/pagefactory/locator/CacheableLocator.java b/src/main/java/io/appium/java_client/pagefactory/locator/CacheableLocator.java index 963a8b63b..fe8d7fce3 100644 --- a/src/main/java/io/appium/java_client/pagefactory/locator/CacheableLocator.java +++ b/src/main/java/io/appium/java_client/pagefactory/locator/CacheableLocator.java @@ -16,7 +16,7 @@ package io.appium.java_client.pagefactory.locator; -import org.openqa.selenium.support.pagefactory.ElementLocator; +import io.appium.java_client.support.pagefactory.ElementLocator; public interface CacheableLocator extends ElementLocator { boolean isLookUpCached(); diff --git a/src/main/java/io/appium/java_client/support/AbstractFindByBuilder.java b/src/main/java/io/appium/java_client/support/AbstractFindByBuilder.java new file mode 100644 index 000000000..1fe1e18e3 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/AbstractFindByBuilder.java @@ -0,0 +1,137 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.By; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; + +/** + * Base class of the builders that transform {@link FindBy}-like annotations into {@link By}. + * Adapted from Selenium's {@code org.openqa.selenium.support.AbstractFindByBuilder} + * (Apache License 2.0). + * + * @param the annotation type handled by the builder + */ +public abstract class AbstractFindByBuilder { + public abstract By buildIt(T annotation, Field field); + + protected By buildByFromFindBy(FindBy findBy) { + assertValidFindBy(findBy); + + By ans = buildByFromShortFindBy(findBy); + if (ans == null) { + ans = buildByFromLongFindBy(findBy); + } + + return ans; + } + + @Nullable + protected By buildByFromShortFindBy(FindBy findBy) { + if (!"".equals(findBy.className())) { + return By.className(findBy.className()); + } + + if (!"".equals(findBy.css())) { + return By.cssSelector(findBy.css()); + } + + if (!"".equals(findBy.id())) { + return By.id(findBy.id()); + } + + if (!"".equals(findBy.linkText())) { + return By.linkText(findBy.linkText()); + } + + if (!"".equals(findBy.name())) { + return By.name(findBy.name()); + } + + if (!"".equals(findBy.partialLinkText())) { + return By.partialLinkText(findBy.partialLinkText()); + } + + if (!"".equals(findBy.tagName())) { + return By.tagName(findBy.tagName()); + } + + if (!"".equals(findBy.xpath())) { + return By.xpath(findBy.xpath()); + } + + return null; + } + + protected By buildByFromLongFindBy(FindBy findBy) { + return findBy.how().buildBy(findBy.using()); + } + + protected void assertValidFindBys(FindBys findBys) { + for (FindBy findBy : findBys.value()) { + assertValidFindBy(findBy); + } + } + + protected void assertValidFindBy(FindBy findBy) { + Set finders = new HashSet<>(); + if (!"".equals(findBy.using())) { + finders.add("how: " + findBy.using()); + } + if (!"".equals(findBy.className())) { + finders.add("class name:" + findBy.className()); + } + if (!"".equals(findBy.css())) { + finders.add("css:" + findBy.css()); + } + if (!"".equals(findBy.id())) { + finders.add("id: " + findBy.id()); + } + if (!"".equals(findBy.linkText())) { + finders.add("link text: " + findBy.linkText()); + } + if (!"".equals(findBy.name())) { + finders.add("name: " + findBy.name()); + } + if (!"".equals(findBy.partialLinkText())) { + finders.add("partial link text: " + findBy.partialLinkText()); + } + if (!"".equals(findBy.tagName())) { + finders.add("tag name: " + findBy.tagName()); + } + if (!"".equals(findBy.xpath())) { + finders.add("xpath: " + findBy.xpath()); + } + + // A zero count is okay: it means to look by name or id. + if (finders.size() > 1) { + throw new IllegalArgumentException( + String.format("You must specify at most one location strategy. Number found: %d (%s)", + finders.size(), finders)); + } + } + + protected void assertValidFindAll(FindAll findBys) { + for (FindBy findBy : findBys.value()) { + assertValidFindBy(findBy); + } + } +} diff --git a/src/main/java/io/appium/java_client/support/ByIdOrName.java b/src/main/java/io/appium/java_client/support/ByIdOrName.java new file mode 100644 index 000000000..820c16aae --- /dev/null +++ b/src/main/java/io/appium/java_client/support/ByIdOrName.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import org.jspecify.annotations.NullMarked; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Finds elements by their id first and then by their name. + * Adapted from Selenium's {@code org.openqa.selenium.support.ByIdOrName} (Apache License 2.0). + */ +@NullMarked +public class ByIdOrName extends By implements Serializable { + private static final long serialVersionUID = 3986638402799576701L; + + private final By idFinder; + private final By nameFinder; + private final String idOrName; + + /** + * Creates a locator that looks for the given value in the id and name attributes. + * + * @param idOrName the value to look for + */ + public ByIdOrName(String idOrName) { + this.idOrName = idOrName; + idFinder = By.id(idOrName); + nameFinder = By.name(idOrName); + } + + @Override + public WebElement findElement(SearchContext context) { + try { + return idFinder.findElement(context); + } catch (NoSuchElementException e) { + return nameFinder.findElement(context); + } + } + + @Override + public List findElements(SearchContext context) { + List elements = new ArrayList<>(); + elements.addAll(idFinder.findElements(context)); + elements.addAll(nameFinder.findElements(context)); + return elements; + } + + @Override + public String toString() { + return "by id or name \"" + idOrName + '"'; + } +} diff --git a/src/main/java/io/appium/java_client/support/CacheLookup.java b/src/main/java/io/appium/java_client/support/CacheLookup.java new file mode 100644 index 000000000..fc3a9afe0 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/CacheLookup.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation to be applied to WebElements to indicate that it never changes (that is, that + * the same instance will always be used). + * Adapted from Selenium's {@code org.openqa.selenium.support.CacheLookup} (Apache License 2.0). + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CacheLookup { +} diff --git a/src/main/java/io/appium/java_client/support/FindAll.java b/src/main/java/io/appium/java_client/support/FindAll.java new file mode 100644 index 000000000..68784c486 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/FindAll.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import io.appium.java_client.support.pagefactory.ByAll; +import org.openqa.selenium.By; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; + +/** + * Used to mark a field on a Page Object to indicate that lookup should use a series of + * {@link FindBy} tags. It will then search for all elements that match any of the FindBy + * criteria. Note that elements are not guaranteed to be in document order. + * + *

Eg: + * + *

+ * @FindAll({@FindBy(how = How.ID, using = "foo"),
+ *           @FindBy(className = "bar")})
+ * 
+ * + *

Adapted from Selenium's {@code org.openqa.selenium.support.FindAll} (Apache License 2.0). + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface FindAll { + /** + * The locators to search with. + * + * @return the {@link FindBy} annotations to combine + */ + FindBy[] value(); + + class FindByBuilder extends AbstractFindByBuilder { + @Override + public By buildIt(FindAll findBys, Field field) { + assertValidFindAll(findBys); + + FindBy[] findByArray = findBys.value(); + By[] byArray = new By[findByArray.length]; + for (int i = 0; i < findByArray.length; i++) { + byArray[i] = buildByFromFindBy(findByArray[i]); + } + + return new ByAll(byArray); + } + } +} diff --git a/src/main/java/io/appium/java_client/support/FindBy.java b/src/main/java/io/appium/java_client/support/FindBy.java new file mode 100644 index 000000000..20762e4ec --- /dev/null +++ b/src/main/java/io/appium/java_client/support/FindBy.java @@ -0,0 +1,131 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import org.openqa.selenium.By; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; + +/** + * Used to mark a field on a Page Object to indicate an alternative mechanism for locating the + * element or a list of elements. Used in conjunction with {@link PageFactory} this allows users to + * quickly and easily create PageObjects. + * + *

You can either use this annotation by specifying both "how" and "using" or by specifying one + * of the location strategies (eg: "id") with an appropriate value to use. Both options will + * delegate down to the matching {@link By} methods in By class. + * + *

For example, these two annotations point to the same element: + * + *

+ * @FindBy(id = "foobar") WebElement foobar;
+ * @FindBy(how = How.ID, using = "foobar") WebElement foobar;
+ * 
+ * + *

Adapted from Selenium's {@code org.openqa.selenium.support.FindBy} (Apache License 2.0). + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface FindBy { + /** + * The location strategy to use together with {@link #using()}. + * + * @return the configured value + */ + How how() default How.UNSET; + + /** + * The value of the strategy given in {@link #how()}. + * + * @return the configured value + */ + String using() default ""; + + /** + * The element id. + * + * @return the configured value + */ + String id() default ""; + + /** + * The element name. + * + * @return the configured value + */ + String name() default ""; + + /** + * The element class name. + * + * @return the configured value + */ + String className() default ""; + + /** + * The CSS selector. + * + * @return the configured value + */ + String css() default ""; + + /** + * The element tag name. + * + * @return the configured value + */ + String tagName() default ""; + + /** + * The exact link text. + * + * @return the configured value + */ + String linkText() default ""; + + /** + * A part of the link text. + * + * @return the configured value + */ + String partialLinkText() default ""; + + /** + * The XPath expression. + * + * @return the configured value + */ + String xpath() default ""; + + class FindByBuilder extends AbstractFindByBuilder { + @Override + public By buildIt(FindBy findBy, Field field) { + assertValidFindBy(findBy); + + By ans = buildByFromShortFindBy(findBy); + if (ans == null) { + ans = buildByFromLongFindBy(findBy); + } + + return ans; + } + } +} diff --git a/src/main/java/io/appium/java_client/support/FindBys.java b/src/main/java/io/appium/java_client/support/FindBys.java new file mode 100644 index 000000000..1a18c47f6 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/FindBys.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import io.appium.java_client.support.pagefactory.ByChained; +import org.openqa.selenium.By; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; + +/** + * Used to mark a field on a Page Object to indicate that lookup should use a series of + * {@link FindBy} tags in a chain as described in {@link ByChained}. + * + *

Eg: + * + *

+ * @FindBys({@FindBy(id = "foo"),
+ *           @FindBy(className = "bar")})
+ * 
+ * + *

Adapted from Selenium's {@code org.openqa.selenium.support.FindBys} (Apache License 2.0). + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface FindBys { + /** + * The locators to chain. + * + * @return the {@link FindBy} annotations to chain + */ + FindBy[] value(); + + class FindByBuilder extends AbstractFindByBuilder { + @Override + public By buildIt(FindBys findBys, Field field) { + assertValidFindBys(findBys); + + FindBy[] findByArray = findBys.value(); + By[] byArray = new By[findByArray.length]; + for (int i = 0; i < findByArray.length; i++) { + byArray[i] = buildByFromFindBy(findByArray[i]); + } + + return new ByChained(byArray); + } + } +} diff --git a/src/main/java/io/appium/java_client/support/How.java b/src/main/java/io/appium/java_client/support/How.java new file mode 100644 index 000000000..f563e5579 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/How.java @@ -0,0 +1,88 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import org.openqa.selenium.By; + +/** + * Location strategies for {@link FindBy#how()}. + * Adapted from Selenium's {@code org.openqa.selenium.support.How} (Apache License 2.0). + */ +public enum How { + CLASS_NAME { + @Override + public By buildBy(String value) { + return By.className(value); + } + }, + CSS { + @Override + public By buildBy(String value) { + return By.cssSelector(value); + } + }, + ID { + @Override + public By buildBy(String value) { + return By.id(value); + } + }, + ID_OR_NAME { + @Override + public By buildBy(String value) { + return new ByIdOrName(value); + } + }, + LINK_TEXT { + @Override + public By buildBy(String value) { + return By.linkText(value); + } + }, + NAME { + @Override + public By buildBy(String value) { + return By.name(value); + } + }, + PARTIAL_LINK_TEXT { + @Override + public By buildBy(String value) { + return By.partialLinkText(value); + } + }, + TAG_NAME { + @Override + public By buildBy(String value) { + return By.tagName(value); + } + }, + XPATH { + @Override + public By buildBy(String value) { + return By.xpath(value); + } + }, + UNSET { + @Override + public By buildBy(String value) { + return ID.buildBy(value); + } + }; + + public abstract By buildBy(String value); +} diff --git a/src/main/java/io/appium/java_client/support/PageFactory.java b/src/main/java/io/appium/java_client/support/PageFactory.java new file mode 100644 index 000000000..79d6f6639 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/PageFactory.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import io.appium.java_client.support.pagefactory.DefaultFieldDecorator; +import io.appium.java_client.support.pagefactory.ElementLocatorFactory; +import io.appium.java_client.support.pagefactory.FieldDecorator; + +import java.lang.reflect.Field; + +/** + * Factory class to make using Page Objects simpler and easier. + * Adapted from Selenium's {@code org.openqa.selenium.support.PageFactory} (Apache License 2.0). + */ +public final class PageFactory { + + private PageFactory() { + } + + /** + * Similar to the other "initElements" methods, but takes an {@link ElementLocatorFactory} which + * is used for providing the mechanism for finding elements. If the ElementLocatorFactory returns + * null then the field won't be decorated. + * + * @param factory The factory to use + * @param page The object to decorate the fields of + */ + public static void initElements(ElementLocatorFactory factory, Object page) { + initElements(new DefaultFieldDecorator(factory), page); + } + + /** + * Takes a {@link FieldDecorator} which is used for decorating each of the fields of the page, + * including the ones declared in its superclasses. + * + * @param decorator the decorator to use + * @param page The object to decorate the fields of + */ + public static void initElements(FieldDecorator decorator, Object page) { + Class proxyIn = page.getClass(); + while (proxyIn != Object.class) { + proxyFields(decorator, page, proxyIn); + proxyIn = proxyIn.getSuperclass(); + } + } + + private static void proxyFields(FieldDecorator decorator, Object page, Class proxyIn) { + Field[] fields = proxyIn.getDeclaredFields(); + for (Field field : fields) { + Object value = decorator.decorate(page.getClass().getClassLoader(), field); + if (value != null) { + try { + field.setAccessible(true); + field.set(page, value); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } + } +} diff --git a/src/main/java/io/appium/java_client/functions/ExpectedCondition.java b/src/main/java/io/appium/java_client/support/package-info.java similarity index 56% rename from src/main/java/io/appium/java_client/functions/ExpectedCondition.java rename to src/main/java/io/appium/java_client/support/package-info.java index 926577c53..43440cd5b 100644 --- a/src/main/java/io/appium/java_client/functions/ExpectedCondition.java +++ b/src/main/java/io/appium/java_client/support/package-info.java @@ -14,19 +14,9 @@ * limitations under the License. */ -package io.appium.java_client.functions; - -import org.openqa.selenium.WebDriver; - /** - * This is extended version of {@link org.openqa.selenium.support.ui.ExpectedCondition}. It is combined - * with {@link java.util.function.Function}. - * - * @param The return type - * @deprecated Use {@link org.openqa.selenium.support.ui.ExpectedCondition} instead + * The page factory and wait engine, adapted from Selenium's {@code org.openqa.selenium.support} + * (Apache License 2.0). This package tree must stay self-contained: it may only depend on + * selenium-api, jspecify and the JDK, which is enforced by the checkstyle import control rules. */ -@Deprecated -@FunctionalInterface -public interface ExpectedCondition extends org.openqa.selenium.support.ui.ExpectedCondition, - AppiumFunction { -} +package io.appium.java_client.support; diff --git a/src/main/java/io/appium/java_client/support/pagefactory/AbstractAnnotations.java b/src/main/java/io/appium/java_client/support/pagefactory/AbstractAnnotations.java new file mode 100644 index 000000000..507385f46 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/AbstractAnnotations.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory; + +import org.openqa.selenium.By; + +/** + * Abstract class to work with fields in Page Objects. Provides methods to process + * {@code @FindBy}, {@code @FindBys} and {@code @FindAll} annotations. + * Adapted from Selenium's {@code org.openqa.selenium.support.pagefactory.AbstractAnnotations} + * (Apache License 2.0). + */ +public abstract class AbstractAnnotations { + /** + * Defines how to transform given object (field, class, etc.) into {@link By} + * class used by webdriver to locate elements. + * + * @return By object + */ + public abstract By buildBy(); + + /** + * Defines whether given element should be returned from cache on further calls. + * + * @return boolean if lookup cached + */ + public abstract boolean isLookupCached(); +} diff --git a/src/main/java/io/appium/java_client/support/pagefactory/ByAll.java b/src/main/java/io/appium/java_client/support/pagefactory/ByAll.java new file mode 100644 index 000000000..cb6570d9b --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/ByAll.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory; + +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Mechanism used to locate elements within a document using a series of lookups. This class will + * find all elements that match any of the locators in sequence, e.g. + * {@code driver.findElements(new ByAll(by1, by2))} will find all elements that match + * by1 and then all elements that match by2. + * This means that the list of elements returned may not be in document order. + * Adapted from Selenium's {@code org.openqa.selenium.support.pagefactory.ByAll} + * (Apache License 2.0). + */ +public class ByAll extends By implements Serializable { + private static final long serialVersionUID = 4573668832699497306L; + + private final By[] bys; + + public ByAll(By... bys) { + this.bys = bys; + } + + @Override + public WebElement findElement(SearchContext context) { + for (By by : bys) { + List elements = context.findElements(by); + if (!elements.isEmpty()) { + return elements.get(0); + } + } + throw new NoSuchElementException("Cannot locate an element using " + this); + } + + @Override + public List findElements(SearchContext context) { + List elems = new ArrayList<>(); + for (By by : bys) { + elems.addAll(context.findElements(by)); + } + return elems; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder("By.all("); + stringBuilder.append("{"); + + boolean first = true; + for (By by : bys) { + stringBuilder.append(first ? "" : ",").append(by); + first = false; + } + stringBuilder.append("})"); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/io/appium/java_client/support/pagefactory/ByChained.java b/src/main/java/io/appium/java_client/support/pagefactory/ByChained.java new file mode 100644 index 000000000..38e96df20 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/ByChained.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory; + +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Mechanism used to locate elements within a document using a series of other lookups. This class + * will find all elements that match each of the locators in sequence, e.g. + * {@code driver.findElements(new ByChained(by1, by2))} will find all elements that match + * by2 and appear under an element that matches by1. + * Adapted from Selenium's {@code org.openqa.selenium.support.pagefactory.ByChained} + * (Apache License 2.0). + */ +public class ByChained extends By implements Serializable { + private static final long serialVersionUID = 1563769051170172451L; + + private final By[] bys; + + public ByChained(By... bys) { + this.bys = bys; + } + + @Override + public WebElement findElement(SearchContext context) { + List elements = findElements(context); + if (elements.isEmpty()) { + throw new NoSuchElementException("Cannot locate an element using " + this); + } + return elements.get(0); + } + + @Override + public List findElements(SearchContext context) { + if (bys.length == 0) { + return new ArrayList<>(); + } + + List elems = bys[0].findElements(context); + for (int i = 1; i < bys.length; i++) { + if (elems.isEmpty()) { + break; // if any one of the bys finds no elements, then return no elements + } + final By by = bys[i]; + elems = elems.stream() + .map(elem -> elem.findElements(by)) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + return elems; + } + + @Override + public String toString() { + return String.format( + "By.chained({%s})", Stream.of(bys).map(By::toString).collect(Collectors.joining(","))); + } +} diff --git a/src/main/java/io/appium/java_client/support/pagefactory/DefaultFieldDecorator.java b/src/main/java/io/appium/java_client/support/pagefactory/DefaultFieldDecorator.java new file mode 100644 index 000000000..7ddee33eb --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/DefaultFieldDecorator.java @@ -0,0 +1,105 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory; + +import io.appium.java_client.support.FindAll; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.pagefactory.internal.LocatingElementHandler; +import io.appium.java_client.support.pagefactory.internal.LocatingElementListHandler; +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.WrapsElement; +import org.openqa.selenium.interactions.Locatable; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Default decorator for use with PageFactory. Will decorate 1) all the WebElement fields and 2) + * {@code List} fields that have {@code @FindBy}, {@code @FindBys}, or {@code @FindAll} + * annotation with a proxy that locates the elements using the passed in ElementLocatorFactory. + * Adapted from Selenium's {@code org.openqa.selenium.support.pagefactory.DefaultFieldDecorator} + * (Apache License 2.0). + */ +public class DefaultFieldDecorator implements FieldDecorator { + + protected ElementLocatorFactory factory; + + public DefaultFieldDecorator(ElementLocatorFactory factory) { + this.factory = factory; + } + + @Nullable + @Override + public Object decorate(ClassLoader loader, Field field) { + if (!(WebElement.class.isAssignableFrom(field.getType()) || isDecoratableList(field))) { + return null; + } + + ElementLocator locator = factory.createLocator(field); + if (locator == null) { + return null; + } + + if (WebElement.class.isAssignableFrom(field.getType())) { + return proxyForLocator(loader, locator); + } else if (List.class.isAssignableFrom(field.getType())) { + return proxyForListLocator(loader, locator); + } else { + return null; + } + } + + protected 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 (!WebElement.class.equals(listType)) { + return false; + } + + return field.getAnnotation(FindBy.class) != null + || field.getAnnotation(FindBys.class) != null + || field.getAnnotation(FindAll.class) != null; + } + + protected WebElement proxyForLocator(ClassLoader loader, ElementLocator locator) { + InvocationHandler handler = new LocatingElementHandler(locator); + return (WebElement) Proxy.newProxyInstance( + loader, new Class[]{WebElement.class, WrapsElement.class, Locatable.class}, handler); + } + + @SuppressWarnings("unchecked") + protected List proxyForListLocator(ClassLoader loader, ElementLocator locator) { + InvocationHandler handler = new LocatingElementListHandler(locator); + return (List) Proxy.newProxyInstance(loader, new Class[]{List.class}, handler); + } +} diff --git a/src/main/java/io/appium/java_client/support/pagefactory/ElementLocator.java b/src/main/java/io/appium/java_client/support/pagefactory/ElementLocator.java new file mode 100644 index 000000000..2fb2409da --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/ElementLocator.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory; + +import org.openqa.selenium.WebElement; + +import java.util.List; + +/** + * The interface used by the {@code PageFactory} to locate elements. + * Adapted from Selenium's {@code org.openqa.selenium.support.pagefactory.ElementLocator} + * (Apache License 2.0). + */ +public interface ElementLocator { + /** + * Find the element. + * + * @return The WebElement that matches the selector. + */ + WebElement findElement(); + + /** + * Find the elements. + * + * @return A list of all {@link WebElement}s that match the selector. + */ + List findElements(); +} diff --git a/src/main/java/io/appium/java_client/support/pagefactory/ElementLocatorFactory.java b/src/main/java/io/appium/java_client/support/pagefactory/ElementLocatorFactory.java new file mode 100644 index 000000000..4f803f8c8 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/ElementLocatorFactory.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory; + +import java.lang.reflect.Field; + +/** + * A factory for producing {@link ElementLocator}s. It is expected that a new ElementLocator will be + * returned per call. + * Adapted from Selenium's {@code org.openqa.selenium.support.pagefactory.ElementLocatorFactory} + * (Apache License 2.0). + */ +public interface ElementLocatorFactory { + /** + * When a field on a class needs to be decorated with an {@link ElementLocator} this method will + * be called. + * + * @param field the field + * @return An ElementLocator object. + */ + ElementLocator createLocator(Field field); +} diff --git a/src/main/java/io/appium/java_client/support/pagefactory/FieldDecorator.java b/src/main/java/io/appium/java_client/support/pagefactory/FieldDecorator.java new file mode 100644 index 000000000..f102db99a --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/FieldDecorator.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory; + +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Field; + +/** + * Allows the PageFactory to decorate fields. + * Adapted from Selenium's {@code org.openqa.selenium.support.pagefactory.FieldDecorator} + * (Apache License 2.0). + */ +public interface FieldDecorator { + /** + * This method is called by PageFactory on all fields to decide how to decorate the field. + * + * @param loader The class loader that was used for the page object + * @param field The field that may be decorated. + * @return Value to decorate the field with or null if it shouldn't be decorated. If non-null, + * must be assignable to the field. + */ + @Nullable Object decorate(ClassLoader loader, Field field); +} diff --git a/src/main/java/io/appium/java_client/support/pagefactory/internal/LocatingElementHandler.java b/src/main/java/io/appium/java_client/support/pagefactory/internal/LocatingElementHandler.java new file mode 100644 index 000000000..f9bb1e6d9 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/internal/LocatingElementHandler.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory.internal; + +import io.appium.java_client.support.pagefactory.ElementLocator; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Adapted from Selenium's + * {@code org.openqa.selenium.support.pagefactory.internal.LocatingElementHandler} + * (Apache License 2.0). + */ +public class LocatingElementHandler implements InvocationHandler { + private final ElementLocator locator; + + public LocatingElementHandler(ElementLocator locator) { + this.locator = locator; + } + + @Override + public Object invoke(Object object, Method method, Object[] objects) throws Throwable { + WebElement element; + try { + element = locator.findElement(); + } catch (NoSuchElementException e) { + if ("toString".equals(method.getName())) { + return "Proxy element for: " + locator; + } + throw e; + } + + if ("getWrappedElement".equals(method.getName())) { + return element; + } + + try { + return method.invoke(element, objects); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } +} diff --git a/src/main/java/io/appium/java_client/support/pagefactory/internal/LocatingElementListHandler.java b/src/main/java/io/appium/java_client/support/pagefactory/internal/LocatingElementListHandler.java new file mode 100644 index 000000000..af78d3d7d --- /dev/null +++ b/src/main/java/io/appium/java_client/support/pagefactory/internal/LocatingElementListHandler.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.pagefactory.internal; + +import io.appium.java_client.support.pagefactory.ElementLocator; +import org.openqa.selenium.WebElement; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Adapted from Selenium's + * {@code org.openqa.selenium.support.pagefactory.internal.LocatingElementListHandler} + * (Apache License 2.0). + */ +public class LocatingElementListHandler implements InvocationHandler { + private final ElementLocator locator; + + public LocatingElementListHandler(ElementLocator locator) { + this.locator = locator; + } + + @Override + public Object invoke(Object object, Method method, Object[] objects) throws Throwable { + List elements = locator.findElements(); + + try { + return method.invoke(elements, objects); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } +} diff --git a/src/main/java/io/appium/java_client/support/ui/FluentWait.java b/src/main/java/io/appium/java_client/support/ui/FluentWait.java new file mode 100644 index 000000000..2ee9e6ed6 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/ui/FluentWait.java @@ -0,0 +1,249 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.ui; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriverException; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.math.BigDecimal.ONE; +import static java.math.RoundingMode.HALF_UP; +import static java.util.Objects.requireNonNull; + +/** + * An implementation of the {@link Wait} interface that may have its timeout and polling interval + * configured on the fly. Each FluentWait instance defines the maximum amount of time to wait for a + * condition, as well as the frequency with which to check the condition. Furthermore, the user may + * configure the wait to ignore specific types of exceptions whilst waiting. + * Adapted from Selenium's {@code org.openqa.selenium.support.ui.FluentWait} (Apache License 2.0). + * + * @param The input type for each condition used with this instance. + */ +public class FluentWait implements Wait { + protected static final long DEFAULT_SLEEP_TIMEOUT = 500; + + private static final Duration DEFAULT_WAIT_DURATION = Duration.ofMillis(DEFAULT_SLEEP_TIMEOUT); + + protected final T input; + protected final Clock clock; + protected final Sleeper sleeper; + + protected Duration timeout = DEFAULT_WAIT_DURATION; + protected Duration interval = DEFAULT_WAIT_DURATION; + protected Supplier<@Nullable String> messageSupplier = () -> null; + + protected final List> ignoredExceptions = new ArrayList<>(); + + /** + * Creates a wait that uses the system clock and {@link Sleeper#SYSTEM_SLEEPER}. + * + * @param input The input value to pass to the evaluated conditions. + */ + public FluentWait(T input) { + this(input, Clock.systemDefaultZone(), Sleeper.SYSTEM_SLEEPER); + } + + /** + * Creates a wait with a custom clock and sleeper, mainly useful for testing. + * + * @param input The input value to pass to the evaluated conditions. + * @param clock The clock to use when measuring the timeout. + * @param sleeper Used to put the thread to sleep between evaluation loops. + */ + public FluentWait(T input, Clock clock, Sleeper sleeper) { + this.input = input; + this.clock = requireNonNull(clock, "Clock must not be null"); + this.sleeper = requireNonNull(sleeper, "Sleeper must not be null"); + } + + /** + * Sets how long to wait for the evaluated condition to be true. The default timeout is + * {@link #DEFAULT_SLEEP_TIMEOUT}. + * + * @param timeout The timeout duration. + * @return A self reference. + */ + public FluentWait withTimeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + /** + * Sets the message to be appended to the {@link TimeoutException} on timeout. + * + * @param message The failing message. + * @return A self reference. + */ + public FluentWait withMessage(final String message) { + requireNonNull(message, "Message must not be null"); + this.messageSupplier = () -> message; + return this; + } + + /** + * Sets the supplier of the message to be appended to the {@link TimeoutException} on timeout. + * + * @param messageSupplier The supplier of the failing message. + * @return A self reference. + */ + public FluentWait withMessage(Supplier messageSupplier) { + this.messageSupplier = requireNonNull(messageSupplier, "Message supplier must not be null"); + return this; + } + + /** + * Sets how often the condition should be evaluated. The default interval is + * {@link #DEFAULT_SLEEP_TIMEOUT}. + * + * @param interval The interval duration. + * @return A self reference. + */ + public FluentWait pollingEvery(Duration interval) { + this.interval = interval; + return this; + } + + /** + * Configures this instance to ignore specific types of exceptions while waiting for a condition. + * + * @param types The types of exceptions to ignore. + * @param an exception that extends Throwable + * @return A self reference. + */ + public FluentWait ignoreAll(Collection> types) { + ignoredExceptions.addAll(types); + return this; + } + + /** + * Configures this instance to ignore a specific type of exception while waiting for a condition. + * + * @param exceptionType exception to ignore + * @return a self reference + */ + public FluentWait ignoring(Class exceptionType) { + return this.ignoreAll(List.of(exceptionType)); + } + + /** + * Configures this instance to ignore two types of exceptions while waiting for a condition. + * + * @param firstType exception to ignore + * @param secondType another exception to ignore + * @return a self reference + */ + public FluentWait ignoring( + Class firstType, Class secondType) { + return this.ignoreAll(List.of(firstType, secondType)); + } + + /** + * Repeatedly applies this instance's input value to the given function until one of the following + * occurs: the function returns neither null nor false, the function throws an unignored exception, + * or the timeout expires. + * + * @param isTrue the parameter to pass to the function + * @param The function's expected return type. + * @return The function's return value if the function returned something different from null or + * false before the timeout expired. + * @throws TimeoutException If the timeout expires. + */ + @Override + public @NonNull V until(Function isTrue) { + Instant end = clock.instant().plus(timeout); + + Throwable lastException; + while (true) { + try { + V value = isTrue.apply(input); + if (value != null && (Boolean.class != value.getClass() || Boolean.TRUE.equals(value))) { + return value; + } + + // The last exception is not the cause of a timeout triggered by a false or null value + lastException = null; + } catch (Throwable e) { + lastException = propagateIfNotIgnored(e); + } + + // Checked after the evaluation so that conditions with a zero timeout can succeed + if (end.isBefore(clock.instant())) { + String message = messageSupplier.get(); + + String timeoutMessage = String.format( + "Expected condition failed: %s%n(tried for %s with %d milliseconds interval)", + message == null ? "waiting for " + isTrue : message, + formatTimeout(timeout), + interval.toMillis()); + throw timeoutException(timeoutMessage, lastException); + } + + try { + sleeper.sleep(interval); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new WebDriverException(e); + } + } + } + + static String formatTimeout(Duration timeout) { + BigDecimal seconds = BigDecimal.valueOf(timeout.toMillis()) + .divide(BigDecimal.valueOf(1000), 3, HALF_UP); + String value = seconds.stripTrailingZeros().toPlainString(); + boolean singular = seconds.compareTo(ONE) == 0; + return value + (singular ? " second" : " seconds"); + } + + private Throwable propagateIfNotIgnored(Throwable e) { + for (Class ignoredException : ignoredExceptions) { + if (ignoredException.isInstance(e)) { + return e; + } + } + if (e instanceof Error) { + throw (Error) e; + } + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new RuntimeException(e); + } + + /** + * Throws a timeout exception. This method may be overridden to throw an exception that is + * idiomatic for a particular test infrastructure. + * + * @param message The timeout message. + * @param lastException The last exception that was ignored while waiting, if any. + * @return nothing will ever be returned; this return type is only specified as a convenience. + */ + protected RuntimeException timeoutException(String message, @Nullable Throwable lastException) { + throw new TimeoutException(message, lastException); + } +} diff --git a/src/main/java/io/appium/java_client/support/ui/Sleeper.java b/src/main/java/io/appium/java_client/support/ui/Sleeper.java new file mode 100644 index 000000000..80ced1c8a --- /dev/null +++ b/src/main/java/io/appium/java_client/support/ui/Sleeper.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.ui; + +import java.time.Duration; + +/** + * Abstraction around {@link Thread#sleep(long)} to permit better testability. + * Adapted from Selenium's {@code org.openqa.selenium.support.ui.Sleeper} (Apache License 2.0). + */ +public interface Sleeper { + Sleeper SYSTEM_SLEEPER = duration -> Thread.sleep(duration.toMillis()); + + /** + * Sleeps for the specified duration of time. + * + * @param duration How long to sleep. + * @throws InterruptedException If the thread is interrupted while sleeping. + */ + void sleep(Duration duration) throws InterruptedException; +} diff --git a/src/main/java/io/appium/java_client/support/ui/Wait.java b/src/main/java/io/appium/java_client/support/ui/Wait.java new file mode 100644 index 000000000..bb3c49ed7 --- /dev/null +++ b/src/main/java/io/appium/java_client/support/ui/Wait.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.ui; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; + +/** + * A generic interface for waiting until a condition is true or not null. + * Adapted from Selenium's {@code org.openqa.selenium.support.ui.Wait} (Apache License 2.0). + * + * @param the argument to pass to any function called + */ +public interface Wait { + + /** + * Implementations should wait until the condition evaluates to a value that is neither null nor + * false. Because of this contract, the return type must not be Void. + * + *

If the condition does not become true within a certain time (as defined by the implementing + * class), this method will throw a non-specified {@link Throwable}. This is so that an + * implementor may throw whatever is idiomatic for a given test infrastructure (e.g. JUnit4 would + * throw {@link AssertionError}). + * + * @param the return type of the method, which must not be Void + * @param isTrue the function to evaluate against the input + * @return truthy value from the isTrue condition + */ + @NonNull V until(Function isTrue); +} diff --git a/src/test/java/io/appium/java_client/pagefactory/AppiumFieldDecoratorTest.java b/src/test/java/io/appium/java_client/pagefactory/AppiumFieldDecoratorTest.java new file mode 100644 index 000000000..1dd22590f --- /dev/null +++ b/src/test/java/io/appium/java_client/pagefactory/AppiumFieldDecoratorTest.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.pagefactory; + +import io.appium.java_client.support.CacheLookup; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.PageFactory; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.HasCapabilities; +import org.openqa.selenium.MutableCapabilities; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.RemoteWebElement; + +import java.lang.reflect.Proxy; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AppiumFieldDecoratorTest { + + private static final class FoundElement extends RemoteWebElement { + @Override + public String getText() { + return "text"; + } + } + + private static final class RecordingDriver { + private final List locators = new ArrayList<>(); + private final WebDriver driver = (WebDriver) Proxy.newProxyInstance( + AppiumFieldDecoratorTest.class.getClassLoader(), + new Class[]{WebDriver.class, HasCapabilities.class}, + (proxy, method, args) -> { + switch (method.getName()) { + case "findElement": + locators.add(args[0].toString()); + return new FoundElement(); + case "findElements": + locators.add(args[0].toString()); + return List.of(new FoundElement(), new FoundElement()); + case "getCapabilities": + return new MutableCapabilities(); + default: + throw new UnsupportedOperationException(method.getName()); + } + }); + } + + @SuppressWarnings("unused") + private static class Page { + @FindBy(id = "appium") + WebElement appiumAnnotated; + + @org.openqa.selenium.support.FindBy(id = "selenium") + @org.openqa.selenium.support.CacheLookup + WebElement seleniumAnnotated; + + @FindBy(css = ".many") + List many; + + @CacheLookup + WebElement nameOrIdFallback; + } + + private static Page initialized(RecordingDriver recorder) { + Page page = new Page(); + PageFactory.initElements(new AppiumFieldDecorator(recorder.driver, Duration.ofMillis(100)), page); + return page; + } + + @Test + void locatesElementsAnnotatedWithAppiumAnnotations() { + RecordingDriver recorder = new RecordingDriver(); + Page page = initialized(recorder); + + assertEquals("text", page.appiumAnnotated.getText()); + + assertEquals(List.of(By.id("appium").toString()), recorder.locators); + } + + @Test + void locatesElementsAnnotatedWithSeleniumAnnotationsAndCachesThem() { + RecordingDriver recorder = new RecordingDriver(); + Page page = initialized(recorder); + + page.seleniumAnnotated.getText(); + page.seleniumAnnotated.getText(); + + assertEquals(List.of(By.id("selenium").toString()), recorder.locators); + } + + @Test + void locatesListsOfElements() { + RecordingDriver recorder = new RecordingDriver(); + Page page = initialized(recorder); + + assertEquals(2, page.many.size()); + + assertEquals(List.of(By.cssSelector(".many").toString()), recorder.locators); + } + + @Test + void fallsBackToTheFieldNameWhenThereAreNoLocatorAnnotations() { + RecordingDriver recorder = new RecordingDriver(); + Page page = initialized(recorder); + + page.nameOrIdFallback.getText(); + + assertEquals(1, recorder.locators.size()); + } +} diff --git a/src/test/java/io/appium/java_client/pagefactory/SeleniumAnnotationsCompatTest.java b/src/test/java/io/appium/java_client/pagefactory/SeleniumAnnotationsCompatTest.java new file mode 100644 index 000000000..4b1057fed --- /dev/null +++ b/src/test/java/io/appium/java_client/pagefactory/SeleniumAnnotationsCompatTest.java @@ -0,0 +1,149 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.pagefactory; + +import io.appium.java_client.support.CacheLookup; +import io.appium.java_client.support.FindAll; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.How; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import java.lang.reflect.Field; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SeleniumAnnotationsCompatTest { + + @SuppressWarnings("unused") + private static class Page { + @org.openqa.selenium.support.FindBy(id = "foo") + @org.openqa.selenium.support.CacheLookup + WebElement seleniumShort; + + @org.openqa.selenium.support.FindBy(how = org.openqa.selenium.support.How.XPATH, using = "//a") + WebElement seleniumLong; + + @org.openqa.selenium.support.FindBys({ + @org.openqa.selenium.support.FindBy(id = "foo"), + @org.openqa.selenium.support.FindBy(className = "bar")}) + WebElement seleniumChained; + + @org.openqa.selenium.support.FindAll({ + @org.openqa.selenium.support.FindBy(name = "foo"), + @org.openqa.selenium.support.FindBy(tagName = "bar")}) + List seleniumAll; + + @FindBy(id = "appium") + @org.openqa.selenium.support.FindBy(id = "selenium") + WebElement both; + + @FindBy(id = "appium") + @CacheLookup + WebElement appiumOnly; + + WebElement none; + } + + private static Field field(String name) throws NoSuchFieldException { + return Page.class.getDeclaredField(name); + } + + private static DefaultElementByBuilder builderFor(String fieldName) throws NoSuchFieldException { + DefaultElementByBuilder builder = new DefaultElementByBuilder(null, null); + builder.setAnnotated(field(fieldName)); + return builder; + } + + @Test + void readsSeleniumFindByAttributes() throws NoSuchFieldException { + FindBy findBy = SeleniumAnnotationsCompat.find(field("seleniumShort"), FindBy.class); + + assertNotNull(findBy); + assertEquals("foo", findBy.id()); + assertEquals("", findBy.css()); + assertEquals(How.UNSET, findBy.how()); + assertEquals(FindBy.class, findBy.annotationType()); + } + + @Test + void convertsTheSeleniumHowEnum() throws NoSuchFieldException { + FindBy findBy = SeleniumAnnotationsCompat.find(field("seleniumLong"), FindBy.class); + + assertNotNull(findBy); + assertEquals(How.XPATH, findBy.how()); + assertEquals("//a", findBy.using()); + } + + @Test + void convertsNestedAnnotationArrays() throws NoSuchFieldException { + FindBys findBys = SeleniumAnnotationsCompat.find(field("seleniumChained"), FindBys.class); + FindAll findAll = SeleniumAnnotationsCompat.find(field("seleniumAll"), FindAll.class); + + assertNotNull(findBys); + assertNotNull(findAll); + assertEquals(2, findBys.value().length); + assertEquals("bar", findBys.value()[1].className()); + assertEquals("foo", findAll.value()[0].name()); + assertEquals("bar", findAll.value()[1].tagName()); + } + + @Test + void detectsTheSeleniumCacheLookupMarker() throws NoSuchFieldException { + assertTrue(SeleniumAnnotationsCompat.isPresent(field("seleniumShort"), CacheLookup.class)); + assertFalse(SeleniumAnnotationsCompat.isPresent(field("seleniumLong"), CacheLookup.class)); + } + + @Test + void appiumAnnotationsTakePrecedence() throws NoSuchFieldException { + FindBy findBy = SeleniumAnnotationsCompat.find(field("both"), FindBy.class); + + assertNotNull(findBy); + assertEquals("appium", findBy.id()); + } + + @Test + void returnsNullWhenNothingIsPresent() throws NoSuchFieldException { + assertNull(SeleniumAnnotationsCompat.find(field("none"), FindBy.class)); + assertNull(SeleniumAnnotationsCompat.find(field("appiumOnly"), FindAll.class)); + } + + @Test + void defaultElementByBuilderUnderstandsSeleniumAnnotations() throws NoSuchFieldException { + assertEquals(By.id("foo"), builderFor("seleniumShort").buildDefaultBy()); + assertTrue(builderFor("seleniumShort").isLookupCached()); + assertEquals(By.xpath("//a"), builderFor("seleniumLong").buildDefaultBy()); + assertEquals("By.chained({By.id: foo,By.className: bar})", + builderFor("seleniumChained").buildDefaultBy().toString()); + assertEquals("By.all({By.name: foo,By.tagName: bar})", + builderFor("seleniumAll").buildDefaultBy().toString()); + } + + @Test + void defaultElementByBuilderStillUnderstandsAppiumAnnotations() throws NoSuchFieldException { + assertEquals(By.id("appium"), builderFor("appiumOnly").buildDefaultBy()); + assertTrue(builderFor("appiumOnly").isLookupCached()); + assertFalse(builderFor("none").isLookupCached()); + } +} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java index c918db58e..21bee8ff2 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java @@ -22,6 +22,9 @@ import io.appium.java_client.pagefactory.HowToUseLocators; import io.appium.java_client.pagefactory.Widget; import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import io.appium.java_client.support.FindBy; +import io.appium.java_client.support.FindBys; +import io.appium.java_client.support.PageFactory; import io.appium.java_client.utils.TestUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -29,9 +32,6 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.PageFactory; import java.util.List; diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java index 32d23c874..cfb3deadc 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java @@ -18,6 +18,8 @@ import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.WithTimeout; +import io.appium.java_client.support.FindAll; +import io.appium.java_client.support.FindBy; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -26,13 +28,12 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.support.FindAll; -import org.openqa.selenium.support.FindBy; import java.time.Duration; import java.util.List; import static io.appium.java_client.pagefactory.AppiumFieldDecorator.DEFAULT_WAITING_TIMEOUT; +import static io.appium.java_client.support.PageFactory.initElements; import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; import static java.lang.Math.abs; import static java.lang.String.format; @@ -42,7 +43,6 @@ import static org.apache.commons.lang3.time.DurationFormatUtils.formatDuration; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.openqa.selenium.support.PageFactory.initElements; public class TimeoutTest { diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/WidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/WidgetTest.java index 2f8e2d60d..79261cb82 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/WidgetTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/WidgetTest.java @@ -7,11 +7,11 @@ import java.util.List; +import static io.appium.java_client.support.PageFactory.initElements; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; -import static org.openqa.selenium.support.PageFactory.initElements; public abstract class WidgetTest { diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedAppTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedAppTest.java index c7e50ef5f..31d9a8861 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedAppTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedAppTest.java @@ -16,12 +16,12 @@ import java.util.List; import java.util.stream.Stream; +import static io.appium.java_client.support.PageFactory.initElements; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.openqa.selenium.support.PageFactory.initElements; @SuppressWarnings({"unused", "unchecked"}) public class CombinedAppTest { diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedWidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedWidgetTest.java index 26e0d2f74..647b92cce 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedWidgetTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedWidgetTest.java @@ -18,12 +18,12 @@ import java.util.Map; import java.util.stream.Stream; +import static io.appium.java_client.support.PageFactory.initElements; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.lessThan; -import static org.openqa.selenium.support.PageFactory.initElements; @SuppressWarnings({"unchecked", "unused"}) diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultFindByWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultFindByWidget.java index 45dd5685c..1e0a6bede 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultFindByWidget.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultFindByWidget.java @@ -1,8 +1,8 @@ package io.appium.java_client.pagefactory_tests.widget.tests.combined; import io.appium.java_client.pagefactory_tests.widget.tests.DefaultStubWidget; +import io.appium.java_client.support.FindBy; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; import java.util.List; diff --git a/src/test/java/io/appium/java_client/support/FindByBuildersTest.java b/src/test/java/io/appium/java_client/support/FindByBuildersTest.java new file mode 100644 index 000000000..f0fd51087 --- /dev/null +++ b/src/test/java/io/appium/java_client/support/FindByBuildersTest.java @@ -0,0 +1,139 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import io.appium.java_client.support.pagefactory.ByAll; +import io.appium.java_client.support.pagefactory.ByChained; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; + +import java.lang.reflect.Field; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FindByBuildersTest { + + @SuppressWarnings("unused") + private static class Page { + @FindBy(id = "foo") + WebElement shortForm; + + @FindBy(how = How.XPATH, using = "//a") + WebElement longForm; + + @FindBy(id = "a", css = "b") + WebElement conflicting; + + @FindBys({@FindBy(id = "foo"), @FindBy(className = "bar")}) + WebElement chained; + + @FindAll({@FindBy(how = How.NAME, using = "foo"), @FindBy(tagName = "bar")}) + List all; + + @FindBy(linkText = "a") + @CacheLookup + WebElement cached; + } + + private static Field field(String name) throws NoSuchFieldException { + return Page.class.getDeclaredField(name); + } + + @Test + void buildsByFromTheShortForm() throws NoSuchFieldException { + Field f = field("shortForm"); + + assertEquals(By.id("foo"), new FindBy.FindByBuilder().buildIt(f.getAnnotation(FindBy.class), f)); + } + + @Test + void buildsByFromTheLongForm() throws NoSuchFieldException { + Field f = field("longForm"); + + assertEquals(By.xpath("//a"), new FindBy.FindByBuilder().buildIt(f.getAnnotation(FindBy.class), f)); + } + + @Test + void rejectsMoreThanOneLocationStrategy() throws NoSuchFieldException { + Field f = field("conflicting"); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new FindBy.FindByBuilder().buildIt(f.getAnnotation(FindBy.class), f)); + assertTrue(e.getMessage().contains("at most one location strategy"), e.getMessage()); + } + + @Test + void findBysBuildsAChain() throws NoSuchFieldException { + Field f = field("chained"); + + By by = new FindBys.FindByBuilder().buildIt(f.getAnnotation(FindBys.class), f); + + assertInstanceOf(ByChained.class, by); + assertEquals("By.chained({By.id: foo,By.className: bar})", by.toString()); + } + + @Test + void findAllBuildsAnAnyOfLookup() throws NoSuchFieldException { + Field f = field("all"); + + By by = new FindAll.FindByBuilder().buildIt(f.getAnnotation(FindAll.class), f); + + assertInstanceOf(ByAll.class, by); + assertEquals("By.all({By.name: foo,By.tagName: bar})", by.toString()); + } + + @Test + void cacheLookupIsARuntimeMarker() throws NoSuchFieldException { + assertTrue(field("cached").isAnnotationPresent(CacheLookup.class)); + } + + @Test + void howUnsetFallsBackToId() { + assertEquals(By.id("x"), How.UNSET.buildBy("x")); + } + + @Test + void byIdOrNameFindsByIdFirstAndFallsBackToName() { + WebElement byName = Stubs.element("by name"); + + By by = new ByIdOrName("foo"); + + assertEquals(byName, by.findElement(Stubs.context(locator -> { + if (locator.toString().contains("name")) { + return List.of(byName); + } + throw new NoSuchElementException("not by id"); + }))); + } + + @Test + void byIdOrNameFindsAllMatches() { + WebElement byId = Stubs.element("by id"); + WebElement byName = Stubs.element("by name"); + + List found = new ByIdOrName("foo").findElements(Stubs.context( + locator -> locator.toString().contains("name") ? List.of(byName) : List.of(byId))); + + assertEquals(List.of(byId, byName), found); + } +} diff --git a/src/test/java/io/appium/java_client/support/PageFactoryTest.java b/src/test/java/io/appium/java_client/support/PageFactoryTest.java new file mode 100644 index 000000000..1e9f5a596 --- /dev/null +++ b/src/test/java/io/appium/java_client/support/PageFactoryTest.java @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import io.appium.java_client.support.pagefactory.DefaultFieldDecorator; +import io.appium.java_client.support.pagefactory.ElementLocator; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.WrapsElement; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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 PageFactoryTest { + + @SuppressWarnings("unused") + private static class BasePage { + @FindBy(id = "base") + WebElement baseElement; + } + + @SuppressWarnings("unused") + private static class Page extends BasePage { + @FindBy(id = "single") + WebElement element; + + @FindBy(css = ".many") + List elements; + + String untouched = "untouched"; + + WebElement notAnnotatedElement; + + @FindBy(id = "list-of-strings") + List notAnElementList; + } + + private static ElementLocator locatorOf(String text) { + return new ElementLocator() { + @Override + public WebElement findElement() { + return Stubs.element(text); + } + + @Override + public List findElements() { + return List.of(Stubs.element(text + "-1"), Stubs.element(text + "-2")); + } + }; + } + + @Test + void decoratesElementsAndListsIncludingInheritedFields() { + Page page = new Page(); + + PageFactory.initElements(new DefaultFieldDecorator(field -> locatorOf(field.getName())), page); + + assertEquals("element", page.element.getText()); + assertEquals("baseElement", page.baseElement.getText()); + assertEquals(2, page.elements.size()); + assertEquals("elements-2", page.elements.get(1).getText()); + } + + @Test + void doesNotTouchFieldsThatAreNotElements() { + Page page = new Page(); + + PageFactory.initElements(field -> locatorOf(field.getName()), page); + + assertEquals("untouched", page.untouched); + assertNull(page.notAnElementList); + } + + @Test + void elementProxiesExposeTheWrappedElement() { + Page page = new Page(); + + PageFactory.initElements(field -> locatorOf(field.getName()), page); + + WebElement wrapped = assertInstanceOf(WrapsElement.class, page.element).getWrappedElement(); + assertNotNull(wrapped); + assertEquals("element", wrapped.getText()); + } + + @Test + void elementProxiesReportTheLocatorWhenTheElementIsMissing() { + Page page = new Page(); + + PageFactory.initElements(field -> new ElementLocator() { + @Override + public WebElement findElement() { + throw new NoSuchElementException("missing"); + } + + @Override + public List findElements() { + return List.of(); + } + + @Override + public String toString() { + return "the locator"; + } + }, page); + + assertEquals("Proxy element for: the locator", page.element.toString()); + assertThrows(NoSuchElementException.class, () -> page.element.getText()); + assertTrue(page.elements.isEmpty()); + } + + @Test + void aFactoryReturningNullLeavesTheFieldAlone() { + Page page = new Page(); + + PageFactory.initElements(field -> null, page); + + assertNull(page.element); + } +} diff --git a/src/test/java/io/appium/java_client/support/Stubs.java b/src/test/java/io/appium/java_client/support/Stubs.java new file mode 100644 index 000000000..ac202b9ed --- /dev/null +++ b/src/test/java/io/appium/java_client/support/Stubs.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support; + +import org.openqa.selenium.By; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.function.Function; + +final class Stubs { + + private Stubs() { + } + + static WebElement element(String text) { + return (WebElement) Proxy.newProxyInstance( + Stubs.class.getClassLoader(), + new Class[]{WebElement.class}, + (proxy, method, args) -> { + if ("getText".equals(method.getName())) { + return text; + } + switch (method.getName()) { + case "toString": + return "element " + text; + case "hashCode": + return System.identityHashCode(proxy); + case "equals": + return proxy == args[0]; + default: + break; + } + throw new UnsupportedOperationException(method.getName()); + }); + } + + static SearchContext context(Function> finder) { + return new SearchContext() { + @Override + public List findElements(By by) { + return finder.apply(by); + } + + @Override + public WebElement findElement(By by) { + return finder.apply(by).get(0); + } + }; + } +} diff --git a/src/test/java/io/appium/java_client/support/ui/FluentWaitTest.java b/src/test/java/io/appium/java_client/support/ui/FluentWaitTest.java new file mode 100644 index 000000000..10dfde90e --- /dev/null +++ b/src/test/java/io/appium/java_client/support/ui/FluentWaitTest.java @@ -0,0 +1,155 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.support.ui; + +import org.junit.jupiter.api.Test; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.TimeoutException; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FluentWaitTest { + + private static final class FakeTime extends Clock implements Sleeper { + private Instant now = Instant.EPOCH; + + @Override + public ZoneOffset getZone() { + return ZoneOffset.UTC; + } + + @Override + public Clock withZone(java.time.ZoneId zone) { + return this; + } + + @Override + public Instant instant() { + return now; + } + + @Override + public void sleep(Duration duration) { + now = now.plus(duration); + } + } + + private static FluentWait newWait(FakeTime time) { + return new FluentWait<>("input", time, time) + .withTimeout(Duration.ofSeconds(5)) + .pollingEvery(Duration.ofSeconds(1)); + } + + @Test + void returnsTheFirstTruthyValue() { + assertEquals("INPUT", newWait(new FakeTime()).until(String::toUpperCase)); + } + + @Test + void pollsUntilTheConditionIsMet() { + AtomicInteger calls = new AtomicInteger(); + + Integer result = newWait(new FakeTime()).until(input -> calls.incrementAndGet() < 3 ? null : calls.get()); + + assertEquals(3, result); + } + + @Test + void falseIsNotTruthy() { + AtomicInteger calls = new AtomicInteger(); + + assertEquals(true, newWait(new FakeTime()).until(input -> calls.incrementAndGet() >= 2)); + assertEquals(2, calls.get()); + } + + @Test + void ignoredExceptionsAreRetried() { + AtomicInteger calls = new AtomicInteger(); + + String result = newWait(new FakeTime()).ignoring(NoSuchElementException.class).until(input -> { + if (calls.incrementAndGet() < 3) { + throw new NoSuchElementException("not yet"); + } + return "found"; + }); + + assertEquals("found", result); + } + + @Test + void notIgnoredExceptionsArePropagated() { + assertThrows(IllegalStateException.class, () -> newWait(new FakeTime()).until(input -> { + throw new IllegalStateException("boom"); + })); + } + + @Test + void timesOutWithTheLastIgnoredExceptionAsTheCause() { + FakeTime time = new FakeTime(); + NoSuchElementException cause = new NoSuchElementException("never there"); + + TimeoutException e = assertThrows(TimeoutException.class, () -> newWait(time) + .ignoring(NoSuchElementException.class) + .withMessage("the element") + .until(input -> { + throw cause; + })); + + assertTrue(e.getMessage().contains("Expected condition failed: the element"), e.getMessage()); + assertTrue(e.getMessage().contains("tried for 5 seconds with 1000 milliseconds interval"), e.getMessage()); + assertEquals(cause, e.getCause()); + assertTrue(time.instant().isAfter(Instant.EPOCH.plusSeconds(5))); + } + + @Test + void zeroTimeoutStillEvaluatesTheConditionOnce() { + assertEquals("INPUT", newWait(new FakeTime()).withTimeout(Duration.ZERO).until(String::toUpperCase)); + } + + @Test + void interruptionIsReportedAsAWebDriverException() { + Sleeper interrupted = duration -> { + throw new InterruptedException(); + }; + FluentWait wait = new FluentWait<>("input", Clock.systemDefaultZone(), interrupted) + .withTimeout(Duration.ofSeconds(5)); + + try { + assertInstanceOf(org.openqa.selenium.WebDriverException.class, + assertThrows(RuntimeException.class, () -> wait.until(input -> null))); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } + + @Test + void formatsTimeouts() { + assertEquals("1 second", FluentWait.formatTimeout(Duration.ofSeconds(1))); + assertEquals("2 seconds", FluentWait.formatTimeout(Duration.ofSeconds(2))); + assertEquals("0.5 seconds", FluentWait.formatTimeout(Duration.ofMillis(500))); + } +}