414 lines
16 KiB
Bash
Executable File
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
|