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 com.google.android.droiddriver.finders; 18 19import static com.google.common.base.Preconditions.checkNotNull; 20 21import com.google.android.droiddriver.UiElement; 22import com.google.common.annotations.Beta; 23import com.google.common.base.Joiner; 24import com.google.common.base.Objects; 25 26/** 27 * Convenience methods to create commonly used finders. 28 */ 29public class By { 30 /** Matches by {@link Object#equals}. */ 31 public static final MatchStrategy<Object> OBJECT_EQUALS = new MatchStrategy<Object>() { 32 @Override 33 public boolean match(Object expected, Object actual) { 34 return Objects.equal(actual, expected); 35 } 36 37 @Override 38 public String toString() { 39 return "equals"; 40 } 41 }; 42 /** Matches by {@link String#matches}. */ 43 public static final MatchStrategy<String> STRING_MATCHES = new MatchStrategy<String>() { 44 @Override 45 public boolean match(String expected, String actual) { 46 return actual != null && actual.matches(expected); 47 } 48 49 @Override 50 public String toString() { 51 return "matches pattern"; 52 } 53 }; 54 /** Matches by {@link String#contains}. */ 55 public static final MatchStrategy<String> STRING_CONTAINS = new MatchStrategy<String>() { 56 @Override 57 public boolean match(String expected, String actual) { 58 return actual != null && actual.contains(expected); 59 } 60 61 @Override 62 public String toString() { 63 return "contains"; 64 } 65 }; 66 67 /** 68 * Creates a new ByAttribute finder. Frequently-used finders have shorthands 69 * below, for example, {@link #text}, {@link #textRegex}. Users can create 70 * custom finders using this method. 71 * 72 * @param attribute the attribute to match against 73 * @param strategy the matching strategy, for instance, equals or matches 74 * regular expression 75 * @param expected the expected attribute value 76 * @return a new ByAttribute finder 77 */ 78 public static <T> ByAttribute<T> attribute(Attribute attribute, 79 MatchStrategy<? super T> strategy, T expected) { 80 return new ByAttribute<T>(attribute, strategy, expected); 81 } 82 83 /** Shorthand for {@link #attribute}{@code (attribute, OBJECT_EQUALS, expected)} */ 84 public static <T> ByAttribute<T> attribute(Attribute attribute, T expected) { 85 return attribute(attribute, OBJECT_EQUALS, expected); 86 } 87 88 /** Shorthand for {@link #attribute}{@code (attribute, true)} */ 89 public static ByAttribute<Boolean> is(Attribute attribute) { 90 return attribute(attribute, true); 91 } 92 93 /** Shorthand for {@link #attribute}{@code (attribute, false)} */ 94 public static ByAttribute<Boolean> not(Attribute attribute) { 95 return attribute(attribute, false); 96 } 97 98 /** 99 * @param resourceId The resource id to match against 100 * @return a finder to find an element by resource id 101 */ 102 public static ByAttribute<String> resourceId(String resourceId) { 103 return attribute(Attribute.RESOURCE_ID, OBJECT_EQUALS, resourceId); 104 } 105 106 /** 107 * @param name The exact package name to match against 108 * @return a finder to find an element by package name 109 */ 110 public static ByAttribute<String> packageName(String name) { 111 return attribute(Attribute.PACKAGE, OBJECT_EQUALS, name); 112 } 113 114 /** 115 * @param text The exact text to match against 116 * @return a finder to find an element by text 117 */ 118 public static ByAttribute<String> text(String text) { 119 return attribute(Attribute.TEXT, OBJECT_EQUALS, text); 120 } 121 122 /** 123 * @param regex The regular expression pattern to match against 124 * @return a finder to find an element by text pattern 125 */ 126 public static ByAttribute<String> textRegex(String regex) { 127 return attribute(Attribute.TEXT, STRING_MATCHES, regex); 128 } 129 130 /** 131 * @param substring String inside a text field 132 * @return a finder to find an element by text substring 133 */ 134 public static ByAttribute<String> textContains(String substring) { 135 return attribute(Attribute.TEXT, STRING_CONTAINS, substring); 136 } 137 138 /** 139 * @param contentDescription The exact content description to match against 140 * @return a finder to find an element by content description 141 */ 142 public static ByAttribute<String> contentDescription(String contentDescription) { 143 return attribute(Attribute.CONTENT_DESC, OBJECT_EQUALS, contentDescription); 144 } 145 146 /** 147 * @param substring String inside a content description 148 * @return a finder to find an element by content description substring 149 */ 150 public static ByAttribute<String> contentDescriptionContains(String substring) { 151 return attribute(Attribute.CONTENT_DESC, STRING_CONTAINS, substring); 152 } 153 154 /** 155 * @param className The exact class name to match against 156 * @return a finder to find an element by class name 157 */ 158 public static ByAttribute<String> className(String className) { 159 return attribute(Attribute.CLASS, OBJECT_EQUALS, className); 160 } 161 162 /** 163 * @param clazz The class whose name is matched against 164 * @return a finder to find an element by class name 165 */ 166 public static ByAttribute<String> className(Class<?> clazz) { 167 return className(clazz.getName()); 168 } 169 170 /** 171 * @return a finder to find an element that is selected 172 */ 173 public static ByAttribute<Boolean> selected() { 174 return is(Attribute.SELECTED); 175 } 176 177 /** 178 * Matches by XPath. When applied on an non-root element, it will not evaluate 179 * above the context element. 180 * <p> 181 * XPath is the domain-specific-language for navigating a node tree. It is 182 * ideal if the UiElement to match has a complex relationship with surrounding 183 * nodes. For simple cases, {@link #withParent} or {@link #withAncestor} are 184 * preferred, which can combine with other {@link MatchFinder}s in 185 * {@link #allOf}. For complex cases like below, XPath is superior: 186 * 187 * <pre> 188 * {@code 189 * <View><!-- a custom view to group a cluster of items --> 190 * <LinearLayout> 191 * <TextView text='Albums'/> 192 * <TextView text='4 MORE'/> 193 * </LinearLayout> 194 * <RelativeLayout> 195 * <TextView text='Forever'/> 196 * <ImageView/> 197 * </RelativeLayout> 198 * </View><!-- end of Albums cluster --> 199 * <!-- imagine there are other clusters for Artists and Songs --> 200 * } 201 * </pre> 202 * 203 * If we need to locate the RelativeLayout containing the album "Forever" 204 * instead of a song or an artist named "Forever", this XPath works: 205 * 206 * <pre> 207 * {@code //*[LinearLayout/*[@text='Albums']]/RelativeLayout[*[@text='Forever']]} 208 * </pre> 209 * 210 * @param xPath The xpath to use 211 * @return a finder which locates elements via XPath 212 */ 213 @Beta 214 public static ByXPath xpath(String xPath) { 215 return new ByXPath(xPath); 216 } 217 218 /** 219 * @return a finder that uses the UiElement returned by parent Finder as 220 * context for the child Finder 221 */ 222 public static ChainFinder chain(Finder parent, Finder child) { 223 return new ChainFinder(parent, child); 224 } 225 226 // Hamcrest style finder aggregators 227 /** 228 * Evaluates given {@finders} in short-circuit fashion in the order 229 * they are passed. Costly finders (for example those returned by with* 230 * methods that navigate the node tree) should be passed after cheap finders 231 * (for example the ByAttribute finders). 232 * 233 * @return a finder that is the logical conjunction of given finders 234 */ 235 public static MatchFinder allOf(final MatchFinder... finders) { 236 return new MatchFinder() { 237 @Override 238 public boolean matches(UiElement element) { 239 for (MatchFinder finder : finders) { 240 if (!finder.matches(element)) { 241 return false; 242 } 243 } 244 return true; 245 } 246 247 @Override 248 public String toString() { 249 return "allOf(" + Joiner.on(",").join(finders) + ")"; 250 } 251 }; 252 } 253 254 /** 255 * Evaluates given {@finders} in short-circuit fashion in the order 256 * they are passed. Costly finders (for example those returned by with* 257 * methods that navigate the node tree) should be passed after cheap finders 258 * (for example the ByAttribute finders). 259 * 260 * @return a finder that is the logical disjunction of given finders 261 */ 262 public static MatchFinder anyOf(final MatchFinder... finders) { 263 return new MatchFinder() { 264 @Override 265 public boolean matches(UiElement element) { 266 for (MatchFinder finder : finders) { 267 if (finder.matches(element)) { 268 return true; 269 } 270 } 271 return false; 272 } 273 274 @Override 275 public String toString() { 276 return "anyOf(" + Joiner.on(",").join(finders) + ")"; 277 } 278 }; 279 } 280 281 /** 282 * Matches a UiElement whose parent matches the given parentFinder. For 283 * complex cases, consider {@link #xpath}. 284 */ 285 public static MatchFinder withParent(final MatchFinder parentFinder) { 286 checkNotNull(parentFinder); 287 return new MatchFinder() { 288 @Override 289 public boolean matches(UiElement element) { 290 UiElement parent = element.getParent(); 291 return parent != null && parentFinder.matches(parent); 292 } 293 294 @Override 295 public String toString() { 296 return "withParent(" + parentFinder + ")"; 297 } 298 }; 299 } 300 301 /** 302 * Matches a UiElement whose ancestor matches the given ancestorFinder. For 303 * complex cases, consider {@link #xpath}. 304 */ 305 public static MatchFinder withAncestor(final MatchFinder ancestorFinder) { 306 checkNotNull(ancestorFinder); 307 return new MatchFinder() { 308 @Override 309 public boolean matches(UiElement element) { 310 UiElement parent = element.getParent(); 311 while (parent != null) { 312 if (ancestorFinder.matches(parent)) { 313 return true; 314 } 315 parent = parent.getParent(); 316 } 317 return false; 318 } 319 320 @Override 321 public String toString() { 322 return "withAncestor(" + ancestorFinder + ")"; 323 } 324 }; 325 } 326 327 /** 328 * Matches a UiElement which has a sibling matching the given siblingFinder. 329 * For complex cases, consider {@link #xpath}. 330 */ 331 public static MatchFinder withSibling(final MatchFinder siblingFinder) { 332 checkNotNull(siblingFinder); 333 return new MatchFinder() { 334 @Override 335 public boolean matches(UiElement element) { 336 UiElement parent = element.getParent(); 337 if (parent == null) { 338 return false; 339 } 340 for (int i = 0; i < parent.getChildCount(); i++) { 341 if (siblingFinder.matches(parent.getChild(i))) { 342 return true; 343 } 344 } 345 return false; 346 } 347 348 @Override 349 public String toString() { 350 return "withSibling(" + siblingFinder + ")"; 351 } 352 }; 353 } 354 355 private By() {} 356} 357