• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""UI Node is used to compose the UI pages."""
2from __future__ import annotations
3
4import collections
5from typing import Any, Dict, List, Optional
6from xml.dom import minidom
7
8# Internal import
9
10
11class UINode:
12  """UI Node to hold element of UI page.
13
14  If both x and y axis are given in constructor, this node will use (x, y)
15  as coordinates. Otherwise, the attribute `bounds` of node will be used to
16  calculate the coordinates.
17
18  Attributes:
19    node: XML node element.
20    x: x point of UI page.
21    y: y point of UI page.
22  """
23
24  STR_FORMAT = "RID='{rid}'/CLASS='{clz}'/TEXT='{txt}'/CD='{ctx}'"
25  PREFIX_SEARCH_IN = 'c:'
26
27  def __init__(self, node: minidom.Element,
28               x: Optional[int] = None, y: Optional[int] = None) -> None:
29    self.node = node
30    if x and y:
31      self.x = x
32      self.y = y
33    else:
34      self.x, self.y = adb_ui.find_point_in_bounds(
35          self.attributes['bounds'].value)
36
37  def __hash__(self) -> int:
38    return id(self.node)
39
40  @property
41  def clz(self) -> str:
42    """Returns the class of node."""
43    return self.attributes['class'].value
44
45  @property
46  def text(self) -> str:
47    """Gets text of node.
48
49    Returns:
50      The text of node.
51    """
52    return self.attributes['text'].value
53
54  @property
55  def content_desc(self) -> str:
56    """Gets content description of node.
57
58    Returns:
59      The content description of node.
60    """
61    return self.attributes['content-desc'].value
62
63  @property
64  def resource_id(self) -> str:
65    """Gets resource id of node.
66
67    Returns:
68      The resource id of node.
69    """
70    return self.attributes['resource-id'].value
71
72  @property
73  def attributes(self) -> Dict[str, Any]:
74    """Gets attributes of node.
75
76    Returns:
77      The attributes of node.
78    """
79    if hasattr(self.node, 'attributes'):
80      return collections.defaultdict(
81          lambda: None,
82          getattr(self.node, 'attributes'))
83    else:
84      return collections.defaultdict(lambda: None)
85
86  @property
87  def child_nodes(self) -> List[UINode]:
88    """Gets child node(s) of current node.
89
90    Returns:
91      The child nodes of current node if any.
92    """
93    return [UINode(n) for n in self.node.childNodes]
94
95  def match_attrs_by_kwargs(self, **kwargs) -> bool:
96    """Matches given attribute key/value pair with current node.
97
98    Args:
99      **kwargs: Key/value pair as attribute key/value.
100        e.g.: resource_id='abc'
101
102    Returns:
103      True iff the given attributes match current node.
104    """
105    if 'clz' in kwargs:
106      kwargs['class'] = kwargs['clz']
107      del kwargs['clz']
108
109    return self.match_attrs(kwargs)
110
111  def match_attrs(self, attrs: Dict[str, Any]) -> bool:
112    """Matches given attributes with current node.
113
114    This method is used to compare the given `attrs` with attributes of
115    current node. Only the keys given in `attrs` will be compared. e.g.:
116    ```
117    # ui_node has attributes {'name': 'john', 'id': '1234'}
118    >>> ui_node.match_attrs({'name': 'john'})
119    True
120
121    >>> ui_node.match_attrs({'name': 'ken'})
122    False
123    ```
124
125    If you don't want exact match and want to check if an attribute value
126    contain specific substring, you can leverage special prefix
127    `PREFIX_SEARCH_IN` to tell this method to use `in` instead of `==` for
128    comparison. e.g.:
129    ```
130    # ui_node has attributes {'name': 'john', 'id': '1234'}
131    >>> ui_node.match_attrs({'name': ui_node.PREFIX_SEARCH_IN + 'oh'})
132    True
133
134    >>> ui_node.match_attrs({'name': 'oh'})
135    False
136    ```
137
138    Args:
139      attrs: Attributes to compare with.
140
141    Returns:
142      True iff the given attributes match current node.
143    """
144    for k, v in attrs.items():
145      if k not in self.attributes:
146        return False
147
148      if v and v.startswith(self.PREFIX_SEARCH_IN):
149        v = v[len(self.PREFIX_SEARCH_IN):]
150        if not v or v not in self.attributes[k].value:
151          return False
152      elif v != self.attributes[k].value:
153        return False
154
155    return True
156
157  def __str__(self) -> str:
158    """The string representation of this object.
159
160    Returns:
161      The string representation including below information:
162      - resource id
163      - class
164      - text
165      - content description.
166    """
167    rid = self.resource_id.strip()
168    clz = self.clz.strip()
169    txt = self.text.strip()
170    ctx = self.content_desc.strip()
171    return f"RID='{rid}'/CLASS='{clz}'/TEXT='{txt}'/CD='{ctx}'"
172