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