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