@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,326 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
run_tests.py — Execute mobile test scenarios via Appium or in static mode.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 run_tests.py --apk app.apk --tests tests.json --output results/
|
|
7
|
+
python3 run_tests.py --static --tests tests.json --output results/
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
import datetime
|
|
14
|
+
import traceback
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ─────────────────────────────────────────
|
|
19
|
+
# Data structures
|
|
20
|
+
# ─────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
class TestResult:
|
|
23
|
+
def __init__(self, test_id: str, title: str):
|
|
24
|
+
self.test_id = test_id
|
|
25
|
+
self.title = title
|
|
26
|
+
self.status = "PENDING" # PASS | FAIL | SKIP | MANUAL_REQUIRED | ERROR
|
|
27
|
+
self.steps_log = []
|
|
28
|
+
self.assertions = []
|
|
29
|
+
self.error = None
|
|
30
|
+
self.screenshot_path = None
|
|
31
|
+
self.duration_ms = 0
|
|
32
|
+
self.issues_found = []
|
|
33
|
+
|
|
34
|
+
def to_dict(self):
|
|
35
|
+
return {
|
|
36
|
+
"test_id": self.test_id,
|
|
37
|
+
"title": self.title,
|
|
38
|
+
"status": self.status,
|
|
39
|
+
"steps_log": self.steps_log,
|
|
40
|
+
"assertions": self.assertions,
|
|
41
|
+
"error": self.error,
|
|
42
|
+
"screenshot_path": self.screenshot_path,
|
|
43
|
+
"duration_ms": self.duration_ms,
|
|
44
|
+
"issues_found": self.issues_found,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ─────────────────────────────────────────
|
|
49
|
+
# Appium driver helpers
|
|
50
|
+
# ─────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
def create_driver(apk_path: str, device_id: str = None):
|
|
53
|
+
"""Create Appium WebDriver for Android."""
|
|
54
|
+
try:
|
|
55
|
+
from appium import webdriver
|
|
56
|
+
from appium.options import AppiumOptions
|
|
57
|
+
except ImportError:
|
|
58
|
+
raise RuntimeError("appium-python-client not installed. Run: pip install Appium-Python-Client --break-system-packages")
|
|
59
|
+
|
|
60
|
+
options = AppiumOptions()
|
|
61
|
+
options.platform_name = "Android"
|
|
62
|
+
options.automation_name = "UiAutomator2"
|
|
63
|
+
options.app = os.path.abspath(apk_path)
|
|
64
|
+
options.no_reset = False
|
|
65
|
+
options.full_reset = False
|
|
66
|
+
|
|
67
|
+
if device_id:
|
|
68
|
+
options.udid = device_id
|
|
69
|
+
|
|
70
|
+
driver = webdriver.Remote("http://localhost:4723/wd/hub", options=options)
|
|
71
|
+
driver.implicitly_wait(10)
|
|
72
|
+
return driver
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def safe_find(driver, by, value, timeout=10):
|
|
76
|
+
"""Find element with explicit wait."""
|
|
77
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
78
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
79
|
+
return WebDriverWait(driver, timeout).until(
|
|
80
|
+
EC.presence_of_element_located((by, value))
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_for_crash(driver) -> str | None:
|
|
85
|
+
"""Returns crash message if a crash dialog is detected, else None."""
|
|
86
|
+
try:
|
|
87
|
+
# Look for common crash dialog text
|
|
88
|
+
from appium.webdriver.common.appiumby import AppiumBy
|
|
89
|
+
crash_indicators = [
|
|
90
|
+
"has stopped",
|
|
91
|
+
"keeps stopping",
|
|
92
|
+
"isn't responding",
|
|
93
|
+
"Unfortunately",
|
|
94
|
+
]
|
|
95
|
+
page_source = driver.page_source
|
|
96
|
+
for indicator in crash_indicators:
|
|
97
|
+
if indicator.lower() in page_source.lower():
|
|
98
|
+
return f"Crash dialog detected: '{indicator}'"
|
|
99
|
+
return None
|
|
100
|
+
except Exception:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def take_screenshot(driver, output_dir: str, test_id: str, step: int) -> str:
|
|
105
|
+
"""Take screenshot and save to output dir."""
|
|
106
|
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
107
|
+
path = os.path.join(output_dir, f"{test_id}_step{step}.png")
|
|
108
|
+
try:
|
|
109
|
+
driver.save_screenshot(path)
|
|
110
|
+
return path
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ─────────────────────────────────────────
|
|
116
|
+
# Core test runner
|
|
117
|
+
# ─────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
def run_test_automated(driver, test: dict, output_dir: str) -> TestResult:
|
|
120
|
+
"""Execute a single test case via Appium driver."""
|
|
121
|
+
from appium.webdriver.common.appiumby import AppiumBy
|
|
122
|
+
|
|
123
|
+
result = TestResult(test["id"], test["title"])
|
|
124
|
+
start = time.time()
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
steps = test.get("steps", [])
|
|
128
|
+
|
|
129
|
+
for i, step in enumerate(steps):
|
|
130
|
+
action = step.get("action", "")
|
|
131
|
+
result.steps_log.append(f"Step {i+1}: {action}")
|
|
132
|
+
|
|
133
|
+
# Crash check after each step
|
|
134
|
+
crash = check_for_crash(driver)
|
|
135
|
+
if crash:
|
|
136
|
+
result.status = "FAIL"
|
|
137
|
+
result.error = crash
|
|
138
|
+
result.issues_found.append({"severity": "Critical", "description": crash})
|
|
139
|
+
result.screenshot_path = take_screenshot(driver, output_dir, test["id"], i)
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
# Execute action
|
|
143
|
+
try:
|
|
144
|
+
act_type = step.get("type", "")
|
|
145
|
+
|
|
146
|
+
if act_type == "tap" and step.get("locator"):
|
|
147
|
+
el = safe_find(driver, AppiumBy.ACCESSIBILITY_ID, step["locator"])
|
|
148
|
+
el.click()
|
|
149
|
+
|
|
150
|
+
elif act_type == "tap_id" and step.get("locator"):
|
|
151
|
+
el = safe_find(driver, AppiumBy.ID, step["locator"])
|
|
152
|
+
el.click()
|
|
153
|
+
|
|
154
|
+
elif act_type == "type" and step.get("locator"):
|
|
155
|
+
el = safe_find(driver, AppiumBy.ACCESSIBILITY_ID, step["locator"])
|
|
156
|
+
el.clear()
|
|
157
|
+
el.send_keys(step.get("value", ""))
|
|
158
|
+
|
|
159
|
+
elif act_type == "type_id" and step.get("locator"):
|
|
160
|
+
el = safe_find(driver, AppiumBy.ID, step["locator"])
|
|
161
|
+
el.clear()
|
|
162
|
+
el.send_keys(step.get("value", ""))
|
|
163
|
+
|
|
164
|
+
elif act_type == "scroll_down":
|
|
165
|
+
driver.swipe(500, 1000, 500, 300, 500)
|
|
166
|
+
|
|
167
|
+
elif act_type == "back":
|
|
168
|
+
driver.back()
|
|
169
|
+
|
|
170
|
+
elif act_type == "wait":
|
|
171
|
+
time.sleep(step.get("seconds", 1))
|
|
172
|
+
|
|
173
|
+
elif act_type == "assert_visible":
|
|
174
|
+
locator = step.get("locator", "")
|
|
175
|
+
try:
|
|
176
|
+
el = safe_find(driver, AppiumBy.ACCESSIBILITY_ID, locator, timeout=5)
|
|
177
|
+
result.assertions.append({"check": f"Element visible: {locator}", "passed": True})
|
|
178
|
+
except Exception:
|
|
179
|
+
result.assertions.append({"check": f"Element visible: {locator}", "passed": False})
|
|
180
|
+
result.status = "FAIL"
|
|
181
|
+
result.screenshot_path = take_screenshot(driver, output_dir, test["id"], i)
|
|
182
|
+
|
|
183
|
+
elif act_type == "assert_text":
|
|
184
|
+
expected = step.get("expected", "")
|
|
185
|
+
page_source = driver.page_source
|
|
186
|
+
found = expected.lower() in page_source.lower()
|
|
187
|
+
result.assertions.append({"check": f"Text present: '{expected}'", "passed": found})
|
|
188
|
+
if not found:
|
|
189
|
+
result.status = "FAIL"
|
|
190
|
+
result.screenshot_path = take_screenshot(driver, output_dir, test["id"], i)
|
|
191
|
+
|
|
192
|
+
elif act_type == "assert_no_error":
|
|
193
|
+
page = driver.page_source.lower()
|
|
194
|
+
error_words = ["error", "ошибка", "failed", "exception"]
|
|
195
|
+
has_error = any(w in page for w in error_words)
|
|
196
|
+
result.assertions.append({"check": "No error on screen", "passed": not has_error})
|
|
197
|
+
if has_error:
|
|
198
|
+
result.status = "FAIL"
|
|
199
|
+
result.issues_found.append({"severity": "Major", "description": "Error text found on screen"})
|
|
200
|
+
|
|
201
|
+
except Exception as step_err:
|
|
202
|
+
result.steps_log.append(f" ⚠️ Step failed: {step_err}")
|
|
203
|
+
result.screenshot_path = take_screenshot(driver, output_dir, test["id"], i)
|
|
204
|
+
|
|
205
|
+
# Final crash check
|
|
206
|
+
crash = check_for_crash(driver)
|
|
207
|
+
if crash:
|
|
208
|
+
result.status = "FAIL"
|
|
209
|
+
result.error = crash
|
|
210
|
+
result.issues_found.append({"severity": "Critical", "description": crash})
|
|
211
|
+
elif result.status == "PENDING":
|
|
212
|
+
result.status = "PASS"
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
result.status = "ERROR"
|
|
216
|
+
result.error = traceback.format_exc()
|
|
217
|
+
finally:
|
|
218
|
+
result.duration_ms = int((time.time() - start) * 1000)
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def run_test_static(test: dict) -> TestResult:
|
|
224
|
+
"""Mark test as requiring manual execution (no emulator available)."""
|
|
225
|
+
result = TestResult(test["id"], test["title"])
|
|
226
|
+
result.status = "MANUAL_REQUIRED"
|
|
227
|
+
result.steps_log = [f"Step {i+1}: {s.get('action', s)}" for i, s in enumerate(test.get("steps", []))]
|
|
228
|
+
result.assertions = [{"check": a, "passed": None} for a in test.get("assertions", [])]
|
|
229
|
+
result.duration_ms = 0
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ─────────────────────────────────────────
|
|
234
|
+
# Main
|
|
235
|
+
# ─────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
def main():
|
|
238
|
+
parser = argparse.ArgumentParser()
|
|
239
|
+
parser.add_argument("--apk", help="Path to APK file")
|
|
240
|
+
parser.add_argument("--tests", help="Path to tests.json", default="tests.json")
|
|
241
|
+
parser.add_argument("--output", help="Output directory", default="results")
|
|
242
|
+
parser.add_argument("--device", help="Device/emulator ID", default=None)
|
|
243
|
+
parser.add_argument("--suite", help="Test suite filter: smoke|functional|all", default="all")
|
|
244
|
+
parser.add_argument("--static", action="store_true", help="Static mode — no emulator")
|
|
245
|
+
args = parser.parse_args()
|
|
246
|
+
|
|
247
|
+
Path(args.output).mkdir(parents=True, exist_ok=True)
|
|
248
|
+
|
|
249
|
+
# Load tests
|
|
250
|
+
if not os.path.exists(args.tests):
|
|
251
|
+
print(f"❌ Tests file not found: {args.tests}")
|
|
252
|
+
print(" Generate tests first with Claude's Phase 2-3 workflow.")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
with open(args.tests) as f:
|
|
256
|
+
tests_data = json.load(f)
|
|
257
|
+
|
|
258
|
+
tests = tests_data.get("tests", tests_data) if isinstance(tests_data, dict) else tests_data
|
|
259
|
+
|
|
260
|
+
# Filter by suite
|
|
261
|
+
if args.suite != "all":
|
|
262
|
+
tests = [t for t in tests if t.get("type", "").lower() == args.suite.lower()]
|
|
263
|
+
|
|
264
|
+
print(f"\n🚀 Running {len(tests)} tests | Mode: {'STATIC' if args.static else 'AUTOMATED'}")
|
|
265
|
+
print("=" * 60)
|
|
266
|
+
|
|
267
|
+
results = []
|
|
268
|
+
driver = None
|
|
269
|
+
|
|
270
|
+
if not args.static and args.apk:
|
|
271
|
+
try:
|
|
272
|
+
print("📱 Connecting to device via Appium...")
|
|
273
|
+
driver = create_driver(args.apk, args.device)
|
|
274
|
+
print("✅ Device connected\n")
|
|
275
|
+
except Exception as e:
|
|
276
|
+
print(f"⚠️ Could not connect to device: {e}")
|
|
277
|
+
print(" Falling back to STATIC mode.\n")
|
|
278
|
+
driver = None
|
|
279
|
+
|
|
280
|
+
for test in tests:
|
|
281
|
+
test_id = test.get("id", "?")
|
|
282
|
+
title = test.get("title", "Untitled")
|
|
283
|
+
print(f" {'▶' if driver else '📋'} {test_id}: {title}", end=" ", flush=True)
|
|
284
|
+
|
|
285
|
+
if driver:
|
|
286
|
+
result = run_test_automated(driver, test, args.output)
|
|
287
|
+
else:
|
|
288
|
+
result = run_test_static(test)
|
|
289
|
+
|
|
290
|
+
results.append(result)
|
|
291
|
+
|
|
292
|
+
status_icon = {"PASS": "✅", "FAIL": "❌", "ERROR": "💥",
|
|
293
|
+
"SKIP": "⏭️", "MANUAL_REQUIRED": "📋"}.get(result.status, "?")
|
|
294
|
+
print(f"→ {status_icon} {result.status} ({result.duration_ms}ms)")
|
|
295
|
+
|
|
296
|
+
if driver:
|
|
297
|
+
try:
|
|
298
|
+
driver.quit()
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
# Save raw results
|
|
303
|
+
results_path = os.path.join(args.output, "results.json")
|
|
304
|
+
summary = {
|
|
305
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
306
|
+
"mode": "static" if not driver else "automated",
|
|
307
|
+
"apk": args.apk,
|
|
308
|
+
"total": len(results),
|
|
309
|
+
"passed": sum(1 for r in results if r.status == "PASS"),
|
|
310
|
+
"failed": sum(1 for r in results if r.status == "FAIL"),
|
|
311
|
+
"errors": sum(1 for r in results if r.status == "ERROR"),
|
|
312
|
+
"manual": sum(1 for r in results if r.status == "MANUAL_REQUIRED"),
|
|
313
|
+
"tests": [r.to_dict() for r in results],
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
with open(results_path, "w") as f:
|
|
317
|
+
json.dump(summary, f, indent=2, ensure_ascii=False)
|
|
318
|
+
|
|
319
|
+
print("\n" + "=" * 60)
|
|
320
|
+
print(f"📊 RESULTS: {summary['passed']} passed | {summary['failed']} failed | {summary['errors']} errors | {summary['manual']} manual")
|
|
321
|
+
print(f"💾 Saved to: {results_path}")
|
|
322
|
+
print(f"\n📄 Generate report: python3 scripts/generate_report.py --results {args.output}/ --output test_report.html")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
if __name__ == "__main__":
|
|
326
|
+
main()
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-web-ui
|
|
3
|
+
description: >
|
|
4
|
+
Automated web QA skill: analyzes a website or project, generates end-user use cases,
|
|
5
|
+
derives a structured test plan, executes tests via Playwright browser automation, and
|
|
6
|
+
produces a full HTML/Markdown QA report with screenshots and pass/fail results.
|
|
7
|
+
|
|
8
|
+
TRIGGER this skill whenever the user asks to: test a website, run QA on a web app,
|
|
9
|
+
check if a site works, find bugs on a site, validate a web project, create a test plan
|
|
10
|
+
for a website, run functional tests, check a landing page, audit a web app for issues,
|
|
11
|
+
test user flows — or any variation of "проверить сайт", "протестировать сайт",
|
|
12
|
+
"QA сайта", "тест веб-приложения", "найти баги на сайте". Even if the user just says
|
|
13
|
+
"посмотри работает ли всё нормально на сайте" — use this skill.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Web Tester Skill
|
|
17
|
+
|
|
18
|
+
Transforms any website or project description into a structured QA run:
|
|
19
|
+
**Discover → Plan → Execute → Report**
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Overview of Phases
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Phase 1 → DISCOVERY : Explore the site, understand its purpose and features
|
|
27
|
+
Phase 2 → USE CASES : Generate end-user use case list
|
|
28
|
+
Phase 3 → TEST PLAN : Convert use cases into concrete, executable test cases
|
|
29
|
+
Phase 4 → EXECUTION : Run tests with Playwright, capture screenshots
|
|
30
|
+
Phase 5 → REPORT : Compile HTML + Markdown report with all results
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Phase 1: Discovery
|
|
36
|
+
|
|
37
|
+
### What to gather
|
|
38
|
+
- **URL** — if the user provides a live URL, use Playwright to crawl it
|
|
39
|
+
- **Project files** — if no live URL, inspect source files in `/mnt/user-data/uploads/`
|
|
40
|
+
- **Purpose** — what does the site do? (landing page, e-commerce, dashboard, blog, etc.)
|
|
41
|
+
- **Key pages** — home, auth, main feature pages, forms, checkout, etc.
|
|
42
|
+
- **Tech stack** — optional but helpful for targeted checks
|
|
43
|
+
|
|
44
|
+
### Discovery script
|
|
45
|
+
Run `scripts/discover.py` (see below) to auto-detect pages, links, forms, interactive elements.
|
|
46
|
+
|
|
47
|
+
**If no live URL is available** (e.g., network blocked or local project):
|
|
48
|
+
- Read source files (HTML, JSX, Vue, etc.) in uploads directory
|
|
49
|
+
- Extract routes, components, visible text, form fields, navigation links
|
|
50
|
+
- Use Playwright with `page.set_content()` to render and inspect local HTML files
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Phase 2: Generate Use Cases
|
|
55
|
+
|
|
56
|
+
After discovery, produce a use case list **from the perspective of an end user**.
|
|
57
|
+
|
|
58
|
+
### Format
|
|
59
|
+
```
|
|
60
|
+
UC-01: [Actor] can [action] so that [goal]
|
|
61
|
+
UC-02: [Actor] can [action] so that [goal]
|
|
62
|
+
...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Categories to cover
|
|
66
|
+
- **Navigation** — user browses pages, menu works, links resolve
|
|
67
|
+
- **Authentication** — sign up, log in, password reset, logout
|
|
68
|
+
- **Core feature flows** — the main thing the site does (purchase, search, submit form, etc.)
|
|
69
|
+
- **Content validation** — required text, images, prices, labels appear correctly
|
|
70
|
+
- **Forms** — fill in, submit, validate errors, success states
|
|
71
|
+
- **Responsiveness** — works on mobile viewport
|
|
72
|
+
- **Error handling** — 404 pages, empty states, invalid inputs
|
|
73
|
+
- **Performance / visual** — no broken images, no console errors, reasonable load
|
|
74
|
+
|
|
75
|
+
Aim for **10–25 use cases** depending on site complexity.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Phase 3: Test Plan
|
|
80
|
+
|
|
81
|
+
Convert each use case into a concrete test case:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
TC-01 [UC-01]: Homepage Navigation
|
|
85
|
+
Given: User opens the site root URL
|
|
86
|
+
When: Page finishes loading
|
|
87
|
+
Then: - Page title is not empty
|
|
88
|
+
- Navigation menu is visible
|
|
89
|
+
- Logo/brand element is present
|
|
90
|
+
- No 404/500 status code
|
|
91
|
+
Checks: title, nav links count > 0, hero text present
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Test case types to include
|
|
95
|
+
| Type | Examples |
|
|
96
|
+
|------|---------|
|
|
97
|
+
| **Presence checks** | Element exists, text is visible, image loads |
|
|
98
|
+
| **Functional checks** | Button clickable, form submits, menu expands |
|
|
99
|
+
| **Data validation** | Price format, phone format, required fields |
|
|
100
|
+
| **Navigation checks** | Links don't 404, routing works |
|
|
101
|
+
| **Form validation** | Empty submit shows errors, valid submit succeeds |
|
|
102
|
+
| **Responsiveness** | Mobile viewport renders without overflow |
|
|
103
|
+
| **Console errors** | No JS errors on page load |
|
|
104
|
+
| **Accessibility basics** | Images have alt text, headings hierarchy |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Phase 4: Test Execution
|
|
109
|
+
|
|
110
|
+
### Use the execution script
|
|
111
|
+
```bash
|
|
112
|
+
python3 /mnt/skills/user/web-tester/scripts/run_tests.py \
|
|
113
|
+
--url <URL_OR_LOCAL_PATH> \
|
|
114
|
+
--test-plan /home/claude/test_plan.json \
|
|
115
|
+
--output /home/claude/test_results/
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### What the script does
|
|
119
|
+
1. Launches headless Chromium (no-sandbox mode for container)
|
|
120
|
+
2. For each test case:
|
|
121
|
+
- Navigates to the target page
|
|
122
|
+
- Captures a **before screenshot**
|
|
123
|
+
- Executes assertions (element presence, text content, clicks, form fills)
|
|
124
|
+
- Captures an **after screenshot** if interaction occurred
|
|
125
|
+
- Records PASS / FAIL / SKIP + error message
|
|
126
|
+
3. Saves all screenshots to `test_results/screenshots/`
|
|
127
|
+
4. Writes `test_results/results.json`
|
|
128
|
+
|
|
129
|
+
### Manual execution (when no URL — code project)
|
|
130
|
+
If only source files are available:
|
|
131
|
+
1. Serve them locally: `python3 -m http.server 8080 --directory /home/claude/project/`
|
|
132
|
+
2. Run tests against `http://localhost:8080`
|
|
133
|
+
|
|
134
|
+
### Key Playwright patterns to use
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# Launch
|
|
138
|
+
from playwright.sync_api import sync_playwright
|
|
139
|
+
browser = p.chromium.launch(args=['--no-sandbox', '--disable-dev-shm-usage'])
|
|
140
|
+
context = browser.new_context(viewport={'width': 1280, 'height': 800})
|
|
141
|
+
page = context.new_page()
|
|
142
|
+
|
|
143
|
+
# Console error collection
|
|
144
|
+
errors = []
|
|
145
|
+
page.on('console', lambda msg: errors.append(msg.text) if msg.type == 'error' else None)
|
|
146
|
+
|
|
147
|
+
# Mobile viewport test
|
|
148
|
+
mobile = browser.new_context(viewport={'width': 390, 'height': 844}, is_mobile=True)
|
|
149
|
+
|
|
150
|
+
# Screenshot
|
|
151
|
+
page.screenshot(path='screenshots/tc01_home.png', full_page=True)
|
|
152
|
+
|
|
153
|
+
# Assertions
|
|
154
|
+
page.wait_for_load_state('networkidle')
|
|
155
|
+
assert page.locator('nav').is_visible()
|
|
156
|
+
assert page.title() != ''
|
|
157
|
+
count = page.locator('a').count()
|
|
158
|
+
text = page.locator('h1').text_content()
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Phase 5: Report Generation
|
|
164
|
+
|
|
165
|
+
After execution, generate report using `scripts/generate_report.py`:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
python3 /mnt/skills/user/web-tester/scripts/generate_report.py \
|
|
169
|
+
--results /home/claude/test_results/results.json \
|
|
170
|
+
--screenshots /home/claude/test_results/screenshots/ \
|
|
171
|
+
--output /mnt/user-data/outputs/qa_report.html
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Report contents
|
|
175
|
+
- **Summary dashboard** — total tests, pass/fail/skip counts, pass rate %, tested URL, timestamp
|
|
176
|
+
- **Use case list** with traceability to test cases
|
|
177
|
+
- **Test results table** — TC ID, name, status badge, duration, error message
|
|
178
|
+
- **Screenshot gallery** — inline base64 screenshots per test case
|
|
179
|
+
- **Console errors log** — any JS errors captured
|
|
180
|
+
- **Recommendations** — auto-generated improvement suggestions based on failures
|
|
181
|
+
|
|
182
|
+
### Report format
|
|
183
|
+
- Primary: **HTML** (self-contained, with embedded screenshots)
|
|
184
|
+
- Secondary: **Markdown** summary for quick reading
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Workflow Summary (step-by-step)
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
1. Receive URL or project files from user
|
|
192
|
+
2. Run discovery → understand pages, structure, features
|
|
193
|
+
3. Generate use case list → show to user, get approval or continue
|
|
194
|
+
4. Generate test plan JSON → structured test cases
|
|
195
|
+
5. Run Playwright tests → collect results + screenshots
|
|
196
|
+
6. Generate HTML report → save to /mnt/user-data/outputs/
|
|
197
|
+
7. Present report to user using present_files tool
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Handling Common Situations
|
|
203
|
+
|
|
204
|
+
**No internet access (container network restrictions)**
|
|
205
|
+
→ Ask user to provide HTML files, or serve local project
|
|
206
|
+
→ Use `page.set_content()` for static HTML testing
|
|
207
|
+
→ Use `python3 -m http.server` for multi-page projects
|
|
208
|
+
|
|
209
|
+
**Site requires authentication**
|
|
210
|
+
→ Ask user for test credentials OR
|
|
211
|
+
→ Test only the public-facing pages
|
|
212
|
+
→ Mark auth-gated tests as SKIP with note
|
|
213
|
+
|
|
214
|
+
**Single-page app (React/Vue/Angular)**
|
|
215
|
+
→ Wait for `networkidle` state
|
|
216
|
+
→ Use `page.wait_for_selector()` before asserting dynamic content
|
|
217
|
+
→ Check that JS bundle loads without console errors
|
|
218
|
+
|
|
219
|
+
**Large site (many pages)**
|
|
220
|
+
→ Focus on critical user paths first
|
|
221
|
+
→ Limit to top 5–10 most important flows
|
|
222
|
+
→ Mention in report which pages were NOT covered
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Reference Files
|
|
227
|
+
|
|
228
|
+
- `references/test_case_schema.md` — JSON schema for test_plan.json
|
|
229
|
+
- `references/report_template.md` — Report structure reference
|
|
230
|
+
- `scripts/discover.py` — Site discovery automation
|
|
231
|
+
- `scripts/run_tests.py` — Test execution engine
|
|
232
|
+
- `scripts/generate_report.py` — HTML report generator
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Test Plan JSON Schema
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"meta": {
|
|
8
|
+
"site_name": "My Site",
|
|
9
|
+
"url": "https://example.com",
|
|
10
|
+
"created_at": "2025-01-01T12:00:00",
|
|
11
|
+
"description": "QA test plan for homepage and auth flows"
|
|
12
|
+
},
|
|
13
|
+
"use_cases": [
|
|
14
|
+
{
|
|
15
|
+
"id": "UC-01",
|
|
16
|
+
"actor": "Visitor",
|
|
17
|
+
"action": "views the homepage",
|
|
18
|
+
"goal": "understand what the product offers",
|
|
19
|
+
"test_cases": ["TC-01", "TC-02"]
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"test_cases": [
|
|
23
|
+
{
|
|
24
|
+
"id": "TC-01",
|
|
25
|
+
"use_case": "UC-01",
|
|
26
|
+
"name": "Homepage loads and shows key content",
|
|
27
|
+
"path": "/",
|
|
28
|
+
"mobile": false,
|
|
29
|
+
"steps": [
|
|
30
|
+
{ "action": "scroll", "selector": "", "value": "" }
|
|
31
|
+
],
|
|
32
|
+
"assertions": [
|
|
33
|
+
{ "type": "title_not_empty", "description": "Page has a title" },
|
|
34
|
+
{ "type": "visible", "selector": "nav", "description": "Navigation is visible" },
|
|
35
|
+
{ "type": "visible", "selector": "h1", "description": "Main heading exists" },
|
|
36
|
+
{ "type": "count_gt", "selector": "a", "expected": "2", "description": "Multiple links exist" },
|
|
37
|
+
{ "type": "images_loaded", "description": "All images loaded" },
|
|
38
|
+
{ "type": "no_console_errors", "description": "No JS console errors" }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Assertion Types
|
|
46
|
+
|
|
47
|
+
| type | required fields | description |
|
|
48
|
+
|------|----------------|-------------|
|
|
49
|
+
| `visible` | selector | Element is visible on page |
|
|
50
|
+
| `not_visible` | selector | Element is NOT visible |
|
|
51
|
+
| `text_contains` | selector, expected | Element text contains string |
|
|
52
|
+
| `title_not_empty` | — | Page title is not blank |
|
|
53
|
+
| `title_contains` | expected | Page title contains string |
|
|
54
|
+
| `count_gt` | selector, expected | Count of elements > N |
|
|
55
|
+
| `count_eq` | selector, expected | Count of elements == N |
|
|
56
|
+
| `url_contains` | expected | Current URL contains string |
|
|
57
|
+
| `no_console_errors` | — | No JS errors in console |
|
|
58
|
+
| `images_loaded` | — | All img elements loaded (naturalWidth > 0) |
|
|
59
|
+
| `has_alt_text` | — | All images have alt attributes |
|
|
60
|
+
| `clickable` | selector | Element is visible and enabled |
|
|
61
|
+
| `attribute_equals` | selector, attribute, expected | Element attribute == value |
|
|
62
|
+
|
|
63
|
+
## Step Actions
|
|
64
|
+
|
|
65
|
+
| action | selector | value | description |
|
|
66
|
+
|--------|----------|-------|-------------|
|
|
67
|
+
| `click` | CSS selector | — | Click element |
|
|
68
|
+
| `fill` | CSS selector | text | Fill input with text |
|
|
69
|
+
| `wait_for` | CSS selector | — | Wait until element appears |
|
|
70
|
+
| `scroll` | — | — | Scroll to bottom of page |
|
|
71
|
+
| `hover` | CSS selector | — | Hover over element |
|
|
72
|
+
|
|
73
|
+
## Common Selectors Cheatsheet
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
/* Navigation */
|
|
77
|
+
nav, header nav, [role=navigation]
|
|
78
|
+
|
|
79
|
+
/* Buttons */
|
|
80
|
+
button, [role=button], input[type=submit]
|
|
81
|
+
|
|
82
|
+
/* Forms */
|
|
83
|
+
form, input, textarea, select
|
|
84
|
+
|
|
85
|
+
/* Images */
|
|
86
|
+
img
|
|
87
|
+
|
|
88
|
+
/* Headings */
|
|
89
|
+
h1, h2, h3
|
|
90
|
+
|
|
91
|
+
/* Links */
|
|
92
|
+
a[href]
|
|
93
|
+
|
|
94
|
+
/* Footer */
|
|
95
|
+
footer
|
|
96
|
+
|
|
97
|
+
/* Error messages */
|
|
98
|
+
.error, [role=alert], .alert-danger
|
|
99
|
+
|
|
100
|
+
/* Success messages */
|
|
101
|
+
.success, .alert-success, [role=status]
|
|
102
|
+
```
|