#!/bin/bash # ╔═══════════════════════════════════════════════════════════════════╗ # ║ qa.config.sh — Central Configuration for QA Automation Skills ║ # ╠═══════════════════════════════════════════════════════════════════╣ # ║ Source this file at the top of every QA script: ║ # ║ source "$(dirname "$0")/../qa.config.sh" ║ # ║ ║ # ║ Override any variable by exporting it before sourcing: ║ # ║ export APP_BUNDLE_ID="com.myapp.dev" ║ # ║ source qa.config.sh ║ # ║ ║ # ║ Or create a local override file: ║ # ║ qa.config.local.sh (gitignored, sourced automatically) ║ # ╚═══════════════════════════════════════════════════════════════════╝ set -euo pipefail # ── Resolve paths ──────────────────────────────────────────────────── QA_AUTOMATION_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ── App Configuration ──────────────────────────────────────────────── # Bundle/package identifier for your app (dev build) export APP_BUNDLE_ID="${APP_BUNDLE_ID:-com.example.app.dev}" # Bundle/package identifier for production builds export APP_BUNDLE_ID_PROD="${APP_BUNDLE_ID_PROD:-com.example.app}" # Project root directory (where package.json lives) export PROJECT_DIR="${PROJECT_DIR:-$(pwd)}" # ── iOS Simulator ──────────────────────────────────────────────────── # Simulator UDID — set to "auto" to detect the first booted simulator export SIMULATOR_UDID="${SIMULATOR_UDID:-auto}" # Simulator device name (used when creating/finding simulators) export SIMULATOR_DEVICE_NAME="${SIMULATOR_DEVICE_NAME:-iPhone 16 Pro}" # ── Android Emulator ───────────────────────────────────────────────── # Android AVD name export ANDROID_AVD="${ANDROID_AVD:-Pixel_8}" # Android serial (usually emulator-5554) export ANDROID_SERIAL="${ANDROID_SERIAL:-emulator-5554}" # Android SDK paths export ANDROID_SDK="${ANDROID_SDK:-$HOME/Library/Android/sdk}" export ADB_PATH="${ADB_PATH:-$ANDROID_SDK/platform-tools/adb}" export EMULATOR_PATH="${EMULATOR_PATH:-$ANDROID_SDK/emulator/emulator}" # ── Dev Server / Metro ─────────────────────────────────────────────── # Port for the dev server (Metro bundler, Vite, webpack, etc.) export METRO_PORT="${METRO_PORT:-8081}" # URL for the dev server export METRO_URL="${METRO_URL:-http://localhost:${METRO_PORT}}" # Command to start the dev server (run from PROJECT_DIR) export DEV_SERVER_CMD="${DEV_SERVER_CMD:-npx expo start --port $METRO_PORT}" # Health check endpoint (returns 200 when server is ready) export DEV_SERVER_HEALTH="${DEV_SERVER_HEALTH:-${METRO_URL}/status}" # ── CDP (Chrome DevTools Protocol) ─────────────────────────────────── # CDP discovery URL (Metro exposes debug targets here) export CDP_DISCOVERY_URL="${CDP_DISCOVERY_URL:-${METRO_URL}/json}" # CDP WebSocket URL — set to "auto" for auto-discovery via /json endpoint export CDP_WS_URL="${CDP_WS_URL:-auto}" # CDP device ID — set to "auto" to pick from /json response export CDP_DEVICE_ID="${CDP_DEVICE_ID:-auto}" # ── Navigation ─────────────────────────────────────────────────────── # React Navigation module ID cache file export NAV_MODULE_CACHE="${NAV_MODULE_CACHE:-/tmp/qa-nav-module-id}" # Video/media player module cache file export VIDEO_MODULE_CACHE="${VIDEO_MODULE_CACHE:-/tmp/qa-video-module-id}" # Module scan range for auto-discovery (start, end) export MODULE_SCAN_START="${MODULE_SCAN_START:-0}" export MODULE_SCAN_END="${MODULE_SCAN_END:-5000}" # ── Screen Names (configure per app) ──────────────────────────────── # These are used by CDP navigation helpers. Set them to your app's # React Navigation screen names. Leave empty to skip. export SCREEN_HOME="${SCREEN_HOME:-HomeScreen}" export SCREEN_EXPLORE="${SCREEN_EXPLORE:-ExploreScreen}" export SCREEN_SEARCH="${SCREEN_SEARCH:-SearchScreen}" export SCREEN_PROFILE="${SCREEN_PROFILE:-ProfileScreen}" export SCREEN_SETTINGS="${SCREEN_SETTINGS:-SettingsScreen}" # Tab navigator name (for BottomTab-style navigation) export TAB_NAVIGATOR_NAME="${TAB_NAVIGATOR_NAME:-BottomTab}" # ── Coordinate System ──────────────────────────────────────────────── # Override these for your specific device's logical point dimensions. # Default: iPhone 16 Pro logical resolution (402 × 874 points) export SCREEN_WIDTH="${SCREEN_WIDTH:-402}" export SCREEN_HEIGHT="${SCREEN_HEIGHT:-874}" export SCREEN_SCALE="${SCREEN_SCALE:-3}" # Tab bar Y position (bottom of screen, above safe area) export TAB_BAR_Y="${TAB_BAR_Y:-855}" # Tab positions (x coordinates, left to right) # Set these to match your app's bottom tab layout export TAB_1_X="${TAB_1_X:-60}" export TAB_2_X="${TAB_2_X:-170}" export TAB_3_X="${TAB_3_X:-290}" export TAB_4_X="${TAB_4_X:-400}" export TAB_5_X="${TAB_5_X:-520}" # Common UI element coordinates export BACK_BUTTON_X="${BACK_BUTTON_X:-30}" export BACK_BUTTON_Y="${BACK_BUTTON_Y:-60}" export SETTINGS_BUTTON_X="${SETTINGS_BUTTON_X:-510}" export SETTINGS_BUTTON_Y="${SETTINGS_BUTTON_Y:-140}" # ── Video/Media Player ────────────────────────────────────────────── # Class name to look for when installing debug hooks # For expo-video: "VideoPlayer" (looks for m.default.VideoPlayer or m.VideoPlayer) export VIDEO_PLAYER_CLASS="${VIDEO_PLAYER_CLASS:-VideoPlayer}" # Global variable name for tracking video player instances export GLOBAL_PLAYERS_VAR="${GLOBAL_PLAYERS_VAR:-__qaVideoPlayers}" # Global variable name for feed state debug hook export GLOBAL_FEED_VAR="${GLOBAL_FEED_VAR:-__qaFeedState}" # Maximum tracked player instances (prevents memory leaks) export MAX_TRACKED_PLAYERS="${MAX_TRACKED_PLAYERS:-20}" # ── Feed/List State (for state persistence tests) ─────────────────── # Property name to test for state persistence (e.g., "isLiked", "isBookmarked", "isInCart") export STATE_PROPERTY="${STATE_PROPERTY:-isLiked}" # Counter property that accompanies the state (e.g., "likesCount", "saveCount") export STATE_COUNTER_PROPERTY="${STATE_COUNTER_PROPERTY:-likesCount}" # Number of items to scroll through before checking persistence export STATE_SCROLL_COUNT="${STATE_SCROLL_COUNT:-5}" # ── Output & Reporting ────────────────────────────────────────────── # Base directory for test output export TEST_OUTPUT_DIR="${TEST_OUTPUT_DIR:-/tmp/qa-tests}" # Screenshot subdirectory export SCREENSHOT_DIR="${SCREENSHOT_DIR:-$TEST_OUTPUT_DIR/screenshots}" # Test results log export RESULTS_FILE="${RESULTS_FILE:-$TEST_OUTPUT_DIR/results.log}" # ── Timeouts ───────────────────────────────────────────────────────── # Dev server startup timeout (seconds) export DEV_SERVER_TIMEOUT="${DEV_SERVER_TIMEOUT:-60}" # CDP target discovery timeout (seconds) export CDP_TIMEOUT="${CDP_TIMEOUT:-30}" # App settle time after launch (seconds) export APP_SETTLE_TIME="${APP_SETTLE_TIME:-6}" # agent-device command timeout (seconds) export DEVICE_CMD_TIMEOUT="${DEVICE_CMD_TIMEOUT:-5}" # ── agent-device Sessions ─────────────────────────────────────────── # iOS session name export IOS_SESSION="${IOS_SESSION:-default}" # Android session name export ANDROID_SESSION="${ANDROID_SESSION:-android}" # Active session (set at runtime) export ACTIVE_SESSION="${ACTIVE_SESSION:-$IOS_SESSION}" # ── Web Testing (agent-browser) ───────────────────────────────────── # Base URL for web testing export WEB_BASE_URL="${WEB_BASE_URL:-http://localhost:3000}" # Browser session name export WEB_SESSION="${WEB_SESSION:-qa}" # Default viewport for web tests export WEB_VIEWPORT_WIDTH="${WEB_VIEWPORT_WIDTH:-1280}" export WEB_VIEWPORT_HEIGHT="${WEB_VIEWPORT_HEIGHT:-720}" # ── Auto-Detection Functions ───────────────────────────────────────── # Auto-detect simulator UDID (finds first booted iOS simulator) qa_detect_simulator_udid() { if [ "$SIMULATOR_UDID" != "auto" ]; then echo "$SIMULATOR_UDID" return 0 fi local udid udid=$(xcrun simctl list devices 2>/dev/null | grep "Booted" | head -1 | grep -oE '[A-F0-9-]{36}' || echo "") if [ -n "$udid" ]; then export SIMULATOR_UDID="$udid" echo "$udid" return 0 fi echo "" return 1 } # Auto-detect CDP WebSocket URL from Metro /json endpoint qa_detect_cdp_ws_url() { if [ "$CDP_WS_URL" != "auto" ]; then echo "$CDP_WS_URL" return 0 fi local json json=$(curl -s "$CDP_DISCOVERY_URL" 2>/dev/null || echo "[]") local ws_url 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); // Prefer 'Bridgeless' target (Hermes), fall back to first 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 "$ws_url" ]; then export CDP_WS_URL="$ws_url" echo "$ws_url" return 0 fi echo "" return 1 } # Auto-detect and cache the React Navigation module ID qa_detect_nav_module() { # Check cache first if [ -f "$NAV_MODULE_CACHE" ]; then local cached cached=$(cat "$NAV_MODULE_CACHE" 2>/dev/null || echo "") if [ -n "$cached" ]; then echo "$cached" return 0 fi fi local ws_url ws_url=$(qa_detect_cdp_ws_url) if [ -z "$ws_url" ]; then echo "" return 1 fi local module_id module_id=$(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||'');ws.close();process.exit(0);} }); ws.on('error',()=>{console.log('');process.exit(1)}); setTimeout(()=>{console.log('');process.exit(1)},10000); " 2>/dev/null || echo "") local id id=$(echo "$module_id" | 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 "$id" ]; then echo "$id" > "$NAV_MODULE_CACHE" echo "$id" return 0 fi echo "" return 1 } # ── Source local overrides (if present) ────────────────────────────── if [ -f "$QA_AUTOMATION_DIR/qa.config.local.sh" ]; then source "$QA_AUTOMATION_DIR/qa.config.local.sh" fi # ── Ensure output directories exist ───────────────────────────────── mkdir -p "$TEST_OUTPUT_DIR" "$SCREENSHOT_DIR" 2>/dev/null || true