Kea2-python 1.0.4__tar.gz → 1.0.5__tar.gz

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.
Files changed (55) hide show
  1. {kea2_python-1.0.4 → kea2_python-1.0.5}/Kea2_python.egg-info/PKG-INFO +4 -3
  2. {kea2_python-1.0.4 → kea2_python-1.0.5}/PKG-INFO +4 -3
  3. {kea2_python-1.0.4 → kea2_python-1.0.5}/README.md +3 -2
  4. kea2_python-1.0.5/kea2/assets/monkeyq.jar +0 -0
  5. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/fastbotManager.py +8 -0
  6. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/keaUtils.py +21 -6
  7. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/kea_launcher.py +12 -0
  8. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/report/bug_report_generator.py +33 -5
  9. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/report/mixin.py +23 -9
  10. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/report/templates/bug_report_template.html +208 -12
  11. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/resultSyncer.py +9 -13
  12. {kea2_python-1.0.4 → kea2_python-1.0.5}/pyproject.toml +1 -1
  13. kea2_python-1.0.5/tests/test_xpath.py +67 -0
  14. kea2_python-1.0.4/kea2/assets/monkeyq.jar +0 -0
  15. kea2_python-1.0.4/tests/test_xpath.py +0 -36
  16. {kea2_python-1.0.4 → kea2_python-1.0.5}/Kea2_python.egg-info/SOURCES.txt +0 -0
  17. {kea2_python-1.0.4 → kea2_python-1.0.5}/Kea2_python.egg-info/dependency_links.txt +0 -0
  18. {kea2_python-1.0.4 → kea2_python-1.0.5}/Kea2_python.egg-info/entry_points.txt +0 -0
  19. {kea2_python-1.0.4 → kea2_python-1.0.5}/Kea2_python.egg-info/requires.txt +0 -0
  20. {kea2_python-1.0.4 → kea2_python-1.0.5}/Kea2_python.egg-info/top_level.txt +0 -0
  21. {kea2_python-1.0.4 → kea2_python-1.0.5}/LICENSE +0 -0
  22. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/__init__.py +0 -0
  23. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/absDriver.py +0 -0
  24. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/adbUtils.py +0 -0
  25. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/config_version.json +0 -0
  26. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot-thirdpart.jar +0 -0
  27. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/abl.strings +0 -0
  28. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/awl.strings +0 -0
  29. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/max.config +0 -0
  30. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/max.fuzzing.strings +0 -0
  31. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/max.schema.strings +0 -0
  32. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/max.strings +0 -0
  33. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/max.tree.pruning +0 -0
  34. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/teardown.py +0 -0
  35. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_configs/widget.block.py +0 -0
  36. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  37. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  38. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
  39. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  40. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/framework.jar +0 -0
  41. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/kea2-thirdpart.jar +0 -0
  42. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/assets/quicktest.py +0 -0
  43. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/cli.py +0 -0
  44. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/kea2_api.py +0 -0
  45. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/logWatcher.py +0 -0
  46. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/mixin.py +0 -0
  47. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/report/__init__.py +0 -0
  48. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/report/report_merger.py +0 -0
  49. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/report/templates/merged_bug_report_template.html +0 -0
  50. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/report/utils.py +0 -0
  51. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/u2Driver.py +0 -0
  52. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/utils.py +0 -0
  53. {kea2_python-1.0.4 → kea2_python-1.0.5}/kea2/version_manager.py +0 -0
  54. {kea2_python-1.0.4 → kea2_python-1.0.5}/setup.cfg +0 -0
  55. {kea2_python-1.0.4 → kea2_python-1.0.5}/tests/test_u2Selector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 1.0.4
3
+ Version: 1.0.5
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
@@ -21,9 +21,10 @@ Dynamic: license-file
21
21
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ecnusse/Kea2)
22
22
 
23
23
  <div>
24
- <img src="https://github.com/user-attachments/assets/84e47b87-2dd2-4d7e-91d1-e8c1d1db0cf4" style="border-radius: 14px; width: 20%; height: 20%;"/>
24
+ <img src="https://github.com/user-attachments/assets/8d9f8750-1e10-411b-a49f-7d8367bbe9fe" style="border-radius: 14px; width: 20%; height: 20%;"/>
25
25
  </div>
26
26
 
27
+
27
28
  Please contact Xixian Liang at [xixian@stu.ecnu.edu.cn](xixian@stu.ecnu.edu.cn) with your Wechat ID / QR code to be invited to the WeChat discussion group. Of course, we are also ready on GitHub to answer your questions/feedback.
28
29
 
29
30
  #### Github repo [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
@@ -77,7 +78,7 @@ Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operatin
77
78
 
78
79
  Kea2 (and its idea) has been used/integrated by
79
80
 
