Kea2-python 0.2.3__tar.gz → 0.2.4__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.

Potentially problematic release.


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

Files changed (43) hide show
  1. {kea2_python-0.2.3 → kea2_python-0.2.4}/Kea2_python.egg-info/PKG-INFO +4 -5
  2. {kea2_python-0.2.3 → kea2_python-0.2.4}/PKG-INFO +4 -5
  3. {kea2_python-0.2.3 → kea2_python-0.2.4}/README.md +3 -4
  4. kea2_python-0.2.4/kea2/assets/monkeyq.jar +0 -0
  5. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/bug_report_generator.py +188 -80
  6. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/cli.py +42 -0
  7. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/fastbotManager.py +1 -0
  8. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/keaUtils.py +2 -0
  9. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/kea_launcher.py +2 -0
  10. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/logWatcher.py +13 -1
  11. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/templates/bug_report_template.html +429 -22
  12. {kea2_python-0.2.3 → kea2_python-0.2.4}/pyproject.toml +1 -1
  13. kea2_python-0.2.3/kea2/assets/monkeyq.jar +0 -0
  14. {kea2_python-0.2.3 → kea2_python-0.2.4}/Kea2_python.egg-info/SOURCES.txt +0 -0
  15. {kea2_python-0.2.3 → kea2_python-0.2.4}/Kea2_python.egg-info/dependency_links.txt +0 -0
  16. {kea2_python-0.2.3 → kea2_python-0.2.4}/Kea2_python.egg-info/entry_points.txt +0 -0
  17. {kea2_python-0.2.3 → kea2_python-0.2.4}/Kea2_python.egg-info/requires.txt +0 -0
  18. {kea2_python-0.2.3 → kea2_python-0.2.4}/Kea2_python.egg-info/top_level.txt +0 -0
  19. {kea2_python-0.2.3 → kea2_python-0.2.4}/LICENSE +0 -0
  20. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/__init__.py +0 -0
  21. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/absDriver.py +0 -0
  22. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/adbUtils.py +0 -0
  23. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot-thirdpart.jar +0 -0
  24. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_configs/abl.strings +0 -0
  25. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_configs/awl.strings +0 -0
  26. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_configs/max.config +0 -0
  27. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_configs/max.fuzzing.strings +0 -0
  28. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_configs/max.schema.strings +0 -0
  29. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_configs/max.strings +0 -0
  30. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_configs/max.tree.pruning +0 -0
  31. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_configs/widget.block.py +0 -0
  32. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  33. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  34. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
  35. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  36. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/framework.jar +0 -0
  37. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/kea2-thirdpart.jar +0 -0
  38. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/assets/quicktest.py +0 -0
  39. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/resultSyncer.py +0 -0
  40. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/u2Driver.py +0 -0
  41. {kea2_python-0.2.3 → kea2_python-0.2.4}/kea2/utils.py +0 -0
  42. {kea2_python-0.2.3 → kea2_python-0.2.4}/setup.cfg +0 -0
  43. {kea2_python-0.2.3 → kea2_python-0.2.4}/tests/test_u2Selector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 0.2.3
