322 lines
14 KiB
Bash
Executable File
322 lines
14 KiB
Bash
Executable File
#!/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
|