@biggora/claude-plugins 1.0.0 → 1.1.0

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 (80) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/CLAUDE.md +55 -0
  3. package/LICENSE +1 -1
  4. package/README.md +208 -39
  5. package/bin/cli.js +39 -0
  6. package/package.json +30 -17
  7. package/registry/registry.json +166 -1
  8. package/registry/schema.json +10 -0
  9. package/src/commands/skills/add.js +194 -0
  10. package/src/commands/skills/list.js +52 -0
  11. package/src/commands/skills/remove.js +27 -0
  12. package/src/commands/skills/update.js +74 -0
  13. package/src/config.js +5 -0
  14. package/src/skills/codex-cli/SKILL.md +265 -0
  15. package/src/skills/commafeed-api/SKILL.md +1012 -0
  16. package/src/skills/gemini-cli/SKILL.md +379 -0
  17. package/src/skills/gemini-cli/references/commands.md +145 -0
  18. package/src/skills/gemini-cli/references/configuration.md +182 -0
  19. package/src/skills/gemini-cli/references/headless-and-scripting.md +181 -0
  20. package/src/skills/gemini-cli/references/mcp-and-extensions.md +254 -0
  21. package/src/skills/n8n-api/SKILL.md +623 -0
  22. package/src/skills/notebook-lm/SKILL.md +217 -0
  23. package/src/skills/notebook-lm/references/artifact-options.md +168 -0
  24. package/src/skills/notebook-lm/references/auth.md +58 -0
  25. package/src/skills/notebook-lm/references/workflows.md +144 -0
  26. package/src/skills/screen-recording/SKILL.md +309 -0
  27. package/src/skills/screen-recording/references/approach1-programmatic.md +311 -0
  28. package/src/skills/screen-recording/references/approach2-xvfb.md +232 -0
  29. package/src/skills/screen-recording/references/design-patterns.md +168 -0
  30. package/src/skills/test-mobile-app/SKILL.md +212 -0
  31. package/src/skills/test-mobile-app/references/report-template.md +95 -0
  32. package/src/skills/test-mobile-app/references/setup-appium.md +154 -0
  33. package/src/skills/test-mobile-app/scripts/analyze_apk.py +164 -0
  34. package/src/skills/test-mobile-app/scripts/check_environment.py +116 -0
  35. package/src/skills/test-mobile-app/scripts/generate_report.py +250 -0
  36. package/src/skills/test-mobile-app/scripts/run_tests.py +326 -0
  37. package/src/skills/test-web-ui/SKILL.md +232 -0
  38. package/src/skills/test-web-ui/references/test_case_schema.md +102 -0
  39. package/src/skills/test-web-ui/scripts/discover.py +176 -0
  40. package/src/skills/test-web-ui/scripts/generate_report.py +237 -0
  41. package/src/skills/test-web-ui/scripts/run_tests.py +296 -0
  42. package/src/skills/text-to-speech/SKILL.md +236 -0
  43. package/src/skills/text-to-speech/references/espeak-cli.md +277 -0
  44. package/src/skills/text-to-speech/references/kokoro-onnx.md +124 -0
  45. package/src/skills/text-to-speech/references/online-engines.md +128 -0
  46. package/src/skills/text-to-speech/references/pyttsx3-espeak.md +143 -0
  47. package/src/skills/tm-search/SKILL.md +240 -0
  48. package/src/skills/tm-search/references/field-guide.md +79 -0
  49. package/src/skills/tm-search/references/scraping-fallback.md +140 -0
  50. package/src/skills/tm-search/scripts/tm_search.py +375 -0
  51. package/src/skills/wp-rest-api/SKILL.md +114 -0
  52. package/src/skills/wp-rest-api/references/authentication.md +18 -0
  53. package/src/skills/wp-rest-api/references/custom-content-types.md +20 -0
  54. package/src/skills/wp-rest-api/references/discovery-and-params.md +20 -0
  55. package/src/skills/wp-rest-api/references/responses-and-fields.md +30 -0
  56. package/src/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
  57. package/src/skills/wp-rest-api/references/schema.md +22 -0
  58. package/src/skills/youtube-search/SKILL.md +412 -0
  59. package/src/skills/youtube-search/references/parsing-examples.md +159 -0
  60. package/src/skills/youtube-search/references/youtube-api-quota.md +85 -0
  61. package/src/skills/youtube-thumbnail/SKILL.md +1060 -0
  62. package/tests/commands/info.test.js +49 -0
  63. package/tests/commands/install.test.js +36 -0
  64. package/tests/commands/list.test.js +66 -0
  65. package/tests/commands/publish.test.js +182 -0
  66. package/tests/commands/search.test.js +45 -0
  67. package/tests/commands/uninstall.test.js +29 -0
  68. package/tests/commands/update.test.js +59 -0
  69. package/tests/functional/skills-lifecycle.test.js +293 -0
  70. package/tests/helpers/fixtures.js +63 -0
  71. package/tests/integration/cli.test.js +83 -0
  72. package/tests/skills/add.test.js +138 -0
  73. package/tests/skills/list.test.js +63 -0
  74. package/tests/skills/remove.test.js +38 -0
  75. package/tests/skills/update.test.js +60 -0
  76. package/tests/unit/config.test.js +31 -0
  77. package/tests/unit/registry.test.js +79 -0
  78. package/tests/unit/utils.test.js +150 -0
  79. package/tests/validation/registry-schema.test.js +112 -0
  80. package/tests/validation/skills-validation.test.js +96 -0
