Files
2026-05-25 16:41:08 +07:00

454 lines
15 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"