341 lines
11 KiB
Bash
Executable File
341 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# ╔═══════════════════════════════════════════════════════════════════╗
|
|
# ║ cdp-helpers.sh — Chrome DevTools Protocol Helpers ║
|
|
# ╠═══════════════════════════════════════════════════════════════════╣
|
|
# ║ Source this file for CDP interaction with React Native apps: ║
|
|
# ║ source "$(dirname "$0")/../../lib/cdp-helpers.sh" ║
|
|
# ║ ║
|
|
# ║ Provides: ║
|
|
# ║ • CDP evaluation (cdp_eval, cdp_eval_safe) ║
|
|
# ║ • Navigation (cdp_navigate, cdp_navigate_tab) ║
|
|
# ║ • State queries (cdp_get_route, cdp_get_state) ║
|
|
# ║ • Auto-discovery (CDP target, navigation module) ║
|
|
# ║ ║
|
|
# ║ Requires: Node.js, 'ws' npm package, Metro dev server running ║
|
|
# ╚═══════════════════════════════════════════════════════════════════╝
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Source configuration ─────────────────────────────────────────────
|
|
CDP_HELPERS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
CDP_QA_ROOT="$(cd "$CDP_HELPERS_DIR/../.." && pwd)"
|
|
|
|
# Only source config if not already loaded
|
|
if [ -z "${QA_AUTOMATION_DIR:-}" ]; then
|
|
source "$CDP_QA_ROOT/qa.config.sh"
|
|
fi
|
|
|
|
# ── CDP State ────────────────────────────────────────────────────────
|
|
_CDP_WS_URL_RESOLVED=""
|
|
_CDP_NAV_MODULE_ID=""
|
|
|
|
# ── CDP Auto-Discovery ──────────────────────────────────────────────
|
|
|
|
# Ensure we have a CDP WebSocket URL
|
|
_ensure_cdp_ws_url() {
|
|
if [ -n "$_CDP_WS_URL_RESOLVED" ]; then
|
|
echo "$_CDP_WS_URL_RESOLVED"
|
|
return 0
|
|
fi
|
|
|
|
local ws_url
|
|
ws_url=$(qa_detect_cdp_ws_url 2>/dev/null || echo "")
|
|
|
|
if [ -n "$ws_url" ]; then
|
|
_CDP_WS_URL_RESOLVED="$ws_url"
|
|
echo "$ws_url"
|
|
return 0
|
|
fi
|
|
|
|
# Last resort: construct from config
|
|
if [ "$CDP_WS_URL" != "auto" ] && [ -n "$CDP_WS_URL" ]; then
|
|
_CDP_WS_URL_RESOLVED="$CDP_WS_URL"
|
|
echo "$CDP_WS_URL"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
return 1
|
|
}
|
|
|
|
# Ensure we have a navigation module ID
|
|
_ensure_nav_module_id() {
|
|
if [ -n "$_CDP_NAV_MODULE_ID" ]; then
|
|
echo "$_CDP_NAV_MODULE_ID"
|
|
return 0
|
|
fi
|
|
|
|
# Check cache
|
|
if [ -f "$NAV_MODULE_CACHE" ]; then
|
|
local cached
|
|
cached=$(cat "$NAV_MODULE_CACHE" 2>/dev/null || echo "")
|
|
if [ -n "$cached" ]; then
|
|
_CDP_NAV_MODULE_ID="$cached"
|
|
echo "$cached"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Auto-detect
|
|
local module_id
|
|
module_id=$(qa_detect_nav_module 2>/dev/null || echo "")
|
|
|
|
if [ -n "$module_id" ]; then
|
|
_CDP_NAV_MODULE_ID="$module_id"
|
|
echo "$module_id"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
return 1
|
|
}
|
|
|
|
# ── Core CDP Functions ───────────────────────────────────────────────
|
|
|
|
# Execute JavaScript in the React Native Hermes runtime via CDP.
|
|
# Usage: cdp_eval "javascript expression"
|
|
# Returns: the evaluated result
|
|
cdp_eval() {
|
|
local expression="$1"
|
|
local ws_url
|
|
ws_url=$(_ensure_cdp_ws_url)
|
|
|
|
if [ -z "$ws_url" ]; then
|
|
echo '{"error":"no CDP WebSocket URL"}'
|
|
return 1
|
|
fi
|
|
|
|
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: \`$expression\`,
|
|
returnByValue:true
|
|
}
|
|
}));
|
|
});
|
|
ws.on('message',d=>{
|
|
const m=JSON.parse(d);
|
|
if(m.id===1){
|
|
if(m.result?.result?.value) console.log(m.result.result.value);
|
|
else if(m.result?.exceptionDetails) console.error('CDP Error:', m.result.exceptionDetails.text);
|
|
else console.log(JSON.stringify(m.result));
|
|
ws.close();
|
|
process.exit(0);
|
|
}
|
|
});
|
|
ws.on('error',e=>{console.error('WS Error:',e.message);process.exit(1)});
|
|
setTimeout(()=>{console.error('CDP Timeout');process.exit(1)},8000);
|
|
" 2>&1
|
|
}
|
|
|
|
# Safe CDP eval — suppresses ErrorUtils/LogBox during execution.
|
|
# Use for any eval that touches Metro modules or could trigger side effects.
|
|
# Usage: cdp_eval_safe "javascript expression"
|
|
cdp_eval_safe() {
|
|
local expression="$1"
|
|
cdp_eval "
|
|
(function() {
|
|
var _origHandler = globalThis.ErrorUtils ? ErrorUtils.getGlobalHandler() : null;
|
|
var _origCE = console.error;
|
|
if (globalThis.ErrorUtils) ErrorUtils.setGlobalHandler(function() {});
|
|
console.error = function() {};
|
|
try {
|
|
return (function() { $expression })();
|
|
} finally {
|
|
if (globalThis.ErrorUtils && _origHandler) ErrorUtils.setGlobalHandler(_origHandler);
|
|
console.error = _origCE;
|
|
}
|
|
})();
|
|
"
|
|
}
|
|
|
|
# ── Navigation Functions ─────────────────────────────────────────────
|
|
|
|
# Navigate to a screen by name.
|
|
# Usage: cdp_navigate "ScreenName" [params_json]
|
|
cdp_navigate() {
|
|
local screen="$1"
|
|
local params="${2:-}"
|
|
local nav_id
|
|
nav_id=$(_ensure_nav_module_id)
|
|
|
|
if [ -z "$nav_id" ]; then
|
|
echo '{"error":"navigation module not found"}'
|
|
return 1
|
|
fi
|
|
|
|
if [ -n "$params" ]; then
|
|
cdp_eval "
|
|
const ref = __r(${nav_id}).navigationRef;
|
|
ref.current.navigate('${screen}', ${params});
|
|
JSON.stringify({ ok: true, route: ref.current.getCurrentRoute()?.name });
|
|
"
|
|
else
|
|
cdp_eval "
|
|
const ref = __r(${nav_id}).navigationRef;
|
|
ref.current.navigate('${screen}');
|
|
JSON.stringify({ ok: true, route: ref.current.getCurrentRoute()?.name });
|
|
"
|
|
fi
|
|
}
|
|
|
|
# Navigate to a bottom tab screen.
|
|
# Usage: cdp_navigate_tab "ScreenName"
|
|
cdp_navigate_tab() {
|
|
local tab="$1"
|
|
local nav_id
|
|
nav_id=$(_ensure_nav_module_id)
|
|
|
|
if [ -z "$nav_id" ]; then
|
|
echo '{"error":"navigation module not found"}'
|
|
return 1
|
|
fi
|
|
|
|
cdp_eval "
|
|
const ref = __r(${nav_id}).navigationRef;
|
|
ref.current.navigate('${TAB_NAVIGATOR_NAME}', { screen: '${tab}' });
|
|
JSON.stringify({ ok: true, route: ref.current.getCurrentRoute()?.name });
|
|
"
|
|
}
|
|
|
|
# Get current route name.
|
|
# Usage: route=$(cdp_get_route)
|
|
cdp_get_route() {
|
|
local nav_id
|
|
nav_id=$(_ensure_nav_module_id)
|
|
|
|
if [ -z "$nav_id" ]; then
|
|
echo "unknown"
|
|
return 1
|
|
fi
|
|
|
|
cdp_eval "
|
|
const ref = __r(${nav_id}).navigationRef;
|
|
ref.current.getCurrentRoute()?.name || 'unknown';
|
|
"
|
|
}
|
|
|
|
# Get full navigation state as JSON.
|
|
cdp_get_state() {
|
|
local nav_id
|
|
nav_id=$(_ensure_nav_module_id)
|
|
|
|
if [ -z "$nav_id" ]; then
|
|
echo '{"error":"navigation module not found"}'
|
|
return 1
|
|
fi
|
|
|
|
cdp_eval "
|
|
const ref = __r(${nav_id}).navigationRef;
|
|
const state = ref.current.getRootState();
|
|
function getRoutes(s, depth) {
|
|
let routes = [];
|
|
if (s.routes) {
|
|
for (const r of s.routes) {
|
|
routes.push({name: r.name, depth});
|
|
if (r.state) routes = routes.concat(getRoutes(r.state, depth+1));
|
|
}
|
|
}
|
|
return routes;
|
|
}
|
|
JSON.stringify({
|
|
current: ref.current.getCurrentRoute()?.name,
|
|
routes: getRoutes(state, 0)
|
|
});
|
|
"
|
|
}
|
|
|
|
# Go back in navigation.
|
|
cdp_go_back() {
|
|
local nav_id
|
|
nav_id=$(_ensure_nav_module_id)
|
|
|
|
if [ -z "$nav_id" ]; then
|
|
echo "error: no nav module"
|
|
return 1
|
|
fi
|
|
|
|
cdp_eval "
|
|
const ref = __r(${nav_id}).navigationRef;
|
|
if (ref.current.canGoBack()) {
|
|
ref.current.goBack();
|
|
'went back to: ' + ref.current.getCurrentRoute()?.name;
|
|
} else {
|
|
'cannot go back';
|
|
}
|
|
"
|
|
}
|
|
|
|
# Reset navigation to initial state.
|
|
cdp_reset_navigation() {
|
|
local nav_id
|
|
nav_id=$(_ensure_nav_module_id)
|
|
|
|
if [ -z "$nav_id" ]; then
|
|
echo "error: no nav module"
|
|
return 1
|
|
fi
|
|
|
|
cdp_eval "
|
|
const ref = __r(${nav_id}).navigationRef;
|
|
ref.current.reset({
|
|
index: 0,
|
|
routes: [{ name: 'Main', state: { routes: [{ name: '${TAB_NAVIGATOR_NAME}' }] } }]
|
|
});
|
|
'reset to ${TAB_NAVIGATOR_NAME}';
|
|
"
|
|
}
|
|
|
|
# ── Convenience Tab Navigation ───────────────────────────────────────
|
|
# These use the screen names from qa.config.sh
|
|
|
|
nav_tab_1() { [ -n "$SCREEN_EXPLORE" ] && cdp_navigate_tab "$SCREEN_EXPLORE" || echo "SCREEN_EXPLORE not configured"; }
|
|
nav_tab_2() { [ -n "$SCREEN_SEARCH" ] && cdp_navigate_tab "$SCREEN_SEARCH" || echo "SCREEN_SEARCH not configured"; }
|
|
nav_tab_3() { [ -n "$SCREEN_HOME" ] && cdp_navigate_tab "$SCREEN_HOME" || echo "SCREEN_HOME not configured"; }
|
|
nav_tab_4() { echo "Tab 4 not configured — set SCREEN name and add to cdp-helpers.sh"; }
|
|
nav_tab_5() { [ -n "$SCREEN_PROFILE" ] && cdp_navigate_tab "$SCREEN_PROFILE" || echo "SCREEN_PROFILE not configured"; }
|
|
|
|
# Generic by name
|
|
nav_explore() { nav_tab_1; }
|
|
nav_search() { nav_tab_2; }
|
|
nav_home() { nav_tab_3; }
|
|
nav_profile() { nav_tab_5; }
|
|
|
|
# ── Utility Functions ────────────────────────────────────────────────
|
|
|
|
# Check if user is logged in (heuristic: check current route)
|
|
cdp_is_logged_in() {
|
|
local route
|
|
route=$(cdp_get_route 2>/dev/null || echo "unknown")
|
|
if echo "$route" | grep -qiE "login|signup|auth|welcome|launch"; then
|
|
echo "false"
|
|
else
|
|
echo "true"
|
|
fi
|
|
}
|
|
|
|
# Clear cached module IDs (force re-discovery on next call)
|
|
cdp_clear_cache() {
|
|
_CDP_WS_URL_RESOLVED=""
|
|
_CDP_NAV_MODULE_ID=""
|
|
[ -f "$NAV_MODULE_CACHE" ] && rm -f "$NAV_MODULE_CACHE" 2>/dev/null || true
|
|
[ -f "$VIDEO_MODULE_CACHE" ] && rm -f "$VIDEO_MODULE_CACHE" 2>/dev/null || true
|
|
echo "CDP cache cleared"
|
|
}
|
|
|
|
# ── Export Functions ─────────────────────────────────────────────────
|
|
|
|
export -f cdp_eval cdp_eval_safe 2>/dev/null || true
|
|
export -f cdp_navigate cdp_navigate_tab cdp_get_route cdp_get_state 2>/dev/null || true
|
|
export -f cdp_go_back cdp_reset_navigation cdp_is_logged_in 2>/dev/null || true
|
|
export -f nav_explore nav_search nav_home nav_profile 2>/dev/null || true
|
|
export -f cdp_clear_cache 2>/dev/null || true
|
|
|
|
echo "CDP helpers loaded. WebSocket: ${_CDP_WS_URL_RESOLVED:-auto-detect}"
|