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