80
- - [OPay Business](https://play.google.com/store/apps/details?id=team.opay.pay.merchant.service) --- a financial & payment app. OPay uses Kea2 for regression testing on POS machines and mobile devices.
81
+ - [OPay Business](https://play.google.com/store/apps/details?id=team.opay.pay.merchant.service) --- a financial & payment app (2 millions of active users daily). OPay uses Kea2 for regression testing on POS machines and mobile devices.
81
82
 
82
83
  - [WeChat's iExplorer]() --- WeChat's in-house testing platform (coming with an interactive UI-based tool to ease writing scripts)
83
84
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 1.0.4
3
+ Version: 1.0.5
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
@@ -21,9 +21,10 @@ Dynamic: license-file
21
21
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ecnusse/Kea2)
22
22
 
23
23
  <div>
24
- <img src="https://github.com/user-attachments/assets/84e47b87-2dd2-4d7e-91d1-e8c1d1db0cf4" style="border-radius: 14px; width: 20%; height: 20%;"/>
24
+ <img src="https://github.com/user-attachments/assets/8d9f8750-1e10-411b-a49f-7d8367bbe9fe" style="border-radius: 14px; width: 20%; height: 20%;"/>
25
25
  </div>
26
26
 
27
+
27
28
  Please contact Xixian Liang at [xixian@stu.ecnu.edu.cn](xixian@stu.ecnu.edu.cn) with your Wechat ID / QR code to be invited to the WeChat discussion group. Of course, we are also ready on GitHub to answer your questions/feedback.
28
29
 
29
30
  #### Github repo [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
@@ -77,7 +78,7 @@ Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operatin
77
78
 
78
79
  Kea2 (and its idea) has been used/integrated by
79
80
 
80
- - [OPay Business](https://play.google.com/store/apps/details?id=team.opay.pay.merchant.service) --- a financial & payment app. OPay uses Kea2 for regression testing on POS machines and mobile devices.
81
+ - [OPay Business](https://play.google.com/store/apps/details?id=team.opay.pay.merchant.service) --- a financial & payment app (2 millions of active users daily). OPay uses Kea2 for regression testing on POS machines and mobile devices.
81
82
 
82
83
  - [WeChat's iExplorer]() --- WeChat's in-house testing platform (coming with an interactive UI-based tool to ease writing scripts)
83
84
 
@@ -6,9 +6,10 @@
6
6
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ecnusse/Kea2)
7
7
 
8
8
  <div>
9
- <img src="https://github.com/user-attachments/assets/84e47b87-2dd2-4d7e-91d1-e8c1d1db0cf4" style="border-radius: 14px; width: 20%; height: 20%;"/>
9
+ <img src="https://github.com/user-attachments/assets/8d9f8750-1e10-411b-a49f-7d8367bbe9fe" style="border-radius: 14px; width: 20%; height: 20%;"/>
10
10
  </div>
11
11
 
12
+
12
13
  Please contact Xixian Liang at [xixian@stu.ecnu.edu.cn](xixian@stu.ecnu.edu.cn) with your Wechat ID / QR code to be invited to the WeChat discussion group. Of course, we are also ready on GitHub to answer your questions/feedback.
13
14
 
14
15
  #### Github repo [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
@@ -62,7 +63,7 @@ Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operatin
62
63
 
63
64
  Kea2 (and its idea) has been used/integrated by
64
65
 
65
- - [OPay Business](https://play.google.com/store/apps/details?id=team.opay.pay.merchant.service) --- a financial & payment app. OPay uses Kea2 for regression testing on POS machines and mobile devices.
66
+ - [OPay Business](https://play.google.com/store/apps/details?id=team.opay.pay.merchant.service) --- a financial & payment app (2 millions of active users daily). OPay uses Kea2 for regression testing on POS machines and mobile devices.
66
67
 
67
68
  - [WeChat's iExplorer]() --- WeChat's in-house testing platform (coming with an interactive UI-based tool to ease writing scripts)
68
69
 
@@ -129,6 +129,14 @@ class FastbotManager:
129
129
  path="/dumpHierarchy",
130
130
  )
131
131
  return r.json()['result']
132
+
133
+ @retry(Exception, tries=2, delay=2)
134
+ def sendInfo(self, info: str):
135
+ r = self.request(
136
+ method="POST",
137
+ path="/sendInfo",
138
+ data=info
139
+ )
132
140
 
133
141
  @property
134
142
  def device_output_dir(self):
@@ -8,7 +8,7 @@ import os
8
8
  from collections import deque
9
9
  from copy import deepcopy
10
10
  from pathlib import Path
11
- from time import perf_counter
11
+ from time import perf_counter, sleep
12
12
  from typing import Callable, Any, Deque, Dict, List, Literal, NewType, Tuple, Union
13
13
  from contextvars import ContextVar
14
14
  from unittest import TextTestRunner, registerResult, TestSuite, TestCase, TextTestResult, defaultTestLoader, SkipTest
@@ -155,6 +155,8 @@ class Options:
155
155
  act_blacklist_file: str = None
156
156
  # propertytest sub-commands args (eg. discover -s xxx -p xxx)
157
157
  propertytest_args: List[str] = None
158
+ # period (N steps) to restart the app under test
159
+ restart_app_period: int = None
158
160
  # unittest sub-commands args (Feat 4)
159
161
  unittest_args: List[str] = None
160
162
  # Extra args (directly passed to fastbot)
@@ -468,21 +470,30 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter):
468
470
  start_time = perf_counter()
469
471
  fb_is_running = True
470
472
  self.stepsCount = 0
473
+
471
474
  while self.stepsCount < self.options.maxStep:
472
475
  if self.shouldStop(start_time):
473
476
  logger.info("Exploration time up (--running-minutes).")
474
477
  break
478
+
479
+ if self.options.restart_app_period and self.stepsCount and self.stepsCount % self.options.restart_app_period == 0:
480
+ self.stepsCount += 1
481
+ logger.info(f"Sending monkeyEvent {self._monkey_event_count}")
482
+ logger.info("Kill all test apps to restart the app under test.")
483
+ for app in self.options.packageNames:
484
+ logger.info(f"Stopping app: {app}")
485
+ self.scriptDriver.app_stop(app)
486
+ sleep(3)
487
+ fb.sendInfo("kill_apps")
488
+ continue
489
+
475
490
  try:
476
491
  if fb.executed_prop:
477
492
  fb.executed_prop = False
478
493
  xml_raw = fb.dumpHierarchy()
479
494
  else:
480
495
  self.stepsCount += 1
481
- logger.info("Sending monkeyEvent {}".format(
482
- f"({self.stepsCount} / {self.options.maxStep})" if self.options.maxStep != float("inf")
483
- else f"({self.stepsCount})"
484
- )
485
- )
496
+ logger.info(f"Sending monkeyEvent {self._monkey_event_count}")
486
497
  xml_raw = fb.stepMonkey(self._monkeyStepInfo)
487
498
  propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
488
499
  except u2.HTTPError:
@@ -559,6 +570,10 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter):
559
570
  r = self._get_block_widgets()
560
571
  r["steps_count"] = self.stepsCount
561
572
  return r
573
+
574
+ @property
575
+ def _monkey_event_count(self):
576
+ return f"({self.stepsCount} / {self.options.maxStep})" if self.options.maxStep != float("inf") else f"({self.stepsCount})"
562
577
 
563
578
  def _get_block_widgets(self):
564
579
  block_dict = self._getBlockedWidgets()
@@ -158,6 +158,15 @@ def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.Argument
158
158
  help="Activity BlackList File. The activities listed in the file will be avoided during testing.",
159
159
  )
160
160
 
161
+ parser.add_argument(
162
+ "--restart-app-period",
163
+ dest="restart_app_period",
164
+ type=int,
165
+ required=False,
166
+ default=0,
167
+ help="The period (in the numbers of monkey events) to restart the app under test. 0 means no restart.",
168
+ )
169
+
161
170
  parser.add_argument(
162
171
  "extra",
163
172
  nargs=argparse.REMAINDER,
@@ -200,6 +209,8 @@ def driver_info_logger(args):
200
209
  print(" post_failure_screenshots:", args.post_failure_screenshots, flush=True)
201
210
  if args.max_step:
202
211
  print(" max_step:", args.max_step, flush=True)
212
+ if args.restart_app_period > 0:
213
+ print(" restart_app_period:", args.restart_app_period, flush=True)
203
214
 
204
215
 
205
216
  def parse_args(argv: List):
@@ -270,6 +281,7 @@ def run(args=None):
270
281
  device_output_root=args.device_output_root,
271
282
  act_whitelist_file=args.act_whitelist_file,
272
283
  act_blacklist_file=args.act_blacklist_file,
284
+ restart_app_period=args.restart_app_period,
273
285
  propertytest_args=args.propertytest_args,
274
286
  unittest_args=args.unittest_args,
275
287
  extra_args=args.extra,
@@ -59,6 +59,7 @@ class ReportData(TypedDict):
59
59
  activity_count_history: Dict[str, int] # Activity traversal count from final coverage data
60
60
  crash_events: List[Dict] # Crash events from crash-dump.log
61
61
  anr_events: List[Dict] # ANR events from crash-dump.log
62
+ kill_apps_events: List[Dict] # kill_apps info events from steps.log
62
63
 
63
64
 
64
65
  class PropertyExecResult(TypedDict):
@@ -250,6 +251,7 @@ class BugReportGenerator(CrashAnrMixin, PathParserMixin, ScreenshotsMixin):
250
251
  "activity_count_history": {},
251
252
  "crash_events": [],
252
253
  "anr_events": [],
254
+ "kill_apps_events": [],
253
255
  }
254
256
 
255
257
  # Parse steps.log file to get test step numbers and screenshot mappings
@@ -283,6 +285,25 @@ class BugReportGenerator(CrashAnrMixin, PathParserMixin, ScreenshotsMixin):
283
285
  if step_type == "Monkey" or step_type == "Fuzz":
284
286
  monkey_events_count += 1
285
287
 
288
+ # Record restart-app marker events (no screenshot expected)
289
+ if step_type == "Monkey" and info == "kill_apps":
290
+ monkey_steps_count = step_data.get("MonkeyStepsCount", "N/A")
291
+ caption = f"Monkey Step {monkey_steps_count}: restart app"
292
+
293
+ data["kill_apps_events"].append({
294
+ "step_index": step_index,
295
+ "monkey_steps_count": monkey_steps_count,
296
+ })
297
+
298
+ # Show this info event in the Test Screenshots timeline
299
+ self.screenshots.append({
300
+ "id": step_index,
301
+ "path": "",
302
+ "caption": f"{step_index}. {caption}",
303
+ "kind": "info",
304
+ "info": "kill_apps",
305
+ })
306
+
286
307
  # If screenshots are enabled, mark the screenshot
287
308
  if self.take_screenshots and step_data["Screenshot"]:
288
309
  executor.submit(self._mark_screenshot, step_data)
@@ -390,8 +411,12 @@ class BugReportGenerator(CrashAnrMixin, PathParserMixin, ScreenshotsMixin):
390
411
 
391
412
  def _parse_step_data(self, raw_step_info: str) -> StepData:
392
413
  step_data: StepData = json.loads(raw_step_info)
393
- if step_data["Type"] in {"Monkey", "Script", "ScriptInfo"}:
394
- step_data["Info"] = json.loads(step_data["Info"])
414
+ if step_data.get("Type") in {"Monkey", "Script", "ScriptInfo"}:
415
+ info = step_data.get("Info")
416
+ if isinstance(info, str):
417
+ stripped = info.strip()
418
+ if stripped and stripped[0] in "{[":
419
+ step_data["Info"] = json.loads(stripped)
395
420
  return step_data
396
421
 
397
422
 
@@ -443,7 +468,8 @@ class BugReportGenerator(CrashAnrMixin, PathParserMixin, ScreenshotsMixin):
443
468
  'anr_events': data["anr_events"],
444
469
  'triggered_crash_count': len(data["crash_events"]),
445
470
  'triggered_anr_count': len(data["anr_events"]),
446
- 'property_stats_summary': data["property_stats_summary"]
471
+ 'property_stats_summary': data["property_stats_summary"],
472
+ 'kill_apps_events': data.get("kill_apps_events", []),
447
473
  }
448
474
 
449
475
  # Check if template exists, if not create it
@@ -498,7 +524,8 @@ class BugReportGenerator(CrashAnrMixin, PathParserMixin, ScreenshotsMixin):
498
524
  "start": current_test["start"],
499
525
  "end": current_test["end"],
500
526
  "screenshot_start": current_test["screenshot_start"],
501
- "screenshot_end": screenshot
527
+ "screenshot_end": screenshot,
528
+ "state": state
502
529
  })
