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