Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi

This commit is contained in:
Kunthawat Greethong
2026-05-25 16:38:02 +07:00
commit 69f7d8bdda
1689 changed files with 342427 additions and 0 deletions

View File

@@ -0,0 +1,340 @@
#!/bin/bash
# ╔═══════════════════════════════════════════════════════════════════╗
# ║ cdp-helpers.sh — Chrome DevTools Protocol Helpers ║
# ╠═══════════════════════════════════════════════════════════════════╣
# ║ Source this file for CDP interaction with React Native apps: ║
# ║ source "$(dirname "$0")/../../lib/cdp-helpers.sh" ║
# ║ ║
# ║ Provides: ║
# ║ • CDP evaluation (cdp_eval, cdp_eval_safe) ║
# ║ • Navigation (cdp_navigate, cdp_navigate_tab) ║
# ║ • State queries (cdp_get_route, cdp_get_state) ║
# ║ • Auto-discovery (CDP target, navigation module) ║
# ║ ║
# ║ Requires: Node.js, 'ws' npm package, Metro dev server running ║
# ╚═══════════════════════════════════════════════════════════════════╝
set -euo pipefail
# ── Source configuration ─────────────────────────────────────────────
CDP_HELPERS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CDP_QA_ROOT="$(cd "$CDP_HELPERS_DIR/../.." && pwd)"
# Only source config if not already loaded
if [ -z "${QA_AUTOMATION_DIR:-}" ]; then
source "$CDP_QA_ROOT/qa.config.sh"
fi
# ── CDP State ────────────────────────────────────────────────────────
_CDP_WS_URL_RESOLVED=""
_CDP_NAV_MODULE_ID=""
# ── CDP Auto-Discovery ──────────────────────────────────────────────
# Ensure we have a CDP WebSocket URL
_ensure_cdp_ws_url() {
if [ -n "$_CDP_WS_URL_RESOLVED" ]; then
echo "$_CDP_WS_URL_RESOLVED"
return 0
fi
local ws_url
ws_url=$(qa_detect_cdp_ws_url 2>/dev/null || echo "")
if [ -n "$ws_url" ]; then
_CDP_WS_URL_RESOLVED="$ws_url"
echo "$ws_url"
return 0
fi
# Last resort: construct from config
if [ "$CDP_WS_URL" != "auto" ] && [ -n "$CDP_WS_URL" ]; then
_CDP_WS_URL_RESOLVED="$CDP_WS_URL"
echo "$CDP_WS_URL"
return 0
fi
echo ""
return 1
}
# Ensure we have a navigation module ID
_ensure_nav_module_id() {
if [ -n "$_CDP_NAV_MODULE_ID" ]; then
echo "$_CDP_NAV_MODULE_ID"
return 0
fi
# Check cache
if [ -f "$NAV_MODULE_CACHE" ]; then
local cached
cached=$(cat "$NAV_MODULE_CACHE" 2>/dev/null || echo "")
if [ -n "$cached" ]; then
_CDP_NAV_MODULE_ID="$cached"
echo "$cached"
return 0
fi
fi
# Auto-detect
local module_id
module_id=$(qa_detect_nav_module 2>/dev/null || echo "")
if [ -n "$module_id" ]; then
_CDP_NAV_MODULE_ID="$module_id"
echo "$module_id"
return 0
fi
echo ""
return 1
}
# ── Core CDP Functions ───────────────────────────────────────────────
# Execute JavaScript in the React Native Hermes runtime via CDP.
# Usage: cdp_eval "javascript expression"
# Returns: the evaluated result
cdp_eval() {
local expression="$1"
local ws_url
ws_url=$(_ensure_cdp_ws_url)
if [ -z "$ws_url" ]; then
echo '{"error":"no CDP WebSocket URL"}'
return 1
fi
cd "$PROJECT_DIR"
node -e "
const WebSocket=require('ws');
const ws=new WebSocket('$ws_url');
ws.on('open',()=>{
ws.send(JSON.stringify({
id:1,
method:'Runtime.evaluate',
params:{
expression: \`$expression\`,
returnByValue:true
}
}));
});
ws.on('message',d=>{
const m=JSON.parse(d);
if(m.id===1){
if(m.result?.result?.value) console.log(m.result.result.value);
else if(m.result?.exceptionDetails) console.error('CDP Error:', m.result.exceptionDetails.text);
else console.log(JSON.stringify(m.result));
ws.close();
process.exit(0);
}
});
ws.on('error',e=>{console.error('WS Error:',e.message);process.exit(1)});
setTimeout(()=>{console.error('CDP Timeout');process.exit(1)},8000);
" 2>&1
}
# Safe CDP eval — suppresses ErrorUtils/LogBox during execution.
# Use for any eval that touches Metro modules or could trigger side effects.
# Usage: cdp_eval_safe "javascript expression"
cdp_eval_safe() {
local expression="$1"
cdp_eval "
(function() {
var _origHandler = globalThis.ErrorUtils ? ErrorUtils.getGlobalHandler() : null;
var _origCE = console.error;
if (globalThis.ErrorUtils) ErrorUtils.setGlobalHandler(function() {});
console.error = function() {};
try {
return (function() { $expression })();
} finally {
if (globalThis.ErrorUtils && _origHandler) ErrorUtils.setGlobalHandler(_origHandler);
console.error = _origCE;
}
})();
"
}
# ── Navigation Functions ─────────────────────────────────────────────
# Navigate to a screen by name.
# Usage: cdp_navigate "ScreenName" [params_json]
cdp_navigate() {
local screen="$1"
local params="${2:-}"
local nav_id
nav_id=$(_ensure_nav_module_id)
if [ -z "$nav_id" ]; then
echo '{"error":"navigation module not found"}'
return 1
fi
if [ -n "$params" ]; then
cdp_eval "
const ref = __r(${nav_id}).navigationRef;
ref.current.navigate('${screen}', ${params});
JSON.stringify({ ok: true, route: ref.current.getCurrentRoute()?.name });
"
else
cdp_eval "
const ref = __r(${nav_id}).navigationRef;
ref.current.navigate('${screen}');
JSON.stringify({ ok: true, route: ref.current.getCurrentRoute()?.name });
"
fi
}
# Navigate to a bottom tab screen.
# Usage: cdp_navigate_tab "ScreenName"
cdp_navigate_tab() {
local tab="$1"
local nav_id
nav_id=$(_ensure_nav_module_id)
if [ -z "$nav_id" ]; then
echo '{"error":"navigation module not found"}'
return 1
fi
cdp_eval "
const ref = __r(${nav_id}).navigationRef;
ref.current.navigate('${TAB_NAVIGATOR_NAME}', { screen: '${tab}' });
JSON.stringify({ ok: true, route: ref.current.getCurrentRoute()?.name });
"
}
# Get current route name.
# Usage: route=$(cdp_get_route)
cdp_get_route() {
local nav_id
nav_id=$(_ensure_nav_module_id)
if [ -z "$nav_id" ]; then
echo "unknown"
return 1
fi
cdp_eval "
const ref = __r(${nav_id}).navigationRef;
ref.current.getCurrentRoute()?.name || 'unknown';
"
}
# Get full navigation state as JSON.
cdp_get_state() {
local nav_id
nav_id=$(_ensure_nav_module_id)
if [ -z "$nav_id" ]; then
echo '{"error":"navigation module not found"}'
return 1
fi
cdp_eval "
const ref = __r(${nav_id}).navigationRef;
const state = ref.current.getRootState();
function getRoutes(s, depth) {
let routes = [];
if (s.routes) {
for (const r of s.routes) {
routes.push({name: r.name, depth});
if (r.state) routes = routes.concat(getRoutes(r.state, depth+1));
}
}
return routes;
}
JSON.stringify({
current: ref.current.getCurrentRoute()?.name,
routes: getRoutes(state, 0)
});
"
}
# Go back in navigation.
cdp_go_back() {
local nav_id
nav_id=$(_ensure_nav_module_id)
if [ -z "$nav_id" ]; then
echo "error: no nav module"
return 1
fi
cdp_eval "
const ref = __r(${nav_id}).navigationRef;
if (ref.current.canGoBack()) {
ref.current.goBack();
'went back to: ' + ref.current.getCurrentRoute()?.name;
} else {
'cannot go back';
}
"
}
# Reset navigation to initial state.
cdp_reset_navigation() {
local nav_id
nav_id=$(_ensure_nav_module_id)
if [ -z "$nav_id" ]; then
echo "error: no nav module"
return 1
fi
cdp_eval "
const ref = __r(${nav_id}).navigationRef;
ref.current.reset({
index: 0,
routes: [{ name: 'Main', state: { routes: [{ name: '${TAB_NAVIGATOR_NAME}' }] } }]
});
'reset to ${TAB_NAVIGATOR_NAME}';
"
}
# ── Convenience Tab Navigation ───────────────────────────────────────
# These use the screen names from qa.config.sh
nav_tab_1() { [ -n "$SCREEN_EXPLORE" ] && cdp_navigate_tab "$SCREEN_EXPLORE" || echo "SCREEN_EXPLORE not configured"; }
nav_tab_2() { [ -n "$SCREEN_SEARCH" ] && cdp_navigate_tab "$SCREEN_SEARCH" || echo "SCREEN_SEARCH not configured"; }
nav_tab_3() { [ -n "$SCREEN_HOME" ] && cdp_navigate_tab "$SCREEN_HOME" || echo "SCREEN_HOME not configured"; }
nav_tab_4() { echo "Tab 4 not configured — set SCREEN name and add to cdp-helpers.sh"; }
nav_tab_5() { [ -n "$SCREEN_PROFILE" ] && cdp_navigate_tab "$SCREEN_PROFILE" || echo "SCREEN_PROFILE not configured"; }
# Generic by name
nav_explore() { nav_tab_1; }
nav_search() { nav_tab_2; }
nav_home() { nav_tab_3; }
nav_profile() { nav_tab_5; }
# ── Utility Functions ────────────────────────────────────────────────
# Check if user is logged in (heuristic: check current route)
cdp_is_logged_in() {
local route
route=$(cdp_get_route 2>/dev/null || echo "unknown")
if echo "$route" | grep -qiE "login|signup|auth|welcome|launch"; then
echo "false"
else
echo "true"
fi
}
# Clear cached module IDs (force re-discovery on next call)
cdp_clear_cache() {
_CDP_WS_URL_RESOLVED=""
_CDP_NAV_MODULE_ID=""
[ -f "$NAV_MODULE_CACHE" ] && rm -f "$NAV_MODULE_CACHE" 2>/dev/null || true
[ -f "$VIDEO_MODULE_CACHE" ] && rm -f "$VIDEO_MODULE_CACHE" 2>/dev/null || true
echo "CDP cache cleared"
}
# ── Export Functions ─────────────────────────────────────────────────
export -f cdp_eval cdp_eval_safe 2>/dev/null || true
export -f cdp_navigate cdp_navigate_tab cdp_get_route cdp_get_state 2>/dev/null || true
export -f cdp_go_back cdp_reset_navigation cdp_is_logged_in 2>/dev/null || true
export -f nav_explore nav_search nav_home nav_profile 2>/dev/null || true
export -f cdp_clear_cache 2>/dev/null || true
echo "CDP helpers loaded. WebSocket: ${_CDP_WS_URL_RESOLVED:-auto-detect}"

View File

@@ -0,0 +1,453 @@
#!/bin/bash
# ╔═══════════════════════════════════════════════════════════════════╗
# ║ test-helpers.sh — Core Test Framework for QA Automation ║
# ╠═══════════════════════════════════════════════════════════════════╣
# ║ Source this file at the top of every test flow script: ║
# ║ source "$(dirname "$0")/../../lib/test-helpers.sh" ║
# ║ ║
# ║ Provides: ║
# ║ • Test lifecycle (setup_test, teardown_test, step) ║
# ║ • Logging (log_pass, log_fail, log_info, log_warn) ║
# ║ • agent-device wrappers (tap, swipe, scroll, screenshot) ║
# ║ • Assertion functions (assert_app_foreground, etc.) ║
# ║ • Navigation helpers (tap_tab, launch_app, close_app) ║
# ╚═══════════════════════════════════════════════════════════════════╝
set -euo pipefail
# ── Source configuration ─────────────────────────────────────────────
HELPERS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
QA_ROOT="$(cd "$HELPERS_DIR/../.." && pwd)"
source "$QA_ROOT/qa.config.sh"
# ── Auto-detect simulator UDID if set to "auto" ─────────────────────
if [ "$SIMULATOR_UDID" = "auto" ]; then
_detected_udid=$(qa_detect_simulator_udid 2>/dev/null || echo "")
if [ -n "$_detected_udid" ]; then
SIMULATOR_UDID="$_detected_udid"
fi
fi
# ── Test State ───────────────────────────────────────────────────────
TEST_NAME=""
STEP_COUNT=0
PASS_COUNT=0
FAIL_COUNT=0
TEST_START_TIME=""
# ── Initialization ───────────────────────────────────────────────────
init_test_env() {
mkdir -p "$TEST_OUTPUT_DIR"
mkdir -p "$SCREENSHOT_DIR"
if [ ! -f "$RESULTS_FILE" ]; then
echo "Test Run: $(date '+%Y-%m-%d %H:%M:%S')" > "$RESULTS_FILE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >> "$RESULTS_FILE"
fi
}
init_test_env
# ── Test Lifecycle ───────────────────────────────────────────────────
setup_test() {
local name="$1"
TEST_NAME="$name"
TEST_START_TIME=$(date +%s)
STEP_COUNT=0
PASS_COUNT=0
FAIL_COUNT=0
mkdir -p "$SCREENSHOT_DIR/$TEST_NAME"
echo ""
echo "═══════════════════════════════════════════"
echo "🧪 TEST: $TEST_NAME"
echo "═══════════════════════════════════════════"
echo "Started: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
}
teardown_test() {
local name="${1:-$TEST_NAME}"
local end_time=$(date +%s)
local duration=$((end_time - TEST_START_TIME))
echo ""
echo "───────────────────────────────────────────"
echo "📊 RESULTS: $name"
echo " Steps: $STEP_COUNT | Passed: $PASS_COUNT | Failed: $FAIL_COUNT"
echo " Duration: ${duration}s"
if [ $FAIL_COUNT -eq 0 ]; then
echo " Status: ✅ ALL PASSED"
else
echo " Status: ❌ $FAIL_COUNT FAILURES"
fi
echo "───────────────────────────────────────────"
echo ""
echo "$(date '+%Y-%m-%d %H:%M:%S') | $name | Steps:$STEP_COUNT Pass:$PASS_COUNT Fail:$FAIL_COUNT | ${duration}s" >> "$RESULTS_FILE"
}
# ── Logging ──────────────────────────────────────────────────────────
step() {
STEP_COUNT=$((STEP_COUNT + 1))
echo " [$STEP_COUNT] $1"
}
log_pass() {
PASS_COUNT=$((PASS_COUNT + 1))
echo "$1"
}
log_fail() {
FAIL_COUNT=$((FAIL_COUNT + 1))
echo "$1"
}
log_info() {
echo " $1"
}
log_warn() {
echo " ⚠️ $1"
}
# ── agent-device Wrappers ────────────────────────────────────────────
# Take a screenshot using xcrun simctl (avoids agent-device focus issues)
# Falls back to agent-device if xcrun isn't available.
# Usage: take_screenshot "step_name" [session]
take_screenshot() {
local name="$1"
local session="${2:-$ACTIVE_SESSION}"
local path="$SCREENSHOT_DIR/$TEST_NAME/${name}.png"
mkdir -p "$(dirname "$path")"
# Prefer xcrun simctl for iOS (doesn't steal focus)
if [ "$SIMULATOR_UDID" != "auto" ] && command -v xcrun >/dev/null 2>&1; then
xcrun simctl io "$SIMULATOR_UDID" screenshot "$path" 2>/dev/null || {
# Fallback to agent-device
agent-device screenshot "$path" --session "$session" 2>/dev/null || {
log_warn "Screenshot failed for: $name"
return 1
}
}
else
agent-device screenshot "$path" --session "$session" 2>/dev/null || {
log_warn "Screenshot failed for: $name"
return 1
}
fi
echo "$path"
}
# Tap at coordinates with timeout handling
# Usage: tap x y [session]
tap() {
local x="$1"
local y="$2"
local session="${3:-$ACTIVE_SESSION}"
agent-device click "$x" "$y" --session "$session" 2>/dev/null &
local pid=$!
sleep 0.5
if ps -p $pid > /dev/null 2>&1; then
kill $pid 2>/dev/null || true
wait $pid 2>/dev/null || true
fi
sleep 1
}
# Swipe gesture
# Usage: swipe x1 y1 x2 y2 [session]
swipe() {
local x1="$1"
local y1="$2"
local x2="$3"
local y2="$4"
local session="${5:-$ACTIVE_SESSION}"
agent-device swipe "$x1" "$y1" "$x2" "$y2" --session "$session" 2>/dev/null &
local pid=$!
sleep 0.5
if ps -p $pid > /dev/null 2>&1; then
kill $pid 2>/dev/null || true
wait $pid 2>/dev/null || true
fi
sleep 1
}
# Scroll in a direction
# Usage: scroll_dir direction [session]
scroll_dir() {
local direction="$1"
local session="${2:-$ACTIVE_SESSION}"
agent-device scroll "$direction" --session "$session" 2>/dev/null &
local pid=$!
sleep 0.5
if ps -p $pid > /dev/null 2>&1; then
kill $pid 2>/dev/null || true
wait $pid 2>/dev/null || true
fi
sleep 1
}
# Type text into focused field
# Usage: type_text ref_or_coords text [session]
type_text() {
local ref="$1"
local text="$2"
local session="${3:-$ACTIVE_SESSION}"
agent-device fill "$ref" "$text" --session "$session" 2>/dev/null &
local pid=$!
sleep 0.5
if ps -p $pid > /dev/null 2>&1; then
kill $pid 2>/dev/null || true
wait $pid 2>/dev/null || true
fi
sleep 1
}
# Press home button
go_home() {
local session="${1:-$ACTIVE_SESSION}"
agent-device home --session "$session" 2>/dev/null &
local pid=$!
sleep 1
if ps -p $pid > /dev/null 2>&1; then
kill $pid 2>/dev/null || true
wait $pid 2>/dev/null || true
fi
sleep 2
}
# Go back
go_back() {
local session="${1:-$ACTIVE_SESSION}"
agent-device back --session "$session" 2>/dev/null &
local pid=$!
sleep 1
if ps -p $pid > /dev/null 2>&1; then
kill $pid 2>/dev/null || true
wait $pid 2>/dev/null || true
fi
sleep 2
}
# Check if the app process is running on the simulator
check_appstate() {
local session="${1:-$ACTIVE_SESSION}"
if [ "$SIMULATOR_UDID" != "auto" ] && command -v xcrun >/dev/null 2>&1; then
xcrun simctl spawn "$SIMULATOR_UDID" launchctl list 2>/dev/null | grep "UIKitApplication:${APP_BUNDLE_ID}" || echo ""
else
agent-device appstate --session "$session" 2>/dev/null || echo ""
fi
}
# Get accessibility snapshot
get_snapshot() {
local session="${1:-$ACTIVE_SESSION}"
local depth="${2:-5}"
timeout 10 agent-device snapshot -i --depth "$depth" --session "$session" 2>/dev/null || echo "(snapshot timeout)"
}
# ── Assertion Functions ──────────────────────────────────────────────
# Assert app is in foreground
assert_app_foreground() {
local session="${1:-$ACTIVE_SESSION}"
local state=$(check_appstate "$session" 2>/dev/null | grep -o "$APP_BUNDLE_ID" || true)
if [ -n "$state" ]; then
log_pass "App is in foreground ($APP_BUNDLE_ID)"
return 0
else
log_fail "App is NOT in foreground"
return 1
fi
}
# Assert screenshot was captured
assert_screenshot() {
local name="$1"
local path="$SCREENSHOT_DIR/$TEST_NAME/${name}.png"
if [ -f "$path" ] && [ -s "$path" ]; then
log_pass "Screenshot captured: $name"
return 0
else
log_fail "Screenshot missing or empty: $name"
return 1
fi
}
# Assert text is visible in accessibility tree
assert_text_visible() {
local text="$1"
local session="${2:-$ACTIVE_SESSION}"
local snapshot=$(get_snapshot "$session")
if echo "$snapshot" | grep -qi "$text"; then
log_pass "Text visible: '$text'"
return 0
else
log_warn "Text not found in accessibility tree: '$text'"
return 1
fi
}
# Assert element exists in accessibility tree
assert_element_exists() {
local label="$1"
local session="${2:-$ACTIVE_SESSION}"
local snapshot=$(get_snapshot "$session")
if echo "$snapshot" | grep -qi "label.*$label\|name.*$label"; then
log_pass "Element found: '$label'"
return 0
else
log_fail "Element not found: '$label'"
return 1
fi
}
# ── Navigation Helpers ───────────────────────────────────────────────
# Tap a tab by position (1-5)
# Usage: tap_tab 1 (first tab)
tap_tab_by_index() {
local index="$1"
local x_var="TAB_${index}_X"
local x="${!x_var:-}"
if [ -z "$x" ]; then
log_fail "No coordinate defined for tab index $index (set TAB_${index}_X)"
return 1
fi
tap "$x" "$TAB_BAR_Y"
sleep 2
}
# Tap a tab by name (reads screen names from config and maps to tab index)
# Usage: tap_tab "explore"
tap_tab() {
local tab_name="$1"
local tab_name_upper=$(echo "$tab_name" | tr '[:lower:]' '[:upper:]')
# Map common names to tab indices — customize in qa.config.sh
case "$tab_name_upper" in
EXPLORE|HOME|TAB1|1) tap_tab_by_index 1 ;;
SEARCH|TAB2|2) tap_tab_by_index 2 ;;
CREATE|TAB3|3) tap_tab_by_index 3 ;;
WALLET|TAB4|4) tap_tab_by_index 4 ;;
PROFILE|TAB5|5) tap_tab_by_index 5 ;;
*)
log_fail "Unknown tab: $tab_name"
return 1
;;
esac
}
# Launch the app fresh
launch_app() {
local session="${1:-$ACTIVE_SESSION}"
step "Launching app ($APP_BUNDLE_ID)"
if [ "$SIMULATOR_UDID" != "auto" ] && command -v xcrun >/dev/null 2>&1; then
xcrun simctl terminate "$SIMULATOR_UDID" "$APP_BUNDLE_ID" 2>/dev/null || true
sleep 1
xcrun simctl launch "$SIMULATOR_UDID" "$APP_BUNDLE_ID" 2>/dev/null || {
log_fail "Failed to launch app via xcrun"
return 1
}
else
agent-device open "$APP_BUNDLE_ID" --session "$session" --relaunch 2>/dev/null || {
log_fail "Failed to launch app"
return 1
}
fi
sleep "$APP_SETTLE_TIME"
log_pass "App launched"
}
# Close the app
close_app() {
local session="${1:-$ACTIVE_SESSION}"
if [ "$SIMULATOR_UDID" != "auto" ] && command -v xcrun >/dev/null 2>&1; then
xcrun simctl terminate "$SIMULATOR_UDID" "$APP_BUNDLE_ID" 2>/dev/null || {
log_warn "Could not terminate app via xcrun"
return 1
}
else
agent-device close --session "$session" 2>/dev/null || {
log_warn "Could not close app"
return 1
}
fi
sleep 2
}
# Wait for app to settle
wait_settle() {
local seconds="${1:-3}"
sleep "$seconds"
}
# ── Utility Functions ────────────────────────────────────────────────
# Print test summary
print_test_summary() {
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 TEST EXECUTION SUMMARY"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
tail -20 "$RESULTS_FILE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}
log_debug() {
local msg="$1"
echo "[DEBUG] $(date '+%H:%M:%S') $msg" >> "$RESULTS_FILE"
}
save_snapshot() {
local desc="$1"
local session="${2:-$ACTIVE_SESSION}"
local filename="$(echo "$desc" | tr ' ' '_' | tr -cd '[:alnum:]._-')"
local snap=$(get_snapshot "$session")
echo "$snap" > "$SCREENSHOT_DIR/$TEST_NAME/${filename}_snapshot.txt"
log_info "Saved snapshot: $filename"
}
# ── Error Handling ───────────────────────────────────────────────────
trap 'teardown_test 2>/dev/null || true' EXIT
on_error() {
local line=$1
log_fail "Error on line $line"
log_debug "Exit code: $?"
}
trap 'on_error ${LINENO}' ERR
# ── Export Functions ─────────────────────────────────────────────────
export -f setup_test teardown_test step log_pass log_fail log_info log_warn 2>/dev/null || true
export -f take_screenshot tap swipe scroll_dir type_text 2>/dev/null || true
export -f go_home go_back check_appstate get_snapshot 2>/dev/null || true
export -f assert_app_foreground assert_screenshot assert_text_visible assert_element_exists 2>/dev/null || true
export -f tap_tab tap_tab_by_index launch_app close_app wait_settle 2>/dev/null || true
export -f print_test_summary log_debug save_snapshot 2>/dev/null || true
echo "Test helpers loaded. Session: $ACTIVE_SESSION | App: $APP_BUNDLE_ID"