#!/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"