aboutsummaryrefslogtreecommitdiff
path: root/src/io/appium/droiddriver/finders/XPaths.java
blob: 31add4e51a470ebc04866d6081d95de51974a524 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/*
 * Copyright (C) 2013 DroidDriver committers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.appium.droiddriver.finders;

import android.text.TextUtils;

/**
 * Convenience methods and constants for XPath.
 * <p>
 * DroidDriver implementation uses default XPath library on device, so the
 * support may be limited to <a href="http://www.w3.org/TR/xpath/">XPath
 * 1.0</a>. Newer XPath features may not be supported, for example, the
 * fn:matches function.
 */
public class XPaths {

  private XPaths() {}

  /**
   * @return The tag name used to build UiElement DOM. It is preferable to use
   *         this to build XPath instead of String literals.
   */
  public static String tag(String className) {
    return simpleClassName(className);
  }

  /**
   * @return The tag name used to build UiElement DOM. It is preferable to use
   *         this to build XPath instead of String literals.
   */
  public static String tag(Class<?> clazz) {
    return tag(clazz.getSimpleName());
  }

  private static String simpleClassName(String name) {
    // the nth anonymous class has a class name ending in "Outer$n"
    // and local inner classes have names ending in "Outer.$1Inner"
    name = name.replaceAll("\\$[0-9]+", "\\$");

    // we want the name of the inner class all by its lonesome
    int start = name.lastIndexOf('$');

    // if this isn't an inner class, just find the start of the
    // top level class name.
    if (start == -1) {
      start = name.lastIndexOf('.');
    }
    return name.substring(start + 1);
  }

  /**
   * @return XPath predicate (with enclosing []) for boolean attribute that is
   *         present
   */
  public static String is(Attribute attribute) {
    return "[@" + attribute.getName() + "]";
  }

  /**
   * @return XPath predicate (with enclosing []) for boolean attribute that is
   *         NOT present
   */
  public static String not(Attribute attribute) {
    return "[not(@" + attribute.getName() + ")]";
  }

  /** @return XPath predicate (with enclosing []) for attribute with value */
  public static String attr(Attribute attribute, String value) {
    return String.format("[@%s=%s]", attribute.getName(), quoteXPathLiteral(value));
  }

  /** @return XPath predicate (with enclosing []) for attribute containing value */
  public static String containsAttr(Attribute attribute, String containedValue) {
    return String.format("[contains(@%s, %s)]", attribute.getName(),
        quoteXPathLiteral(containedValue));
  }

  /** Shorthand for {@link #attr}{@code (Attribute.TEXT, value)} */
  public static String text(String value) {
    return attr(Attribute.TEXT, value);
  }

  /** Shorthand for {@link #attr}{@code (Attribute.RESOURCE_ID, value)} */
  public static String resourceId(String value) {
    return attr(Attribute.RESOURCE_ID, value);
  }

  /**
   * @return XPath predicate (with enclosing []) that filters nodes with
   *         descendants satisfying {@code descendantPredicate}.
   */
  public static String withDescendant(String descendantPredicate) {
    return "[.//*" + descendantPredicate + "]";
  }

  /**
   * Adapted from http://stackoverflow.com/questions/1341847/.
   * <p>
   * Produce an XPath literal equal to the value if possible; if not, produce an
   * XPath expression that will match the value. Note that this function will
   * produce very long XPath expressions if a value contains a long run of
   * double quotes.
   */
  static String quoteXPathLiteral(String value) {
    // if the value contains only single or double quotes, construct an XPath
    // literal
    if (!value.contains("\"")) {
      return "\"" + value + "\"";
    }
    if (!value.contains("'")) {
      return "'" + value + "'";
    }

    // if the value contains both single and double quotes, construct an
    // expression that concatenates all non-double-quote substrings with
    // the quotes, e.g.:
    // concat("foo", '"', "bar")
    StringBuilder sb = new StringBuilder();
    sb.append("concat(\"");
    sb.append(TextUtils.join("\",'\"',\"", value.split("\"")));
    sb.append("\")");
    return sb.toString();
  }
}