Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
453
skills/qa-automation/qa-test-flows/lib/test-helpers.sh
Executable file
453
skills/qa-automation/qa-test-flows/lib/test-helpers.sh
Executable 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"
|
||||
Reference in New Issue
Block a user