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

414 lines
16 KiB
Bash
Executable File

#!/bin/bash
# ╔═══════════════════════════════════════════════════════════════════╗
# ║ setup-guard.sh — Prerequisites Checker & Auto-Fixer ║
# ╠═══════════════════════════════════════════════════════════════════╣
# ║ Source at the top of any test flow or runner script: ║
# ║ source "$(dirname "$0")/../lib/setup-guard.sh" ║
# ║ ║
# ║ Checks (in order): ║
# ║ 1. iOS Simulator booted ║
# ║ 2. Dev server (Metro/Vite/etc.) running ║
# ║ 3. App in foreground ║
# ║ 4. CDP Hermes target available ║
# ║ 5. CDP connection functional (eval 1+1) ║
# ║ 6. Navigation module ID valid ║
# ║ 7. LogBox/error overlay dismissed ║
# ║ ║
# ║ Each check logs [SETUP] with OK/FIXING/FAILED status. ║
# ║ If a critical check fails, the guard exits non-zero. ║
# ╚═══════════════════════════════════════════════════════════════════╝
set -euo pipefail
# ── Source configuration and helpers ─────────────────────────────────
SETUP_GUARD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
QA_ROOT_SG="$(cd "$SETUP_GUARD_DIR/../.." && pwd 2>/dev/null || cd "$SETUP_GUARD_DIR/.." && pwd)"
# Source config if not already loaded
if [ -z "${QA_AUTOMATION_DIR:-}" ]; then
source "$QA_ROOT_SG/qa.config.sh" 2>/dev/null || source "$SETUP_GUARD_DIR/../../qa.config.sh"
fi
# Track state
_SETUP_GUARD_RAN=false
_SETUP_CDP_WS_URL=""
# ── Logging ──────────────────────────────────────────────────────────
_setup_log() {
local status="$1"
local message="$2"
printf " [SETUP] %-40s %s\n" "$message" "$status"
}
_setup_ok() { _setup_log "OK" "$1"; }
_setup_fixing() { _setup_log "FIXING" "$1"; }
_setup_failed() { _setup_log "FAILED" "$1"; }
_setup_skip() { _setup_log "SKIP" "$1"; }
# ── 1. Check/Boot iOS Simulator ─────────────────────────────────────
_check_simulator() {
# Auto-detect UDID if needed
if [ "$SIMULATOR_UDID" = "auto" ]; then
local detected
detected=$(qa_detect_simulator_udid 2>/dev/null || echo "")
if [ -n "$detected" ]; then
SIMULATOR_UDID="$detected"
_setup_ok "iOS Simulator auto-detected: $SIMULATOR_UDID"
return 0
fi
# No booted simulator — try to find any available one and boot it
_setup_fixing "No booted simulator — finding one to boot..."
local first_sim
first_sim=$(xcrun simctl list devices available 2>/dev/null | grep "iPhone" | head -1 | grep -oE '[A-F0-9-]{36}' || echo "")
if [ -n "$first_sim" ]; then
xcrun simctl boot "$first_sim" 2>/dev/null || true
open -a Simulator 2>/dev/null || true
sleep 5
SIMULATOR_UDID="$first_sim"
_setup_ok "iOS Simulator booted: $SIMULATOR_UDID"
return 0
fi
_setup_failed "No iOS Simulator found"
return 1
fi
# Specific UDID provided
local booted
booted=$(xcrun simctl list devices 2>/dev/null | grep "$SIMULATOR_UDID" | grep -c "Booted" || true)
if [ "$booted" -ge 1 ]; then
_setup_ok "iOS Simulator booted"
return 0
fi
_setup_fixing "iOS Simulator not booted — booting..."
xcrun simctl boot "$SIMULATOR_UDID" 2>/dev/null || true
open -a Simulator 2>/dev/null || true
sleep 5
booted=$(xcrun simctl list devices 2>/dev/null | grep "$SIMULATOR_UDID" | grep -c "Booted" || true)
if [ "$booted" -ge 1 ]; then
_setup_ok "iOS Simulator booted (after fix)"
return 0
fi
_setup_failed "iOS Simulator could not be booted"
return 1
}
# ── 2. Check/Start Dev Server ────────────────────────────────────────
_check_dev_server() {
local status_code
status_code=$(curl -s -o /dev/null -w "%{http_code}" "$DEV_SERVER_HEALTH" 2>/dev/null || echo "000")
if [ "$status_code" = "200" ]; then
_setup_ok "Dev server running on :${METRO_PORT}"
return 0
fi
_setup_fixing "Dev server not running — starting in background..."
cd "$PROJECT_DIR"
eval "$DEV_SERVER_CMD" > /tmp/qa-dev-server.log 2>&1 &
local server_pid=$!
echo "$server_pid" > /tmp/qa-dev-server.pid
local elapsed=0
while [ $elapsed -lt $DEV_SERVER_TIMEOUT ]; do
status_code=$(curl -s -o /dev/null -w "%{http_code}" "$DEV_SERVER_HEALTH" 2>/dev/null || echo "000")
if [ "$status_code" = "200" ]; then
_setup_ok "Dev server started (took ${elapsed}s)"
return 0
fi
sleep 2
elapsed=$((elapsed + 2))
done
_setup_failed "Dev server did not start within ${DEV_SERVER_TIMEOUT}s"
return 1
}
# ── 3. Check/Launch App in Foreground ────────────────────────────────
_check_app_foreground() {
# Use xcrun simctl launch (idempotent: if already running, brings to front)
local launch_result
launch_result=$(xcrun simctl launch "$SIMULATOR_UDID" "$APP_BUNDLE_ID" 2>&1 || true)
if echo "$launch_result" | grep -q "$APP_BUNDLE_ID"; then
_setup_ok "App in foreground ($APP_BUNDLE_ID)"
sleep 2
return 0
fi
_setup_fixing "App not launching — terminating and retrying..."
xcrun simctl terminate "$SIMULATOR_UDID" "$APP_BUNDLE_ID" 2>/dev/null || true
sleep 2
launch_result=$(xcrun simctl launch "$SIMULATOR_UDID" "$APP_BUNDLE_ID" 2>&1 || true)
sleep "$APP_SETTLE_TIME"
if echo "$launch_result" | grep -q "$APP_BUNDLE_ID"; then
_setup_ok "App launched and in foreground"
return 0
fi
_setup_failed "Could not launch app"
return 1
}
# ── 4. Wait for CDP Hermes Target ───────────────────────────────────
_check_cdp_target() {
local elapsed=0
local json
local relaunched=false
while [ $elapsed -lt $CDP_TIMEOUT ]; do
json=$(curl -s "$CDP_DISCOVERY_URL" 2>/dev/null || echo "[]")
if echo "$json" | grep -q '"webSocketDebuggerUrl"'; then
_SETUP_CDP_WS_URL=$(echo "$json" | node -e "
let d='';process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
try{
const targets=JSON.parse(d);
const hermes=targets.find(t=>t.description && t.description.includes('Bridgeless'));
console.log(hermes?hermes.webSocketDebuggerUrl:targets[0].webSocketDebuggerUrl);
}catch(e){console.log('');}
});
" 2>/dev/null || echo "")
if [ -n "$_SETUP_CDP_WS_URL" ]; then
export CDP_WS_URL="$_SETUP_CDP_WS_URL"
_setup_ok "CDP Hermes target available"
return 0
fi
fi
# If no CDP target after 10s, try relaunching the app
if [ $elapsed -ge 10 ] && [ "$relaunched" = false ]; then
_setup_fixing "No CDP target — relaunching app..."
xcrun simctl terminate "$SIMULATOR_UDID" "$APP_BUNDLE_ID" 2>/dev/null || true
sleep 2
xcrun simctl launch "$SIMULATOR_UDID" "$APP_BUNDLE_ID" 2>/dev/null || true
relaunched=true
sleep "$APP_SETTLE_TIME"
fi
sleep 2
elapsed=$((elapsed + 2))
done
_setup_failed "No CDP target found within ${CDP_TIMEOUT}s"
return 1
}
# ── 5. Validate CDP Connection ───────────────────────────────────────
_check_cdp_connection() {
local ws_url="${_SETUP_CDP_WS_URL:-${CDP_WS_URL:-}}"
if [ -z "$ws_url" ] || [ "$ws_url" = "auto" ]; then
_setup_failed "No CDP WebSocket URL available"
return 1
fi
local attempt=0
local max_attempts=3
while [ $attempt -lt $max_attempts ]; do
local result
result=$(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:'1+1',returnByValue:true}}));
});
ws.on('message',d=>{
const m=JSON.parse(d);
if(m.id===1){
const v=m.result?.result?.value;
console.log(v===2?'ok':'fail');
ws.close();process.exit(0);
}
});
ws.on('error',e=>{console.log('error');process.exit(1)});
setTimeout(()=>{console.log('timeout');process.exit(1)},5000);
" 2>/dev/null || echo "error")
if [ "$result" = "ok" ]; then
_setup_ok "CDP connection functional (eval 1+1=2)"
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
_setup_failed "CDP connection failed after $max_attempts attempts"
return 1
}
# ── 6. Validate Navigation Module ID ────────────────────────────────
_check_nav_module() {
local ws_url="${_SETUP_CDP_WS_URL:-${CDP_WS_URL:-}}"
if [ -z "$ws_url" ] || [ "$ws_url" = "auto" ]; then
_setup_failed "No CDP URL for nav module check"
return 1
fi
local attempt=0
local max_attempts=3
local result=""
while [ $attempt -lt $max_attempts ]; do
result=$(cd "$PROJECT_DIR" && node -e "
const WebSocket=require('ws');
const ws=new WebSocket('$ws_url');
ws.on('open',()=>{
const expr=\`
(function(){
var origHandler=globalThis.ErrorUtils?ErrorUtils.getGlobalHandler():null;
var origCE=console.error;
if(globalThis.ErrorUtils)ErrorUtils.setGlobalHandler(function(){});
console.error=function(){};
try{
for(var j=${MODULE_SCAN_START};j<${MODULE_SCAN_END};j++){
try{
var m=__r(j);
if(m&&m.navigationRef&&m.navigationRef.current){
return JSON.stringify({ok:true,moduleId:j});
}
}catch(e){}
}
return JSON.stringify({error:'not found'});
}finally{
if(globalThis.ErrorUtils&&origHandler)ErrorUtils.setGlobalHandler(origHandler);
console.error=origCE;
}
})();
\`;
ws.send(JSON.stringify({id:1,method:'Runtime.evaluate',params:{expression:expr,returnByValue:true}}));
});
ws.on('message',d=>{
const m=JSON.parse(d);
if(m.id===1){console.log(m.result?.result?.value||'error');ws.close();process.exit(0);}
});
ws.on('error',()=>{console.log('error');process.exit(1)});
setTimeout(()=>{console.log('timeout');process.exit(1)},10000);
" 2>/dev/null || echo '{"error":"node failed"}')
local module_id
module_id=$(echo "$result" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const o=JSON.parse(d);console.log(o.moduleId||'')}catch(e){console.log('')}})" 2>/dev/null || echo "")
if [ -n "$module_id" ]; then
echo "$module_id" > "$NAV_MODULE_CACHE"
_setup_ok "Navigation module ID: $module_id"
export NAV_MODULE_ID="$module_id"
return 0
fi
attempt=$((attempt + 1))
if [ $attempt -lt $max_attempts ]; then
sleep $((attempt * 3))
fi
done
_setup_failed "Navigation module not found after $max_attempts attempts"
return 1
}
# ── 7. Dismiss LogBox Error Overlay ──────────────────────────────────
_dismiss_logbox() {
local ws_url="${_SETUP_CDP_WS_URL:-${CDP_WS_URL:-}}"
if [ -z "$ws_url" ] || [ "$ws_url" = "auto" ]; then
_setup_ok "LogBox check skipped (no CDP URL)"
return 0
fi
local result
result=$(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:\`
(function(){
try{
var LogBox=require('react-native/Libraries/LogBox/LogBox');
if(LogBox&&LogBox.ignoreAllLogs) LogBox.ignoreAllLogs(true);
var LogBoxData=require('react-native/Libraries/LogBox/Data/LogBoxData');
if(LogBoxData&&LogBoxData.clear) LogBoxData.clear();
return 'suppressed';
}catch(e){
return 'no-logbox: '+e.message;
}
})();
\`,
returnByValue:true
}}));
});
ws.on('message',d=>{
const m=JSON.parse(d);
if(m.id===1){console.log(m.result?.result?.value||'unknown');ws.close();process.exit(0);}
});
ws.on('error',()=>{console.log('error');process.exit(1)});
setTimeout(()=>{console.log('timeout');process.exit(1)},5000);
" 2>/dev/null || echo "error")
if [ "$result" = "suppressed" ]; then
_setup_ok "LogBox suppressed for session"
else
_setup_ok "LogBox suppress attempted (result: $result)"
fi
return 0
}
# ── Main: Run All Checks ────────────────────────────────────────────
run_setup_guard() {
if [ "$_SETUP_GUARD_RAN" = true ]; then
return 0
fi
echo ""
echo "┌─────────────────────────────────────────────┐"
echo "│ SETUP GUARD — Checking prerequisites... │"
echo "└─────────────────────────────────────────────┘"
echo ""
local failed=0
_check_simulator || failed=$((failed + 1))
_check_dev_server || failed=$((failed + 1))
_check_app_foreground || failed=$((failed + 1))
_check_cdp_target || failed=$((failed + 1))
_check_cdp_connection || failed=$((failed + 1))
_check_nav_module || failed=$((failed + 1))
_dismiss_logbox || true # Non-critical
echo ""
if [ $failed -gt 0 ]; then
echo " SETUP GUARD: $failed critical check(s) failed. Aborting."
echo ""
return 1
fi
echo " SETUP GUARD: All checks passed. Ready to test."
echo ""
_SETUP_GUARD_RAN=true
return 0
}
# ── Export ────────────────────────────────────────────────────────────
export -f run_setup_guard 2>/dev/null || true
export VIDEO_MODULE_CACHE NAV_MODULE_CACHE 2>/dev/null || true