503
530
 
504
531
  # Reset current test
@@ -524,7 +551,8 @@ class BugReportGenerator(CrashAnrMixin, PathParserMixin, ScreenshotsMixin):
524
551
  data["property_violations"].append({
525
552
  "index": index,
526
553
  "property_name": property_name,
527
- "interaction_pages": [start_step, end_step]
554
+ "interaction_pages": [start_step, end_step],
555
+ "state": violation.get("state", "fail")
528
556
  })
529
557
  index += 1
530
558
 
@@ -311,16 +311,19 @@ class ScreenshotsMixin:
311
311
  screenshot_name = step_data["Screenshot"]
312
312
  if not screenshot_name:
313
313
  return
314
+ info = step_data.get("Info")
315
+ if not isinstance(info, dict):
316
+ return
314
317
 
315
318
  if step_type == "Monkey":
316
- act = step_data["Info"].get("act")
317
- pos = step_data["Info"].get("pos")
319
+ act = info.get("act")
320
+ pos = info.get("pos")
318
321
  if act in ["CLICK", "LONG_CLICK"] or act.startswith("SCROLL"):
319
322
  self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
320
323
 
321
324
  elif step_type == "Script":
322
- act = step_data["Info"].get("method")
323
- pos = step_data["Info"].get("params")
325
+ act = info.get("method")
326
+ pos = info.get("params")
324
327
  if act in ["click", "setText", "swipe"]:
