@leejungkiin/awkit 1.7.1 → 1.7.4
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/bin/awk.js +576 -84
- package/core/CLAUDE.md +1 -1
- package/core/GEMINI.md +148 -167
- package/core/GEMINI.md.bak +149 -116
- package/core/skill-runtime-manifest.json +3 -0
- package/docs/Claude Fable 5.md +3826 -0
- package/docs/android_kotlin_system_instruction.md +210 -0
- package/docs/brainstorm_ponytail_integration.md +146 -0
- package/docs/brainstorm_smart_setup.md +113 -0
- package/docs/deep-research-report (1).md +293 -0
- package/docs/history/GEMINI.v1.md +135 -0
- package/docs/history/brainstorm_antigravity_unified_architecture.v1.md +105 -0
- package/docs/history/implementation_plan.v1.md +58 -0
- package/package.json +4 -1
- package/scripts/artifact-storage.js +130 -0
- package/scripts/automation-gate.js +35 -2
- package/scripts/claude-plan.js +76 -0
- package/scripts/dependency-manager.js +210 -0
- package/scripts/exec-rtk.js +11 -5
- package/scripts/i18n-helper.js +381 -0
- package/scripts/multi-model-pipeline.js +144 -0
- package/skill-packs/mobile-ios/pack.json +4 -2
- package/skill-packs/reverse-engineering/pack.json +1 -0
- package/skills/CATALOG.md +20 -0
- package/skills/GEMINI.md +9 -1
- package/skills/TRIGGER_INDEX.md +10 -0
- package/skills/ai-music/SKILL.md +275 -0
- package/skills/android-re-analyzer/SKILL.md +238 -0
- package/skills/android-re-analyzer/references/api-extraction-patterns.md +119 -0
- package/skills/android-re-analyzer/references/call-flow-analysis.md +176 -0
- package/skills/android-re-analyzer/references/fernflower-usage.md +115 -0
- package/skills/android-re-analyzer/references/jadx-usage.md +116 -0
- package/skills/android-re-analyzer/references/setup-guide.md +221 -0
- package/skills/android-re-analyzer/scripts/check-deps.sh +129 -0
- package/skills/android-re-analyzer/scripts/decompile.sh +375 -0
- package/skills/android-re-analyzer/scripts/find-api-calls.sh +118 -0
- package/skills/android-re-analyzer/scripts/install-dep.sh +448 -0
- package/skills/animal-island-ui-style/SKILL.md +1450 -0
- package/skills/app-store-review-agent/SKILL.md +164 -0
- package/skills/app-store-review-agent/references/guidelines/README.md +154 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/ai_apps.md +37 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/all_apps.md +50 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/crypto_finance.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/games.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/health_fitness.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/kids.md +27 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/macos.md +38 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/social_ugc.md +32 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/subscription_iap.md +34 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/vpn.md +18 -0
- package/skills/app-store-review-agent/references/rules/design/minimum_functionality.md +96 -0
- package/skills/app-store-review-agent/references/rules/design/sign_in_with_apple.md +54 -0
- package/skills/app-store-review-agent/references/rules/entitlements/unused_entitlements.md +83 -0
- package/skills/app-store-review-agent/references/rules/metadata/accurate_metadata.md +54 -0
- package/skills/app-store-review-agent/references/rules/metadata/apple_trademark.md +99 -0
- package/skills/app-store-review-agent/references/rules/metadata/china_storefront.md +72 -0
- package/skills/app-store-review-agent/references/rules/metadata/competitor_terms.md +56 -0
- package/skills/app-store-review-agent/references/rules/metadata/subscription_metadata.md +81 -0
- package/skills/app-store-review-agent/references/rules/privacy/privacy_manifest.md +84 -0
- package/skills/app-store-review-agent/references/rules/privacy/unnecessary_data.md +60 -0
- package/skills/app-store-review-agent/references/rules/subscription/misleading_pricing.md +63 -0
- package/skills/app-store-review-agent/references/rules/subscription/missing_tos_pp.md +54 -0
- package/skills/awf-ponytail/SKILL.md +91 -0
- package/skills/awf-ponytail-review/SKILL.md +67 -0
- package/skills/awf-session-restore/SKILL.md +3 -3
- package/skills/brainstorm-agent/SKILL.md +11 -2
- package/skills/brainstorm-agent/templates/brief-template.md +8 -0
- package/skills/claude-planner/SKILL.md +47 -0
- package/skills/code-review/SKILL.md +87 -0
- package/skills/expo-game-development/SKILL.md +163 -0
- package/skills/flutter/LICENSE.txt +202 -0
- package/skills/flutter/SKILL.md +127 -0
- package/skills/flutter-project-creater/LICENSE.txt +202 -0
- package/skills/flutter-project-creater/SKILL.md +106 -0
- package/skills/game-developer/SKILL.md +163 -0
- package/skills/game-developer/references/ecs-patterns.md +501 -0
- package/skills/game-developer/references/multiplayer-networking.md +475 -0
- package/skills/game-developer/references/performance-optimization.md +422 -0
- package/skills/game-developer/references/unity-patterns.md +271 -0
- package/skills/game-developer/references/unreal-cpp.md +352 -0
- package/skills/generate-gui-assets/SKILL.md +305 -0
- package/skills/generate-gui-assets/agents/openai.yaml +4 -0
- package/skills/generate-gui-assets/references/catalog-schema.md +58 -0
- package/skills/generate-gui-assets/references/extraction-techniques.md +21 -0
- package/skills/generate-gui-assets/references/prompt-patterns.md +58 -0
- package/skills/generate-gui-assets/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
- package/skills/generate-gui-assets/scripts/build_gui_contact_sheet.py +51 -0
- package/skills/generate-gui-assets/scripts/clean_chroma_edges.py +262 -0
- package/skills/generate-gui-assets/scripts/copy_approved_icons.py +64 -0
- package/skills/generate-gui-assets/scripts/prepare_gui_asset_run.py +91 -0
- package/skills/generate-gui-assets/scripts/suggest_grid_options.py +63 -0
- package/skills/generate-gui-assets/scripts/validate_gui_catalog.py +50 -0
- package/skills/godot-game-development/SKILL.md +142 -0
- package/skills/hatch-pet/LICENSE.txt +201 -0
- package/skills/hatch-pet/SKILL.md +420 -0
- package/skills/hatch-pet/agents/openai.yaml +4 -0
- package/skills/hatch-pet/references/animation-rows.md +29 -0
- package/skills/hatch-pet/references/codex-pet-contract.md +35 -0
- package/skills/hatch-pet/references/qa-rubric.md +60 -0
- package/skills/hatch-pet/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
- package/skills/hatch-pet/scripts/clean_chroma_edges.py +262 -0
- package/skills/hatch-pet/scripts/compose_atlas.py +150 -0
- package/skills/hatch-pet/scripts/derive_running_left_from_running_right.py +143 -0
- package/skills/hatch-pet/scripts/extract_strip_frames.py +323 -0
- package/skills/hatch-pet/scripts/finalize_pet_run.py +382 -0
- package/skills/hatch-pet/scripts/generate_pet_images.py +287 -0
- package/skills/hatch-pet/scripts/inspect_frames.py +246 -0
- package/skills/hatch-pet/scripts/make_contact_sheet.py +96 -0
- package/skills/hatch-pet/scripts/package_custom_pet.py +108 -0
- package/skills/hatch-pet/scripts/pet_job_status.py +117 -0
- package/skills/hatch-pet/scripts/prepare_pet_run.py +673 -0
- package/skills/hatch-pet/scripts/queue_pet_repairs.py +172 -0
- package/skills/hatch-pet/scripts/record_imagegen_result.py +250 -0
- package/skills/hatch-pet/scripts/render_animation_videos.py +134 -0
- package/skills/hatch-pet/scripts/render_animation_videos.sh +5 -0
- package/skills/hatch-pet/scripts/validate_atlas.py +139 -0
- package/skills/i18n-orchestrator/SKILL.md +37 -0
- package/skills/ios-simulator-skill/SKILL.md +390 -0
- package/skills/ios-simulator-skill/scripts/accessibility_audit.py +300 -0
- package/skills/ios-simulator-skill/scripts/app_launcher.py +326 -0
- package/skills/ios-simulator-skill/scripts/app_state_capture.py +400 -0
- package/skills/ios-simulator-skill/scripts/appearance.py +385 -0
- package/skills/ios-simulator-skill/scripts/build_and_test.py +348 -0
- package/skills/ios-simulator-skill/scripts/clipboard.py +103 -0
- package/skills/ios-simulator-skill/scripts/common/__init__.py +61 -0
- package/skills/ios-simulator-skill/scripts/common/cache_utils.py +289 -0
- package/skills/ios-simulator-skill/scripts/common/device_utils.py +462 -0
- package/skills/ios-simulator-skill/scripts/common/env_config.py +35 -0
- package/skills/ios-simulator-skill/scripts/common/hang_pipeline.py +862 -0
- package/skills/ios-simulator-skill/scripts/common/hang_sessions.py +490 -0
- package/skills/ios-simulator-skill/scripts/common/idb_utils.py +180 -0
- package/skills/ios-simulator-skill/scripts/common/screenshot_utils.py +338 -0
- package/skills/ios-simulator-skill/scripts/container.py +668 -0
- package/skills/ios-simulator-skill/scripts/gesture.py +394 -0
- package/skills/ios-simulator-skill/scripts/hang_watcher.py +1533 -0
- package/skills/ios-simulator-skill/scripts/keyboard.py +391 -0
- package/skills/ios-simulator-skill/scripts/localization_audit.py +483 -0
- package/skills/ios-simulator-skill/scripts/location.py +467 -0
- package/skills/ios-simulator-skill/scripts/log_monitor.py +493 -0
- package/skills/ios-simulator-skill/scripts/model_inspector.py +645 -0
- package/skills/ios-simulator-skill/scripts/navigator.py +461 -0
- package/skills/ios-simulator-skill/scripts/privacy_manager.py +310 -0
- package/skills/ios-simulator-skill/scripts/push_notification.py +240 -0
- package/skills/ios-simulator-skill/scripts/screen_mapper.py +296 -0
- package/skills/ios-simulator-skill/scripts/sim_health_check.sh +245 -0
- package/skills/ios-simulator-skill/scripts/sim_list.py +299 -0
- package/skills/ios-simulator-skill/scripts/simctl_boot.py +312 -0
- package/skills/ios-simulator-skill/scripts/simctl_create.py +316 -0
- package/skills/ios-simulator-skill/scripts/simctl_delete.py +357 -0
- package/skills/ios-simulator-skill/scripts/simctl_erase.py +351 -0
- package/skills/ios-simulator-skill/scripts/simctl_shutdown.py +290 -0
- package/skills/ios-simulator-skill/scripts/simulator_selector.py +375 -0
- package/skills/ios-simulator-skill/scripts/status_bar.py +250 -0
- package/skills/ios-simulator-skill/scripts/test_recorder.py +323 -0
- package/skills/ios-simulator-skill/scripts/visual_diff.py +235 -0
- package/skills/ios-simulator-skill/scripts/xcode/__init__.py +13 -0
- package/skills/ios-simulator-skill/scripts/xcode/builder.py +397 -0
- package/skills/ios-simulator-skill/scripts/xcode/cache.py +204 -0
- package/skills/ios-simulator-skill/scripts/xcode/config.py +178 -0
- package/skills/ios-simulator-skill/scripts/xcode/reporter.py +343 -0
- package/skills/ios-simulator-skill/scripts/xcode/xcresult.py +451 -0
- package/skills/ios-visual-qa-strategist/SKILL.md +111 -0
- package/skills/ios-visual-qa-strategist/agents/openai.yaml +4 -0
- package/skills/ios-visual-qa-strategist/references/ios-tool-selection.md +61 -0
- package/skills/ios-visual-qa-strategist/references/minimal-capture-policy.md +56 -0
- package/skills/ios-visual-qa-strategist/references/visual-reasoning-heuristics.md +53 -0
- package/skills/orchestrator/SKILL.md +0 -20
- package/skills/persistent-storage/SKILL.md +55 -0
- package/skills/short-maker/SKILL.md +23 -0
- package/skills/short-maker/scripts/effects.js +56 -0
- package/skills/short-maker/scripts/shortmaker-bridge.js +332 -0
- package/skills/short-maker/scripts/videomix.js +601 -0
- package/skills/short-maker/templates/hyperframes/cinematic-character.template.html +172 -0
- package/skills/short-maker/templates/hyperframes/index.template.html +194 -0
- package/skills/smali-to-kotlin/SKILL.md +128 -0
- package/skills/smali-to-kotlin/examples/getting-started/tech-stack.md +58 -0
- package/skills/smali-to-kotlin/examples/pipeline/data-ui-parity.md +118 -0
- package/skills/smali-to-kotlin/examples/pipeline/scanner-and-bootstrap.md +106 -0
- package/skills/smali-to-kotlin/library-patterns.md +189 -0
- package/skills/smali-to-kotlin/phase-0-discovery.md +128 -0
- package/skills/smali-to-kotlin/phase-1-architecture.md +166 -0
- package/skills/smali-to-kotlin/phase-2-blueprint-ui.md +347 -0
- package/skills/smali-to-kotlin/phase-2-blueprint.md +228 -0
- package/skills/smali-to-kotlin/phase-3-build.md +248 -0
- package/skills/smali-to-kotlin/phase-3-logic-build.md +268 -0
- package/skills/smali-to-kotlin/smali-reading-guide.md +310 -0
- package/skills/smali-to-kotlin/templates/app-map.md +101 -0
- package/skills/smali-to-kotlin/templates/architecture.md +142 -0
- package/skills/smali-to-kotlin/templates/blueprint.md +145 -0
- package/skills/spec-gate/SKILL.md +6 -2
- package/skills/symphony-enforcer/SKILL.md +8 -0
- package/skills/symphony-enforcer/examples/mindful-stop.md +2 -0
- package/skills/symphony-enforcer/examples/three-phase.md +16 -0
- package/skills/symphony-enforcer/examples/trigger-points.md +7 -1
- package/skills/unity-game-development/SKILL.md +231 -0
- package/skills/video-edit/SKILL.md +36 -0
- package/skills/video-edit/scripts/video_edit.py +324 -0
- package/templates/project-identity/android.json +2 -2
- package/templates/project-identity/backend-nestjs.json +2 -2
- package/templates/project-identity/expo.json +2 -2
- package/templates/project-identity/ios.json +2 -2
- package/templates/project-identity/web-nextjs.json +2 -2
- package/templates/setup-mapping.json +48 -0
- package/templates/specs/design-template.md +161 -71
- package/templates/specs/requirements-template.md +65 -133
- package/templates/specs/task-spec-template.xml +3 -0
- package/workflows/_uncategorized/critic.md +40 -0
- package/workflows/_uncategorized/git-rebase-flow.md +81 -0
- package/workflows/_uncategorized/image-gen.md +118 -0
- package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
- package/workflows/_uncategorized/pixel-gen.md +86 -0
- package/workflows/_uncategorized/pixel-setup.md +90 -0
- package/workflows/_uncategorized/ponytail-review.md +59 -0
- package/workflows/_uncategorized/reverse-android-build.md +222 -0
- package/workflows/_uncategorized/reverse-android-design.md +139 -0
- package/workflows/_uncategorized/reverse-android-discover.md +150 -0
- package/workflows/_uncategorized/reverse-android-scan.md +158 -0
- package/workflows/_uncategorized/reverse-android.md +143 -0
- package/workflows/_uncategorized/reverse-ios-build.md +240 -0
- package/workflows/_uncategorized/reverse-ios-design.md +112 -0
- package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
- package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
- package/workflows/_uncategorized/reverse-ios.md +152 -0
- package/workflows/_uncategorized/safety-router.md +34 -0
- package/workflows/_uncategorized/teach.md +89 -0
- package/workflows/_uncategorized/verify-ui.md +53 -0
- package/workflows/_uncategorized/visualize-screenshots.md +34 -0
- package/workflows/ads/ads-analyst.md +201 -0
- package/workflows/ads/ads-audit.md +106 -0
- package/workflows/ads/ads-optimize.md +97 -0
- package/workflows/ads/ads-targeting.md +241 -0
- package/workflows/ads/adsExpert.md +160 -0
- package/workflows/ads/smali-ads-config.md +400 -0
- package/workflows/ads/smali-ads-flow.md +331 -0
- package/workflows/ads/smali-ads-interstitial.md +377 -0
- package/workflows/ads/smali-ads-native.md +382 -0
- package/workflows/context/teach.md +89 -0
- package/workflows/gitnexus.md +8 -8
- package/workflows/lifecycle/brainstorm.md +43 -0
- package/workflows/lifecycle/code.md +5 -0
- package/workflows/lifecycle/init.md +23 -5
- package/workflows/lifecycle/multi-model-pipeline.md +60 -0
- package/workflows/quality/ponytail-review.md +59 -0
- package/workflows/roles/critic.md +40 -0
- package/workflows/roles/safety-router.md +34 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XCResult bundle parser.
|
|
3
|
+
|
|
4
|
+
Extracts structured data from xcresult bundles using xcresulttool.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class XCResultParser:
|
|
16
|
+
"""
|
|
17
|
+
Parse xcresult bundles to extract build/test data.
|
|
18
|
+
|
|
19
|
+
Uses xcresulttool to extract structured JSON data from Apple's
|
|
20
|
+
xcresult bundle format.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, xcresult_path: Path, stderr: str = ""):
|
|
24
|
+
"""
|
|
25
|
+
Initialize parser.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
xcresult_path: Path to xcresult bundle
|
|
29
|
+
stderr: Optional stderr output for fallback parsing
|
|
30
|
+
"""
|
|
31
|
+
self.xcresult_path = xcresult_path
|
|
32
|
+
self.stderr = stderr
|
|
33
|
+
|
|
34
|
+
if xcresult_path and not xcresult_path.exists():
|
|
35
|
+
raise FileNotFoundError(f"XCResult bundle not found: {xcresult_path}")
|
|
36
|
+
|
|
37
|
+
def get_build_results(self) -> dict | None:
|
|
38
|
+
"""
|
|
39
|
+
Get build results as JSON.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Parsed JSON dict or None on error
|
|
43
|
+
"""
|
|
44
|
+
return self._run_xcresulttool(["get", "build-results"])
|
|
45
|
+
|
|
46
|
+
def get_test_results(self) -> dict | None:
|
|
47
|
+
"""
|
|
48
|
+
Get test results summary as JSON.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Parsed JSON dict or None on error
|
|
52
|
+
"""
|
|
53
|
+
return self._run_xcresulttool(["get", "test-results", "summary"])
|
|
54
|
+
|
|
55
|
+
def get_failed_tests(self) -> list[dict]:
|
|
56
|
+
"""
|
|
57
|
+
Get failed test details from xcresult bundle.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of dicts with test_name and failure_message.
|
|
61
|
+
Returns [] if parsing fails or no failures found.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
data = self._run_xcresulttool(["get", "test-results", "tests"])
|
|
65
|
+
if not data:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
failed = []
|
|
69
|
+
# The structure varies by Xcode version — walk recursively
|
|
70
|
+
nodes = data if isinstance(data, list) else data.get("testNodes", [])
|
|
71
|
+
self._collect_failed_tests(nodes, failed)
|
|
72
|
+
return failed
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"Warning: Could not parse failed tests: {e}", file=sys.stderr)
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
def _collect_failed_tests(self, nodes: list, failed: list[dict]) -> None:
|
|
79
|
+
"""Recursively collect failed test cases from test node tree."""
|
|
80
|
+
if not isinstance(nodes, list):
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
for node in nodes:
|
|
84
|
+
if not isinstance(node, dict):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
is_test_case = node.get("nodeType") == "Test Case"
|
|
88
|
+
is_failed = node.get("result") == "Failed"
|
|
89
|
+
|
|
90
|
+
if is_test_case and is_failed:
|
|
91
|
+
failed.append(
|
|
92
|
+
{
|
|
93
|
+
"test_name": node.get("name", "Unknown"),
|
|
94
|
+
"failure_message": node.get("details", ""),
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Recurse into children
|
|
99
|
+
children = node.get("children", [])
|
|
100
|
+
self._collect_failed_tests(children, failed)
|
|
101
|
+
|
|
102
|
+
def get_build_log(self) -> str | None:
|
|
103
|
+
"""
|
|
104
|
+
Get build log as plain text.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Build log string or None on error
|
|
108
|
+
"""
|
|
109
|
+
result = self._run_xcresulttool(["get", "log", "--type", "build"], parse_json=False)
|
|
110
|
+
return result if result else None
|
|
111
|
+
|
|
112
|
+
def count_issues(self) -> tuple[int, int]:
|
|
113
|
+
"""
|
|
114
|
+
Count errors and warnings from build results.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Tuple of (error_count, warning_count)
|
|
118
|
+
"""
|
|
119
|
+
error_count = 0
|
|
120
|
+
warning_count = 0
|
|
121
|
+
|
|
122
|
+
build_results = self.get_build_results()
|
|
123
|
+
|
|
124
|
+
if build_results:
|
|
125
|
+
try:
|
|
126
|
+
# Try top-level errors/warnings first (newer xcresult format)
|
|
127
|
+
if "errors" in build_results and isinstance(build_results.get("errors"), list):
|
|
128
|
+
error_count = len(build_results["errors"])
|
|
129
|
+
if "warnings" in build_results and isinstance(build_results.get("warnings"), list):
|
|
130
|
+
warning_count = len(build_results["warnings"])
|
|
131
|
+
|
|
132
|
+
# If not found, try legacy format: actions[0].buildResult.issues
|
|
133
|
+
if error_count == 0 and warning_count == 0:
|
|
134
|
+
actions = build_results.get("actions", {}).get("_values", [])
|
|
135
|
+
if actions:
|
|
136
|
+
build_result = actions[0].get("buildResult", {})
|
|
137
|
+
issues = build_result.get("issues", {})
|
|
138
|
+
|
|
139
|
+
# Count errors
|
|
140
|
+
error_summaries = issues.get("errorSummaries", {}).get("_values", [])
|
|
141
|
+
error_count = len(error_summaries)
|
|
142
|
+
|
|
143
|
+
# Count warnings
|
|
144
|
+
warning_summaries = issues.get("warningSummaries", {}).get("_values", [])
|
|
145
|
+
warning_count = len(warning_summaries)
|
|
146
|
+
|
|
147
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
148
|
+
print(f"Warning: Could not parse issue counts from xcresult: {e}", file=sys.stderr)
|
|
149
|
+
|
|
150
|
+
# If no errors found in xcresult but stderr available, count stderr errors
|
|
151
|
+
if error_count == 0 and self.stderr:
|
|
152
|
+
stderr_errors = self._parse_stderr_errors()
|
|
153
|
+
error_count = len(stderr_errors)
|
|
154
|
+
|
|
155
|
+
return (error_count, warning_count)
|
|
156
|
+
|
|
157
|
+
def get_errors(self) -> list[dict]:
|
|
158
|
+
"""
|
|
159
|
+
Get detailed error information.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of error dicts with message, file, line info
|
|
163
|
+
"""
|
|
164
|
+
build_results = self.get_build_results()
|
|
165
|
+
errors = []
|
|
166
|
+
|
|
167
|
+
# Try to get errors from xcresult
|
|
168
|
+
if build_results:
|
|
169
|
+
try:
|
|
170
|
+
# Try top-level errors first (newer xcresult format)
|
|
171
|
+
if "errors" in build_results and isinstance(build_results.get("errors"), list):
|
|
172
|
+
for error in build_results["errors"]:
|
|
173
|
+
errors.append(
|
|
174
|
+
{
|
|
175
|
+
"message": error.get("message", "Unknown error"),
|
|
176
|
+
"type": error.get("issueType", "error"),
|
|
177
|
+
"location": self._extract_location_from_url(error.get("sourceURL")),
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# If not found, try legacy format: actions[0].buildResult.issues
|
|
182
|
+
if not errors:
|
|
183
|
+
actions = build_results.get("actions", {}).get("_values", [])
|
|
184
|
+
if actions:
|
|
185
|
+
build_result = actions[0].get("buildResult", {})
|
|
186
|
+
issues = build_result.get("issues", {})
|
|
187
|
+
error_summaries = issues.get("errorSummaries", {}).get("_values", [])
|
|
188
|
+
|
|
189
|
+
for error in error_summaries:
|
|
190
|
+
errors.append(
|
|
191
|
+
{
|
|
192
|
+
"message": error.get("message", {}).get(
|
|
193
|
+
"_value", "Unknown error"
|
|
194
|
+
),
|
|
195
|
+
"type": error.get("issueType", {}).get("_value", "error"),
|
|
196
|
+
"location": self._extract_location(error),
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
201
|
+
print(f"Warning: Could not parse errors from xcresult: {e}", file=sys.stderr)
|
|
202
|
+
|
|
203
|
+
# If no errors found in xcresult but stderr available, parse stderr
|
|
204
|
+
if not errors and self.stderr:
|
|
205
|
+
errors = self._parse_stderr_errors()
|
|
206
|
+
|
|
207
|
+
return errors
|
|
208
|
+
|
|
209
|
+
def get_warnings(self) -> list[dict]:
|
|
210
|
+
"""
|
|
211
|
+
Get detailed warning information.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of warning dicts with message, file, line info
|
|
215
|
+
"""
|
|
216
|
+
build_results = self.get_build_results()
|
|
217
|
+
if not build_results:
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
warnings = []
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
# Try top-level warnings first (newer xcresult format)
|
|
224
|
+
if "warnings" in build_results and isinstance(build_results.get("warnings"), list):
|
|
225
|
+
for warning in build_results["warnings"]:
|
|
226
|
+
warnings.append(
|
|
227
|
+
{
|
|
228
|
+
"message": warning.get("message", "Unknown warning"),
|
|
229
|
+
"type": warning.get("issueType", "warning"),
|
|
230
|
+
"location": self._extract_location_from_url(warning.get("sourceURL")),
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# If not found, try legacy format: actions[0].buildResult.issues
|
|
235
|
+
if not warnings:
|
|
236
|
+
actions = build_results.get("actions", {}).get("_values", [])
|
|
237
|
+
if not actions:
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
build_result = actions[0].get("buildResult", {})
|
|
241
|
+
issues = build_result.get("issues", {})
|
|
242
|
+
warning_summaries = issues.get("warningSummaries", {}).get("_values", [])
|
|
243
|
+
|
|
244
|
+
for warning in warning_summaries:
|
|
245
|
+
warnings.append(
|
|
246
|
+
{
|
|
247
|
+
"message": warning.get("message", {}).get("_value", "Unknown warning"),
|
|
248
|
+
"type": warning.get("issueType", {}).get("_value", "warning"),
|
|
249
|
+
"location": self._extract_location(warning),
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
254
|
+
print(f"Warning: Could not parse warnings: {e}", file=sys.stderr)
|
|
255
|
+
|
|
256
|
+
return warnings
|
|
257
|
+
|
|
258
|
+
def _extract_location(self, issue: dict) -> dict:
|
|
259
|
+
"""
|
|
260
|
+
Extract file location from issue.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
issue: Issue dict from xcresult
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Location dict with file, line, column
|
|
267
|
+
"""
|
|
268
|
+
location = {"file": None, "line": None, "column": None}
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
doc_location = issue.get("documentLocationInCreatingWorkspace", {})
|
|
272
|
+
location["file"] = doc_location.get("url", {}).get("_value")
|
|
273
|
+
location["line"] = doc_location.get("startingLineNumber", {}).get("_value")
|
|
274
|
+
location["column"] = doc_location.get("startingColumnNumber", {}).get("_value")
|
|
275
|
+
except (KeyError, TypeError):
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
return location
|
|
279
|
+
|
|
280
|
+
def _extract_location_from_url(self, source_url: str | None) -> dict:
|
|
281
|
+
"""
|
|
282
|
+
Extract file location from sourceURL (newer xcresult format).
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
source_url: Source URL like "file:///path/to/file.swift#StartingLineNumber=134&..."
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Location dict with file, line, column
|
|
289
|
+
"""
|
|
290
|
+
location = {"file": None, "line": None, "column": None}
|
|
291
|
+
|
|
292
|
+
if not source_url:
|
|
293
|
+
return location
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
# Split URL and fragment
|
|
297
|
+
if "#" in source_url:
|
|
298
|
+
file_part, fragment = source_url.split("#", 1)
|
|
299
|
+
|
|
300
|
+
# Extract file path
|
|
301
|
+
location["file"] = file_part.replace("file://", "")
|
|
302
|
+
|
|
303
|
+
# Parse fragment parameters
|
|
304
|
+
params = {}
|
|
305
|
+
for param in fragment.split("&"):
|
|
306
|
+
if "=" in param:
|
|
307
|
+
key, value = param.split("=", 1)
|
|
308
|
+
params[key] = value
|
|
309
|
+
|
|
310
|
+
# Extract line and column
|
|
311
|
+
location["line"] = (
|
|
312
|
+
int(params.get("StartingLineNumber", 0)) + 1
|
|
313
|
+
if "StartingLineNumber" in params
|
|
314
|
+
else None
|
|
315
|
+
)
|
|
316
|
+
location["column"] = (
|
|
317
|
+
int(params.get("StartingColumnNumber", 0)) + 1
|
|
318
|
+
if "StartingColumnNumber" in params
|
|
319
|
+
else None
|
|
320
|
+
)
|
|
321
|
+
else:
|
|
322
|
+
# No fragment, just file path
|
|
323
|
+
location["file"] = source_url.replace("file://", "")
|
|
324
|
+
|
|
325
|
+
except (ValueError, AttributeError):
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
return location
|
|
329
|
+
|
|
330
|
+
def _run_xcresulttool(self, args: list[str], parse_json: bool = True) -> Any | None:
|
|
331
|
+
"""
|
|
332
|
+
Run xcresulttool command.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
args: Command arguments (after 'xcresulttool')
|
|
336
|
+
parse_json: Whether to parse output as JSON
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Parsed JSON dict, plain text, or None on error
|
|
340
|
+
"""
|
|
341
|
+
if not self.xcresult_path:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
cmd = ["xcrun", "xcresulttool"] + args + ["--path", str(self.xcresult_path)]
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
348
|
+
|
|
349
|
+
if parse_json:
|
|
350
|
+
return json.loads(result.stdout)
|
|
351
|
+
return result.stdout
|
|
352
|
+
|
|
353
|
+
except subprocess.CalledProcessError as e:
|
|
354
|
+
print(f"Error running xcresulttool: {e}", file=sys.stderr)
|
|
355
|
+
print(f"stderr: {e.stderr}", file=sys.stderr)
|
|
356
|
+
return None
|
|
357
|
+
except json.JSONDecodeError as e:
|
|
358
|
+
print(f"Error parsing JSON from xcresulttool: {e}", file=sys.stderr)
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
def _parse_stderr_errors(self) -> list[dict]:
|
|
362
|
+
"""
|
|
363
|
+
Parse common errors from stderr output as fallback.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
List of error dicts parsed from stderr
|
|
367
|
+
"""
|
|
368
|
+
errors = []
|
|
369
|
+
|
|
370
|
+
if not self.stderr:
|
|
371
|
+
return errors
|
|
372
|
+
|
|
373
|
+
# Pattern 0: Swift/Clang compilation errors (e.g., "/path/file.swift:135:59: error: message")
|
|
374
|
+
compilation_error_pattern = (
|
|
375
|
+
r"^(?P<file>[^:]+):(?P<line>\d+):(?P<column>\d+):\s*error:\s*(?P<message>.+?)$"
|
|
376
|
+
)
|
|
377
|
+
for match in re.finditer(compilation_error_pattern, self.stderr, re.MULTILINE):
|
|
378
|
+
errors.append(
|
|
379
|
+
{
|
|
380
|
+
"message": match.group("message").strip(),
|
|
381
|
+
"type": "compilation",
|
|
382
|
+
"location": {
|
|
383
|
+
"file": match.group("file"),
|
|
384
|
+
"line": int(match.group("line")),
|
|
385
|
+
"column": int(match.group("column")),
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Pattern 1: xcodebuild top-level errors (e.g., "xcodebuild: error: Unable to find...")
|
|
391
|
+
xcodebuild_error_pattern = r"xcodebuild:\s*error:\s*(?P<message>.*?)(?:\n\n|\Z)"
|
|
392
|
+
for match in re.finditer(xcodebuild_error_pattern, self.stderr, re.DOTALL):
|
|
393
|
+
message = match.group("message").strip()
|
|
394
|
+
# Clean up multi-line messages
|
|
395
|
+
message = " ".join(line.strip() for line in message.split("\n") if line.strip())
|
|
396
|
+
errors.append(
|
|
397
|
+
{
|
|
398
|
+
"message": message,
|
|
399
|
+
"type": "build",
|
|
400
|
+
"location": {"file": None, "line": None, "column": None},
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Pattern 2: Provisioning profile errors
|
|
405
|
+
provisioning_pattern = r"error:.*?provisioning profile.*?(?:doesn't|does not|cannot).*?(?P<message>.*?)(?:\n|$)"
|
|
406
|
+
for match in re.finditer(provisioning_pattern, self.stderr, re.IGNORECASE):
|
|
407
|
+
errors.append(
|
|
408
|
+
{
|
|
409
|
+
"message": f"Provisioning profile error: {match.group('message').strip()}",
|
|
410
|
+
"type": "provisioning",
|
|
411
|
+
"location": {"file": None, "line": None, "column": None},
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Pattern 3: Code signing errors
|
|
416
|
+
signing_pattern = r"error:.*?(?:code sign|signing).*?(?P<message>.*?)(?:\n|$)"
|
|
417
|
+
for match in re.finditer(signing_pattern, self.stderr, re.IGNORECASE):
|
|
418
|
+
errors.append(
|
|
419
|
+
{
|
|
420
|
+
"message": f"Code signing error: {match.group('message').strip()}",
|
|
421
|
+
"type": "signing",
|
|
422
|
+
"location": {"file": None, "line": None, "column": None},
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Pattern 4: Generic compilation errors (but not if already captured)
|
|
427
|
+
if not errors:
|
|
428
|
+
generic_error_pattern = r"^(?:\*\*\s)?(?:error|❌):\s*(?P<message>.*?)(?:\n|$)"
|
|
429
|
+
for match in re.finditer(generic_error_pattern, self.stderr, re.MULTILINE):
|
|
430
|
+
message = match.group("message").strip()
|
|
431
|
+
errors.append(
|
|
432
|
+
{
|
|
433
|
+
"message": message,
|
|
434
|
+
"type": "build",
|
|
435
|
+
"location": {"file": None, "line": None, "column": None},
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Pattern 5: Specific "No profiles" error
|
|
440
|
+
if "No profiles for" in self.stderr:
|
|
441
|
+
no_profile_pattern = r"No profiles for '(?P<bundle_id>.*?)' were found"
|
|
442
|
+
for match in re.finditer(no_profile_pattern, self.stderr):
|
|
443
|
+
errors.append(
|
|
444
|
+
{
|
|
445
|
+
"message": f"No provisioning profile found for bundle ID '{match.group('bundle_id')}'",
|
|
446
|
+
"type": "provisioning",
|
|
447
|
+
"location": {"file": None, "line": None, "column": None},
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return errors
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ios-visual-qa-strategist
|
|
3
|
+
description: Plan and execute iOS app QA with adaptive visual reasoning and minimal screenshots. Use when Codex needs to test an iOS, Swift, SwiftUI, UIKit, Xcode, or App Store build by inspecting screenshots, simulator/device state, accessibility output, logs, Maestro/XCTest flows, or user-provided screen images; especially when the goal is to infer the highest-value test path while reducing redundant screenshot capture.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# iOS Visual QA Strategist
|
|
7
|
+
|
|
8
|
+
Use this skill to act as a QA strategist that sees the app like a user, reasons from available screen evidence, and captures only the screenshots needed to prove meaningful UI state.
|
|
9
|
+
|
|
10
|
+
## Core Principle
|
|
11
|
+
|
|
12
|
+
Prefer cheap evidence before visual evidence:
|
|
13
|
+
|
|
14
|
+
1. Read specs, tasks, source, route definitions, accessibility identifiers, and known flows.
|
|
15
|
+
2. Use accessibility tree, test logs, console logs, and deterministic assertions to navigate.
|
|
16
|
+
3. Capture screenshots only at high-value visual checkpoints.
|
|
17
|
+
4. Let each screenshot answer multiple assertions.
|
|
18
|
+
5. State confidence honestly when a flow was inferred rather than fully observed.
|
|
19
|
+
|
|
20
|
+
Do not treat screenshots as the only source of truth. Screenshots are strong for layout, content, and visible state; they are weak for persistence, network correctness, analytics, hidden navigation state, and business logic.
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
### 1. Establish Test Context
|
|
25
|
+
|
|
26
|
+
Identify:
|
|
27
|
+
|
|
28
|
+
- app target, scheme, simulator/device, OS version, and launch method
|
|
29
|
+
- feature under test and expected user outcome
|
|
30
|
+
- available automation tools: Maestro, XCTest, simctl, accessibility inspector, app logs, existing test files
|
|
31
|
+
- visual artifacts already provided by the user or stored in `docs/design/`, `docs/screenshots/`, `.kiro/specs/`, or test output folders
|
|
32
|
+
|
|
33
|
+
If this is an AWKit-managed project, respect its build/test wrapper rules. Do not bypass project automation by calling native build commands when the project requires a wrapper.
|
|
34
|
+
|
|
35
|
+
### 2. Build a Screen Model
|
|
36
|
+
|
|
37
|
+
Before interacting heavily, infer:
|
|
38
|
+
|
|
39
|
+
- entry points and navigation graph
|
|
40
|
+
- critical flows and the shortest representative path through them
|
|
41
|
+
- states that need visual proof: initial, loading, populated, empty, error, disabled, modal, keyboard, permission, and destructive-confirmation
|
|
42
|
+
- likely layout risks: text overflow, clipped controls, hidden safe-area content, keyboard overlap, dynamic type, localization, long names, and dark mode
|
|
43
|
+
|
|
44
|
+
For unfamiliar codebases, search for accessibility identifiers, screen names, route names, SwiftUI `View` types, UIKit controllers, and existing UI tests.
|
|
45
|
+
|
|
46
|
+
### 3. Write a Minimal-Capture Test Plan
|
|
47
|
+
|
|
48
|
+
Create a concise test plan with:
|
|
49
|
+
|
|
50
|
+
- primary user flow
|
|
51
|
+
- secondary/risk flows
|
|
52
|
+
- evidence type for each step: `accessibility`, `log`, `assertion`, `screenshot`, or `user-provided-image`
|
|
53
|
+
- screenshot budget and the reason each screenshot is worth taking
|
|
54
|
+
- stop conditions: what evidence is enough to pass, fail, or request clarification
|
|
55
|
+
|
|
56
|
+
Read `references/minimal-capture-policy.md` when deciding screenshot budget.
|
|
57
|
+
|
|
58
|
+
### 4. Execute Adaptively
|
|
59
|
+
|
|
60
|
+
For each step:
|
|
61
|
+
|
|
62
|
+
1. Predict expected screen state.
|
|
63
|
+
2. Navigate using the least expensive reliable signal.
|
|
64
|
+
3. Verify with accessibility/log/assertion first.
|
|
65
|
+
4. Capture a screenshot only when visual proof adds information.
|
|
66
|
+
5. Inspect the screenshot for multiple checks before taking another.
|
|
67
|
+
6. If the screenshot contradicts the predicted state, revise the test plan instead of continuing blindly.
|
|
68
|
+
|
|
69
|
+
Read `references/visual-reasoning-heuristics.md` when analyzing screenshots.
|
|
70
|
+
Read `references/ios-tool-selection.md` when choosing between Maestro, XCTest, simctl, and manual simulator interaction.
|
|
71
|
+
|
|
72
|
+
### 5. Report Findings
|
|
73
|
+
|
|
74
|
+
Always report:
|
|
75
|
+
|
|
76
|
+
- flows tested and flows intentionally skipped
|
|
77
|
+
- screenshot count and why each screenshot was necessary
|
|
78
|
+
- pass/fail findings with evidence
|
|
79
|
+
- visual issues, functional issues, and uncertainty separated
|
|
80
|
+
- recommended next test with the highest information gain
|
|
81
|
+
|
|
82
|
+
Use clear confidence labels:
|
|
83
|
+
|
|
84
|
+
- `High`: observed directly with screenshot or deterministic assertion
|
|
85
|
+
- `Medium`: verified through accessibility/logs and inferred visual continuity
|
|
86
|
+
- `Low`: inferred from code/spec only or blocked by missing evidence
|
|
87
|
+
|
|
88
|
+
## Screenshot Budget Rules
|
|
89
|
+
|
|
90
|
+
Default target for a single feature smoke pass:
|
|
91
|
+
|
|
92
|
+
- 1 entry screenshot
|
|
93
|
+
- 1 screenshot per distinct screen class
|
|
94
|
+
- 1 screenshot for the most important post-action state
|
|
95
|
+
- 1 failure screenshot when a bug is found
|
|
96
|
+
|
|
97
|
+
Add more only when the new capture can answer a question that the previous evidence cannot.
|
|
98
|
+
|
|
99
|
+
## Anti-Patterns
|
|
100
|
+
|
|
101
|
+
- Do not screenshot after every tap.
|
|
102
|
+
- Do not continue a scripted path after the visible state diverges from the plan.
|
|
103
|
+
- Do not mark visual QA complete using only logs or accessibility when the user asked for visual inspection.
|
|
104
|
+
- Do not ignore accessibility output; it often removes the need for another screenshot.
|
|
105
|
+
- Do not over-test low-risk decorative states while skipping checkout, auth, save, delete, onboarding, or permission flows.
|
|
106
|
+
|
|
107
|
+
## Resources
|
|
108
|
+
|
|
109
|
+
- `references/minimal-capture-policy.md` - screenshot budget and capture decision rules
|
|
110
|
+
- `references/visual-reasoning-heuristics.md` - what to inspect in screenshots
|
|
111
|
+
- `references/ios-tool-selection.md` - choosing iOS QA tools and evidence types
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# iOS Tool Selection
|
|
2
|
+
|
|
3
|
+
Choose the cheapest reliable tool for the assertion.
|
|
4
|
+
|
|
5
|
+
## Maestro
|
|
6
|
+
|
|
7
|
+
Use Maestro when:
|
|
8
|
+
|
|
9
|
+
- testing end-to-end user flows
|
|
10
|
+
- tapping visible text or accessibility identifiers
|
|
11
|
+
- capturing screenshots during flow checkpoints
|
|
12
|
+
- validating onboarding, auth, forms, tabs, sheets, and common navigation
|
|
13
|
+
|
|
14
|
+
Avoid Maestro as the only evidence for:
|
|
15
|
+
|
|
16
|
+
- low-level UIKit/SwiftUI component behavior
|
|
17
|
+
- hidden business logic
|
|
18
|
+
- persistence unless paired with relaunch or data checks
|
|
19
|
+
|
|
20
|
+
## XCTest / XCUITest
|
|
21
|
+
|
|
22
|
+
Use XCTest or XCUITest when:
|
|
23
|
+
|
|
24
|
+
- the repo already has UI tests
|
|
25
|
+
- deterministic assertions are more important than ad hoc exploration
|
|
26
|
+
- testing accessibility identifiers, navigation state, or repeated regression flows
|
|
27
|
+
- needing CI-ready tests after an exploratory bug is found
|
|
28
|
+
|
|
29
|
+
Prefer adding or updating tests only after the exploratory visual pass identifies stable assertions.
|
|
30
|
+
|
|
31
|
+
## simctl
|
|
32
|
+
|
|
33
|
+
Use `simctl` when:
|
|
34
|
+
|
|
35
|
+
- launching, terminating, or relaunching the app
|
|
36
|
+
- resetting permissions or app data
|
|
37
|
+
- recording screenshots or videos when project policy allows
|
|
38
|
+
- controlling simulator state in a repeatable way
|
|
39
|
+
|
|
40
|
+
Respect project wrappers and automation rules for build/test commands.
|
|
41
|
+
|
|
42
|
+
## Accessibility Tree
|
|
43
|
+
|
|
44
|
+
Use accessibility output when:
|
|
45
|
+
|
|
46
|
+
- checking whether a control exists, is enabled, or has expected text
|
|
47
|
+
- avoiding redundant screenshots after navigation
|
|
48
|
+
- confirming screen identity cheaply
|
|
49
|
+
- extracting labels from dense UIs
|
|
50
|
+
|
|
51
|
+
Accessibility does not prove layout quality. Capture a screenshot when visual arrangement matters.
|
|
52
|
+
|
|
53
|
+
## Logs
|
|
54
|
+
|
|
55
|
+
Use logs when:
|
|
56
|
+
|
|
57
|
+
- confirming network, persistence, analytics, or error conditions
|
|
58
|
+
- explaining why a visible state failed to update
|
|
59
|
+
- distinguishing UI bug from backend/data issue
|
|
60
|
+
|
|
61
|
+
Logs do not prove what the user saw. Pair them with screenshot evidence for user-facing failures.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Minimal Capture Policy
|
|
2
|
+
|
|
3
|
+
Use screenshots as expensive evidence. Every screenshot must have a reason before it is taken.
|
|
4
|
+
|
|
5
|
+
## Capture Decision
|
|
6
|
+
|
|
7
|
+
Take a screenshot when at least one condition is true:
|
|
8
|
+
|
|
9
|
+
- It is the first visual observation of the feature.
|
|
10
|
+
- A new screen class appears.
|
|
11
|
+
- The test reaches a visually important state: loaded data, empty state, error state, disabled state, destructive confirmation, permission prompt, modal, sheet, keyboard, or toast.
|
|
12
|
+
- Accessibility/log evidence confirms behavior but cannot prove layout or visible content.
|
|
13
|
+
- A bug is suspected and the screenshot will preserve evidence.
|
|
14
|
+
- The user explicitly asked for visual proof.
|
|
15
|
+
|
|
16
|
+
Skip a screenshot when:
|
|
17
|
+
|
|
18
|
+
- The accessibility tree already proves the expected state and no layout risk changed.
|
|
19
|
+
- The action only toggled hidden internal state.
|
|
20
|
+
- The next step will immediately produce a more informative screen.
|
|
21
|
+
- A previous screenshot of the same screen state already proves the assertion.
|
|
22
|
+
|
|
23
|
+
## Budget Heuristics
|
|
24
|
+
|
|
25
|
+
For one focused feature:
|
|
26
|
+
|
|
27
|
+
- 2-4 screenshots is usually enough for smoke QA.
|
|
28
|
+
- 4-7 screenshots is reasonable for a multi-screen critical flow.
|
|
29
|
+
- More than 7 screenshots needs a clear reason, such as visual regression, localization, dynamic type, dark mode, or a bug investigation.
|
|
30
|
+
|
|
31
|
+
For broad app exploration:
|
|
32
|
+
|
|
33
|
+
- Capture one screenshot per major screen category.
|
|
34
|
+
- Prefer a contact sheet or grouped artifacts if many screens must be compared.
|
|
35
|
+
- Do not exhaustively capture every tab, modal, or variant unless the task is specifically visual inventory.
|
|
36
|
+
|
|
37
|
+
## High-Information Captures
|
|
38
|
+
|
|
39
|
+
Prefer screenshots that prove several things at once:
|
|
40
|
+
|
|
41
|
+
- navigation arrived at the right screen
|
|
42
|
+
- important content is visible
|
|
43
|
+
- primary action is reachable
|
|
44
|
+
- layout respects safe areas
|
|
45
|
+
- loading/empty/error state resolved correctly
|
|
46
|
+
|
|
47
|
+
If a screenshot only proves "the button was tapped", avoid it.
|
|
48
|
+
|
|
49
|
+
## Failure Captures
|
|
50
|
+
|
|
51
|
+
When a failure occurs:
|
|
52
|
+
|
|
53
|
+
1. Capture the failing visible state.
|
|
54
|
+
2. Save or summarize the relevant accessibility tree/log excerpt.
|
|
55
|
+
3. Note the exact prior action.
|
|
56
|
+
4. Stop or branch the test plan; do not keep executing stale assumptions.
|