Kea2-python 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Kea2-python might be problematic. Click here for more details.

@@ -356,48 +356,7 @@
356
356
 
357
357
  <!-- Key Statistics -->
358
358
  <div class="row g-4 mb-4">
359
- <div class="col-md-6">
360
- <div class="stats-card">
361
- <div class="card-header bg-primary text-white">
362
- <i class="bi bi-stopwatch"></i> Time Statistics
363
- </div>
364
- <div class="card-body">
365
- <div class="mb-3">
366
- <h5 class="d-flex justify-content-between">
367
- <span>First Bug Discovery Time:</span>
368
- <span class="value-danger">{{ (first_bug_time / 60)|int }} min: {{ (first_bug_time % 60)|int }} sec</span>
369
- </h5>
370
- <div class="progress">
371
- <div class="progress-bar bg-danger" role="progressbar"
372
- style="width: calc({{ first_bug_time }} / {{ total_testing_time }} * 100%);"
373
- aria-valuenow="{{ first_bug_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
374
- </div>
375
- </div>
376
- <div class="mb-3">
377
- <h5 class="d-flex justify-content-between">
378
- <span>First Precondition Satisfaction Time:</span>
379
- <span class="value-success">{{ (first_precondition_time / 60)|int }} min: {{ (first_precondition_time % 60)|int }} sec</span>
380
- </h5>
381
- <div class="progress">
382
- <div class="progress-bar bg-success" role="progressbar"
383
- style="width: calc({{ first_precondition_time }} / {{ total_testing_time }} * 100%);"
384
- aria-valuenow="{{ first_precondition_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
385
- </div>
386
- </div>
387
- <div>
388
- <h5 class="d-flex justify-content-between">
389
- <span>Total Testing Time:</span>
390
- <span class="value-primary">{{ (total_testing_time / 60)|int }} min: {{ (total_testing_time % 60)|int }} sec</span>
391
- </h5>
392
- <div class="progress">
393
- <div class="progress-bar bg-primary" role="progressbar" style="width: 100%;"
394
- aria-valuenow="{{ total_testing_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
395
- </div>
396
- </div>
397
- </div>
398
- </div>
399
- </div>
400
- <div class="col-md-6">
359
+ <div class="col-12">
401
360
  <div class="stats-card">
402
361
  <div class="card-header bg-success text-white">
403
362
  <i class="bi bi-bar-chart"></i> Coverage Statistics
@@ -437,7 +396,7 @@
437
396
  <!-- Tested Activities Panel -->
438
397
  <div class="col-md-6">
439
398
  <div class="card">
440
- <div class="card-header bg-success text-white">
399
+ <div class="card-header bg-warning text-white">
441
400
  <div class="d-flex justify-content-between align-items-center">
442
401
  <span><i class="bi bi-check-circle"></i> Tested Activities ({{ tested_activities|length }})</span>
443
402
  <span class="badge bg-light text-dark">{{ tested_activities|length }} / {{ total_activities_count }}</span>
@@ -605,14 +564,14 @@
605
564
  </tr>
606
565
  </thead>
607
566
  <tbody id="property-stats-container">
608
- {% for stat in property_stats %}
567
+ {% for property_name, test_result in property_stats.items() %}
609
568
  <tr class="property-stat-row" data-page="1">
610
- <td>{{ stat.index }}</td>
611
- <td><span class="badge bg-light text-dark badge-custom">{{ stat.property_name }}</span></td>
612
- <td>{{ stat.precond_satisfied }}</td>
613
- <td>{{ stat.precond_checked }}</td>
614
- <td><span class="badge bg-danger text-white">{{ stat.postcond_violated }}</span></td>
615
- <td><span class="badge bg-warning text-dark">{{ stat.error }}</span></td>
569
+ <td>{{ loop.index }}</td>
570
+ <td><span class="badge bg-light text-dark badge-custom">{{ property_name }}</span></td>
571
+ <td>{{ test_result.precond_satisfied|default(0) }}</td>
572
+ <td>{{ test_result.executed|default(0) }}</td>
573
+ <td><span class="badge bg-danger text-white">{{ test_result.fail|default(0) }}</span></td>
574
+ <td><span class="badge bg-warning text-dark">{{ test_result.error|default(0) }}</span></td>
616
575
  </tr>