325
328
  self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
326
329
 
@@ -425,19 +428,30 @@ class ScreenshotsMixin:
425
428
  data: Data dictionary to update
426
429
  """
427
430
  caption = ""
431
+ info = step_data.get("Info")
428
432
 
429
433
  if step_data["Type"] == "Monkey":
430
434
  # Extract 'act' attribute for Monkey type and add MonkeyStepsCount
431
435
  monkey_steps_count = step_data.get('MonkeyStepsCount', 'N/A')
432
- action = step_data['Info'].get('act', 'N/A')
436
+ if isinstance(info, dict):
437
+ action = info.get('act', 'N/A')
438
+ else:
439
+ action = str(info) if info else 'N/A'
433
440
  caption = f"Monkey Step {monkey_steps_count}: {action}"
434
441
  elif step_data["Type"] == "Script":
435
442
  # Extract 'method' attribute for Script type
436
- caption = f"{step_data['Info'].get('method', 'N/A')}"
443
+ if isinstance(info, dict):
444
+ caption = f"{info.get('method', 'N/A')}"
445
+ else:
446
+ caption = str(info) if info else "N/A"
437
447
  elif step_data["Type"] == "ScriptInfo":
438
448
  # Extract 'propName' and 'state' attributes for ScriptInfo type
439
- prop_name = step_data["Info"].get('propName', '')
440
- state = step_data["Info"].get('state', 'N/A')
449
+ if isinstance(info, dict):
450
+ prop_name = info.get('propName', '')
451
+ state = info.get('state', 'N/A')
452
+ else:
453
+ prop_name = ''
454
+ state = str(info) if info else 'N/A'
441
455
  caption = f"{prop_name}: {state}" if prop_name else f"{state}"
442
456
  elif step_data["Type"] == "Fuzz":
443
457
  monkey_steps_count = step_data.get('MonkeyStepsCount', 'N/A')
@@ -465,4 +479,4 @@ class ScreenshotsMixin:
465
479
  'id': step_index,
466
480
  'path': relative_screenshot_path, # Now using string path
467
481
  'caption': f"{step_index}. {caption}"
468
- })
482
+ })
@@ -140,6 +140,107 @@
140
140
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
141
141
  margin-bottom: 8px;
142
142
  }
143
+
144
+ .screenshot-placeholder {
145
+ width: 300px;
146
+ height: 400px;
147
+ border-radius: 8px;
148
+ background: linear-gradient(180deg, rgba(52, 152, 219, 0.08), rgba(255, 255, 255, 0.98));
149
+ border: 1px solid rgba(52, 152, 219, 0.18);
150
+ box-shadow: 0 8px 24px rgba(44, 62, 80, 0.08);
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ margin-bottom: 8px;
155
+ color: #6c757d;
156
+ }
157
+
158
+ .screenshot-placeholder::before {
159
+ content: "";
160
+ position: absolute;
161
+ inset: 10px;
162
+ border-radius: 10px;
163
+ border: 1px dashed rgba(52, 152, 219, 0.25);
164
+ pointer-events: none;
165
+ }
166
+
167
+ .screenshot-item .screenshot-placeholder {
168
+ position: relative;
169
+ overflow: hidden;
170
+ }
171
+
172
+ .screenshot-placeholder::after {
173
+ content: "";
174
+ position: absolute;
175
+ inset: 0;
176
+ background:
177
+ radial-gradient(circle at 20% 25%, rgba(46, 204, 113, 0.18), transparent 42%),
178
+ radial-gradient(circle at 80% 70%, rgba(52, 152, 219, 0.16), transparent 48%);
179
+ pointer-events: none;
180
+ opacity: 0.9;
181
+ }
182
+
183
+ .screenshot-placeholder .placeholder-content {
184
+ position: relative;
185
+ z-index: 1;
186
+ display: flex;
187
+ flex-direction: column;
188
+ align-items: center;
189
+ gap: 12px;
190
+ text-align: center;
191
+ }
192
+
193
+ .screenshot-placeholder .placeholder-icon {
194
+ width: 92px;
195
+ height: 92px;
196
+ border-radius: 999px;
197
+ display: inline-flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ box-shadow: 0 16px 32px rgba(52, 152, 219, 0.22);
201
+ border: 1px solid rgba(52, 152, 219, 0.22);
202
+ background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.9), rgba(52, 152, 219, 0.10));
203
+ }
204
+
205
+ .screenshot-placeholder .placeholder-icon i {
206
+ font-size: 40px;
207
+ color: var(--primary-color);
208
+ }
209
+
210
+ .screenshot-placeholder .placeholder-icon.info i {
211
+ color: #6c757d;
212
+ }
213
+
214
+ .screenshot-placeholder .placeholder-icon.reboot {
215
+ background: conic-gradient(from 210deg, rgba(46, 204, 113, 0.22), rgba(52, 152, 219, 0.22), rgba(46, 204, 113, 0.22));
216
+ }
217
+
218
+ .screenshot-placeholder .placeholder-text {
219
+ font-size: 13px;
220
+ font-weight: 600;
221
+ letter-spacing: 0.2px;
222
+ color: rgba(44, 62, 80, 0.78);
223
+ padding: 6px 10px;
224
+ border-radius: 999px;
225
+ background: rgba(255, 255, 255, 0.72);
226
+ border: 1px solid rgba(52, 152, 219, 0.14);
227
+ box-shadow: 0 6px 18px rgba(44, 62, 80, 0.06);
228
+ }
229
+
230
+ .screenshot-item:hover .screenshot-placeholder {
231
+ border-color: rgba(52, 152, 219, 0.28);
232
+ box-shadow: 0 12px 30px rgba(44, 62, 80, 0.10);
233
+ }
234
+
235
+ .screenshot-item:hover .placeholder-icon.reboot i {
236
+ animation: reboot-spin 1.4s ease-in-out infinite;
237
+ }
238
+
239
+ @keyframes reboot-spin {
240
+ 0% { transform: rotate(0deg); }
241
+ 60% { transform: rotate(220deg); }
242
+ 100% { transform: rotate(360deg); }
243
+ }
143
244
 
144
245
  .screenshot-caption {
145
246
  font-size: 13px;
@@ -524,6 +625,10 @@
524
625
  border-radius: 0 0 8px 8px;
525
626
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
526
627
  }
628
+
629
+ .pagination-container.pagination-violations {
630
+ padding: 10px 16px;
631
+ }
527
632
 
528
633
  .activity-item {
529
634
  padding: 10px 15px;
@@ -1435,20 +1540,58 @@
1435
1540
 
1436
1541
  <!-- Screenshots Section -->
1437
1542
  {% if take_screenshots %}
1438
- {% if screenshots %}
1543
+ {% if screenshots or kill_apps_events %}
1439
1544
  <div class="section-block">
1440
1545
  <h2 class="section-title">Test Screenshots</h2>
1441
1546
  <div class="card">
1442
1547
  <div class="card-body">
1443
1548
  <div class="screenshots-container" id="screenshots">
1444
- {% for screenshot in screenshots %}
1445
- <div class="screenshot-item">
1446
- <a href="{{ screenshot.path }}" target="_blank">
1447
- <img src="{{ screenshot.path }}" class="screenshot-img" id="{{ screenshot.id }}">
1448
- </a>
1449
- <div class="screenshot-caption">{{ screenshot.caption }}</div>
1450
- </div>
1451
- {% endfor %}
1549
+ {% if screenshots %}
1550
+ {% for screenshot in screenshots %}
1551
+ <div class="screenshot-item">
1552
+ {% if screenshot.path %}
1553
+ <a href="{{ screenshot.path }}" target="_blank">
1554
+ <img src="{{ screenshot.path }}" class="screenshot-img" id="{{ screenshot.id }}">
1555
+ </a>
1556
+ {% else %}
1557
+ <div class="screenshot-placeholder" id="{{ screenshot.id }}">
1558
+ {% if screenshot.info == "kill_apps" %}
1559
+ <div class="placeholder-content">
1560
+ <span class="placeholder-icon reboot">
1561
+ <i class="bi bi-bootstrap-reboot"></i>
1562
+ </span>
1563
+ <div class="placeholder-text">Restart app</div>
1564
+ </div>
1565
+ {% else %}
1566
+ <div class="placeholder-content">
1567
+ <span class="placeholder-icon info">
1568
+ <i class="bi bi-info-circle"></i>
1569
+ </span>
1570
+ <div class="placeholder-text">Info event</div>
1571
+ </div>
1572
+ {% endif %}
1573
+ </div>
1574
+ {% endif %}
1575
+ <div class="screenshot-caption">{{ screenshot.caption }}</div>
1576
+ </div>
1577
+ {% endfor %}
1578
+ {% else %}
1579
+ {% for ev in kill_apps_events %}
1580
+ <div class="screenshot-item">
1581
+ <div class="screenshot-placeholder" id="{{ ev.step_index }}">
1582
+ <div class="placeholder-content">
1583
+ <span class="placeholder-icon reboot">
1584
+ <i class="bi bi-bootstrap-reboot"></i>
1585
+ </span>
1586
+ <div class="placeholder-text">Restart app</div>
1587
+ </div>
1588
+ </div>
1589
+ <div class="screenshot-caption">
1590
+ {{ ev.step_index }}. Monkey Step {{ ev.monkey_steps_count }}: restart app
1591
+ </div>
1592
+ </div>
1593
+ {% endfor %}
1594
+ {% endif %}
1452
1595
  </div>
1453
1596
  </div>
1454
1597
  </div>
@@ -1630,7 +1773,7 @@
1630
1773
  </thead>
1631
1774
  <tbody id="property-violations-container">
1632
1775
  {% for violation in property_violations %}
1633
- <tr class="property-violation-row" data-page="1">
1776
+ <tr class="property-violation-row" data-page="1" data-status="{{ violation.state|default('fail') }}">
1634
1777
  <td>{{ violation.index }}</td>
1635
1778
  <td><span class="badge bg-light text-dark badge-custom">{{ violation.property_name }}</span></td>
1636
1779
  <td>
@@ -1645,8 +1788,8 @@
1645
1788
  </table>
1646
1789
 
1647
1790
  <!-- Pagination for Property Violations -->
1648
- <div class="d-flex justify-content-between align-items-center mt-3">
1649
- <div class="d-flex align-items-center">
1791
+ <div class="d-flex justify-content-between align-items-center mt-3 pagination-container pagination-violations">
1792
+ <div class="d-flex align-items-center gap-3">
1650
1793
  <label for="violations-page-size" class="form-label me-2 mb-0">Show:</label>
1651
1794
  <select class="form-select form-select-sm" id="violations-page-size" style="width: auto;">
1652
1795
  <option value="5">5</option>
@@ -1655,6 +1798,11 @@
1655
1798
  <option value="50">50</option>
1656
1799
  <option value="100">100</option>
1657
1800
  </select>
1801
+ <label for="violations-status-filter" class="form-label mb-0">Status:</label>
1802
+ <select class="form-select form-select-sm" id="violations-status-filter" style="width: auto;">
1803
+ <option value="all" selected>All</option>
1804
+ <option value="fail">Fail only</option>
1805
+ </select>
1658
1806
  </div>
1659
1807
  <nav aria-label="Property Violations Pagination">
1660
1808
  <ul class="pagination pagination-sm mb-0" id="violations-pagination">
@@ -2232,6 +2380,7 @@
2232
2380
 
2233
2381
  // Initialize pagination for Property tables
2234
2382
  initPagination('property-violations-container', 'property-violation-row', 'violations-pagination', 'violations-page-size');
2383
+ initViolationFilter();
2235
2384
  initPagination('property-stats-container', 'property-stat-row', 'stats-pagination', 'stats-page-size');
2236
2385
 
2237
2386
  // Initialize sorting for Property Checking Statistics
@@ -2402,6 +2551,10 @@
2402
2551
  const allItems = Array.from(container.getElementsByClassName(itemClass));
2403
2552
  const paginationElement = document.getElementById(paginationId);
2404
2553
 
2554
+ if (containerId === 'property-stats-container') {
2555
+ closePropertyErrorDetails(container);
2556
+ }
2557
+
2405
2558
  console.log('goToPage called with:', {pageNumber, containerId, itemClass, itemsPerPage, paginationId});
2406
2559
  console.log('Total items found:', allItems.length);
2407
2560
 
@@ -2946,6 +3099,18 @@
2946
3099
  }, 3000);
2947
3100
  }
2948
3101
  });
3102
+
3103
+ // Close opened Property Checking Statistics error details when pagination changes
3104
+ function closePropertyErrorDetails(container) {
3105
+ if (!container) return;
3106
+
3107
+ const openDetails = container.querySelectorAll('.property-detail-row.show');
3108
+ openDetails.forEach(function(detailRow) {
3109
+ const collapseInstance = bootstrap.Collapse.getInstance(detailRow) ||
3110
+ new bootstrap.Collapse(detailRow, { toggle: false });
3111
+ collapseInstance.hide();
3112
+ });
3113
+ }
2949
3114
 
2950
3115
  // Global activity search functions
2951
3116
  function performActivitySearch(searchInput) {
@@ -3140,6 +3305,33 @@
3140
3305
  }
3141
3306
  }
3142
3307
  }
3308
+
3309
+ function initViolationFilter() {
3310
+ const filterSelect = document.getElementById('violations-status-filter');
3311
+ const container = document.getElementById('property-violations-container');
3312
+
3313
+ if (!filterSelect || !container) return;
3314
+
3315
+ filterSelect.addEventListener('change', function() {
3316
+ const filterValue = this.value;
3317
+ const rows = Array.from(container.getElementsByClassName('property-violation-row'));
3318
+
3319
+ rows.forEach(function(row) {
3320
+ const status = row.getAttribute('data-status') || 'fail';
3321
+ if (filterValue === 'all') {
3322
+ row.removeAttribute('data-search-visible');
3323
+ } else {
3324
+ row.setAttribute('data-search-visible', status === filterValue ? 'true' : 'false');
3325
+ }
3326
+ });
3327
+
3328
+ initPagination('property-violations-container', 'property-violation-row', 'violations-pagination', 'violations-page-size');
3329
+
3330
+ const currentPageSize = document.getElementById('violations-page-size') ?
3331
+ parseInt(document.getElementById('violations-page-size').value) : 10;
3332
+ goToPage(1, 'property-violations-container', 'property-violation-row', currentPageSize, 'violations-pagination');
3333
+ });
3334
+ }
3143
3335
 
3144
3336
  // Global pagination and navigation functions
3145
3337
  function goToPage(pageNumber, containerId, itemClass, itemsPerPage, paginationId) {
@@ -3147,6 +3339,10 @@
3147
3339
  const allItems = Array.from(container.getElementsByClassName(itemClass));
3148
3340
  const paginationElement = document.getElementById(paginationId);
3149
3341
 
3342
+ if (containerId === 'property-stats-container') {
3343
+ closePropertyErrorDetails(container);
3344
+ }
3345
+
3150
3346
  // Get items that should be visible based on search filter
3151
3347
  const filteredItems = allItems.filter(item => {
3152
3348
  const searchVisible = item.getAttribute('data-search-visible');
@@ -3,7 +3,7 @@ import threading
3
3
  from pathlib import Path
4
4
 
5
5
  from .adbUtils import ADBDevice
6
- from .utils import getLogger
6
+ from .utils import getLogger, catchException, timer
7
7
 
8
8
  from typing import TYPE_CHECKING
9
9
  if TYPE_CHECKING:
@@ -34,36 +34,32 @@ class ResultSyncer:
34
34
  """Thread function that waits for sync event and then syncs data"""
35
35
  while self.running:
36
36
  # Wait for sync event with a timeout to periodically check if still running
37
- if self.sync_event.wait(timeout=3):
37
+ if self.sync_event.wait(timeout=1):
38
38
  self._sync_device_data()
39
39
  self.sync_event.clear()
40
40
 
41
+ @timer("Data Sync cost %cost_time seconds")
41
42
  def close(self):
42
43
  self.running = False
43
44
  self.sync_event.set()
44
45
  if self.thread and self.thread.is_alive():
46
+ logger.info("Syncing result data from device. Please wait...")
45
47
  self.thread.join(timeout=10)
46
48
  self._sync_device_data()
47
49
  try:
48
50
  logger.debug(f"Removing device output directory: {self.device_output_dir}")
49
51
  remove_device_dir = ["rm", "-rf", self.device_output_dir]
50
- # adb_shell(remove_device_dir)
51
52
  self.dev.shell(remove_device_dir)
52
53
  except Exception as e:
53
54
  logger.error(f"Error removing device output directory: {e}", flush=True)
54
55
 
56
+ @catchException("Error during device data sync.")
55
57
  def _sync_device_data(self):
56
58
  """
