@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,323 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test Recorder for iOS Simulator Testing
|
|
4
|
+
|
|
5
|
+
Records test execution with automatic screenshots and documentation.
|
|
6
|
+
Optimized for minimal token output during execution.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
As a script: python scripts/test_recorder.py --test-name "Test Name" --output dir/
|
|
10
|
+
As a module: from scripts.test_recorder import TestRecorder
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
import time
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from common import (
|
|
21
|
+
capture_screenshot,
|
|
22
|
+
count_elements,
|
|
23
|
+
generate_screenshot_name,
|
|
24
|
+
get_accessibility_tree,
|
|
25
|
+
resolve_udid,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestRecorder:
|
|
30
|
+
"""Records test execution with screenshots and accessibility snapshots."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
test_name: str,
|
|
35
|
+
output_dir: str = "test-artifacts",
|
|
36
|
+
udid: str | None = None,
|
|
37
|
+
inline: bool = False,
|
|
38
|
+
screenshot_size: str = "half",
|
|
39
|
+
app_name: str | None = None,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Initialize test recorder.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
test_name: Name of the test being recorded
|
|
46
|
+
output_dir: Directory for test artifacts
|
|
47
|
+
udid: Optional device UDID (uses booted if not specified)
|
|
48
|
+
inline: If True, return screenshots as base64 (for vision-based automation)
|
|
49
|
+
screenshot_size: 'full', 'half', 'quarter', 'thumb' (default: 'half')
|
|
50
|
+
app_name: App name for semantic screenshot naming
|
|
51
|
+
"""
|
|
52
|
+
self.test_name = test_name
|
|
53
|
+
self.udid = udid
|
|
54
|
+
self.inline = inline
|
|
55
|
+
self.screenshot_size = screenshot_size
|
|
56
|
+
self.app_name = app_name
|
|
57
|
+
self.start_time = time.time()
|
|
58
|
+
self.steps: list[dict] = []
|
|
59
|
+
self.current_step = 0
|
|
60
|
+
|
|
61
|
+
# Create timestamped output directory
|
|
62
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
63
|
+
safe_name = test_name.lower().replace(" ", "-")
|
|
64
|
+
self.output_dir = Path(output_dir) / f"{safe_name}-{timestamp}"
|
|
65
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
# Create subdirectories (only if not in inline mode)
|
|
68
|
+
if not inline:
|
|
69
|
+
self.screenshots_dir = self.output_dir / "screenshots"
|
|
70
|
+
self.screenshots_dir.mkdir(exist_ok=True)
|
|
71
|
+
else:
|
|
72
|
+
self.screenshots_dir = None
|
|
73
|
+
|
|
74
|
+
self.accessibility_dir = self.output_dir / "accessibility"
|
|
75
|
+
self.accessibility_dir.mkdir(exist_ok=True)
|
|
76
|
+
|
|
77
|
+
# Token-efficient output
|
|
78
|
+
mode_str = "(inline mode)" if inline else ""
|
|
79
|
+
print(f"Recording: {test_name} {mode_str}")
|
|
80
|
+
print(f"Output: {self.output_dir}/")
|
|
81
|
+
|
|
82
|
+
def step(
|
|
83
|
+
self,
|
|
84
|
+
description: str,
|
|
85
|
+
screen_name: str | None = None,
|
|
86
|
+
state: str | None = None,
|
|
87
|
+
assertion: str | None = None,
|
|
88
|
+
metadata: dict | None = None,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Record a test step with automatic screenshot.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
description: Step description
|
|
95
|
+
screen_name: Screen name for semantic naming
|
|
96
|
+
state: State description for semantic naming
|
|
97
|
+
assertion: Optional assertion to verify
|
|
98
|
+
metadata: Optional metadata for the step
|
|
99
|
+
"""
|
|
100
|
+
self.current_step += 1
|
|
101
|
+
step_time = time.time() - self.start_time
|
|
102
|
+
|
|
103
|
+
# Capture screenshot using new utility
|
|
104
|
+
screenshot_result = capture_screenshot(
|
|
105
|
+
self.udid,
|
|
106
|
+
size=self.screenshot_size,
|
|
107
|
+
inline=self.inline,
|
|
108
|
+
app_name=self.app_name,
|
|
109
|
+
screen_name=screen_name or description,
|
|
110
|
+
state=state,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Capture accessibility tree
|
|
114
|
+
accessibility_path = (
|
|
115
|
+
self.accessibility_dir
|
|
116
|
+
/ f"{self.current_step:03d}-{description.lower().replace(' ', '-')[:20]}.json"
|
|
117
|
+
)
|
|
118
|
+
element_count = self._capture_accessibility(accessibility_path)
|
|
119
|
+
|
|
120
|
+
# Store step data
|
|
121
|
+
step_data = {
|
|
122
|
+
"number": self.current_step,
|
|
123
|
+
"description": description,
|
|
124
|
+
"timestamp": step_time,
|
|
125
|
+
"element_count": element_count,
|
|
126
|
+
"accessibility": accessibility_path.name,
|
|
127
|
+
"screenshot_mode": screenshot_result["mode"],
|
|
128
|
+
"screenshot_size": self.screenshot_size,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Handle screenshot data based on mode
|
|
132
|
+
if screenshot_result["mode"] == "file":
|
|
133
|
+
step_data["screenshot"] = screenshot_result["file_path"]
|
|
134
|
+
step_data["screenshot_name"] = Path(screenshot_result["file_path"]).name
|
|
135
|
+
else:
|
|
136
|
+
# Inline mode
|
|
137
|
+
step_data["screenshot_base64"] = screenshot_result["base64_data"]
|
|
138
|
+
step_data["screenshot_dimensions"] = (
|
|
139
|
+
screenshot_result["width"],
|
|
140
|
+
screenshot_result["height"],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if assertion:
|
|
144
|
+
step_data["assertion"] = assertion
|
|
145
|
+
step_data["assertion_passed"] = True
|
|
146
|
+
|
|
147
|
+
if metadata:
|
|
148
|
+
step_data["metadata"] = metadata
|
|
149
|
+
|
|
150
|
+
self.steps.append(step_data)
|
|
151
|
+
|
|
152
|
+
# Token-efficient output (single line)
|
|
153
|
+
status = "✓" if not assertion or step_data.get("assertion_passed") else "✗"
|
|
154
|
+
screenshot_info = (
|
|
155
|
+
f" [{screenshot_result['width']}x{screenshot_result['height']}]" if self.inline else ""
|
|
156
|
+
)
|
|
157
|
+
print(
|
|
158
|
+
f"{status} Step {self.current_step}: {description} ({step_time:.1f}s){screenshot_info}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _capture_screenshot(self, output_path: Path) -> bool:
|
|
162
|
+
"""Capture screenshot using simctl."""
|
|
163
|
+
cmd = ["xcrun", "simctl", "io"]
|
|
164
|
+
|
|
165
|
+
if self.udid:
|
|
166
|
+
cmd.append(self.udid)
|
|
167
|
+
else:
|
|
168
|
+
cmd.append("booted")
|
|
169
|
+
|
|
170
|
+
cmd.extend(["screenshot", str(output_path)])
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
subprocess.run(cmd, capture_output=True, check=True)
|
|
174
|
+
return True
|
|
175
|
+
except subprocess.CalledProcessError:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def _capture_accessibility(self, output_path: Path) -> int:
|
|
179
|
+
"""Capture accessibility tree and return element count."""
|
|
180
|
+
try:
|
|
181
|
+
# Use shared utility to fetch tree
|
|
182
|
+
tree = get_accessibility_tree(self.udid, nested=True)
|
|
183
|
+
|
|
184
|
+
# Save tree
|
|
185
|
+
with open(output_path, "w") as f:
|
|
186
|
+
json.dump(tree, f, indent=2)
|
|
187
|
+
|
|
188
|
+
# Count elements using shared utility
|
|
189
|
+
return count_elements(tree)
|
|
190
|
+
except Exception:
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
def generate_report(self) -> dict[str, str]:
|
|
194
|
+
"""
|
|
195
|
+
Generate markdown test report.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Dictionary with paths to generated files
|
|
199
|
+
"""
|
|
200
|
+
duration = time.time() - self.start_time
|
|
201
|
+
report_path = self.output_dir / "report.md"
|
|
202
|
+
|
|
203
|
+
# Generate markdown
|
|
204
|
+
with open(report_path, "w") as f:
|
|
205
|
+
f.write(f"# Test Report: {self.test_name}\n\n")
|
|
206
|
+
f.write(f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
207
|
+
f.write(f"**Duration:** {duration:.1f} seconds\n")
|
|
208
|
+
f.write(f"**Steps:** {len(self.steps)}\n\n")
|
|
209
|
+
|
|
210
|
+
# Steps section
|
|
211
|
+
f.write("## Test Steps\n\n")
|
|
212
|
+
for step in self.steps:
|
|
213
|
+
f.write(
|
|
214
|
+
f"### Step {step['number']}: {step['description']} ({step['timestamp']:.1f}s)\n\n"
|
|
215
|
+
)
|
|
216
|
+
f.write(f"\n\n")
|
|
217
|
+
|
|
218
|
+
if step.get("assertion"):
|
|
219
|
+
status = "✓" if step.get("assertion_passed") else "✗"
|
|
220
|
+
f.write(f"**Assertion:** {step['assertion']} {status}\n\n")
|
|
221
|
+
|
|
222
|
+
if step.get("metadata"):
|
|
223
|
+
f.write("**Metadata:**\n")
|
|
224
|
+
for key, value in step["metadata"].items():
|
|
225
|
+
f.write(f"- {key}: {value}\n")
|
|
226
|
+
f.write("\n")
|
|
227
|
+
|
|
228
|
+
f.write(f"**Accessibility Elements:** {step['element_count']}\n\n")
|
|
229
|
+
f.write("---\n\n")
|
|
230
|
+
|
|
231
|
+
# Summary
|
|
232
|
+
f.write("## Summary\n\n")
|
|
233
|
+
f.write(f"- Total steps: {len(self.steps)}\n")
|
|
234
|
+
f.write(f"- Duration: {duration:.1f}s\n")
|
|
235
|
+
f.write(f"- Screenshots: {len(self.steps)}\n")
|
|
236
|
+
f.write(f"- Accessibility snapshots: {len(self.steps)}\n")
|
|
237
|
+
|
|
238
|
+
# Save metadata JSON
|
|
239
|
+
metadata_path = self.output_dir / "metadata.json"
|
|
240
|
+
with open(metadata_path, "w") as f:
|
|
241
|
+
json.dump(
|
|
242
|
+
{
|
|
243
|
+
"test_name": self.test_name,
|
|
244
|
+
"duration": duration,
|
|
245
|
+
"steps": self.steps,
|
|
246
|
+
"timestamp": datetime.now().isoformat(),
|
|
247
|
+
},
|
|
248
|
+
f,
|
|
249
|
+
indent=2,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Token-efficient output
|
|
253
|
+
print(f"Report: {report_path}")
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"markdown_path": str(report_path),
|
|
257
|
+
"metadata_path": str(metadata_path),
|
|
258
|
+
"output_dir": str(self.output_dir),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def main():
|
|
263
|
+
"""Main entry point for command-line usage."""
|
|
264
|
+
parser = argparse.ArgumentParser(
|
|
265
|
+
description="Record test execution with screenshots and documentation"
|
|
266
|
+
)
|
|
267
|
+
parser.add_argument("--test-name", required=True, help="Name of the test being recorded")
|
|
268
|
+
parser.add_argument(
|
|
269
|
+
"--output", default="test-artifacts", help="Output directory for test artifacts"
|
|
270
|
+
)
|
|
271
|
+
parser.add_argument(
|
|
272
|
+
"--udid",
|
|
273
|
+
help="Device UDID (auto-detects booted simulator if not provided)",
|
|
274
|
+
)
|
|
275
|
+
parser.add_argument(
|
|
276
|
+
"--inline",
|
|
277
|
+
action="store_true",
|
|
278
|
+
help="Return screenshots as base64 (inline mode for vision-based automation)",
|
|
279
|
+
)
|
|
280
|
+
parser.add_argument(
|
|
281
|
+
"--size",
|
|
282
|
+
choices=["full", "half", "quarter", "thumb"],
|
|
283
|
+
default="half",
|
|
284
|
+
help="Screenshot size for token optimization (default: half)",
|
|
285
|
+
)
|
|
286
|
+
parser.add_argument("--app-name", help="App name for semantic screenshot naming")
|
|
287
|
+
|
|
288
|
+
args = parser.parse_args()
|
|
289
|
+
|
|
290
|
+
# Resolve UDID with auto-detection
|
|
291
|
+
try:
|
|
292
|
+
udid = resolve_udid(args.udid)
|
|
293
|
+
except RuntimeError as e:
|
|
294
|
+
print(f"Error: {e}")
|
|
295
|
+
import sys
|
|
296
|
+
|
|
297
|
+
sys.exit(1)
|
|
298
|
+
|
|
299
|
+
# Create recorder
|
|
300
|
+
TestRecorder(
|
|
301
|
+
test_name=args.test_name,
|
|
302
|
+
output_dir=args.output,
|
|
303
|
+
udid=udid,
|
|
304
|
+
inline=args.inline,
|
|
305
|
+
screenshot_size=args.size,
|
|
306
|
+
app_name=args.app_name,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
print("Test recorder initialized. Use the following methods:")
|
|
310
|
+
print(' recorder.step("description") - Record a test step')
|
|
311
|
+
print(" recorder.generate_report() - Generate final report")
|
|
312
|
+
print()
|
|
313
|
+
print("Example:")
|
|
314
|
+
print(' recorder.step("Launch app", screen_name="Splash")')
|
|
315
|
+
print(
|
|
316
|
+
' recorder.step("Enter credentials", screen_name="Login", state="Empty", metadata={"user": "test"})'
|
|
317
|
+
)
|
|
318
|
+
print(' recorder.step("Verify login", assertion="Home screen visible")')
|
|
319
|
+
print(" recorder.generate_report()")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
main()
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Visual Diff Tool for iOS Simulator Screenshots
|
|
4
|
+
|
|
5
|
+
Compares two screenshots pixel-by-pixel to detect visual changes.
|
|
6
|
+
Optimized for minimal token output.
|
|
7
|
+
|
|
8
|
+
Usage: python scripts/visual_diff.py baseline.png current.png [options]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from PIL import Image, ImageChops, ImageDraw
|
|
19
|
+
except ImportError:
|
|
20
|
+
print("Error: Pillow not installed. Run: pip3 install pillow")
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VisualDiffer:
|
|
25
|
+
"""Performs visual comparison between screenshots."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, threshold: float = 0.01):
|
|
28
|
+
"""
|
|
29
|
+
Initialize differ with threshold.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
threshold: Maximum acceptable difference ratio (0.01 = 1%)
|
|
33
|
+
"""
|
|
34
|
+
self.threshold = threshold
|
|
35
|
+
|
|
36
|
+
def compare(self, baseline_path: str, current_path: str) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
Compare two images and return difference metrics.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
baseline_path: Path to baseline image
|
|
42
|
+
current_path: Path to current image
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dictionary with comparison results
|
|
46
|
+
"""
|
|
47
|
+
# Load images
|
|
48
|
+
try:
|
|
49
|
+
baseline = Image.open(baseline_path)
|
|
50
|
+
current = Image.open(current_path)
|
|
51
|
+
except FileNotFoundError as e:
|
|
52
|
+
print(f"Error: Image not found - {e}")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"Error: Failed to load image - {e}")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
# Verify dimensions match
|
|
59
|
+
if baseline.size != current.size:
|
|
60
|
+
return {
|
|
61
|
+
"error": "Image dimensions do not match",
|
|
62
|
+
"baseline_size": baseline.size,
|
|
63
|
+
"current_size": current.size,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Convert to RGB if needed
|
|
67
|
+
if baseline.mode != "RGB":
|
|
68
|
+
baseline = baseline.convert("RGB")
|
|
69
|
+
if current.mode != "RGB":
|
|
70
|
+
current = current.convert("RGB")
|
|
71
|
+
|
|
72
|
+
# Calculate difference
|
|
73
|
+
diff = ImageChops.difference(baseline, current)
|
|
74
|
+
|
|
75
|
+
# Calculate metrics
|
|
76
|
+
total_pixels = baseline.size[0] * baseline.size[1]
|
|
77
|
+
diff_pixels = self._count_different_pixels(diff)
|
|
78
|
+
diff_percentage = (diff_pixels / total_pixels) * 100
|
|
79
|
+
|
|
80
|
+
# Determine pass/fail
|
|
81
|
+
passed = diff_percentage <= (self.threshold * 100)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"dimensions": baseline.size,
|
|
85
|
+
"total_pixels": total_pixels,
|
|
86
|
+
"different_pixels": diff_pixels,
|
|
87
|
+
"difference_percentage": round(diff_percentage, 2),
|
|
88
|
+
"threshold_percentage": self.threshold * 100,
|
|
89
|
+
"passed": passed,
|
|
90
|
+
"verdict": "PASS" if passed else "FAIL",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def _count_different_pixels(self, diff_image: Image.Image) -> int:
|
|
94
|
+
"""Count number of pixels that are different."""
|
|
95
|
+
# Convert to grayscale for easier processing
|
|
96
|
+
diff_gray = diff_image.convert("L")
|
|
97
|
+
|
|
98
|
+
# Count non-zero pixels (different)
|
|
99
|
+
pixels = diff_gray.getdata()
|
|
100
|
+
return sum(1 for pixel in pixels if pixel > 10) # Threshold for noise
|
|
101
|
+
|
|
102
|
+
def generate_diff_image(self, baseline_path: str, current_path: str, output_path: str) -> None:
|
|
103
|
+
"""Generate highlighted difference image."""
|
|
104
|
+
baseline = Image.open(baseline_path).convert("RGB")
|
|
105
|
+
current = Image.open(current_path).convert("RGB")
|
|
106
|
+
|
|
107
|
+
# Create difference image
|
|
108
|
+
diff = ImageChops.difference(baseline, current)
|
|
109
|
+
|
|
110
|
+
# Enhance differences with red overlay
|
|
111
|
+
diff_enhanced = Image.new("RGB", baseline.size)
|
|
112
|
+
for x in range(baseline.size[0]):
|
|
113
|
+
for y in range(baseline.size[1]):
|
|
114
|
+
diff_pixel = diff.getpixel((x, y))
|
|
115
|
+
if sum(diff_pixel) > 30: # Threshold for visibility
|
|
116
|
+
# Highlight in red
|
|
117
|
+
diff_enhanced.putpixel((x, y), (255, 0, 0))
|
|
118
|
+
else:
|
|
119
|
+
# Keep original
|
|
120
|
+
diff_enhanced.putpixel((x, y), current.getpixel((x, y)))
|
|
121
|
+
|
|
122
|
+
diff_enhanced.save(output_path)
|
|
123
|
+
|
|
124
|
+
def generate_side_by_side(
|
|
125
|
+
self, baseline_path: str, current_path: str, output_path: str
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Generate side-by-side comparison image."""
|
|
128
|
+
baseline = Image.open(baseline_path)
|
|
129
|
+
current = Image.open(current_path)
|
|
130
|
+
|
|
131
|
+
# Create combined image
|
|
132
|
+
width = baseline.size[0] * 2 + 10 # 10px separator
|
|
133
|
+
height = max(baseline.size[1], current.size[1])
|
|
134
|
+
combined = Image.new("RGB", (width, height), color=(128, 128, 128))
|
|
135
|
+
|
|
136
|
+
# Paste images
|
|
137
|
+
combined.paste(baseline, (0, 0))
|
|
138
|
+
combined.paste(current, (baseline.size[0] + 10, 0))
|
|
139
|
+
|
|
140
|
+
combined.save(output_path)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main():
|
|
144
|
+
"""Main entry point."""
|
|
145
|
+
parser = argparse.ArgumentParser(description="Compare screenshots for visual differences")
|
|
146
|
+
parser.add_argument("baseline", help="Path to baseline screenshot")
|
|
147
|
+
parser.add_argument("current", help="Path to current screenshot")
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"--output",
|
|
150
|
+
default=".",
|
|
151
|
+
help="Output directory for diff artifacts (default: current directory)",
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--threshold",
|
|
155
|
+
type=float,
|
|
156
|
+
default=0.01,
|
|
157
|
+
help="Acceptable difference threshold (0.01 = 1%%, default: 0.01)",
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--details", action="store_true", help="Show detailed output (increases tokens)"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
|
|
165
|
+
# Create output directory if needed
|
|
166
|
+
output_dir = Path(args.output)
|
|
167
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
|
|
169
|
+
# Initialize differ
|
|
170
|
+
differ = VisualDiffer(threshold=args.threshold)
|
|
171
|
+
|
|
172
|
+
# Perform comparison
|
|
173
|
+
result = differ.compare(args.baseline, args.current)
|
|
174
|
+
|
|
175
|
+
# Handle dimension mismatch
|
|
176
|
+
if "error" in result:
|
|
177
|
+
print(f"Error: {result['error']}")
|
|
178
|
+
print(f"Baseline: {result['baseline_size']}")
|
|
179
|
+
print(f"Current: {result['current_size']}")
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
# Generate artifacts
|
|
183
|
+
diff_image_path = output_dir / "diff.png"
|
|
184
|
+
comparison_image_path = output_dir / "side-by-side.png"
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
differ.generate_diff_image(args.baseline, args.current, str(diff_image_path))
|
|
188
|
+
differ.generate_side_by_side(args.baseline, args.current, str(comparison_image_path))
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print(f"Warning: Could not generate images - {e}")
|
|
191
|
+
|
|
192
|
+
# Output results (token-optimized)
|
|
193
|
+
if args.details:
|
|
194
|
+
# Detailed output
|
|
195
|
+
report = {
|
|
196
|
+
"summary": {
|
|
197
|
+
"baseline": args.baseline,
|
|
198
|
+
"current": args.current,
|
|
199
|
+
"threshold": args.threshold,
|
|
200
|
+
"passed": result["passed"],
|
|
201
|
+
},
|
|
202
|
+
"results": result,
|
|
203
|
+
"artifacts": {
|
|
204
|
+
"diff_image": str(diff_image_path),
|
|
205
|
+
"comparison_image": str(comparison_image_path),
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
print(json.dumps(report, indent=2))
|
|
209
|
+
else:
|
|
210
|
+
# Minimal output (default)
|
|
211
|
+
print(f"Difference: {result['difference_percentage']}% ({result['verdict']})")
|
|
212
|
+
if result["different_pixels"] > 0:
|
|
213
|
+
print(f"Changed pixels: {result['different_pixels']:,}")
|
|
214
|
+
print(f"Artifacts saved to: {output_dir}/")
|
|
215
|
+
|
|
216
|
+
# Save JSON report
|
|
217
|
+
report_path = output_dir / "diff-report.json"
|
|
218
|
+
with open(report_path, "w") as f:
|
|
219
|
+
json.dump(
|
|
220
|
+
{
|
|
221
|
+
"baseline": os.path.basename(args.baseline),
|
|
222
|
+
"current": os.path.basename(args.current),
|
|
223
|
+
"results": result,
|
|
224
|
+
"artifacts": {"diff": "diff.png", "comparison": "side-by-side.png"},
|
|
225
|
+
},
|
|
226
|
+
f,
|
|
227
|
+
indent=2,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Exit with error if test failed
|
|
231
|
+
sys.exit(0 if result["passed"] else 1)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Xcode build automation module.
|
|
3
|
+
|
|
4
|
+
Provides structured, modular access to xcodebuild and xcresult functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .builder import BuildRunner
|
|
8
|
+
from .cache import XCResultCache
|
|
9
|
+
from .config import Config
|
|
10
|
+
from .reporter import OutputFormatter
|
|
11
|
+
from .xcresult import XCResultParser
|
|
12
|
+
|
|
13
|
+
__all__ = ["BuildRunner", "Config", "OutputFormatter", "XCResultCache", "XCResultParser"]
|