diff --git a/src/main/java/io/appium/java_client/FindsByAndroidViewMatcher.java b/src/main/java/io/appium/java_client/FindsByAndroidViewMatcher.java new file mode 100644 index 000000000..1370cf3ae --- /dev/null +++ b/src/main/java/io/appium/java_client/FindsByAndroidViewMatcher.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; + +import org.openqa.selenium.WebElement; + +import java.util.List; + +public interface FindsByAndroidViewMatcher extends FindsByFluentSelector { + + default T findElementByAndroidViewMatcher(String using) { + return findElement(MobileSelector.ANDROID_VIEW_MATCHER.toString(), using); + } + + default List findElementsByAndroidViewMatcher(String using) { + return findElements(MobileSelector.ANDROID_VIEW_MATCHER.toString(), using); + } +} diff --git a/src/main/java/io/appium/java_client/MobileBy.java b/src/main/java/io/appium/java_client/MobileBy.java index c04af3ea1..c76542da9 100644 --- a/src/main/java/io/appium/java_client/MobileBy.java +++ b/src/main/java/io/appium/java_client/MobileBy.java @@ -97,8 +97,8 @@ public static By iOSClassChain(final String iOSClassChainString) { /** * This locator strategy is only available in Espresso Driver mode. - * @param dataMatcherString is a valid class chain locator string. - * See + * @param dataMatcherString is a valid json string detailing hamcrest matcher for Espresso onData(). + * See * the documentation for more details * @return an instance of {@link io.appium.java_client.MobileBy.ByAndroidDataMatcher} */ @@ -106,6 +106,17 @@ public static By androidDataMatcher(final String dataMatcherString) { return new ByAndroidDataMatcher(dataMatcherString); } + /** + * This locator strategy is only available in Espresso Driver mode. + * @param viewMatcherString is a valid json string detailing hamcrest matcher for Espresso onView(). + * See + * the documentation for more details + * @return an instance of {@link io.appium.java_client.MobileBy.ByAndroidViewMatcher} + */ + public static By androidViewMatcher(final String viewMatcherString) { + return new ByAndroidViewMatcher(viewMatcherString); + } + /** * This locator strategy is available in XCUITest Driver mode. * @param iOSNsPredicateString is an an iOS NsPredicate String @@ -407,6 +418,66 @@ protected ByAndroidDataMatcher(String locatorString) { } } + public static class ByAndroidViewMatcher extends MobileBy implements Serializable { + + protected ByAndroidViewMatcher(String locatorString) { + super(MobileSelector.ANDROID_VIEW_MATCHER, locatorString); + } + + /** + * {@inheritDoc} + * + * @throws WebDriverException when current session doesn't support the given selector or when + * value of the selector is not consistent. + * @throws IllegalArgumentException when it is impossible to find something on the given + * {@link SearchContext} instance + */ + @SuppressWarnings("unchecked") + @Override public List findElements(SearchContext context) { + Class contextClass = context.getClass(); + + if (FindsByAndroidViewMatcher.class.isAssignableFrom(contextClass)) { + return FindsByAndroidViewMatcher.class.cast(context) + .findElementsByAndroidViewMatcher(getLocatorString()); + } + + if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { + return super.findElements(context); + } + + throw formIllegalArgumentException(contextClass, FindsByAndroidViewMatcher.class, + FindsByFluentSelector.class); + } + + /** + * {@inheritDoc} + * + * @throws WebDriverException when current session doesn't support the given selector or when + * value of the selector is not consistent. + * @throws IllegalArgumentException when it is impossible to find something on the given + * {@link SearchContext} instance + */ + @Override public WebElement findElement(SearchContext context) { + Class contextClass = context.getClass(); + + if (FindsByAndroidViewMatcher.class.isAssignableFrom(contextClass)) { + return FindsByAndroidViewMatcher.class.cast(context) + .findElementByAndroidViewMatcher(getLocatorString()); + } + + if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { + return super.findElement(context); + } + + throw formIllegalArgumentException(contextClass, FindsByAndroidViewMatcher.class, + FindsByFluentSelector.class); + } + + @Override public String toString() { + return "By.FindsByAndroidViewMatcher: " + getLocatorString(); + } + } + public static class ByIosNsPredicate extends MobileBy implements Serializable { protected ByIosNsPredicate(String locatorString) { diff --git a/src/main/java/io/appium/java_client/MobileSelector.java b/src/main/java/io/appium/java_client/MobileSelector.java index 4b01d38a3..0fbe3284e 100644 --- a/src/main/java/io/appium/java_client/MobileSelector.java +++ b/src/main/java/io/appium/java_client/MobileSelector.java @@ -26,6 +26,7 @@ public enum MobileSelector { IMAGE("-image"), ANDROID_VIEWTAG("-android viewtag"), ANDROID_DATA_MATCHER("-android datamatcher"), + ANDROID_VIEW_MATCHER("-android viewmatcher"), CUSTOM("-custom"); private final String selector; diff --git a/src/main/java/io/appium/java_client/android/AndroidDriver.java b/src/main/java/io/appium/java_client/android/AndroidDriver.java index 926363602..9fc18bd51 100644 --- a/src/main/java/io/appium/java_client/android/AndroidDriver.java +++ b/src/main/java/io/appium/java_client/android/AndroidDriver.java @@ -26,6 +26,7 @@ import io.appium.java_client.AppiumDriver; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.FindsByAndroidDataMatcher; +import io.appium.java_client.FindsByAndroidViewMatcher; import io.appium.java_client.FindsByAndroidUIAutomator; import io.appium.java_client.FindsByAndroidViewTag; import io.appium.java_client.HasOnScreenKeyboard; @@ -62,7 +63,7 @@ public class AndroidDriver extends AppiumDriver implements PressesKey, HasNetworkConnection, PushesFiles, StartsActivity, FindsByAndroidUIAutomator, FindsByAndroidViewTag, FindsByAndroidDataMatcher, - LocksDevice, HasAndroidSettings, HasAndroidDeviceDetails, + FindsByAndroidViewMatcher, LocksDevice, HasAndroidSettings, HasAndroidDeviceDetails, HasSupportedPerformanceDataType, AuthenticatesByFinger, HasOnScreenKeyboard, CanRecordScreen, SupportsSpecialEmulatorCommands, SupportsNetworkStateManagement, ListensToLogcatMessages, HasAndroidClipboard, diff --git a/src/main/java/io/appium/java_client/android/AndroidElement.java b/src/main/java/io/appium/java_client/android/AndroidElement.java index 0a68124e1..95899e4db 100644 --- a/src/main/java/io/appium/java_client/android/AndroidElement.java +++ b/src/main/java/io/appium/java_client/android/AndroidElement.java @@ -20,13 +20,14 @@ import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.FindsByAndroidDataMatcher; +import io.appium.java_client.FindsByAndroidViewMatcher; import io.appium.java_client.FindsByAndroidUIAutomator; import io.appium.java_client.FindsByAndroidViewTag; import io.appium.java_client.MobileElement; public class AndroidElement extends MobileElement implements FindsByAndroidUIAutomator, FindsByAndroidDataMatcher, - FindsByAndroidViewTag { + FindsByAndroidViewMatcher, FindsByAndroidViewTag { /** * This method replace current text value. * @param value a new value diff --git a/src/main/java/io/appium/java_client/events/DefaultAspect.java b/src/main/java/io/appium/java_client/events/DefaultAspect.java index 6e0aaf5c4..345ec77ee 100644 --- a/src/main/java/io/appium/java_client/events/DefaultAspect.java +++ b/src/main/java/io/appium/java_client/events/DefaultAspect.java @@ -99,6 +99,8 @@ class DefaultAspect { + "execution(* org.openqa.selenium.ContextAware.*(..)) || " + "execution(* io.appium.java_client.FindsByAccessibilityId.*(..)) || " + "execution(* io.appium.java_client.FindsByAndroidUIAutomator.*(..)) || " + + "execution(* io.appium.java_client.FindsByAndroidDataMatcher.*(..)) || " + + "execution(* io.appium.java_client.FindsByAndroidViewMatcher.*(..)) || " + "execution(* io.appium.java_client.FindsByWindowsAutomation.*(..)) || " + "execution(* io.appium.java_client.FindsByIosNSPredicate.*(..)) || " + "execution(* org.openqa.selenium.internal.FindsByClassName.*(..)) || " 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 9d84b324d..d789f20ec 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java @@ -81,6 +81,13 @@ */ String androidDataMatcher() default ""; + /** + * It is a desired view matcher expression. + * + * @return a desired view matcher expression + */ + String androidViewMatcher() default ""; + /** * It is a xpath to the target element. * diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/Strategies.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/Strategies.java index 104448bb9..718cd403c 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/Strategies.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/Strategies.java @@ -102,6 +102,12 @@ enum Strategies { .androidDataMatcher(getValue(annotation, this)); } }, + BY_VIEW_MATCHER("androidViewMatcher") { + @Override By getBy(Annotation annotation) { + return MobileBy + .androidViewMatcher(getValue(annotation, this)); + } + }, BY_NS_PREDICATE("iOSNsPredicate") { @Override By getBy(Annotation annotation) { return MobileBy diff --git a/src/test/java/io/appium/java_client/android/AndroidDataMatcherTest.java b/src/test/java/io/appium/java_client/android/AndroidDataMatcherTest.java new file mode 100644 index 000000000..5e75653e6 --- /dev/null +++ b/src/test/java/io/appium/java_client/android/AndroidDataMatcherTest.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.android; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.appium.java_client.MobileBy; +import org.junit.Test; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import static org.junit.Assert.assertNotNull; + +public class AndroidDataMatcherTest extends BaseEspressoTest { + + @Test + public void testFindByDataMatcher() { + final WebDriverWait wait = new WebDriverWait(driver, 10); + wait.until(ExpectedConditions + .elementToBeClickable(MobileBy.AccessibilityId("Graphics"))); + driver.findElement(MobileBy.AccessibilityId("Graphics")).click(); + + String selector = new Json().toJson(ImmutableMap.of( + "name", "hasEntry", + "args", ImmutableList.of("title", "Sweep") + )); + + assertNotNull(wait.until(ExpectedConditions + .presenceOfElementLocated(MobileBy.androidDataMatcher(selector)))); + } +} diff --git a/src/test/java/io/appium/java_client/android/AndroidViewMatcherTest.java b/src/test/java/io/appium/java_client/android/AndroidViewMatcherTest.java new file mode 100644 index 000000000..e8253074e --- /dev/null +++ b/src/test/java/io/appium/java_client/android/AndroidViewMatcherTest.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.android; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.appium.java_client.MobileBy; +import org.junit.Test; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import static org.junit.Assert.assertNotNull; + +public class AndroidViewMatcherTest extends BaseEspressoTest { + + @Test + public void testFindByViewMatcher() { + String selector = new Json().toJson(ImmutableMap.of( + "name", "withText", + "args", ImmutableList.of("Animation"), + "class", "androidx.test.espresso.matcher.ViewMatchers" + )); + final WebDriverWait wait = new WebDriverWait(driver, 10); + assertNotNull(wait.until(ExpectedConditions + .presenceOfElementLocated(MobileBy.androidViewMatcher(selector)))); + } +} diff --git a/src/test/java/io/appium/java_client/android/BaseEspressoTest.java b/src/test/java/io/appium/java_client/android/BaseEspressoTest.java new file mode 100644 index 000000000..889aa238f --- /dev/null +++ b/src/test/java/io/appium/java_client/android/BaseEspressoTest.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.android; + +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.MobileCapabilityType; +import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.openqa.selenium.remote.DesiredCapabilities; + +import java.io.File; + +public class BaseEspressoTest { + + private static AppiumDriverLocalService service; + protected static AndroidDriver driver; + + /** + * initialization. + */ + @BeforeClass public static void beforeClass() { + service = AppiumDriverLocalService.buildDefaultService(); + service.start(); + + if (service == null || !service.isRunning()) { + throw new AppiumServerHasNotBeenStartedLocallyException( + "An appium server node is not started!"); + } + + File appDir = new File("src/test/java/io/appium/java_client"); + File app = new File(appDir, "ApiDemos-debug.apk"); + DesiredCapabilities capabilities = new DesiredCapabilities(); + capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.ESPRESSO); + capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); + capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); + capabilities.setCapability("eventTimings", true); + driver = new AndroidDriver<>(service.getUrl(), capabilities); + } + + /** + * finishing. + */ + @AfterClass public static void afterClass() { + if (driver != null) { + driver.quit(); + } + if (service != null) { + service.stop(); + } + } +}