@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.
- package/.claude/settings.local.json +13 -0
- package/CLAUDE.md +55 -0
- package/LICENSE +1 -1
- package/README.md +208 -39
- package/bin/cli.js +39 -0
- package/package.json +30 -17
- package/registry/registry.json +166 -1
- package/registry/schema.json +10 -0
- package/src/commands/skills/add.js +194 -0
- package/src/commands/skills/list.js +52 -0
- package/src/commands/skills/remove.js +27 -0
- package/src/commands/skills/update.js +74 -0
- package/src/config.js +5 -0
- package/src/skills/codex-cli/SKILL.md +265 -0
- package/src/skills/commafeed-api/SKILL.md +1012 -0
- package/src/skills/gemini-cli/SKILL.md +379 -0
- package/src/skills/gemini-cli/references/commands.md +145 -0
- package/src/skills/gemini-cli/references/configuration.md +182 -0
- package/src/skills/gemini-cli/references/headless-and-scripting.md +181 -0
- package/src/skills/gemini-cli/references/mcp-and-extensions.md +254 -0
- package/src/skills/n8n-api/SKILL.md +623 -0
- package/src/skills/notebook-lm/SKILL.md +217 -0
- package/src/skills/notebook-lm/references/artifact-options.md +168 -0
- package/src/skills/notebook-lm/references/auth.md +58 -0
- package/src/skills/notebook-lm/references/workflows.md +144 -0
- package/src/skills/screen-recording/SKILL.md +309 -0
- package/src/skills/screen-recording/references/approach1-programmatic.md +311 -0
- package/src/skills/screen-recording/references/approach2-xvfb.md +232 -0
- package/src/skills/screen-recording/references/design-patterns.md +168 -0
- package/src/skills/test-mobile-app/SKILL.md +212 -0
- package/src/skills/test-mobile-app/references/report-template.md +95 -0
- package/src/skills/test-mobile-app/references/setup-appium.md +154 -0
- package/src/skills/test-mobile-app/scripts/analyze_apk.py +164 -0
- package/src/skills/test-mobile-app/scripts/check_environment.py +116 -0
- package/src/skills/test-mobile-app/scripts/generate_report.py +250 -0
- package/src/skills/test-mobile-app/scripts/run_tests.py +326 -0
- package/src/skills/test-web-ui/SKILL.md +232 -0
- package/src/skills/test-web-ui/references/test_case_schema.md +102 -0
- package/src/skills/test-web-ui/scripts/discover.py +176 -0
- package/src/skills/test-web-ui/scripts/generate_report.py +237 -0
- package/src/skills/test-web-ui/scripts/run_tests.py +296 -0
- package/src/skills/text-to-speech/SKILL.md +236 -0
- package/src/skills/text-to-speech/references/espeak-cli.md +277 -0
- package/src/skills/text-to-speech/references/kokoro-onnx.md +124 -0
- package/src/skills/text-to-speech/references/online-engines.md +128 -0
- package/src/skills/text-to-speech/references/pyttsx3-espeak.md +143 -0
- package/src/skills/tm-search/SKILL.md +240 -0
- package/src/skills/tm-search/references/field-guide.md +79 -0
- package/src/skills/tm-search/references/scraping-fallback.md +140 -0
- package/src/skills/tm-search/scripts/tm_search.py +375 -0
- package/src/skills/wp-rest-api/SKILL.md +114 -0
- package/src/skills/wp-rest-api/references/authentication.md +18 -0
- package/src/skills/wp-rest-api/references/custom-content-types.md +20 -0
- package/src/skills/wp-rest-api/references/discovery-and-params.md +20 -0
- package/src/skills/wp-rest-api/references/responses-and-fields.md +30 -0
- package/src/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
- package/src/skills/wp-rest-api/references/schema.md +22 -0
- package/src/skills/youtube-search/SKILL.md +412 -0
- package/src/skills/youtube-search/references/parsing-examples.md +159 -0
- package/src/skills/youtube-search/references/youtube-api-quota.md +85 -0
- package/src/skills/youtube-thumbnail/SKILL.md +1060 -0
- package/tests/commands/info.test.js +49 -0
- package/tests/commands/install.test.js +36 -0
- package/tests/commands/list.test.js +66 -0
- package/tests/commands/publish.test.js +182 -0
- package/tests/commands/search.test.js +45 -0
- package/tests/commands/uninstall.test.js +29 -0
- package/tests/commands/update.test.js +59 -0
- package/tests/functional/skills-lifecycle.test.js +293 -0
- package/tests/helpers/fixtures.js +63 -0
- package/tests/integration/cli.test.js +83 -0
- package/tests/skills/add.test.js +138 -0
- package/tests/skills/list.test.js +63 -0
- package/tests/skills/remove.test.js +38 -0
- package/tests/skills/update.test.js +60 -0
- package/tests/unit/config.test.js +31 -0
- package/tests/unit/registry.test.js +79 -0
- package/tests/unit/utils.test.js +150 -0
- package/tests/validation/registry-schema.test.js +112 -0
- 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} | {timestamp} | 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 • {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()
|