diff --git a/src/main/java/io/appium/java_client/FindsByIosClassChain.java b/src/main/java/io/appium/java_client/FindsByIosClassChain.java new file mode 100644 index 000000000..92482663a --- /dev/null +++ b/src/main/java/io/appium/java_client/FindsByIosClassChain.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 FindsByIosClassChain extends FindsByFluentSelector { + + default T findElementByIosClassChain(String using) { + return findElement(MobileSelector.IOS_CLASS_CHAIN.toString(), using); + } + + default List findElementsByIosClassChain(String using) { + return findElements(MobileSelector.IOS_CLASS_CHAIN.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 f9917ef01..c4209523d 100644 --- a/src/main/java/io/appium/java_client/MobileBy.java +++ b/src/main/java/io/appium/java_client/MobileBy.java @@ -97,6 +97,17 @@ public static By AccessibilityId(final String accessibilityId) { return new ByAccessibilityId(accessibilityId); } + /** + * This locator strategy is available in XCUITest Driver mode + * @param iOSClassChainString is a valid class chain locator string. + * See + * the documentation for more details + * @return an instance of {@link io.appium.java_client.MobileBy.ByIosClassChain} + */ + public static By iOSClassChain(final String iOSClassChainString) { + return new ByIosClassChain(iOSClassChainString); + } + /** * This locator strategy is available in XCUITest Driver mode * @param iOSNsPredicateString is an an iOS NsPredicate String @@ -290,6 +301,62 @@ public List findElements(SearchContext context) throws WebDriverExce } } + public static class ByIosClassChain extends MobileBy implements Serializable { + + protected ByIosClassChain(String locatorString) { + super(MobileSelector.IOS_CLASS_CHAIN, locatorString); + } + + /** + * @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 (FindsByIosClassChain.class.isAssignableFrom(contextClass)) { + return FindsByIosClassChain.class.cast(context) + .findElementsByIosClassChain(getLocatorString()); + } + + if (FindsByFluentSelector.class.isAssignableFrom(context.getClass())) { + return super.findElements(context); + } + + throw formIllegalArgumentException(contextClass, FindsByIosClassChain.class, + FindsByFluentSelector.class); + } + + /** + * @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 (FindsByIosClassChain.class.isAssignableFrom(contextClass)) { + return FindsByIosClassChain.class.cast(context) + .findElementByIosClassChain(getLocatorString()); + } + + if (FindsByFluentSelector.class.isAssignableFrom(context.getClass())) { + return super.findElement(context); + } + + throw formIllegalArgumentException(contextClass, FindsByIosClassChain.class, + FindsByFluentSelector.class); + } + + @Override public String toString() { + return "By.IosClassChain: " + 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 f70c64fc1..d3afac5bd 100644 --- a/src/main/java/io/appium/java_client/MobileSelector.java +++ b/src/main/java/io/appium/java_client/MobileSelector.java @@ -21,6 +21,7 @@ public enum MobileSelector { ANDROID_UI_AUTOMATOR("-android uiautomator"), IOS_UI_AUTOMATION("-ios uiautomation"), IOS_PREDICATE_STRING("-ios predicate string"), + IOS_CLASS_CHAIN("-ios class chain"), WINDOWS_UI_AUTOMATION("-windows uiautomation"); private final String selector; diff --git a/src/main/java/io/appium/java_client/ios/IOSDriver.java b/src/main/java/io/appium/java_client/ios/IOSDriver.java index ce71c5591..eb36d64ba 100644 --- a/src/main/java/io/appium/java_client/ios/IOSDriver.java +++ b/src/main/java/io/appium/java_client/ios/IOSDriver.java @@ -19,6 +19,7 @@ import static io.appium.java_client.MobileCommand.prepareArguments; import io.appium.java_client.AppiumDriver; +import io.appium.java_client.FindsByIosClassChain; import io.appium.java_client.FindsByIosNSPredicate; import io.appium.java_client.FindsByIosUIAutomation; import io.appium.java_client.HidesKeyboardWithKeyName; @@ -50,7 +51,8 @@ public class IOSDriver extends AppiumDriver implements HidesKeyboardWithKeyName, ShakesDevice, - FindsByIosUIAutomation, LocksIOSDevice, PerformsTouchID, FindsByIosNSPredicate { + FindsByIosUIAutomation, LocksIOSDevice, PerformsTouchID, FindsByIosNSPredicate, + FindsByIosClassChain { private static final String IOS_PLATFORM = MobilePlatform.IOS; diff --git a/src/main/java/io/appium/java_client/ios/IOSElement.java b/src/main/java/io/appium/java_client/ios/IOSElement.java index 1bbe1a854..8702497aa 100644 --- a/src/main/java/io/appium/java_client/ios/IOSElement.java +++ b/src/main/java/io/appium/java_client/ios/IOSElement.java @@ -16,10 +16,12 @@ package io.appium.java_client.ios; +import io.appium.java_client.FindsByIosClassChain; import io.appium.java_client.FindsByIosNSPredicate; import io.appium.java_client.FindsByIosUIAutomation; import io.appium.java_client.MobileElement; public class IOSElement extends MobileElement - implements FindsByIosUIAutomation, FindsByIosNSPredicate { + implements FindsByIosUIAutomation, FindsByIosNSPredicate, + FindsByIosClassChain { } 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 923779511..3f3961578 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 @@ -91,6 +91,12 @@ enum Strategies { .windowsAutomation(getValue(annotation, this)); } }, + BY_CLASS_CHAIN("iOSClassChain") { + @Override By getBy(Annotation annotation) { + return MobileBy + .iOSClassChain(getValue(annotation, this)); + } + }, BY_NS_PREDICATE("iOSNsPredicate") { @Override By getBy(Annotation annotation) { return MobileBy diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java index 70c241bdb..2baf2e866 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java @@ -26,6 +26,14 @@ @Repeatable(iOSXCUITFindBySet.class) public @interface iOSXCUITFindBy { + /** + * The Class Chain locator is similar to xpath, but it's faster and can only + * search direct children elements. See the + * + * documentation for more details. + */ + String iOSClassChain() default ""; + /** * The NSPredicate class is used to define logical conditions used to constrain * a search either for a fetch or for in-memory filtering. diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java index 1b181524d..13f6d66f7 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java @@ -19,6 +19,10 @@ import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.CHAIN; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; @@ -41,6 +45,7 @@ import org.openqa.selenium.support.PageFactory; import org.openqa.selenium.support.ui.WebDriverWait; +import java.util.List; import java.util.function.Supplier; @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -85,7 +90,14 @@ public class XCUITModeTest extends AppXCUITTest { @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'location'") private MobileElement locationAlert; + @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeTextField[2]") + private MobileElement secondTextField; + @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeButton[-1]") + private MobileElement lastButton; + + @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeButton") + private List allButtons; /** * The setting up. @@ -133,6 +145,18 @@ public class XCUITModeTest extends AppXCUITTest { assertTrue(locationAlert.isDisplayed()); } + @Test public void findElementByClassChain() { + assertThat(secondTextField.getAttribute("name"), equalTo("IntegerB")); + } + + @Test public void findElementByClassChainWithNegativeIndex() { + assertThat(lastButton.getAttribute("name"), equalTo("Test Gesture")); + } + + @Test public void findMultipleElementsByClassChain() { + assertThat(allButtons.size(), is(greaterThan(1))); + } + @Test public void findElementByXUISelectorTest() { assertNotNull(gesture.getText()); }