"""UI Node is used to compose the UI pages.""" from __future__ import annotations import collections from typing import Any, Dict, List, Optional from xml.dom import minidom # Internal import class UINode: """UI Node to hold element of UI page. If both x and y axis are given in constructor, this node will use (x, y) as coordinates. Otherwise, the attribute `bounds` of node will be used to calculate the coordinates. Attributes: node: XML node element. x: x point of UI page. y: y point of UI page. """ STR_FORMAT = "RID='{rid}'/CLASS='{clz}'/TEXT='{txt}'/CD='{ctx}'" PREFIX_SEARCH_IN = 'c:' def __init__(self, node: minidom.Element, x: Optional[int] = None, y: Optional[int] = None) -> None: self.node = node if x and y: self.x = x self.y = y else: self.x, self.y = adb_ui.find_point_in_bounds( self.attributes['bounds'].value) def __hash__(self) -> int: return id(self.node) @property def clz(self) -> str: """Returns the class of node.""" return self.attributes['class'].value @property def text(self) -> str: """Gets text of node. Returns: The text of node. """ return self.attributes['text'].value @property def content_desc(self) -> str: """Gets content description of node. Returns: The content description of node. """ return self.attributes['content-desc'].value @property def resource_id(self) -> str: """Gets resource id of node. Returns: The resource id of node. """ return self.attributes['resource-id'].value @property def attributes(self) -> Dict[str, Any]: """Gets attributes of node. Returns: The attributes of node. """ if hasattr(self.node, 'attributes'): return collections.defaultdict( lambda: None, getattr(self.node, 'attributes')) else: return collections.defaultdict(lambda: None) @property def child_nodes(self) -> List[UINode]: """Gets child node(s) of current node. Returns: The child nodes of current node if any. """ return [UINode(n) for n in self.node.childNodes] def match_attrs_by_kwargs(self, **kwargs) -> bool: """Matches given attribute key/value pair with current node. Args: **kwargs: Key/value pair as attribute key/value. e.g.: resource_id='abc' Returns: True iff the given attributes match current node. """ if 'clz' in kwargs: kwargs['class'] = kwargs['clz'] del kwargs['clz'] return self.match_attrs(kwargs) def match_attrs(self, attrs: Dict[str, Any]) -> bool: """Matches given attributes with current node. This method is used to compare the given `attrs` with attributes of current node. Only the keys given in `attrs` will be compared. e.g.: ``` # ui_node has attributes {'name': 'john', 'id': '1234'} >>> ui_node.match_attrs({'name': 'john'}) True >>> ui_node.match_attrs({'name': 'ken'}) False ``` If you don't want exact match and want to check if an attribute value contain specific substring, you can leverage special prefix `PREFIX_SEARCH_IN` to tell this method to use `in` instead of `==` for comparison. e.g.: ``` # ui_node has attributes {'name': 'john', 'id': '1234'} >>> ui_node.match_attrs({'name': ui_node.PREFIX_SEARCH_IN + 'oh'}) True >>> ui_node.match_attrs({'name': 'oh'}) False ``` Args: attrs: Attributes to compare with. Returns: True iff the given attributes match current node. """ for k, v in attrs.items(): if k not in self.attributes: return False if v and v.startswith(self.PREFIX_SEARCH_IN): v = v[len(self.PREFIX_SEARCH_IN):] if not v or v not in self.attributes[k].value: return False elif v != self.attributes[k].value: return False return True def __str__(self) -> str: """The string representation of this object. Returns: The string representation including below information: - resource id - class - text - content description. """ rid = self.resource_id.strip() clz = self.clz.strip() txt = self.text.strip() ctx = self.content_desc.strip() return f"RID='{rid}'/CLASS='{clz}'/TEXT='{txt}'/CD='{ctx}'"