@@ -0,0 +1,154 @@
1
+ # Appium + Android Emulator Setup Guide
2
+
3
+ ## Quick Start
4
+
5
+ ### 1. Install Java (required for Android SDK)
6
+ ```bash
7
+ sudo apt-get install -y openjdk-11-jdk
8
+ export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
9
+ ```
10
+
11
+ ### 2. Install Android SDK Command-line Tools
12
+ ```bash
13
+ # Download from https://developer.android.com/studio#command-tools
14
+ # Or use sdkmanager:
15
+ sdkmanager "platform-tools" "emulator" "system-images;android-30;google_apis;x86_64"
16
+ ```
17
+
18
+ Set environment:
19
+ ```bash
20
+ export ANDROID_HOME=$HOME/Android/Sdk
21
+ export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin
22
+ ```
23
+
24
+ ### 3. Create an Android Virtual Device (AVD)
25
+ ```bash
26
+ # List available system images
27
+ avdmanager list target
28
+
29
+ # Create AVD
30
+ avdmanager create avd \
31
+ --name "TestDevice_API30" \
32
+ --package "system-images;android-30;google_apis;x86_64" \
33
+ --device "pixel_4"
34
+
35
+ # Start emulator
36
+ emulator -avd TestDevice_API30 -no-snapshot-load &
37
+
38
+ # Wait for boot
39
+ adb wait-for-device
40
+ adb shell getprop sys.boot_completed # wait until "1"
41
+ ```
42
+
43
+ ### 4. Install Node.js and Appium
44
+ ```bash
45
+ # Node.js (if not installed)
46
+ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
47
+ sudo apt-get install -y nodejs
48
+
49
+ # Appium
50
+ npm install -g appium
51
+
52
+ # UiAutomator2 driver (for Android)
53
+ appium driver install uiautomator2
54
+
55
+ # XCUITest driver (for iOS — macOS only)
56
+ # appium driver install xcuitest
57
+ ```
58
+
59
+ ### 5. Install Python client
60
+ ```bash
61
+ pip install Appium-Python-Client --break-system-packages
62
+ ```
63
+
64
+ ### 6. Start Appium Server
65
+ ```bash
66
+ appium --base-path /wd/hub --port 4723
67
+ # Or in background:
68
+ appium --base-path /wd/hub --port 4723 > appium.log 2>&1 &
69
+ ```
70
+
71
+ ### 7. Verify Setup
72
+ ```bash
73
+ python3 scripts/check_environment.py
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Desired Capabilities Reference
79
+
80
+ ```python
81
+ from appium.options import AppiumOptions
82
+
83
+ options = AppiumOptions()
84
+ options.platform_name = "Android"
85
+ options.automation_name = "UiAutomator2"
86
+ options.app = "/abs/path/to/app.apk"
87
+ options.device_name = "emulator-5554" # from `adb devices`
88
+ options.no_reset = False # reinstall app each run
89
+ options.full_reset = False # keep app data between runs if True
90
+ options.new_command_timeout = 60 # seconds before timeout
91
+ options.auto_grant_permissions = True # auto-grant runtime permissions
92
+ ```
93
+
94
+ For iOS:
95
+ ```python
96
+ options.platform_name = "iOS"
97
+ options.automation_name = "XCUITest"
98
+ options.platform_version = "16.0"
99
+ options.device_name = "iPhone 14"
100
+ options.bundle_id = "com.example.app"
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Element Locator Strategies (priority order)
106
+
107
+ 1. **accessibility id** — best, platform-independent
108
+ ```python
109
+ driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Login Button")
110
+ ```
111
+
112
+ 2. **id / resource-id** — Android: `com.example.app:id/login_button`
113
+ ```python
114
+ driver.find_element(AppiumBy.ID, "com.example.app:id/login_btn")
115
+ ```
116
+
117
+ 3. **uiautomator** — powerful Android selector
118
+ ```python
119
+ driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Login")')
120
+ ```
121
+
122
+ 4. **xpath** — use sparingly, fragile
123
+ ```python
124
+ driver.find_element(AppiumBy.XPATH, '//android.widget.Button[@text="Login"]')
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Common Issues
130
+
131
+ | Problem | Solution |
132
+ |---------|----------|
133
+ | `adb: command not found` | Add `$ANDROID_HOME/platform-tools` to PATH |
134
+ | Emulator stuck at boot | Run `adb reboot` or recreate AVD |
135
+ | `Could not start a new session` | Check Appium server is running at port 4723 |
136
+ | `UiAutomator2 not installed` | Run `appium driver install uiautomator2` |
137
+ | App crashes on install | Check minSdk in APK vs emulator API level |
138
+ | Elements not found | Increase implicit wait or use explicit `WebDriverWait` |
139
+ | Permission dialogs blocking test | Set `options.auto_grant_permissions = True` |
140
+
141
+ ---
142
+
143
+ ## iOS Notes (macOS only)
144
+
145
+ iOS testing requires Xcode + iOS Simulator and is only possible on macOS.
146
+ For CI/CD: use a macOS GitHub Actions runner or a Mac build machine.
147
+
148
+ ```bash
149
+ # Install xcpretty
150
+ gem install xcpretty
151
+
152
+ # Build + install .ipa to simulator
153
+ xcrun simctl install booted path/to/app.ipa
154
+ ```
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ analyze_apk.py — Extract structure from Android APK for use case generation.
4
+ Usage: python3 analyze_apk.py path/to/app.apk [--json]
5
+ """
6
+ import sys
7
+ import json
8
+ import argparse
9
+
10
+ def analyze_apk(apk_path: str) -> dict:
11
+ result = {
12
+ "package": None,
13
+ "version": None,
14
+ "activities": [],
15
+ "permissions": [],
16
+ "services": [],
17
+ "receivers": [],
18
+ "providers": [],
19
+ "strings_sample": [],
20
+ "min_sdk": None,
21
+ "target_sdk": None,
22
+ }
23
+
24
+ try:
25
+ from androguard.core.bytecodes.apk import APK
26
+ apk = APK(apk_path)
27
+
28
+ result["package"] = apk.get_package()
29
+ result["version"] = apk.get_androidversion_name()
30
+ result["min_sdk"] = apk.get_min_sdk_version()
31
+ result["target_sdk"] = apk.get_target_sdk_version()
32
+
33
+ # Activities
34
+ for act in apk.get_activities():
35
+ is_main = act == apk.get_main_activity()
36
+ result["activities"].append({
37
+ "name": act.split(".")[-1],
38
+ "full_name": act,
39
+ "is_main": is_main
40
+ })
41
+
42
+ # Permissions
43
+ result["permissions"] = list(apk.get_permissions())
44
+
45
+ # Services
46
+ result["services"] = [s.split(".")[-1] for s in apk.get_services()]
47
+
48
+ # Broadcast receivers
49
+ result["receivers"] = [r.split(".")[-1] for r in apk.get_receivers()]
50
+
51
+ # String resources (sample)
52
+ try:
53
+ for lang, strings in apk.get_strings_analysis().items():
54
+ sample = list(strings.keys())[:30]
55
+ result["strings_sample"] = sample
56
+ break
57
+ except Exception:
58
+ pass
59
+
60
+ except ImportError:
61
+ result["error"] = "androguard not installed. Run: pip install androguard --break-system-packages"
62
+ except Exception as e:
63
+ result["error"] = str(e)
64
+
65
+ return result
66
+
67
+
68
+ def print_human_readable(data: dict):
69
+ print(f"\n{'='*60}")
70
+ print(f" APK ANALYSIS REPORT")
71
+ print(f"{'='*60}")
72
+ print(f"Package: {data.get('package', 'unknown')}")
73
+ print(f"Version: {data.get('version', 'unknown')}")
74
+ print(f"Min SDK: {data.get('min_sdk', '?')} | Target SDK: {data.get('target_sdk', '?')}")
75
+
76
+ activities = data.get("activities", [])
77
+ print(f"\n📱 SCREENS / ACTIVITIES ({len(activities)}):")
78
+ for act in activities:
79
+ marker = " ← MAIN" if act.get("is_main") else ""
80
+ print(f" • {act['name']}{marker}")
81
+
82
+ permissions = data.get("permissions", [])
83
+ if permissions:
84
+ print(f"\n🔒 PERMISSIONS ({len(permissions)}):")
85
+ for p in permissions:
86
+ short = p.replace("android.permission.", "")
87
+ print(f" • {short}")
88
+
89
+ services = data.get("services", [])
90
+ if services:
91
+ print(f"\n⚙️ SERVICES: {', '.join(services)}")
92
+
93
+ strings = data.get("strings_sample", [])
94
+ if strings:
95
+ print(f"\n📝 STRING KEYS (sample): {', '.join(strings[:15])}")
96
+
97
+ if "error" in data:
98
+ print(f"\n⚠️ Error: {data['error']}")
99
+
100
+ print(f"\n{'='*60}")
101
+
102
+ # Inferred features
103
+ print("\n💡 INFERRED FEATURES (for use case generation):")
104
+ perms = " ".join(data.get("permissions", []))
105
+ acts = [a["name"].lower() for a in activities]
106
+
107
+ hints = []
108
+ if "CAMERA" in perms: hints.append("📷 Camera / photo capture")
109
+ if "READ_CONTACTS" in perms or "WRITE_CONTACTS" in perms: hints.append("👥 Contacts access")
110
+ if "INTERNET" in perms: hints.append("🌐 Network/API calls")
111
+ if "ACCESS_FINE_LOCATION" in perms or "ACCESS_COARSE_LOCATION" in perms: hints.append("📍 Location/maps")
112
+ if "READ_EXTERNAL_STORAGE" in perms or "WRITE_EXTERNAL_STORAGE" in perms: hints.append("💾 File storage")
113
+ if "RECORD_AUDIO" in perms: hints.append("🎙️ Audio recording")
114
+ if "SEND_SMS" in perms or "RECEIVE_SMS" in perms: hints.append("💬 SMS")
115
+ if "VIBRATE" in perms: hints.append("📳 Notifications")
116
+ if "USE_BIOMETRIC" in perms or "USE_FINGERPRINT" in perms: hints.append("🔐 Biometric auth")
117
+
118
+ for act_name in acts:
119
+ if "login" in act_name or "auth" in act_name or "sign" in act_name:
120
+ hints.append("🔑 Authentication screen detected")
121
+ if "register" in act_name or "signup" in act_name:
122
+ hints.append("📝 Registration screen detected")
123
+ if "main" in act_name or "home" in act_name or "dashboard" in act_name:
124
+ hints.append("🏠 Main/Dashboard screen")
125
+ if "settings" in act_name or "profile" in act_name:
126
+ hints.append("⚙️ Settings/Profile screen")
127
+ if "payment" in act_name or "checkout" in act_name:
128
+ hints.append("💳 Payment/Checkout flow")
129
+ if "map" in act_name or "location" in act_name:
130
+ hints.append("🗺️ Map screen")
131
+ if "list" in act_name or "feed" in act_name:
132
+ hints.append("📋 List/Feed screen")
133
+ if "detail" in act_name:
134
+ hints.append("🔍 Detail view")
135
+ if "search" in act_name:
136
+ hints.append("🔍 Search functionality")
137
+ if "chat" in act_name or "message" in act_name:
138
+ hints.append("💬 Messaging/Chat")
139
+ if "notification" in act_name:
140
+ hints.append("🔔 Notification screen")
141
+
142
+ # Deduplicate
143
+ seen = set()
144
+ for h in hints:
145
+ if h not in seen:
146
+ print(f" {h}")
147
+ seen.add(h)
148
+
149
+ if not hints:
150
+ print(" (Could not infer specific features — generate use cases from activity names)")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ parser = argparse.ArgumentParser(description="Analyze APK for mobile testing")
155
+ parser.add_argument("apk", help="Path to APK file")
156
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
157
+ args = parser.parse_args()
158
+
159
+ data = analyze_apk(args.apk)
160
+
161
+ if args.json:
162
+ print(json.dumps(data, indent=2))
163
+ else:
164
+ print_human_readable(data)
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ check_environment.py — Verify all dependencies for mobile testing are available.
4
+ """
5
+ import subprocess
6
+ import sys
7
+ import json
8
+ import shutil
9
+
10
+ def check(label: str, fn) -> dict:
11
+ try:
12
+ result = fn()
13
+ return {"label": label, "ok": True, "detail": result}
14
+ except Exception as e:
15
+ return {"label": label, "ok": False, "detail": str(e)}
16
+
17
+ def check_adb():
18
+ if not shutil.which("adb"):
19
+ raise RuntimeError("adb not found — install Android SDK Platform Tools")
20
+ out = subprocess.run(["adb", "version"], capture_output=True, text=True)
21
+ return out.stdout.strip().split("\n")[0]
22
+
23
+ def check_emulator():
24
+ if not shutil.which("emulator"):
25
+ raise RuntimeError("emulator binary not found — install Android SDK Emulator")
26
+ out = subprocess.run(["adb", "devices"], capture_output=True, text=True)
27
+ devices = [l for l in out.stdout.strip().split("\n")[1:] if l.strip()]
28
+ if not devices:
29
+ raise RuntimeError("No devices/emulators connected. Run an emulator first.")
30
+ return f"{len(devices)} device(s): {', '.join(d.split()[0] for d in devices)}"
31
+
32
+ def check_appium_server():
33
+ import urllib.request
34
+ try:
35
+ resp = urllib.request.urlopen("http://localhost:4723/status", timeout=3)
36
+ data = json.loads(resp.read())
37
+ return f"Appium running — {data.get('value', {}).get('build', {}).get('version', 'unknown')}"
38
+ except Exception:
39
+ raise RuntimeError("Appium server not running. Start with: appium --base-path /wd/hub")
40
+
41
+ def check_appium_python():
42
+ import appium
43
+ return f"appium-python-client {appium.__version__}"
44
+
45
+ def check_python_deps():
46
+ missing = []
47
+ for pkg in ["pytest", "jinja2", "PIL"]:
48
+ try:
49
+ __import__(pkg)
50
+ except ImportError:
51
+ missing.append(pkg)
52
+ if missing:
53
+ raise RuntimeError(f"Missing packages: {missing}. Run: pip install {' '.join(missing)} --break-system-packages")
54
+ return "All Python deps OK"
55
+
56
+ def check_avd():
57
+ """List available Android Virtual Devices"""
58
+ if not shutil.which("avdmanager"):
59
+ raise RuntimeError("avdmanager not found — install Android SDK")
60
+ out = subprocess.run(["avdmanager", "list", "avd"], capture_output=True, text=True)
61
+ avds = [l.strip() for l in out.stdout.split("\n") if "Name:" in l]
62
+ if not avds:
63
+ raise RuntimeError("No AVDs configured. Create one with: avdmanager create avd -n test -k 'system-images;android-30;google_apis;x86_64'")
64
+ return f"{len(avds)} AVD(s): {', '.join(a.replace('Name:', '').strip() for a in avds)}"
65
+
66
+ def main():
67
+ checks = [
68
+ ("ADB", check_adb),
69
+ ("Android Emulator Binary", check_emulator),
70
+ ("Connected Device/Emulator", check_emulator),
71
+ ("Appium Server", check_appium_server),
72
+ ("Appium Python Client", check_appium_python),
73
+ ("Python Dependencies", check_python_deps),
74
+ ("Android Virtual Devices", check_avd),
75
+ ]
76
+
77
+ print("\n🔍 MOBILE TESTING ENVIRONMENT CHECK")
78
+ print("=" * 50)
79
+
80
+ all_ok = True
81
+ critical_missing = []
82
+
83
+ for label, fn in checks:
84
+ r = check(label, fn)
85
+ icon = "✅" if r["ok"] else "❌"
86
+ print(f"{icon} {label}")
87
+ if r["ok"]:
88
+ print(f" {r['detail']}")
89
+ else:
90
+ print(f" ⚠️ {r['detail']}")
91
+ all_ok = False
92
+ if label in ("ADB", "Appium Python Client"):
93
+ critical_missing.append(label)
94
+
95
+ print("=" * 50)
96
+
97
+ if all_ok:
98
+ print("\n✅ All checks passed — ready for automated testing!")
99
+ elif not critical_missing:
100
+ print("\n⚠️ Some optional components missing — can run in STATIC mode.")
101
+ print(" Use: python3 run_tests.py --static")
102
+ else:
103
+ print(f"\n❌ Critical missing: {critical_missing}")
104
+ print(" Install missing tools and re-run.")
105
+ print("\n📖 See references/setup-appium.md for setup instructions.")
106
+ sys.exit(1)
107
+
108
+ # Summary JSON for scripts
109
+ return {
110
+ "all_ok": all_ok,
111
+ "critical_missing": critical_missing,
112
+ "mode": "automated" if all_ok else "static"
113
+ }
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ generate_report.py — Generate a rich HTML test report from results.json.
4
+ Usage: python3 generate_report.py --results results/ --output test_report.html
5
+ """
6
+ import argparse
7
+ import json
8
+ import os
9
+ import datetime
10
+ from pathlib import Path
11
+
12
+ HTML_TEMPLATE = """<!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="UTF-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <title>Mobile Test Report — {app_name}</title>
18
+ <style>
19
+ :root {{
20
+ --pass: #22c55e; --fail: #ef4444; --error: #f97316;
21
+ --manual: #8b5cf6; --skip: #94a3b8; --bg: #0f172a;
22
+ --card: #1e293b; --border: #334155; --text: #e2e8f0;
23
+ }}
24
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
25
+ body {{ font-family: 'Inter', system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 24px; }}
26
+ h1 {{ font-size: 1.8rem; margin-bottom: 4px; color: #f8fafc; }}
27
+ .subtitle {{ color: #94a3b8; margin-bottom: 24px; font-size: 0.9rem; }}
28
+ .summary-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 16px; margin-bottom: 32px; }}
29
+ .stat-card {{ background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px; text-align: center; }}
30
+ .stat-card .number {{ font-size: 2.4rem; font-weight: 700; }}
31
+ .stat-card .label {{ font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; }}
32
+ .pass {{ color: var(--pass); }} .fail {{ color: var(--fail); }}
33
+ .error {{ color: var(--error); }} .manual {{ color: var(--manual); }}
34
+ .skip {{ color: var(--skip); }}
35
+ .progress-bar {{ height: 8px; border-radius: 4px; background: var(--border); overflow: hidden; margin-bottom: 32px; display: flex; }}
36
+ .pb-pass {{ background: var(--pass); }} .pb-fail {{ background: var(--fail); }}
37
+ .pb-error {{ background: var(--error); }} .pb-manual {{ background: var(--manual); }}
38
+ .section-title {{ font-size: 1.1rem; font-weight: 600; margin-bottom: 16px; color: #f1f5f9; border-bottom: 1px solid var(--border); padding-bottom: 8px; }}
39
+ .test-card {{ background: var(--card); border: 1px solid var(--border); border-radius: 10px; margin-bottom: 12px; overflow: hidden; }}
40
+ .test-header {{ display: flex; align-items: center; gap: 12px; padding: 14px 16px; cursor: pointer; user-select: none; }}
41
+ .test-header:hover {{ background: #263148; }}
42
+ .status-badge {{ font-size: 0.7rem; font-weight: 700; padding: 3px 10px; border-radius: 20px; text-transform: uppercase; letter-spacing: 0.05em; }}
43
+ .badge-PASS {{ background: #14532d; color: var(--pass); }}
44
+ .badge-FAIL {{ background: #7f1d1d; color: var(--fail); }}
45
+ .badge-ERROR {{ background: #431407; color: var(--error); }}
46
+ .badge-MANUAL_REQUIRED {{ background: #2e1065; color: var(--manual); }}
47
+ .badge-SKIP {{ background: #1e293b; color: var(--skip); }}
48
+ .test-id {{ color: #64748b; font-family: monospace; font-size: 0.85rem; }}
49
+ .test-title {{ font-size: 0.95rem; flex: 1; }}
50
+ .duration {{ color: #64748b; font-size: 0.8rem; }}
51
+ .test-body {{ display: none; padding: 0 16px 16px; border-top: 1px solid var(--border); }}
52
+ .test-body.open {{ display: block; }}
53
+ .steps {{ margin-top: 12px; }}
54
+ .step {{ font-size: 0.85rem; color: #94a3b8; margin-bottom: 4px; padding-left: 12px; border-left: 2px solid var(--border); }}
55
+ .assertions {{ margin-top: 12px; }}
56
+ .assertion {{ display: flex; align-items: center; gap: 8px; font-size: 0.85rem; margin-bottom: 4px; }}
57
+ .assert-pass {{ color: var(--pass); }} .assert-fail {{ color: var(--fail); }} .assert-null {{ color: var(--manual); }}
58
+ .error-box {{ background: #1c0a0a; border: 1px solid #7f1d1d; border-radius: 6px; padding: 10px; margin-top: 10px; font-family: monospace; font-size: 0.8rem; color: #fca5a5; white-space: pre-wrap; }}
59
+ .issues {{ margin-top: 12px; }}
60
+ .issue {{ display: flex; gap: 8px; font-size: 0.85rem; margin-bottom: 4px; }}
61
+ .severity-Critical {{ color: var(--fail); font-weight: 600; }}
62
+ .severity-Major {{ color: var(--error); font-weight: 600; }}
63
+ .severity-Minor {{ color: #fbbf24; }}
64
+ .screenshot {{ margin-top: 12px; }}
65
+ .screenshot img {{ max-width: 200px; border-radius: 8px; border: 1px solid var(--border); }}
66
+ .issues-section {{ margin-top: 32px; }}
67
+ .issue-card {{ background: var(--card); border-left: 4px solid var(--fail); border-radius: 6px; padding: 12px 16px; margin-bottom: 8px; }}
68
+ .footer {{ margin-top: 48px; color: #475569; font-size: 0.8rem; text-align: center; }}
69
+ .toggle-icon {{ color: #64748b; transition: transform 0.2s; }}
70
+ .open .toggle-icon {{ transform: rotate(180deg); }}
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <h1>📱 Mobile Test Report</h1>
75
+ <div class="subtitle">App: {app_name} &nbsp;|&nbsp; {timestamp} &nbsp;|&nbsp; Mode: {mode}</div>
76
+
77
+ <div class="summary-grid">
78
+ <div class="stat-card"><div class="number">{total}</div><div class="label">Total</div></div>
79
+ <div class="stat-card"><div class="number pass">{passed}</div><div class="label">Passed</div></div>
80
+ <div class="stat-card"><div class="number fail">{failed}</div><div class="label">Failed</div></div>
81
+ <div class="stat-card"><div class="number error">{errors}</div><div class="label">Errors</div></div>
82
+ <div class="stat-card"><div class="number manual">{manual}</div><div class="label">Manual</div></div>
83
+ <div class="stat-card"><div class="number" style="color:#60a5fa">{pass_rate}%</div><div class="label">Pass Rate</div></div>
84
+ </div>
85
+
86
+ <div class="progress-bar">
87
+ {progress_bars}
88
+ </div>
89
+
90
+ <div class="section-title">Test Results</div>
91
+ {test_cards}
92
+
93
+ {issues_html}
94
+
95
+ <div class="footer">Generated by Mobile App Testing Skill &nbsp;•&nbsp; {timestamp}</div>
96
+
97
+ <script>
98
+ document.querySelectorAll('.test-header').forEach(h => {{
99
+ h.addEventListener('click', () => {{
100
+ const body = h.nextElementSibling;
101
+ body.classList.toggle('open');
102
+ h.classList.toggle('open');
103
+ }});
104
+ }});
105
+ </script>
106
+ </body>
107
+ </html>"""
108
+
109
+
110
+ def build_test_card(test: dict) -> str:
111
+ status = test.get("status", "UNKNOWN")
112
+ duration = test.get("duration_ms", 0)
113
+
114
+ steps_html = ""
115
+ if test.get("steps_log"):
116
+ steps_html = '<div class="steps">' + "".join(
117
+ f'<div class="step">{s}</div>' for s in test["steps_log"]
118
+ ) + "</div>"
119
+
120
+ assertions_html = ""
121
+ if test.get("assertions"):
122
+ def ass_icon(a):
123
+ if a.get("passed") is True: return '<span class="assert-pass">✅</span>'
124
+ if a.get("passed") is False: return '<span class="assert-fail">❌</span>'
125
+ return '<span class="assert-null">📋</span>'
126
+ assertions_html = '<div class="assertions"><strong>Assertions:</strong>' + "".join(
127
+ f'<div class="assertion">{ass_icon(a)}<span>{a["check"]}</span></div>'
128
+ for a in test["assertions"]
129
+ ) + "</div>"
130
+
131
+ error_html = ""
132
+ if test.get("error"):
133
+ error_html = f'<div class="error-box">{test["error"]}</div>'
134
+
135
+ issues_html = ""
136
+ if test.get("issues_found"):
137
+ issues_html = '<div class="issues"><strong>Issues:</strong>' + "".join(
138
+ f'<div class="issue"><span class="severity-{i["severity"]}">[{i["severity"]}]</span><span>{i["description"]}</span></div>'
139
+ for i in test["issues_found"]
140
+ ) + "</div>"
141
+
142
+ screenshot_html = ""
143
+ if test.get("screenshot_path") and os.path.exists(test["screenshot_path"]):
144
+ screenshot_html = f'<div class="screenshot"><strong>Screenshot:</strong><br><img src="{test["screenshot_path"]}" alt="screenshot"></div>'
145
+
146
+ return f"""
147
+ <div class="test-card">
148
+ <div class="test-header">
149
+ <span class="test-id">{test.get("test_id", "?")}</span>
150
+ <span class="status-badge badge-{status}">{status.replace("_", " ")}</span>
151
+ <span class="test-title">{test.get("title", "")}</span>
152
+ <span class="duration">{duration}ms</span>
153
+ <span class="toggle-icon">▼</span>
154
+ </div>
155
+ <div class="test-body">
156
+ {steps_html}
157
+ {assertions_html}
158
+ {error_html}
159
+ {issues_html}
160
+ {screenshot_html}
161
+ </div>
162
+ </div>"""
163
+
164
+
165
+ def main():
166
+ parser = argparse.ArgumentParser()
167
+ parser.add_argument("--results", default="results/", help="Results directory with results.json")
168
+ parser.add_argument("--output", default="test_report.html")
169
+ args = parser.parse_args()
170
+
171
+ results_file = os.path.join(args.results, "results.json")
172
+ if not os.path.exists(results_file):
173
+ # Try direct path
174
+ if os.path.exists(args.results) and args.results.endswith(".json"):
175
+ results_file = args.results
176
+ else:
177
+ print(f"❌ results.json not found at {results_file}")
178
+ return
179
+
180
+ with open(results_file) as f:
181
+ data = json.load(f)
182
+
183
+ total = data.get("total", 0)
184
+ passed = data.get("passed", 0)
185
+ failed = data.get("failed", 0)
186
+ errors = data.get("errors", 0)
187
+ manual = data.get("manual", 0)
188
+
189
+ pass_rate = round(passed / total * 100) if total > 0 else 0
190
+
191
+ def pct(n):
192
+ return round(n / total * 100) if total > 0 else 0
193
+
194
+ progress_bars = (
195
+ f'<div class="pb-pass" style="width:{pct(passed)}%"></div>'
196
+ f'<div class="pb-fail" style="width:{pct(failed)}%"></div>'
197
+ f'<div class="pb-error" style="width:{pct(errors)}%"></div>'
198
+ f'<div class="pb-manual" style="width:{pct(manual)}%"></div>'
199
+ )
200
+
201
+ tests = data.get("tests", [])
202
+ test_cards = "".join(build_test_card(t) for t in tests)
203
+
204
+ # Collect all issues
205
+ all_issues = []
206
+ for t in tests:
207
+ for issue in t.get("issues_found", []):
208
+ all_issues.append({**issue, "test": t.get("test_id"), "title": t.get("title")})
209
+
210
+ issues_html = ""
211
+ if all_issues:
212
+ critical = [i for i in all_issues if i.get("severity") == "Critical"]
213
+ major = [i for i in all_issues if i.get("severity") == "Major"]
214
+ minor = [i for i in all_issues if i.get("severity") == "Minor"]
215
+
216
+ def render_issues(issues_list):
217
+ return "".join(
218
+ f'<div class="issue-card"><strong>[{i["severity"]}]</strong> {i["description"]} '
219
+ f'<span style="color:#64748b">— {i.get("test","")}: {i.get("title","")}</span></div>'
220
+ for i in issues_list
221
+ )
222
+
223
+ issues_html = f"""
224
+ <div class="issues-section">
225
+ <div class="section-title">🐛 Issues Found ({len(all_issues)})</div>
226
+ {render_issues(critical + major + minor)}
227
+ </div>"""
228
+
229
+ apk = data.get("apk") or "Unknown App"
230
+ app_name = Path(apk).stem if apk else "Unknown"
231
+ timestamp = data.get("timestamp", datetime.datetime.now().isoformat())[:19].replace("T", " ")
232
+ mode = data.get("mode", "unknown").upper()
233
+
234
+ html = HTML_TEMPLATE.format(
235
+ app_name=app_name, timestamp=timestamp, mode=mode,
236
+ total=total, passed=passed, failed=failed, errors=errors,
237
+ manual=manual, pass_rate=pass_rate,
238
+ progress_bars=progress_bars, test_cards=test_cards,
239
+ issues_html=issues_html,
240
+ )
241
+
242
+ with open(args.output, "w", encoding="utf-8") as f:
243
+ f.write(html)
244
+
245
+ print(f"✅ Report generated: {args.output}")
246
+ print(f" {passed}/{total} tests passed ({pass_rate}%), {len(all_issues)} issues found")
247
+
248
+
249
+ if __name__ == "__main__":
250
+ main()