172 lines
4.3 KiB
Python
172 lines
4.3 KiB
Python
"""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}'"
|