57
59
  Sync the device data to the local directory.
58
60
  """
59
- try:
60
- logger.debug("Syncing data")
61
-
62
- self.dev.sync.pull_dir(self.device_output_dir, self.output_dir, exist_ok=True)
63
- # pull_file(self.device_output_dir, str(self.output_dir))
61
+ logger.debug("Syncing data")
62
+ self.dev.sync.pull_dir(self.device_output_dir, self.output_dir, exist_ok=True)
64
63
 
65
- remove_pulled_screenshots = ["find", self.device_output_dir, "-name", '"*.png"', "-delete"]
66
- self.dev.shell(remove_pulled_screenshots)
67
- # adb_shell(remove_pulled_screenshots)
68
- except Exception as e:
69
- logger.error(f"Error in data sync: {e}")
64
+ remove_pulled_screenshots = ["find", self.device_output_dir, "-name", '"*.png"', "-delete"]
65
+ self.dev.shell(remove_pulled_screenshots)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Kea2-python"
3
- version = "1.0.4"
3
+ version = "1.0.5"
4
4
  description = "A python library for supporting and customizing automated UI testing for mobile apps"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -0,0 +1,67 @@
1
+ import unittest
2
+ from kea2.u2Driver import U2StaticChecker, U2StaticDevice
3
+ from pathlib import Path
4
+
5
+
6
+ XML_PATH = Path(__file__).parent / "xpath_test.xml"
7
+
8
+
9
+ class U2StaticCheckerForTest(U2StaticChecker):
10
+ def __init__(self):
11
+ self.d = U2StaticDevice(script_driver=None)
12
+
13
+
14
+ def get_static_checker():
15
+ xml = ""
16
+ with open(XML_PATH, "r", encoding="utf-8") as f:
17
+ xml = f.read()
18
+ d = U2StaticCheckerForTest()
19
+ return d.getInstance(xml)
20
+
21
+
22
+ class TestXPath(unittest.TestCase):
23
+
24
+ def setUp(self):
25
+ self.d = get_static_checker()
26
+
27
+ def test_basic_xpath(self):
28
+ assert self.d.xpath("""//*[@text="Hrgshsjs"]""").exists
29
+ assert self.d.xpath("""//android.widget.TextView[@text="hehzhe"]""").exists
30
+ assert self.d.xpath(
31
+ """(//*[@resource-id="it.feio.android.omninotes.alpha:id/category_marker"])[3]"""
32
+ ).exists
33
+
34
+ assert self.d.xpath('@com.android.systemui:id/clock').exists
35
+
36
+ assert self.d.xpath('//android.widget.TextView[@text="hehzhe"]')\
37
+ .parent_exists('//androidx.recyclerview.widget.RecyclerView')
38
+
39
+ assert (self.d.xpath('100') &
40
+ self.d.xpath('@com.android.systemui:id/battery_inside_percent')).exists
41
+
42
+ assert (self.d.xpath('100') | self.d.xpath('2:14')).exists # |
43
+
44
+ assert self.d.xpath('//android.widget.TextView[@text="Notes"]')\
45
+ .parent_exists('@it.feio.android.omninotes.alpha:id/toolbar') # parent_exists
46
+
47
+ assert self.d.xpath('@it.feio.android.omninotes.alpha:id/fab')\
48
+ .child('/android.widget.ImageButton').exists # child
49
+
50
+ assert self.d.xpath('//androidx.drawerlayout.widget.DrawerLayout[@resource-id="it.feio.android.omninotes.alpha:id/drawer_layout"]' +
51
+ '//android.view.ViewGroup[@resource-id="it.feio.android.omninotes.alpha:id/toolbar"]').exists
52
+
53
+ assert self.d.xpath('(//android.view.View[@resource-id="it.feio.android.omninotes.alpha:id/category_marker"])[3]')\
54
+ .parent_exists('//androidx.recyclerview.widget.RecyclerView[@resource-id="it.feio.android.omninotes.alpha:id/list"]')
55
+
56
+ assert self.d.xpath('//androidx.drawerlayout.widget.DrawerLayout[@resource-id="it.feio.android.omninotes.alpha:id/drawer_layout"]' +
57
+ '//android.view.ViewGroup[@resource-id="it.feio.android.omninotes.alpha:id/toolbar"]').exists
58
+
59
+ # parent_exists
60
+ node = (self.d.xpath('@com.android.systemui:id/battery_inside_percent') |
61
+ self.d.xpath('@com.android.systemui:id/clock'))
62
+ assert node & self.d.xpath('//android.widget.TextView')
63
+ assert node.parent_exists('@com.android.systemui:id/status_bar') # parent_exists
64
+
65
+
66
+ if __name__ == "__main__":
67
+ unittest.main()
Binary file
@@ -1,36 +0,0 @@
1
- import unittest
2
- from kea2.u2Driver import U2StaticChecker, U2StaticDevice
3
- from pathlib import Path
4
-
5
-
6
- XML_PATH = Path(__file__).parent / "xpath_test.xml"
7
-
8
-
9
- class U2StaticCheckerForTest(U2StaticChecker):
10
- def __init__(self):
11
- self.d = U2StaticDevice(script_driver=None)
12
-
13
-
14
- def get_static_checker():
15
- xml = ""
16
- with open(XML_PATH, "r", encoding="utf-8") as f:
17
- xml = f.read()
18
- d = U2StaticCheckerForTest()
19
- return d.getInstance(xml)
20
-
21
-
22
- class TestXPath(unittest.TestCase):
23
-
24
- def setUp(self):
25
- self.d = get_static_checker()
26
-
27
- def test_basic_xpath(self):
28
- assert self.d.xpath("""//*[@text="Hrgshsjs"]""").exists
29
- assert self.d.xpath("""//android.widget.TextView[@text="hehzhe"]""").exists
30
- assert self.d.xpath(
31
- """(//*[@resource-id="it.feio.android.omninotes.alpha:id/category_marker"])[3]"""
32
- ).exists
33
-
34
-
35
- if __name__ == "__main__":
36
- unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes