1/* 2 * Copyright (C) 2013 DroidDriver committers 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package io.appium.droiddriver.finders; 18 19import static io.appium.droiddriver.util.Preconditions.checkNotNull; 20 21import android.content.Context; 22 23import java.util.ArrayList; 24import java.util.List; 25 26import io.appium.droiddriver.UiElement; 27import io.appium.droiddriver.exceptions.ElementNotFoundException; 28import io.appium.droiddriver.util.InstrumentationUtils; 29 30/** 31 * Convenience methods to create commonly used finders. 32 */ 33public class By { 34 35 private static final MatchFinder ANY = new MatchFinder(null); 36 37 /** 38 * Matches any UiElement. 39 */ 40 public static MatchFinder any() { 41 return ANY; 42 } 43 44 /** 45 * Matches a UiElement whose {@code attribute} is {@code true}. 46 */ 47 public static MatchFinder is(Attribute attribute) { 48 return new MatchFinder(Predicates.attributeTrue(attribute)); 49 } 50 51 /** 52 * Matches a UiElement whose {@code attribute} is {@code false} or is not set. 53 */ 54 public static MatchFinder not(Attribute attribute) { 55 return new MatchFinder(Predicates.attributeFalse(attribute)); 56 } 57 58 /** 59 * Matches a UiElement by a resource id defined in the AUT. 60 */ 61 public static MatchFinder resourceId(int resourceId) { 62 Context targetContext = InstrumentationUtils.getInstrumentation().getTargetContext(); 63 return resourceId(targetContext.getResources().getResourceName(resourceId)); 64 } 65 66 /** 67 * Matches a UiElement by the string representation of a resource id. This works for resources not 68 * belonging to the AUT. 69 */ 70 public static MatchFinder resourceId(String resourceId) { 71 return new MatchFinder(Predicates.attributeEquals(Attribute.RESOURCE_ID, resourceId)); 72 } 73 74 /** 75 * Matches a UiElement by package name. 76 */ 77 public static MatchFinder packageName(String name) { 78 return new MatchFinder(Predicates.attributeEquals(Attribute.PACKAGE, name)); 79 } 80 81 /** 82 * Matches a UiElement by the exact text. 83 */ 84 public static MatchFinder text(String text) { 85 return new MatchFinder(Predicates.attributeEquals(Attribute.TEXT, text)); 86 } 87 88 /** 89 * Matches a UiElement whose text matches {@code regex}. 90 */ 91 public static MatchFinder textRegex(String regex) { 92 return new MatchFinder(Predicates.attributeMatches(Attribute.TEXT, regex)); 93 } 94 95 /** 96 * Matches a UiElement whose text contains {@code substring}. 97 */ 98 public static MatchFinder textContains(String substring) { 99 return new MatchFinder(Predicates.attributeContains(Attribute.TEXT, substring)); 100 } 101 102 /** 103 * Matches a UiElement by content description. 104 */ 105 public static MatchFinder contentDescription(String contentDescription) { 106 return new MatchFinder(Predicates.attributeEquals(Attribute.CONTENT_DESC, contentDescription)); 107 } 108 109 /** 110 * Matches a UiElement whose content description contains {@code substring}. 111 */ 112 public static MatchFinder contentDescriptionContains(String substring) { 113 return new MatchFinder(Predicates.attributeContains(Attribute.CONTENT_DESC, substring)); 114 } 115 116 /** 117 * Matches a UiElement by class name. 118 */ 119 public static MatchFinder className(String className) { 120 return new MatchFinder(Predicates.attributeEquals(Attribute.CLASS, className)); 121 } 122 123 /** 124 * Matches a UiElement by class name. 125 */ 126 public static MatchFinder className(Class<?> clazz) { 127 return className(clazz.getName()); 128 } 129 130 /** 131 * Matches a UiElement that is selected. 132 */ 133 public static MatchFinder selected() { 134 return is(Attribute.SELECTED); 135 } 136 137 /** 138 * Matches by XPath. When applied on an non-root element, it will not evaluate above the context 139 * element. <p> XPath is the domain-specific-language for navigating a node tree. It is ideal if 140 * the UiElement to match has a complex relationship with surrounding nodes. For simple cases, 141 * {@link #withParent} or {@link #withAncestor} are preferred, which can combine with other {@link 142 * MatchFinder}s in {@link #allOf}. For complex cases like below, XPath is superior: 143 * 144 * <pre> 145 * {@code 146 * <View><!-- a custom view to group a cluster of items --> 147 * <LinearLayout> 148 * <TextView text='Albums'/> 149 * <TextView text='4 MORE'/> 150 * </LinearLayout> 151 * <RelativeLayout> 152 * <TextView text='Forever'/> 153 * <ImageView/> 154 * </RelativeLayout> 155 * </View><!-- end of Albums cluster --> 156 * <!-- imagine there are other clusters for Artists and Songs --> 157 * } 158 * </pre> 159 * 160 * If we need to locate the RelativeLayout containing the album "Forever" instead of a song or an 161 * artist named "Forever", this XPath works: 162 * 163 * <pre> 164 * {@code //*[LinearLayout/*[@text='Albums']]/RelativeLayout[*[@text='Forever']]} 165 * </pre> 166 * 167 * @param xPath The xpath to use 168 * @return a finder which locates elements via XPath 169 */ 170 public static ByXPath xpath(String xPath) { 171 return new ByXPath(xPath); 172 } 173 174 /** 175 * Returns a finder that uses the UiElement returned by first Finder as context for the second 176 * Finder. <p> typically first Finder finds the ancestor, then second Finder finds the target 177 * UiElement, which is a descendant. </p> Note that if the first Finder matches multiple 178 * UiElements, only the first match is tried, which usually is not what callers expect. In this 179 * case, allOf(second, withAncesor(first)) may work. 180 */ 181 public static ChainFinder chain(Finder first, Finder second) { 182 return new ChainFinder(first, second); 183 } 184 185 private static List<Predicate<? super UiElement>> getPredicates(MatchFinder... finders) { 186 ArrayList<Predicate<? super UiElement>> predicates = new ArrayList<>(finders.length); 187 for (int i = 0; i < finders.length; i++) { 188 predicates.add(finders[i].predicate); 189 } 190 return predicates; 191 } 192 193 /** 194 * Evaluates given {@code finders} in short-circuit fashion in the order they are passed. Costly 195 * finders (for example those returned by with* methods that navigate the node tree) should be 196 * passed after cheap finders (for example the ByAttribute finders). 197 * 198 * @return a finder that is the logical conjunction of given finders 199 */ 200 public static MatchFinder allOf(final MatchFinder... finders) { 201 return new MatchFinder(Predicates.allOf(getPredicates(finders))); 202 } 203 204 /** 205 * Evaluates given {@code finders} in short-circuit fashion in the order they are passed. Costly 206 * finders (for example those returned by with* methods that navigate the node tree) should be 207 * passed after cheap finders (for example the ByAttribute finders). 208 * 209 * @return a finder that is the logical disjunction of given finders 210 */ 211 public static MatchFinder anyOf(final MatchFinder... finders) { 212 return new MatchFinder(Predicates.anyOf(getPredicates(finders))); 213 } 214 215 /** 216 * Matches a UiElement whose parent matches the given parentFinder. For complex cases, consider 217 * {@link #xpath}. 218 */ 219 public static MatchFinder withParent(MatchFinder parentFinder) { 220 checkNotNull(parentFinder); 221 return new MatchFinder(Predicates.withParent(parentFinder.predicate)); 222 } 223 224 /** 225 * Matches a UiElement whose ancestor matches the given ancestorFinder. For complex cases, 226 * consider {@link #xpath}. 227 */ 228 public static MatchFinder withAncestor(MatchFinder ancestorFinder) { 229 checkNotNull(ancestorFinder); 230 return new MatchFinder(Predicates.withAncestor(ancestorFinder.predicate)); 231 } 232 233 /** 234 * Matches a UiElement which has a visible sibling matching the given siblingFinder. This could be 235 * inefficient; consider {@link #xpath}. 236 */ 237 public static MatchFinder withSibling(MatchFinder siblingFinder) { 238 checkNotNull(siblingFinder); 239 return new MatchFinder(Predicates.withSibling(siblingFinder.predicate)); 240 } 241 242 /** 243 * Matches a UiElement which has a visible child matching the given childFinder. This could be 244 * inefficient; consider {@link #xpath}. 245 */ 246 public static MatchFinder withChild(MatchFinder childFinder) { 247 checkNotNull(childFinder); 248 return new MatchFinder(Predicates.withChild(childFinder.predicate)); 249 } 250 251 /** 252 * Matches a UiElement whose descendant (including self) matches the given descendantFinder. This 253 * could be VERY inefficient; consider {@link #xpath}. 254 */ 255 public static MatchFinder withDescendant(final MatchFinder descendantFinder) { 256 checkNotNull(descendantFinder); 257 return new MatchFinder(new Predicate<UiElement>() { 258 @Override 259 public boolean apply(UiElement element) { 260 try { 261 descendantFinder.find(element); 262 return true; 263 } catch (ElementNotFoundException enfe) { 264 return false; 265 } 266 } 267 268 @Override 269 public String toString() { 270 return "withDescendant(" + descendantFinder + ")"; 271 } 272 }); 273 } 274 275 /** 276 * Matches a UiElement that does not match the provided {@code finder}. 277 */ 278 public static MatchFinder not(MatchFinder finder) { 279 checkNotNull(finder); 280 return new MatchFinder(Predicates.not(finder.predicate)); 281 } 282 283 private By() { 284 } 285} 286