617
576
  {% endfor %}
618
577
  </tbody>
kea2/u2Driver.py CHANGED
@@ -1,10 +1,13 @@
1
1
  import random
2
2
  import socket
3
+ from time import sleep
3
4
  import uiautomator2 as u2
5
+ import adbutils
4
6
  import types
5
7
  import rtree
6
8
  import re
7
- from typing import Dict, List, Union
9
+ from typing import Any, Dict, List, Union
10
+ from http.client import HTTPResponse
8
11
  from lxml import etree
9
12
  from .absDriver import AbstractScriptDriver, AbstractStaticChecker, AbstractDriver
10
13
  from .adbUtils import list_forwards, remove_forward, create_forward
@@ -16,7 +19,7 @@ import logging
16
19
  logging.getLogger("urllib3").setLevel(logging.INFO)
17
20
  logging.getLogger("uiautomator2").setLevel(logging.INFO)
18
21
 
19
- logger = getLogger(__name__)
22
+ logger = getLogger(__name__)
20
23
 
21
24
  """
22
25
  The definition of U2ScriptDriver
@@ -35,34 +38,24 @@ class U2ScriptDriver(AbstractScriptDriver):
35
38
  """
36
39
 
37
40
  deviceSerial: str = None
41
+ transportId: str = None
38
42
  d = None
39
43
 
44
+ @classmethod
45
+ def setTransportId(cls, transportId):
46
+ cls.transportId = transportId
47
+
40
48
  @classmethod
41
49
  def setDeviceSerial(cls, deviceSerial):
42
50
  cls.deviceSerial = deviceSerial
43
51
 
44
52
  def getInstance(self):
45
53
  if self.d is None:
46
- self.d = (
47
- u2.connect() if self.deviceSerial is None
48
- else u2.connect(self.deviceSerial)
49
- )
50
-
51
- def get_u2_forward_port() -> int:
52
- """rewrite forward_port mothod to avoid the relocation of port
53
- :return: the new forward port
54
- """
55
- print("Rewriting forward_port method", flush=True)
56
- self.d._dev.forward_port = types.MethodType(
57
- forward_port, self.d._dev)
58
- lport = self.d._dev.forward_port(8090)
59
- setattr(self.d._dev, "msg", "meta")
60
- print(f"[U2] local port: {lport}", flush=True)
61
- return lport
62
-
63
- self._remove_remote_port(8090)
64
- self.d.lport = get_u2_forward_port()
65
- self._remove_remote_port(9008)
54
+ adb = adbutils.device(serial=self.deviceSerial, transport_id=self.transportId)
55
+ print("[INFO] Connecting to uiautomator2. Please wait ...")
56
+ self.d = u2.connect(adb)
57
+ sleep(5)
58
+ self.d._device_server_port = 8090
66
59
 
67
60
  return self.d
68
61
 
@@ -76,10 +69,11 @@ class U2ScriptDriver(AbstractScriptDriver):
76
69
  remove_forward(local_spec=forward_local, device=self.deviceSerial)
77
70
 
78
71
  def tearDown(self):
79
- logger.debug("U2Driver tearDown: stop_uiautomator")
80
- self.d.stop_uiautomator()
81
- logger.debug("U2Driver tearDown: remove forward")
82
- self._remove_remote_port(8090)
72
+ # logger.debug("U2Driver tearDown: stop_uiautomator")
73
+ # self.d.stop_uiautomator()
74
+ # logger.debug("U2Driver tearDown: remove forward")
75
+ # self._remove_remote_port(8090)
76
+ pass
83
77
 