3
+ Version: 0.2.4
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
@@ -18,22 +18,21 @@ Dynamic: license-file
18
18
  [![PyPI Downloads](https://static.pepy.tech/badge/kea2-python)](https://pepy.tech/projects/kea2-python)
19
19
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
20
20
 
21
-
22
21
  <div>
23
- <img src="https://github.com/user-attachments/assets/36ec9f2f-a3d8-482a-9a61-4785f3278991" style="border-radius: 14px; width: 20%; height: 20%;"/>
22
+ <img src="https://github.com/user-attachments/assets/84e47b87-2dd2-4d7e-91d1-e8c1d1db0cf4" style="border-radius: 14px; width: 20%; height: 20%;"/>
24
23
  </div>
25
24
 
26
25
  ### Github repo link
27
26
  [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
28
27
 
29
- ### [点击此处:查看中文文档](README_cn.md)
28
+ ### [点击此处:查看中文文档](README_cn.md)
30
29
 
31
30
  ## About
32
31
 
33
32
  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*.
34
33
 
35
34
  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.
35
+ Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) apps.
37
36
 
38
37
  ## Novelty & Important features
39
38
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 0.2.3
3
+ Version: 0.2.4
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
@@ -18,22 +18,21 @@ Dynamic: license-file
18
18
  [![PyPI Downloads](https://static.pepy.tech/badge/kea2-python)](https://pepy.tech/projects/kea2-python)
19
19
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
20
20
 
21
-
22
21
  <div>
23
- <img src="https://github.com/user-attachments/assets/36ec9f2f-a3d8-482a-9a61-4785f3278991" style="border-radius: 14px; width: 20%; height: 20%;"/>
22
+ <img src="https://github.com/user-attachments/assets/84e47b87-2dd2-4d7e-91d1-e8c1d1db0cf4" style="border-radius: 14px; width: 20%; height: 20%;"/>
24
23
  </div>
25
24
 
26
25
  ### Github repo link
27
26
  [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
28
27
 
29
- ### [点击此处:查看中文文档](README_cn.md)
28
+ ### [点击此处:查看中文文档](README_cn.md)
30
29
 
31
30
  ## About
32
31
 
33
32
  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*.
34
33
 
35
34
  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.
35
+ Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) apps.
37
36
 
38
37
  ## Novelty & Important features
39
38
 
@@ -4,22 +4,21 @@
4
4
  [![PyPI Downloads](https://static.pepy.tech/badge/kea2-python)](https://pepy.tech/projects/kea2-python)
5
5
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
6
6
 
7
-
8
7
  <div>
9
- <img src="https://github.com/user-attachments/assets/36ec9f2f-a3d8-482a-9a61-4785f3278991" style="border-radius: 14px; width: 20%; height: 20%;"/>
8
+ <img src="https://github.com/user-attachments/assets/84e47b87-2dd2-4d7e-91d1-e8c1d1db0cf4" style="border-radius: 14px; width: 20%; height: 20%;"/>
10
9
  </div>
11
10
 
12
11
  ### Github repo link
13
12
  [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
14
13
 
15
- ### [点击此处:查看中文文档](README_cn.md)
14
+ ### [点击此处:查看中文文档](README_cn.md)
16
15
 
17
16
  ## About
18
17
 
19
18
  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*.
20
19
 
21
20
  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*.
22
- Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) apps.
21
+ Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) apps.
23
22
 
24
23
  ## Novelty & Important features
25
24
 
@@ -2,7 +2,7 @@ import json
2
2
  from datetime import datetime
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
- from typing import Dict, TypedDict, List, Deque, NewType, Union
5
+ from typing import Dict, TypedDict, List, Deque, NewType, Union, Optional
6
6
  from collections import deque
7
7
  from concurrent.futures import ThreadPoolExecutor
8
8
 
@@ -28,12 +28,13 @@ class StepData(TypedDict):
28
28
 
29
29
 
30
30
  class CovData(TypedDict):
31
- stepsCount: int # The MonkeyStepsCount when profiling the Coverage data
31
+ stepsCount: int
32
32
  coverage: float
33
33
  totalActivitiesCount: int
34
34
  testedActivitiesCount: int
35
35
  totalActivities: List[str]
36
36
  testedActivities: List[str]
37
+ activityCountHistory: Dict[str, int]
37
38
 
38
39
 
39
40
  class ReportData(TypedDict):
@@ -53,6 +54,8 @@ class ReportData(TypedDict):
53
54
  property_error_details: Dict[str, List[Dict]] # Support multiple errors per property
54
55
  screenshot_info: Dict
55
56
  coverage_trend: List
57
+ property_execution_trend: List # Track executed properties count over steps
58
+ activity_count_history: Dict[str, int] # Activity traversal count from final coverage data
56
59
 
57
60
 
58
61
  class PropertyExecResult(TypedDict):
@@ -62,6 +65,49 @@ class PropertyExecResult(TypedDict):
62
65
  error: int
63
66
 
64
67
 
68
+ @dataclass
69
+ class PropertyExecInfo:
70
+ """Class representing property execution information from property_exec_info file"""
71
+ prop_name: str
72
+ state: str # start, pass, fail, error
73
+ traceback: str
74
+ start_steps_count: int
75
+ occurrence_count: int = 1
76
+ short_description: str = ""
77
+ start_steps_count_list: List[int] = None
78
+
79
+ def __post_init__(self):
80
+ if self.start_steps_count_list is None:
81
+ self.start_steps_count_list = [self.start_steps_count]
82
+ if not self.short_description and self.traceback:
83
+ self.short_description = self._extract_error_summary(self.traceback)
84
+
85
+ def _extract_error_summary(self, traceback: str) -> str:
86
+ """Extract a short error summary from the full traceback"""
87
+ try:
88
+ lines = traceback.strip().split('\n')
89
+ for line in reversed(lines):
90
+ line = line.strip()
91
+ if line and not line.startswith(' '):
92
+ return line
93
+ return "Unknown error"
94
+ except Exception:
95
+ return "Error parsing traceback"
96
+
97
+ def get_error_hash(self) -> int:
98
+ """Generate hash key for error deduplication"""
99
+ return hash((self.state, self.traceback))
100
+
101
+ def is_error_state(self) -> bool:
102
+ """Check if this is an error or fail state"""
103
+ return self.state in ["fail", "error"]
104
+
105
+ def add_occurrence(self, start_steps_count: int):
106
+ """Add another occurrence of the same error"""
107
+ self.occurrence_count += 1
108
+ self.start_steps_count_list.append(start_steps_count)
109
+
110
+
65
111
  PropertyName = NewType("PropertyName", str)
66
112
  TestResult = NewType("TestResult", Dict[PropertyName, PropertyExecResult])
67
113
 
@@ -239,11 +285,15 @@ class BugReportGenerator:
239
285
  "property_stats": [],
240
286
  "property_error_details": {},
241
287
  "screenshot_info": {},
242
- "coverage_trend": []
288
+ "coverage_trend": [],
289
+ "property_execution_trend": [],
290
+ "activity_count_history": {}
243
291
  }
244
292
 
245
293
  # Parse steps.log file to get test step numbers and screenshot mappings
246
294
  property_violations = {} # Store multiple violation records for each property
295
+ executed_properties_by_step = {} # Track executed properties at each step: {step_count: set()}
296
+ executed_properties = set() # Track unique executed properties
247
297
 
248
298
  if not self.data_path.steps_log.exists():
249
299
  logger.error(f"{self.data_path.steps_log} not exists")
@@ -279,11 +329,18 @@ class BugReportGenerator:
279
329
  if screenshot and screenshot not in data["screenshot_info"]:
280
330
  self._add_screenshot_info(step_data, step_index, data)
281
331
 
282
- # Process ScriptInfo for property violations
332
+ # Process ScriptInfo for property violations and execution tracking
283
333
  if step_type == "ScriptInfo":
284
334
  try:
285
335
  property_name = info.get("propName", "")
286
336
  state = info.get("state", "")
337
+
338
+ # Track executed properties (properties that have been started)
339
+ if property_name and state == "start":
340
+ executed_properties.add(property_name)
341
+ # Record the monkey steps count for this property execution
342
+ executed_properties_by_step[monkey_events_count] = executed_properties.copy()
343
+
287
344
  current_property, current_test = self._process_script_info(
288
345
  property_name, state, step_index, screenshot,
289
346
  current_property, current_test, property_violations
@@ -334,6 +391,10 @@ class BugReportGenerator:
334
391
  data["tested_activities"] = final_trend["testedActivities"]
335
392
  data["total_activities_count"] = final_trend["totalActivitiesCount"]
336
393
  data["tested_activities_count"] = final_trend["testedActivitiesCount"]
394
+ data["activity_count_history"] = final_trend["activityCountHistory"]
395
+
396
+ # Generate property execution trend aligned with coverage trend
397
+ data["property_execution_trend"] = self._generate_property_execution_trend(executed_properties_by_step)
337
398
 
338
399
  # Generate Property Violations list
339
400
  self._generate_property_violations_list(property_violations, data)
@@ -373,16 +434,16 @@ class BugReportGenerator:
373
434
 
374
435
  def _mark_screenshot_interaction(self, step_type: str, screenshot_name: str, action_type: str, position: Union[List, tuple]) -> bool:
375
436
  """
376
- Mark interaction on screenshot with colored rectangle
437
+ Mark interaction on screenshot with colored rectangle
377
438
 
378
- Args:
379
- step_type (str): Type of the step (Monkey or Script)
380
- screenshot_name (str): Name of the screenshot file
381
- action_type (str): Type of action (CLICK/LONG_CLICK/SCROLL for Monkey, click/setText/swipe for Script)
382
- position: Position coordinates or parameters (format varies by action type)
439
+ Args:
440
+ step_type (str): Type of the step (Monkey or Script)
441
+ screenshot_name (str): Name of the screenshot file
442
+ action_type (str): Type of action (CLICK/LONG_CLICK/SCROLL for Monkey, click/setText/swipe for Script)
443
+ position: Position coordinates or parameters (format varies by action type)
383
444
 
384
- Returns:
385
- bool: True if marking was successful, False otherwise
445
+ Returns:
446
+ bool: True if marking was successful, False otherwise
386
447
  """
387
448
  screenshot_path: Path = self.data_path.screenshots_dir / screenshot_name
388
449
  if not screenshot_path.exists():
@@ -492,7 +553,10 @@ class BugReportGenerator:
492
553
  'property_stats': data["property_stats"],
493
554
  'property_error_details': data["property_error_details"],
494
555
  'coverage_data': coverage_trend_json,
495
- 'take_screenshots': self.take_screenshots # Pass screenshot setting to template
556
+ 'take_screenshots': self.take_screenshots, # Pass screenshot setting to template
557
+ 'property_execution_trend': data["property_execution_trend"],
558
+ 'property_execution_data': json.dumps(data["property_execution_trend"]),
559
+ 'activity_count_history': data["activity_count_history"]
496
560
  }
497
561
 
498
562
  # Check if template exists, if not create it
@@ -522,8 +586,10 @@ class BugReportGenerator:
522
586
  caption = ""
523
587
 
524
588
  if step_data["Type"] == "Monkey":
525
- # Extract 'act' attribute for Monkey type and convert to lowercase
526
- caption = f"{step_data['Info'].get('act', 'N/A')}"
589
+ # Extract 'act' attribute for Monkey type and add MonkeyStepsCount
590
+ monkey_steps_count = step_data.get('MonkeyStepsCount', 'N/A')
591
+ action = step_data['Info'].get('act', 'N/A')
592
+ caption = f"Monkey Step {monkey_steps_count}: {action}"
527
593
  elif step_data["Type"] == "Script":
528
594
  # Extract 'method' attribute for Script type
529
595
  caption = f"{step_data['Info'].get('method', 'N/A')}"
@@ -630,90 +696,132 @@ class BugReportGenerator:
630
696
  Returns:
631
697
  Dict[str, List[Dict]]: Mapping of property names to their error tracebacks with context
632
698
  """
633
- error_details = {}
634
-
635
699
  if not self.data_path.property_exec_info.exists():
636
700
  logger.warning(f"Property exec info file {self.data_path.property_exec_info} not found")
637
- return error_details
701
+ return {}
638
702
 
639
703
  try:
640
- with open(self.data_path.property_exec_info, "r", encoding="utf-8") as f:
641
- # Use hash map for efficient deduplication
642
- error_hash_map = {} # property_name -> {error_hash: error_data}
643
-
644
- for line_number, line in enumerate(f, 1):
645
- line = line.strip()
646
- if not line:
647
- continue
648
-
649
- try:
650
- exec_info = json.loads(line)
651
- prop_name = exec_info.get("propName", "")
652
- state = exec_info.get("state", "")
653
- tb = exec_info.get("tb", "")
654
-
655
- # Only process error details for failed or error states
656
- if prop_name and state in ["fail", "error"] and tb:
657
- if prop_name not in error_hash_map:
658
- error_hash_map[prop_name] = {}
659
-
660
- # Create hash key for this specific error (state + traceback)
661
- error_hash = hash((state, tb))
662
-
663
- if error_hash in error_hash_map[prop_name]:
664
- # Error already exists, increment count
665
- error_hash_map[prop_name][error_hash]["occurrence_count"] += 1
666
- else:
667
- # New error, create entry
668
- short_desc = self._extract_error_summary(tb)
669
- error_hash_map[prop_name][error_hash] = {
670
- "state": state,
671
- "traceback": tb,
672
- "occurrence_count": 1,
673
- "short_description": short_desc
674
- }
675
-
676
- except json.JSONDecodeError as e:
677
- logger.warning(f"Failed to parse property exec info line {line_number}: {line[:100]}... Error: {e}")
678
- continue
679
-
680
- # Convert hash map to list format for template compatibility
681
- for prop_name, hash_dict in error_hash_map.items():
682
- error_details[prop_name] = list(hash_dict.values())
683
- # Sort by occurrence count (descending) to show most frequent errors first
684
- error_details[prop_name].sort(key=lambda x: x["occurrence_count"], reverse=True)
685
-
704
+ property_exec_infos = self._parse_property_exec_infos()
705
+ return self._group_errors_by_property(property_exec_infos)
706
+
686
707
  except Exception as e:
687
708
  logger.error(f"Error reading property exec info file: {e}")
709
+ return {}
710
+
711
+ def _parse_property_exec_infos(self) -> List[PropertyExecInfo]:
712
+ """Parse property execution info from file"""
713
+ exec_infos = []
714
+
715
+ with open(self.data_path.property_exec_info, "r", encoding="utf-8") as f:
716
+ for line_number, line in enumerate(f, 1):
717
+ line = line.strip()
718
+ if not line:
719
+ continue
720
+
721
+ try:
722
+ exec_info_data = json.loads(line)
723
+ prop_name = exec_info_data.get("propName", "")
724
+ state = exec_info_data.get("state", "")
725
+ tb = exec_info_data.get("tb", "")
726
+ start_steps_count = exec_info_data.get("startStepsCount", 0)
727
+
728
+ exec_info = PropertyExecInfo(
729
+ prop_name=prop_name,
730
+ state=state,
731
+ traceback=tb,
732
+ start_steps_count=start_steps_count
733
+ )
734
+
735
+ if exec_info.is_error_state() and prop_name and tb:
736
+ exec_infos.append(exec_info)
737
+
738
+ except json.JSONDecodeError as e:
739
+ logger.warning(f"Failed to parse property exec info line {line_number}: {line[:100]}... Error: {e}")
740
+ continue
741
+
742
+ return exec_infos
743
+
744
+ def _group_errors_by_property(self, exec_infos: List[PropertyExecInfo]) -> Dict[str, List[Dict]]:
745
+ """Group errors by property name and deduplicate"""
746
+ error_details = {}
747
+
748
+ for exec_info in exec_infos:
749
+ prop_name = exec_info.prop_name
688
750
 
689
- return error_details
751
+ if prop_name not in error_details:
752
+ error_details[prop_name] = {}
753
+
754
+ error_hash = exec_info.get_error_hash()
755
+
756
+ if error_hash in error_details[prop_name]:
757
+ # Error already exists, add occurrence
758
+ error_details[prop_name][error_hash].add_occurrence(exec_info.start_steps_count)
759
+ else:
760
+ # New error, create entry
761
+ error_details[prop_name][error_hash] = exec_info
762
+
763
+ # Convert to template-compatible format
764
+ result = {}
765
+ for prop_name, hash_dict in error_details.items():
766
+ result[prop_name] = []
767
+ for exec_info in hash_dict.values():
768
+ result[prop_name].append({
769
+ "state": exec_info.state,
770
+ "traceback": exec_info.traceback,
771
+ "occurrence_count": exec_info.occurrence_count,
772
+ "short_description": exec_info.short_description,
773
+ "startStepsCountList": exec_info.start_steps_count_list
774
+ })
775
+
776
+ # Sort by earliest startStepsCount, then by occurrence count (descending)
777
+ result[prop_name].sort(key=lambda x: (min(x["startStepsCountList"]), -x["occurrence_count"]))
778
+
779
+ return result
690
780
 
691
- def _extract_error_summary(self, traceback: str) -> str:
781
+ def _generate_property_execution_trend(self, executed_properties_by_step: Dict[int, set]) -> List[Dict]:
692
782
  """
693
- Extract a short error summary from the full traceback
783
+ Generate property execution trend aligned with coverage trend
694
784
 
695
785
  Args:
696
- traceback: Full error traceback string
786
+ executed_properties_by_step: Dictionary containing executed properties at each step
697
787
 
698
788
  Returns:
699
- str: Short error summary
789
+ List[Dict]: Property execution trend data aligned with coverage trend
700
790
  """
701
- try:
702
- lines = traceback.strip().split('\n')
703
-
704
- for line in reversed(lines):
705
- line = line.strip()
706
- if line and not line.startswith(' '):
707
- return line
708
- return "Unknown error"
709
- except Exception:
710
- return "Error parsing traceback"
791
+ property_execution_trend = []
792
+
793
+ # Get step points from coverage trend to ensure alignment
794
+ coverage_step_points = []
795
+ if self.cov_trend:
796
+ coverage_step_points = [cov_data["stepsCount"] for cov_data in self.cov_trend]
797
+
798
+ # If no coverage data, use property execution data points
799
+ if not coverage_step_points and executed_properties_by_step:
800
+ coverage_step_points = sorted(executed_properties_by_step.keys())
801
+
802
+ # Generate property execution data for each coverage step point
803
+ for step_count in coverage_step_points:
804
+ # Find the latest executed properties count up to this step
805
+ executed_count = 0
806
+ latest_step = 0
807
+
808
+ for exec_step in executed_properties_by_step.keys():
809
+ if exec_step <= step_count and exec_step >= latest_step:
810
+ latest_step = exec_step
811
+ executed_count = len(executed_properties_by_step[exec_step])
812
+
813
+ property_execution_trend.append({
814
+ "stepsCount": step_count,
815
+ "executedPropertiesCount": executed_count
816
+ })
817
+
818
+ return property_execution_trend
711
819
 
712
820
 
713
821
  if __name__ == "__main__":
714
822
  print("Generating bug report")
715
823
  # OUTPUT_PATH = "<Your output path>"
716
- OUTPUT_PATH = "P:/Python/Kea2/output/res_2025062921_4535312225"
824
+ OUTPUT_PATH = "P:/Python/Kea2/output/res_2025070814_4842540549"
717
825
 
718
826
  report_generator = BugReportGenerator()
719
827
  report_path = report_generator.generate_report(OUTPUT_PATH)
@@ -46,6 +46,34 @@ def cmd_load_configs(args):
46
46
  pass
47
47
 
48
48
 
49
+ def cmd_report(args):
50
+ from .bug_report_generator import BugReportGenerator
51
+ try:
52
+ report_dir = args.path
53
+ if not report_dir:
54
+ logger.error("Report directory path is required. Use -p to specify the path.")
55
+ return
56
+
57
+ report_path = Path(report_dir)
58
+ if not report_path.exists():
59
+ logger.error(f"Report directory does not exist: {report_dir}")
60
+ return
61
+
62
+ logger.debug(f"Generating test report from directory: {report_dir}")
63
+
64
+ generator = BugReportGenerator()
65
+ report_file = generator.generate_report(report_path)
66
+
67
+ if report_file:
68
+ logger.debug(f"Test report generated successfully: {report_file}")
69
+ print(f"Report saved to: {report_file}", flush=True)
70
+ else:
71
+ logger.error("Failed to generate test report")
72
+
73
+ except Exception as e:
74
+ logger.error(f"Error generating test report: {e}")
75
+
76
+
49
77
  def cmd_run(args):
50
78
  base_dir = getProjectRoot()
51
79
  if base_dir is None:
@@ -60,6 +88,20 @@ _commands = [
60
88
  action=cmd_init,
61
89
  command="init",
62
90
  help="init the Kea2 project in current directory",
91
+ ),
92
+ dict(
93
+ action=cmd_report,
94
+ command="report",
95
+ help="generate test report from existing test results",
96
+ flags=[
97
+ dict(
98
+ name=["report_dir"],
99
+ args=["-p", "--path"],
100
+ type=str,
101
+ required=True,
102
+ help="Path to the directory containing test results"
103
+ )
104
+ ]
63
105
  )
64
106
  ]
65
107
 
@@ -181,6 +181,7 @@ class FastbotManager:
181
181
  "--running-minutes", f"{self.options.running_mins}",
182
182
  "--throttle", f"{self.options.throttle}",
183
183
  "--bugreport",
184
+ "--output-directory", f"{self.options.device_output_root}/output_{self.options.log_stamp}",
184
185
  ]
185
186
 
186
187
  if self.options.profile_period:
@@ -164,6 +164,8 @@ class Options:
164
164
  f"char: `{char}` is illegal in --log-stamp. current stamp: {self.log_stamp}"
165
165
  )
166
166
  STAMP = self.log_stamp
167
+
168
+ self.log_stamp = STAMP
167
169
 
168
170
  self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
169
171
  LOGFILE = f"fastbot_{STAMP}.log"
@@ -170,6 +170,8 @@ def driver_info_logger(args):
170
170
  print(" log_stamp:", args.log_stamp, flush=True)
171
171
  if args.take_screenshots:
172
172
  print(" take_screenshots:", args.take_screenshots, flush=True)
173
+ if args.max_step:
174
+ print(" max_step:", args.max_step, flush=True)
173
175
 
174
176
 
175
177
  def parse_args(argv: List):
@@ -75,7 +75,19 @@ class LogWatcher:
75
75
  self.end_flag = True
76
76
  if self.t:
77
77
  self.t.join()
78
+
79
+ if not self.statistic_printed:
80
+ self._parse_whole_log()
81
+
82
+ def _parse_whole_log(self):
83
+ logger.warning(
84
+ "LogWatcher closed without reading the statistics, parsing the whole log now."
85
+ )
86
+ with open(self.log_file, "r", encoding="utf-8") as fp:
87
+ content = fp.read()
88
+ self.parse_log(content)
78
89
 
79
90
 
80
91
  if __name__ == "__main__":
81
- LogWatcher("/Users/atria/Desktop/coding/Kea2/output/res_2025062510_0420056539/fastbot_2025062510_0420056539.log")
92
+ # LogWatcher()
93
+ pass
@@ -24,6 +24,47 @@
24
24
  line-height: 1.6;
25
25
  }
26
26
 
27
+ /* Custom container width - wider than Bootstrap default */
28
+ .container {
29
+ max-width: 98% !important;
30
+ width: 98% !important;
31
+ }
32
+
33
+ @media (min-width: 1200px) {
34
+ .container {
35
+ max-width: 1800px !important;
36
+ width: 95% !important;
37
+ }
38
+ }
39
+
40
+ @media (min-width: 1400px) {
41
+ .container {
42
+ max-width: 2000px !important;
43
+ width: 92% !important;
44
+ }
45
+ }
46
+
47
+ @media (min-width: 1600px) {
48
+ .container {
49
+ max-width: 2200px !important;
50
+ width: 90% !important;
51
+ }
52
+ }
53
+
54
+ @media (min-width: 1800px) {
55
+ .container {
56
+ max-width: 2400px !important;
57
+ width: 88% !important;
58
+ }
59
+ }
60
+
61
+ @media (min-width: 2000px) {
62
+ .container {
63
+ max-width: 2600px !important;
64
+ width: 85% !important;
65
+ }
66
+ }
67
+
27
68
  .header {
28
69
  background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
29
70
  color: white;
@@ -81,9 +122,10 @@
81
122
 
82
123
  .screenshot-item {
83
124
  flex: 0 0 auto;
84
- width: 250px;
125
+ width: 300px;
85
126
  position: relative;
86
127
  transition: transform 0.2s;
128
+ margin-bottom: 10px;
87
129
  }
88
130
 
89
131
  .screenshot-item:hover {
@@ -91,30 +133,55 @@
91
133
  }
92
134
 
93
135
  .screenshot-img {
94
- width: 250px;
136
+ width: 300px;
95
137
  height: 400px;
96
138
  object-fit: contain;
97
139
  border-radius: 8px;
98
140
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
141
+ margin-bottom: 8px;
99
142
  }
100
143
 
101
144
  .screenshot-caption {
102
- font-size: 14px;
145
+ font-size: 13px;
103
146
  color: #555;
104
- padding: 8px 4px;
105
- text-overflow: ellipsis;
106
- overflow: hidden;
147
+ padding: 12px 8px;
107
148
  font-weight: 500;
108
149
  text-align: center;
109
150
  background-color: white;
110
- border-radius: 0 0 8px 8px;
151
+ border-radius: 8px;
111
152
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.05);
153
+ line-height: 1.5;
154
+ white-space: normal;
155
+ word-wrap: break-word;
156
+ min-height: 50px;
157
+ display: flex;
158
+ flex-direction: column;
159
+ justify-content: center;
160
+ align-items: center;
161
+ }
162
+
163
+ .screenshot-caption .step-number {
164
+ display: block;
165
+ font-weight: 600;
166
+ color: var(--primary-color);
167
+ font-size: 12px;
168
+ margin-bottom: 4px;
169
+ }
170
+
171
+ .screenshot-caption .step-action {
172
+ display: block;
173
+ font-size: 13px;
174
+ color: #666;
175
+ font-weight: 400;
176
+ line-height: 1.3;
112
177
  }
113
178
 
114
179
  .table-custom {
115
180
  border-radius: 10px;
116
181
  overflow: hidden;
117
182
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
183
+ width: 100%;
184
+ table-layout: auto;
118
185
  }
119
186
 
120
187
  .table-custom thead {
@@ -124,12 +191,53 @@
124
191
 
125
192
  .table-custom th {
126
193
  font-weight: 600;
127
- padding: 15px 20px;
194
+ padding: 15px 12px;
195
+ white-space: nowrap;
196
+ text-align: center;
128
197
  }
129
198
 
130
199
  .table-custom td {
131
- padding: 15px 20px;
200
+ padding: 15px 12px;
132
201
  vertical-align: middle;
202
+ text-align: center;
203
+ }
204
+
205
+ /* Specific column widths for property statistics table */
206
+ .table-custom th:nth-child(1), .table-custom td:nth-child(1) { /* Index */
207
+ width: 8%;
208
+ min-width: 60px;
209
+ }
210
+
211
+ .table-custom th:nth-child(2), .table-custom td:nth-child(2) { /* Property Name */
212
+ width: 25%;
213
+ min-width: 200px;
214
+ text-align: left;
215
+ }
216
+
217
+ .table-custom th:nth-child(3), .table-custom td:nth-child(3) { /* Precondition Satisfied */
218
+ width: 12%;
219
+ min-width: 100px;
220
+ }
221
+
222
+ .table-custom th:nth-child(4), .table-custom td:nth-child(4) { /* Executed */
223
+ width: 10%;
224
+ min-width: 80px;
225
+ }
226
+
227
+ .table-custom th:nth-child(5), .table-custom td:nth-child(5) { /* Fails */
228
+ width: 10%;
229
+ min-width: 80px;
230
+ }
231
+
232
+ .table-custom th:nth-child(6), .table-custom td:nth-child(6) { /* Errors */
233
+ width: 10%;
234
+ min-width: 80px;
235
+ }
236
+
237
+ .table-custom th:nth-child(7), .table-custom td:nth-child(7) { /* Error Details */
238
+ width: 25%;
239
+ min-width: 200px;
240
+ text-align: left;
133
241
  }
134
242
 
135
243
  .table-custom tbody tr:nth-of-type(odd) {
@@ -279,6 +387,28 @@
279
387
  box-shadow: 0 1px 3px rgba(0,0,0,0.05);
280
388
  transition: all 0.2s;
281
389
  line-height: 1.5;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: space-between;
393
+ }
394
+
395
+ .activity-item .activity-content {
396
+ display: flex;
397
+ align-items: center;
398
+ flex: 1;
399
+ min-width: 0;
400
+ }
401
+
402
+ .activity-item .activity-name {
403
+ flex: 1;
404
+ word-break: break-all;
405
+ margin-right: 10px;
406
+ }
407
+
408
+ .activity-item .traversal-badge {
409
+ flex-shrink: 0;
410
+ font-size: 0.85rem;
411
+ font-weight: 500;
282
412
  }
283
413
 
284
414
  .activity-item:hover {
@@ -382,17 +512,64 @@
382
512
  }
383
513
 
384
514
  @media (max-width: 768px) {
515
+ .container {
516
+ max-width: 98% !important;
517
+ width: 98% !important;
518
+ padding-left: 10px !important;
519
+ padding-right: 10px !important;
520
+ }
521
+
385
522
  .stat-value {
386
523
  font-size: 1.5rem;
387
524
  }
388
525
 
389
526
  .screenshot-item {
390
- width: 200px;
527
+ width: 280px;
391
528
  }
392
529
 
393
530
  .screenshot-img {
394
- width: 200px;
395
- height: 320px;
531
+ width: 280px;
532
+ height: 400px;
533
+ }
534
+
535
+ .table-custom {
536
+ font-size: 0.9rem;
537
+ }
538
+
539
+ .table-custom th, .table-custom td {
540
+ padding: 10px 6px;
541
+ }
542
+
543
+ .badge-custom {
544
+ font-size: 0.8rem;
545
+ padding: 4px 8px;
546
+ }
547
+ }
548
+
549
+ @media (max-width: 576px) {
550
+ .container {
551
+ max-width: 100% !important;
552
+ width: 100% !important;
553
+ padding-left: 5px !important;
554
+ padding-right: 5px !important;
555
+ }
556
+
557
+ .screenshot-item {
558
+ width: 260px;
559
+ }
560
+
561
+ .screenshot-img {
562
+ width: 260px;
563
+ height: 380px;
564
+ }
565
+
566
+ .table-custom {
567
+ font-size: 0.8rem;
568
+ }
569
+
570
+ .table-custom th, .table-custom td {
571
+ padding: 8px 4px;
572
+ white-space: normal;
396
573
  }
397
574
  }
398
575
  </style>
@@ -469,6 +646,14 @@
469
646
  </div>
470
647
  </div>
471
648
 
649
+ <!-- Property Execution Trend Chart -->
650
+ <div class="section-block">
651
+ <h2 class="section-title">Property Execution Trend</h2>
652
+ <div class="chart-container">
653
+ <canvas id="propertyExecutionChart"></canvas>
654
+ </div>
655
+ </div>
656
+
472
657
  <!-- Tested Activities List -->
473
658
  <div class="section-block">
474
659
  <h2 class="section-title">Activities Coverage</h2>
@@ -477,10 +662,18 @@
477
662
  <div class="card-header bg-primary text-white">
478
663
  <div class="d-flex justify-content-between align-items-center">
479
664
  <span><i class="bi bi-app"></i> Activities Coverage Overview</span>
480
- <span class="badge bg-light text-dark">Coverage: {{ coverage_percent }}%</span>
665
+ <span class="badge bg-light text-dark" style="font-size: 1.1em; font-weight: 600;">Coverage: {{ coverage_percent }}%</span>
481
666
  </div>
482
667
  </div>
483
668
  <div class="card-body">
669
+ <div class="alert alert-info mb-3" style="border-left: 4px solid #17a2b8; background-color: #f8f9fa;">
670
+ <small class="text-muted">
671
+ <i class="bi bi-info-circle me-1"></i>
672
+ <strong>Traversal Count Explanation:</strong>
673
+ The number after the <i class="bi bi-eye"></i> icon indicates how many times each Activity was visited during testing.
674
+ </small>
675
+ </div>
676
+
484
677
  <!-- Navigation Tabs -->
485
678
  <ul class="nav nav-tabs mb-3" id="activitiesTabs" role="tablist">
486
679
  <li class="nav-item" role="presentation">
@@ -517,8 +710,15 @@
517
710
  <div id="tested-activities-container">
518
711
  {% for activity in tested_activities %}
519
712
  <div class="activity-item tested-activity" data-page="1">
520
- <i class="bi bi-check-circle-fill text-success me-2"></i>
521
- {{ activity }}
713
+ <div class="activity-content">
714
+ <i class="bi bi-check-circle-fill text-success me-2"></i>
715
+ <span class="activity-name">{{ activity }}</span>
716
+ </div>
717
+ {% if activity in activity_count_history %}
718
+ <span class="badge bg-info text-white traversal-badge">
719
+ <i class="bi bi-eye"></i> {{ activity_count_history[activity] }} times
720
+ </span>
721
+ {% endif %}
522
722
  </div>
523
723
  {% endfor %}
524
724
  </div>
@@ -565,12 +765,19 @@
565
765
  <div id="all-activities-container">
566
766
  {% for activity in total_activities %}
567
767
  <div class="activity-item all-activity" data-page="1">
568
- {% if activity in tested_activities %}
569
- <i class="bi bi-check-circle-fill text-success me-2"></i>
570
- {% else %}
571
- <i class="bi bi-dash-circle text-secondary me-2"></i>
768
+ <div class="activity-content">
769
+ {% if activity in tested_activities %}
770
+ <i class="bi bi-check-circle-fill text-success me-2"></i>
771
+ {% else %}
772
+ <i class="bi bi-dash-circle text-secondary me-2"></i>
773
+ {% endif %}
774
+ <span class="activity-name">{{ activity }}</span>
775
+ </div>
776
+ {% if activity in activity_count_history %}
777
+ <span class="badge bg-info text-white traversal-badge">
778
+ <i class="bi bi-eye"></i> {{ activity_count_history[activity] }} times
779
+ </span>
572
780
  {% endif %}
573
- {{ activity }}
574
781
  </div>
575
782
  {% endfor %}
576
783
  </div>
@@ -730,6 +937,11 @@
730
937
  Occurred {{ error_list[0].occurrence_count }} times
731
938
  </span>
732
939
  {% endif %}
940
+ {% if error_list[0].startStepsCountList is defined and error_list[0].startStepsCountList|length > 0 %}
941
+ <span class="badge bg-secondary ms-2">
942
+ <i class="bi bi-step-forward"></i> Monkey Steps: {{ error_list[0].startStepsCountList|join(', ') }}
943
+ </span>
944
+ {% endif %}
733
945
  </div>
734
946
  {% if error_list[0].short_description %}
735
947
  <div class="mb-2">
@@ -759,6 +971,9 @@
759
971
  <span class="badge bg-{{ 'danger' if error.state == 'fail' else 'warning' }}">
760
972
  {{ error.state|upper }} #{{ loop.index }}
761
973
  {% if error.occurrence_count > 1 %} ({{ error.occurrence_count }}x){% endif %}
974
+ {% if error.startStepsCountList is defined and error.startStepsCountList|length > 0 %}
975
+ @{% if error.startStepsCountList|length == 1 %}{{ error.startStepsCountList[0] }}{% else %}{{ error.startStepsCountList[0] }}-{{ error.startStepsCountList[-1] }}{% endif %}
976
+ {% endif %}
762
977
  </span>
763
978
  {% endfor %}
764
979
  </div>
@@ -801,14 +1016,19 @@
801
1016
  {{ error.occurrence_count }} occurrences
802
1017
  </span>
803
1018
  {% endif %}
1019
+ {% if error.startStepsCountList is defined and error.startStepsCountList|length > 0 %}
1020
+ <span class="badge bg-secondary ms-2">
1021
+ <i class="bi bi-step-forward"></i> Monkey Steps: {{ error.startStepsCountList|join(', ') }}
1022
+ </span>
1023
+ {% endif %}
804
1024
  </div>
805
1025
  {% if error.short_description %}
806
1026
  <div class="mb-2">
807
1027
  <strong>Error:</strong> <code>{{ error.short_description }}</code>
808
1028
  </div>
809
1029
  {% endif %}
810
- <details open="{{ loop.first }}">
811
- <summary class="btn btn-sm btn-outline-secondary mb-2">{{ 'Hide' if loop.first else 'Show' }} Full Traceback</summary>
1030
+ <details>
1031
+ <summary class="btn btn-sm btn-outline-secondary mb-2">Show Full Traceback</summary>
812
1032
  <pre class="text-danger mb-0" style="font-size: 0.85rem; white-space: pre-wrap;">{{ error.traceback }}</pre>
813
1033
  </details>
814
1034
  </div>
@@ -1019,6 +1239,143 @@
1019
1239
  }
1020
1240
  });
1021
1241
 
1242
+ // Draw property execution trend chart
1243
+ var propertyExecutionData = {{ property_execution_data|safe }};
1244
+ console.log("Property execution data points:", propertyExecutionData.length);
1245
+
1246
+ // Ensure we have valid data
1247
+ if (propertyExecutionData.length === 0) {
1248
+ propertyExecutionData = [{"stepsCount": 0, "executedPropertiesCount": 0}];
1249
+ }
1250
+
1251
+ propertyExecutionData.sort(function(a, b) {
1252
+ return a.stepsCount - b.stepsCount;
1253
+ });
1254
+
1255
+ var propSteps = propertyExecutionData.map(function(item) { return item.stepsCount; });
1256
+ var executedProps = propertyExecutionData.map(function(item) { return item.executedPropertiesCount; });
1257
+
1258
+ // Add zero starting point if needed
1259
+ if (propSteps.length > 0 && propSteps[0] > 0) {
1260
+ propSteps.unshift(0);
1261
+ executedProps.unshift(0);
1262
+ }
1263
+
1264
+ console.log("Property execution steps:", propSteps);
1265
+ console.log("Executed properties values:", executedProps);
1266
+
1267
+ var propCtx = document.getElementById('propertyExecutionChart').getContext('2d');
1268
+ var propChart = new Chart(propCtx, {
1269
+ type: 'line',
1270
+ data: {
1271
+ labels: propSteps,
1272
+ datasets: [
1273
+ {
1274
+ label: 'Executed Properties',
1275
+ data: executedProps.map((value, index) => ({x: propSteps[index], y: value})),
1276
+ borderColor: '#e74c3c',
1277
+ backgroundColor: 'rgba(231, 76, 60, 0.1)',
1278
+ borderWidth: 3,
1279
+ fill: true,
1280
+ tension: 0.4,
1281
+ pointRadius: 4,
1282
+ pointHoverRadius: 6
1283
+ }
1284
+ ]
1285
+ },
1286
+ options: {
1287
+ responsive: true,
1288
+ maintainAspectRatio: false,
1289
+ aspectRatio: 2,
1290
+ plugins: {
1291
+ legend: {
1292
+ position: 'top',
1293
+ labels: {
1294
+ boxWidth: 15,
1295
+ usePointStyle: true,
1296
+ pointStyle: 'circle'
1297
+ }
1298
+ },
1299
+ tooltip: {
1300
+ mode: 'index',
1301
+ intersect: false,
1302
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
1303
+ titleColor: '#fff',
1304
+ bodyColor: '#fff',
1305
+ borderColor: 'rgba(255, 255, 255, 0.2)',
1306
+ borderWidth: 1,
1307
+ padding: 10,
1308
+ displayColors: true,
1309
+ callbacks: {
1310
+ label: function(context) {
1311
+ return 'Executed Properties: ' + context.parsed.y;
1312
+ }
1313
+ }
1314
+ }
1315
+ },
1316
+ scales: {
1317
+ x: {
1318
+ type: 'linear',
1319
+ beginAtZero: true,
1320
+ max: propSteps.length > 0 ? Math.max(...propSteps) : 10,
1321
+ grid: {
1322
+ display: false
1323
+ },
1324
+ title: {
1325
+ display: true,
1326
+ text: 'Steps Count',
1327
+ font: {
1328
+ size: 14
1329
+ }
1330
+ },
1331
+ ticks: {
1332
+ stepSize: propSteps.length > 0 ? Math.max(1, Math.ceil(Math.max(...propSteps) / 10)) : 1,
1333
+ callback: function(value) {
1334
+ if (Number.isInteger(value) && value <= (propSteps.length > 0 ? Math.max(...propSteps) : 10)) {
1335
+ return value;
1336
+ }
1337
+ return null;
1338
+ }
1339
+ }
1340
+ },
1341
+ y: {
1342
+ beginAtZero: true,
1343
+ title: {
1344
+ display: true,
1345
+ text: 'Executed Properties',
1346
+ font: {
1347
+ size: 14
1348
+ }
1349
+ },
1350
+ grid: {
1351
+ borderDash: [5, 5]
1352
+ },
1353
+ ticks: {
1354
+ stepSize: 1,
1355
+ callback: function(value) {
1356
+ if (Number.isInteger(value)) {
1357
+ return value;
1358
+ }
1359
+ return null;
1360
+ }
1361
+ }
1362
+ }
1363
+ },
1364
+ interaction: {
1365
+ mode: 'index',
1366
+ intersect: false
1367
+ },
1368
+ hover: {
1369
+ mode: 'index',
1370
+ intersect: false
1371
+ },
1372
+ animation: {
1373
+ duration: 1000,
1374
+ easing: 'easeOutQuart'
1375
+ }
1376
+ }
1377
+ });
1378
+
1022
1379
  // Initialize pagination for Activities lists
1023
1380
  initPagination('tested-activities-container', 'tested-activity', 'tested-pagination', 'tested-page-size');
1024
1381
  initPagination('all-activities-container', 'all-activity', 'all-pagination', 'all-page-size');
@@ -1241,6 +1598,56 @@
1241
1598
  }
1242
1599
  });
1243
1600
  }
1601
+
1602
+ // Beautify screenshot captions
1603
+ function beautifyScreenshotCaptions() {
1604
+ const captions = document.querySelectorAll('.screenshot-caption');
1605
+ captions.forEach(function(caption) {
1606
+ const originalText = caption.textContent.trim();
1607
+
1608
+ // Handle Monkey Step format: "1. Monkey Step 6: SCROLL_TOP_DOWN"
1609
+ const monkeyStepMatch = originalText.match(/^(\d+)\.\s*Monkey Step (\d+):\s*(.+)$/);
1610
+ if (monkeyStepMatch) {
1611
+ const stepIndex = monkeyStepMatch[1];
1612
+ const monkeyStep = monkeyStepMatch[2];
1613
+ const action = monkeyStepMatch[3];
1614
+
1615
+ caption.innerHTML = `
1616
+ <div class="step-number">${stepIndex}. Monkey Step ${monkeyStep}</div>
1617
+ <div class="step-action">${action}</div>
1618
+ `;
1619
+ return;
1620
+ }
1621
+
1622
+ // Handle Script format: "1. click" or similar
1623
+ const scriptMatch = originalText.match(/^(\d+)\.\s*(.+)$/);
1624
+ if (scriptMatch) {
1625
+ const stepIndex = scriptMatch[1];
1626
+ const action = scriptMatch[2];
1627
+
1628
+ // Check if it's a property info
1629
+ if (action.includes(':')) {
1630
+ const parts = action.split(':');
1631
+ if (parts.length === 2) {
1632
+ caption.innerHTML = `
1633
+ <div class="step-number">${stepIndex}. ${parts[0].trim()}</div>
1634
+ <div class="step-action">${parts[1].trim()}</div>
1635
+ `;
1636
+ return;
1637
+ }
1638
+ }
1639
+
1640
+ // Simple action
1641
+ caption.innerHTML = `
1642
+ <div class="step-number">${stepIndex}. Script Step</div>
1643
+ <div class="step-action">${action}</div>
1644
+ `;
1645
+ }
1646
+ });
1647
+ }
1648
+
1649
+ // Call beautify function after DOM is ready
1650
+ beautifyScreenshotCaptions();
1244
1651
  });
1245
1652
  </script>
1246
1653
  </body>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Kea2-python"
3
- version = "0.2.3"
3
+ version = "0.2.4"
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"
Binary file
File without changes
File without changes
File without changes