84
78
  """
85
79
  The definition of U2StaticChecker
@@ -100,39 +94,105 @@ class StaticU2UiObject(u2.UiObject):
100
94
  return filterDict[originKey]
101
95
  return originKey
102
96
 
103
- def _getXPath(self, kwargs: Dict[str, str]):
97
+ def selector_to_xpath(self, selector: u2.Selector, is_initial: bool = True) -> str:
98
+ """
99
+ Convert a u2 Selector into an XPath expression compatible with Java Android UI controls.
104
100
 
105
- def filter_selectors(kwargs: Dict[str, str]):
106
- """
107
- filter the selector
108
- """
109
- new_kwargs = dict()
110
- SPECIAL_KEY = {"mask", "childOrSibling", "childOrSiblingSelector"}
111
- for key, val in kwargs.items():
112
- if key in SPECIAL_KEY:
113
- continue
114
- key = self._transferU2Keys(key)
115
- new_kwargs[key] = val
116
- return new_kwargs
101
+ Args:
102
+ selector (u2.Selector): A u2 Selector object
103
+ is_initial (bool): Whether it is the initial node, defaults to True
117
104
 
118
- kwargs = filter_selectors(kwargs)
105
+ Returns:
106
+ str: The corresponding XPath expression
107
+ """
108
+ try:
119
109
 
120
- attrLocs = [
121
- f"[@{k}='{v}']" for k, v in kwargs.items()
122
- ]
123
- xpath = f".//node{''.join(attrLocs)}"
124
- return xpath
110
+ xpath = ".//node" if is_initial else "node"
111
+
112
+ conditions = []
113
+
114
+ if "className" in selector:
115
+ conditions.insert(0, f"[@class='{selector['className']}']")
116
+
117
+ if "text" in selector:
118
+ conditions.append(f"[@text='{selector['text']}']")
119
+ elif "textContains" in selector:
120
+ conditions.append(f"[contains(@text, '{selector['textContains']}')]")
121
+ elif "textStartsWith" in selector:
122
+ conditions.append(f"[starts-with(@text, '{selector['textStartsWith']}')]")
123
+ elif "textMatches" in selector:
124
+ raise NotImplementedError("'textMatches' syntax is not supported")
125
+
126
+ if "description" in selector:
127
+ conditions.append(f"[@content-desc='{selector['description']}']")
128
+ elif "descriptionContains" in selector:
129
+ conditions.append(f"[contains(@content-desc, '{selector['descriptionContains']}')]")
130
+ elif "descriptionStartsWith" in selector:
131
+ conditions.append(f"[starts-with(@content-desc, '{selector['descriptionStartsWith']}')]")
132
+ elif "descriptionMatches" in selector:
133
+ raise NotImplementedError("'descriptionMatches' syntax is not supported")
134
+
135
+ if "packageName" in selector:
136
+ conditions.append(f"[@package='{selector['packageName']}']")
137
+ elif "packageNameMatches" in selector:
138
+ raise NotImplementedError("'packageNameMatches' syntax is not supported")
139
+
140
+ if "resourceId" in selector:
141
+ conditions.append(f"[@resource-id='{selector['resourceId']}']")
142
+ elif "resourceIdMatches" in selector:
143
+ raise NotImplementedError("'resourceIdMatches' syntax is not supported")
144
+
145
+ bool_props = ["checkable", "checked", "clickable", "longClickable", "scrollable", "enabled", "focusable",
146
+ "focused", "selected", "covered"]
147
+
148
+ def str_to_bool(value):
149
+ """Convert string 'true'/'false' to boolean, or return original value if already boolean"""
150
+ if isinstance(value, str):
151
+ return value.lower() == "true"
152
+ return bool(value)
153
+
154
+ for prop in bool_props:
155
+ if prop in selector:
156
+ bool_value = str_to_bool(selector[prop])
157
+ value = "true" if bool_value else "false"
158
+ conditions.append(f"[@{prop}='{value}']")
159
+
160
+ if "index" in selector:
161
+ conditions.append(f"[@index='{selector['index']}']")
162
+
163
+ xpath += "".join(conditions)
164
+
165
+ if "childOrSibling" in selector and selector["childOrSibling"]:
166
+ for i, relation in enumerate(selector["childOrSibling"]):
167
+ sub_selector = selector["childOrSiblingSelector"][i]
168
+ sub_xpath = self.selector_to_xpath(sub_selector, False)
169
+
170
+ if relation == "child":
171
+ xpath += f"//{sub_xpath}"
172
+ elif relation == "sibling":
173
+ cur_root = xpath
174
+ following_sibling = cur_root + f"/following-sibling::{sub_xpath}"
175
+ preceding_sibling = cur_root + f"/preceding-sibling::{sub_xpath}"
176
+ xpath = f"({following_sibling} | {preceding_sibling})"
177
+ if "instance" in selector:
178
+ xpath = f"({xpath})[{selector['instance'] + 1}]"
179
+
180
+ return xpath
181
+
182
+ except Exception as e:
183
+ print(f"Error occurred during selector conversion: {e}")
184
+ return "//error"
125
185
 
126
186
 
127
187
  @property
128
188
  def exists(self):
129
- dict.update(self.selector, {"covered": "false"})
130
- xpath = self._getXPath(self.selector)
189
+ set_covered_to_deepest_node(self.selector)
190
+ xpath = self.selector_to_xpath(self.selector)
131
191
  matched_widgets = self.session.xml.xpath(xpath)
132
192
  return bool(matched_widgets)
133
193
 
134
194
  def __len__(self):
135
- xpath = self._getXPath(self.selector)
195
+ xpath = self.selector_to_xpath(self.selector)
136
196
  matched_widgets = self.session.xml.xpath(xpath)
137
197
  return len(matched_widgets)
138
198
 
@@ -142,6 +202,9 @@ class StaticU2UiObject(u2.UiObject):
142
202
  def sibling(self, **kwargs):
143
203
  return StaticU2UiObject(self.session, self.selector.clone().sibling(**kwargs))
144
204
 
205
+ def __getattr__(self, attr):
206
+ return getattr(super(), attr)
207
+
145
208
 
146
209
  def _get_bounds(raw_bounds):
147
210
  pattern = re.compile(r"\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]")
@@ -227,7 +290,9 @@ class U2StaticDevice(u2.Device):
227
290
  self._script_driver = script_driver
228
291
 
229
292
  def __call__(self, **kwargs):
230
- return StaticU2UiObject(session=self, selector=u2.Selector(**kwargs))
293
+ ui = StaticU2UiObject(session=self, selector=u2.Selector(**kwargs))
294
+ ui.jsonrpc = self._script_driver.jsonrpc
295
+ return ui
231
296
 
232
297
  @property
233
298
  def xpath(self) -> u2.xpath.XPathEntry:
@@ -274,7 +339,12 @@ class U2StaticChecker(AbstractStaticChecker):
274
339
  def setHierarchy(self, hierarchy: str):
275
340
  if hierarchy is None:
276
341
  return
277
- self.d.xml = etree.fromstring(hierarchy.encode("utf-8"))
342
+ if isinstance(hierarchy, str):
343
+ self.d.xml = etree.fromstring(hierarchy.encode("utf-8"))
344
+ elif isinstance(hierarchy, etree._Element):
345
+ self.d.xml = hierarchy
346
+ elif isinstance(hierarchy, etree._ElementTree):
347
+ self.d.xml = hierarchy.getroot()
278
348
  _HindenWidgetFilter(self.d.xml)
279
349
 
280
350
  def getInstance(self, hierarchy: str=None):
@@ -290,8 +360,11 @@ class U2Driver(AbstractDriver):
290
360
  staticChecker = None
291
361
 
292
362
  @classmethod
293
- def setDeviceSerial(cls, deviceSerial):
294
- U2ScriptDriver.setDeviceSerial(deviceSerial)
363
+ def setDevice(cls, kwarg):
364
+ if kwarg.get("serial"):
365
+ U2ScriptDriver.setDeviceSerial(kwarg["serial"])
366
+ if kwarg.get("transport_id"):
367
+ U2ScriptDriver.setTransportId(kwarg["transport_id"])
295
368
 
296
369
  @classmethod
297
370
  def getScriptDriver(self):
@@ -324,96 +397,12 @@ def forward_port(self, remote: Union[int, str]) -> int:
324
397
  and f.remote == remote
325
398
  and f.local.startswith("tcp:")
326
399
  ): # yapf: disable
327
- return int(f.local[len("tcp:") :])
400
+ return int(f.local[len("tcp:"):])
328
401
  local_port = get_free_port()
329
402
  self.forward("tcp:" + str(local_port), remote)
330
403
  logger.debug(f"forwading port: tcp:{local_port} -> {remote}")
331
404
  return local_port
332
405
 
333
-
334
- def selector_to_xpath(selector: u2.Selector, is_initial: bool = True) -> str:
335
- """
336
- Convert a u2 Selector into an XPath expression compatible with Java Android UI controls.
337
-
338
- Args:
339
- selector (u2.Selector): A u2 Selector object
340
- is_initial (bool): Whether it is the initial node, defaults to True
341
-
342
- Returns:
343
- str: The corresponding XPath expression
344
- """
345
- try:
346
- if is_initial:
347
- xpath = ".//node"
348
- else:
349
- xpath = "node"
350
-
351
- conditions = []
352
-
353
- if "className" in selector:
354
- conditions.insert(0, f"[@class='{selector['className']}']") # 将 className 条件放在前面
355
-
356
- if "text" in selector:
357
- conditions.append(f"[@text='{selector['text']}']")
358
- elif "textContains" in selector:
359
- conditions.append(f"[contains(@text, '{selector['textContains']}')]")
360
- elif "textMatches" in selector:
361
- conditions.append(f"[re:match(@text, '{selector['textMatches']}')]")
362
- elif "textStartsWith" in selector:
363
- conditions.append(f"[starts-with(@text, '{selector['textStartsWith']}')]")
364
-
365
- if "description" in selector:
366
- conditions.append(f"[@content-desc='{selector['description']}']")
367
- elif "descriptionContains" in selector:
368
- conditions.append(f"[contains(@content-desc, '{selector['descriptionContains']}')]")
369
- elif "descriptionMatches" in selector:
370
- conditions.append(f"[re:match(@content-desc, '{selector['descriptionMatches']}')]")
371
- elif "descriptionStartsWith" in selector:
372
- conditions.append(f"[starts-with(@content-desc, '{selector['descriptionStartsWith']}')]")
373
-
374
- if "packageName" in selector:
375
- conditions.append(f"[@package='{selector['packageName']}']")
376
- elif "packageNameMatches" in selector:
377
- conditions.append(f"[re:match(@package, '{selector['packageNameMatches']}')]")
378
-
379
- if "resourceId" in selector:
380
- conditions.append(f"[@resource-id='{selector['resourceId']}']")
381
- elif "resourceIdMatches" in selector:
382
- conditions.append(f"[re:match(@resource-id, '{selector['resourceIdMatches']}')]")
383
-
384
- bool_props = [
385
- "checkable", "checked", "clickable", "longClickable", "scrollable",
386
- "enabled", "focusable", "focused", "selected", "covered"
387
- ]
388
- for prop in bool_props:
389
- if prop in selector:
390
- value = "true" if selector[prop] else "false"
391
- conditions.append(f"[@{prop}='{value}']")
392
-
393
- if "index" in selector:
394
- conditions.append(f"[@index='{selector['index']}']")
395
- elif "instance" in selector:
396
- conditions.append(f"[@instance='{selector['instance']}']")
397
-
398
- xpath += "".join(conditions)
399
-
400
- if "childOrSibling" in selector and selector["childOrSibling"]:
401
- for i, relation in enumerate(selector["childOrSibling"]):
402
- sub_selector = selector["childOrSiblingSelector"][i]
403
- sub_xpath = selector_to_xpath(sub_selector, False) # 递归处理子选择器
404
-
405
- if relation == "child":
406
- xpath += f"/{sub_xpath}"
407
- elif relation == "sibling":
408
- xpath_initial = xpath
409
- xpath = '(' + xpath_initial + f"/following-sibling::{sub_xpath} | " + xpath_initial + f"/preceding-sibling::{sub_xpath})"
410
-
411
- return xpath
412
-
413
- except Exception as e:
414
- print(f"Error occurred during selector conversion: {e}")
415
- return "//error"
416
-
417
406
  def is_port_in_use(port: int) -> bool:
418
407
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
419
408
  return s.connect_ex(('127.0.0.1', port)) == 0
@@ -435,3 +424,25 @@ def get_free_port():
435
424
  if not is_port_in_use(port):
436
425
  return port
437
426
  raise RuntimeError("No free port found")
427
+
428
+ def set_covered_to_deepest_node(selector: u2.Selector):
429
+
430
+ def find_deepest_nodes(node):
431
+ deepest_node = None
432
+ is_leaf = True
433
+ if "childOrSibling" in node and node["childOrSibling"]:
434
+ for i, relation in enumerate(node["childOrSibling"]):
435
+ sub_selector = node["childOrSiblingSelector"][i]
436
+ deepest_node = find_deepest_nodes(sub_selector)
437
+ is_leaf = False
438
+
439
+ if is_leaf:
440
+ deepest_node = node
441
+ return deepest_node
442
+
443
+ deepest_node = find_deepest_nodes(selector)
444
+
445
+ if deepest_node is not None:
446
+ dict.update(deepest_node, {"covered": False})
447
+
448
+
kea2/utils.py CHANGED
@@ -13,6 +13,7 @@ def getLogger(name: str) -> logging.Logger:
13
13
  if not logger.handlers:
14
14
  # Configure handler
15
15
  handler = logging.StreamHandler()
16
+ handler.flush = lambda: handler.stream.flush() # 确保每次都flush
16
17
  formatter = logging.Formatter('[%(levelname)1s][%(asctime)s %(module)s:%(lineno)d pid:%(process)d] %(message)s')
17
18
  handler.setFormatter(formatter)
18
19
  logger.addHandler(handler)
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: A python library for supporting and customizing automated UI testing for mobile apps
5
5
  Author-email: Xixian Liang <xixian@stu.ecnu.edu.cn>
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: rtree>=1.3.0
10
- Requires-Dist: uiautomator2>=3.2.9
11
10
  Requires-Dist: jinja2>=3.0.0
11
+ Requires-Dist: uiautomator2>=3.3.3
12
+ Requires-Dist: adbutils>=2.9.3
12
13
  Dynamic: license-file
13
14
 
14
15
 
@@ -17,25 +18,41 @@ Dynamic: license-file
17
18
  [![PyPI Downloads](https://static.pepy.tech/badge/kea2-python)](https://pepy.tech/projects/kea2-python)
18
19
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
19
20
 
21
+
20
22
  <div>
21
- <img src="https://github.com/user-attachments/assets/1a64635b-a8f2-40f1-8f16-55e47b1d74e7" style="border-radius: 14px; width: 20%; height: 20%;"/>
23
+ <img src="https://github.com/user-attachments/assets/aa5839fc-4542-46f6-918b-c9f891356c84" style="border-radius: 14px; width: 20%; height: 20%;"/>
22
24
  </div>
23
25
 
24
- ## About
26
+ ### Github repo link
27
+ [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
28
+
29
+ ### [点击此处:查看中文文档](README_cn.md)
30
+
31
+ ## About
25
32
 
26
- Kea2 is an easy-to-use Python library for supporting, customizing and improving automated UI testing for mobile apps. Kea2's novelty is able to fuse the scripts (usually written by human) with automated UI testing tools, thus allowing many interesting and powerful features.
33
+ Kea2 is an easy-to-use tool for fuzzing mobile apps. Its key *novelty* is able to fuse automated UI testing with scripts (usually written by human), thus empowering automated UI testing with human intelligence for effectively finding *crashing bugs* as well as *non-crashing functional (logic) bugs*.
27
34
 
28
- Kea2 is currently built on top of [Fastbot](https://github.com/bytedance/Fastbot_Android) and [uiautomator2](https://github.com/openatx/uiautomator2) and targets [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) apps.
35
+ Kea2 is currently built on top of [Fastbot](https://github.com/bytedance/Fastbot_Android), *an industrial-strength automated UI testing tool*, and [uiautomator2](https://github.com/openatx/uiautomator2), *an easy-to-use and stable Android automation library*.
36
+ Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) apps.
37
+
38
+ ## Novelty & Important features
39
+
40
+ <div align="center">
41
+ <div style="max-width:80%; max-height:80%">
42
+ <img src="docs/intro.png" style="border-radius: 14px; width: 80%; height: 80%;"/>
43
+ </div>
44
+ </div>
29
45
 
30
- ## Important features
31
46
  - **Feature 1**(查找稳定性问题): coming with the full capability of [Fastbot](https://github.com/bytedance/Fastbot_Android) for stress testing and finding *stability problems* (i.e., *crashing bugs*);
32
47
 
33
48
  - **Feature 2**(自定义测试场景\事件序列\黑白名单\黑白控件[^1]): customizing testing scenarios when running Fastbot (e.g., testing specific app functionalities, executing specific event traces, entering specifc UI pages, reaching specific app states, blacklisting specific activities/UI widgets/UI regions) with the full capability and flexibility powered by *python* language and [uiautomator2](https://github.com/openatx/uiautomator2);
34
49
 
35
- - **Feature 3**(支持断言机制[^2]): supporting auto-assertions when running Fastbot, based on the idea of [property-based testing](https://en.wikipedia.org/wiki/Software_testing#Property_testing) inheritted from [Kea](https://github.com/ecnusse/Kea), for finding *logic bugs* (i.e., *non-crashing bugs*)
50
+ - **Feature 3**(支持断言机制[^2]): supporting auto-assertions when running Fastbot, based on the idea of [property-based testing](https://en.wikipedia.org/wiki/Software_testing#Property_testing) inheritted from [Kea](https://github.com/ecnusse/Kea), for finding *logic bugs* (i.e., *non-crashing functional bugs*).
36
51
 
52
+ For **Feature 2 and 3**, Kea2 allows you to focus on what app functionalities to be tested. You do not need to worry about how to reach these app functionalities. Just let Fastbot help. As a result, your scripts are usually short, robust and easy to maintain, and the corresponding app functionalities are much more stress-tested!
37
53
 
38
54
  **The ability of the three features in Kea2**
55
+
39
56
  | | **Feature 1** | **Feature 2** | **Feature 3** |
40
57
  | --- | --- | --- | ---- |
41
58
  | **Finding crashes** | :+1: | :+1: | :+1: |
@@ -43,24 +60,17 @@ Kea2 is currently built on top of [Fastbot](https://github.com/bytedance/Fastbot
43
60
  | **Finding non-crashing functional (logic) bugs** | | | :+1: |
44
61
 
45
62
 
46
- <div align="center">
47
- <div style="max-width:80%; max-height:80%">
48
- <img src="docs/intro.png" style="border-radius: 14px; width: 80%; height: 80%;"/>
49
- </div>
50
- </div>
51
-
52
-
53
63
 
54
64
  ## Design & Roadmap
55
- Kea2, released as a Python library, currently works with:
56
- - [unittest](https://docs.python.org/3/library/unittest.html) as the testing framework;
65
+ Kea2 currently works with:
66
+ - [unittest](https://docs.python.org/3/library/unittest.html) as the testing framework to manage the scripts;
57
67
  - [uiautomator2](https://github.com/openatx/uiautomator2) as the UI test driver;
58
68
  - [Fastbot](https://github.com/bytedance/Fastbot_Android) as the backend automated UI testing tool.
59
69
 
60
70
  In the future, Kea2 will be extended to support
61
- - [pytest](https://docs.pytest.org/en/stable/)
62
- - [Appium](https://github.com/appium/appium), [Hypium](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hypium-python-guidelines) (for HarmonyOS/Open Harmony)
63
- - other automated UI testing tools (not limited to Fastbot)
71
+ - [pytest](https://docs.pytest.org/en/stable/), another popular python testing framework;
72
+ - [Appium](https://github.com/appium/appium), [Hypium](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hypium-python-guidelines) (for HarmonyOS/Open Harmony);
73
+ - any other automated UI testing tools (not limited to Fastbot)
64
74
 
65
75
 
66
76
  ## Installation
@@ -114,7 +124,7 @@ Test your app with the full capability of Fastbot for stress testing and finding
114
124
  kea2 run -s "emulator-5554" -p it.feio.android.omninotes.alpha --agent native --running-minutes 10 --throttle 200
115
125
  ```
116
126
 
117
- 理解上述选项含义请查看[文档](docs/manual_en.md#launching-kea2)
127
+ To understand the meanings of the options, you can see our [manual](docs/manual_en.md#launching-kea2).
118
128
 
119
129
  > The usage is similar to the the original Fastbot's [shell commands](https://github.com/bytedance/Fastbot_Android?tab=readme-ov-file#run-fastbot-with-shell-command).
120
130
 
@@ -177,13 +187,14 @@ In Feature 3, a script is composed of three elements:
177
187
 
178
188
  In a social media app, message sending is a common feature. On the message sending page, the `send` button should always appears when the input box is not empty (i.e., has some message).
179
189
 
180
- <div align="center" >
181
- <div >
182
- <img src="docs/socialAppBug.png" style="border-radius: 14px; width:30%; height:40%;"/>
183
- </div>
184
- <p>The expected behavior (the upper figure) and the buggy behavior (the lower figure).
185
- <p/>
190
+ <div align="center">
191
+ <img src="docs/socialAppBug.png" style="border-radius: 14px; width:30%; height:40%;"/>
192
+ </div>
193
+
194
+ <div align="center">
195
+ The expected behavior (the upper figure) and the buggy behavior (the lower figure).
186
196
  </div>
197
+
187
198
 
188
199
  For the preceding always-holding property, we can write the following script to validate the functional correctness: when there is an `input_box` widget on the message sending page, we can type any non-empty string text into the input box and assert `send_button` should always exists.
189
200
 
@@ -208,12 +219,12 @@ You can run this example by using the similar command line in Feature 2.
208
219
 
209
220
  ## Documentations(更多文档)
210
221
 
211
- [更多文档](docs/manual_en.md),包括了:
212
- - Kea2的案例教程(基于微信介绍)、
213
- - Kea2脚本的定义方法,支持的脚本装饰器(如`@precondition`、`@prob`、`@max_tries`)
214
- - Kea2的启动方式、命令行选项
215
- - 查看/理解Kea2的运行结果(如界面截图、测试覆盖率、脚本执行成功与否)。
216
- - [如何黑白控件/区域](docs/blacklisting.md)
222
+ You can find the [user manual](docs/manual_en.md), which includes:
223
+ - Examples of using Kea2 on WeChat (in Chinese);
224
+ - How to define Kea2's scripts and use the decorators (e.g., `@precondition`、`@prob`、`@max_tries`);
225
+ - How to run Kea2 and Kea2's command line options
226
+ - How to find and understand Kea2's testing results
227
+ - How to [whitelist or blacklist](docs/blacklisting.md) specific activities, UI widgets and UI regions during fuzzing
217
228
 
218
229
  ## Open-source projects used by Kea2
219
230