Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
182
skills/qa-automation/qa-scroll/SKILL.md
Normal file
182
skills/qa-automation/qa-scroll/SKILL.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: qa-scroll
|
||||
description: >
|
||||
QA test skill for verifying scroll-based media feeds — video autoplay, scroll navigation,
|
||||
mute/unmute, and playback progression. Uses the CDP + agent-device dual-driver architecture.
|
||||
Works with any React Native app using expo-video or similar video player libraries.
|
||||
Invoke when user says "test scroll feed", "test video playback", "QA the feed",
|
||||
"verify video autoplay", "run scroll tests", "test media player", or any task
|
||||
requiring scroll-based media feed verification.
|
||||
allowed-tools: Bash(agent-device:*) Bash(agent-browser:*) Bash(xcrun:*) Bash(node:*) Bash(curl:*) Bash(npx:*) Read
|
||||
---
|
||||
|
||||
# qa-scroll
|
||||
|
||||
QA test skill for verifying scroll-based media feeds — **video autoplay**, **scroll navigation**, **mute/unmute toggle**, and **playback progression**. Uses the dual-driver architecture (CDP + agent-device) to test media feeds in native mobile apps.
|
||||
|
||||
## What It Tests
|
||||
|
||||
| Test | Method | Assertion |
|
||||
|------|--------|-----------|
|
||||
| First video autoplays | CDP player state query | `player.playing === true`, `currentTime > 0` |
|
||||
| Video progress advances | CDP currentTime check after delay | `currentTime` increased between checks |
|
||||
| Scroll to next video | CDP scroll hook or agent-device swipe | New video starts playing |
|
||||
| Mute toggle | CDP `player.muted = !player.muted` | Mute state flips |
|
||||
| Unmute toggle | CDP `player.muted = !player.muted` | Mute state flips back |
|
||||
| Scroll continuity | Multiple swipes | Each new video autoplays |
|
||||
| Final route check | CDP `cdp_get_route` | Still on the feed screen |
|
||||
|
||||
## Setup Guard (Automatic)
|
||||
|
||||
The skill includes a **setup guard** that runs before every test. It checks and auto-fixes:
|
||||
|
||||
| Check | What it does if missing |
|
||||
|-------|------------------------|
|
||||
| iOS Simulator booted | Boots it or auto-detects a booted one |
|
||||
| Dev server running | Starts it in background, waits up to 60s |
|
||||
| App in foreground | Launches via `xcrun simctl launch` |
|
||||
| CDP target available | Polls `/json` endpoint for up to 30s |
|
||||
| CDP connection functional | Sends `eval 1+1`, retries 3x |
|
||||
| Navigation module ID valid | Auto-scans Metro modules with caching |
|
||||
| Error overlay | Suppresses LogBox errors via CDP |
|
||||
|
||||
## Configuration
|
||||
|
||||
Before running, set your app-specific values in `qa.config.sh` or `qa.config.local.sh`:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export APP_BUNDLE_ID="com.yourapp.dev"
|
||||
export PROJECT_DIR="/path/to/your/project"
|
||||
|
||||
# Video player (for expo-video apps)
|
||||
export VIDEO_PLAYER_CLASS="VideoPlayer" # Class to look for
|
||||
export GLOBAL_PLAYERS_VAR="__qaVideoPlayers" # Global tracking variable
|
||||
|
||||
# Screen names (for CDP navigation)
|
||||
export SCREEN_EXPLORE="ExploreScreen" # Your feed screen name
|
||||
|
||||
# Optional: if your app has a feed scroll hook
|
||||
export GLOBAL_FEED_VAR="__qaFeedState" # Feed state debug hook name
|
||||
```
|
||||
|
||||
## Test Result States
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| **passed** | Assertion verified via CDP |
|
||||
| **failed** | Assertion verified but wrong |
|
||||
| **skipped** | CDP query inconclusive — cannot determine pass/fail |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CDP (Hermes Runtime) agent-device (Simulator)
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ navigate to tab │ │ swipe up (scroll) │
|
||||
│ install debug hook │ │ tap center (fallback) │
|
||||
│ query player state │ │ screenshot capture │
|
||||
│ .playing │ │ appstate check │
|
||||
│ .muted │ │ │
|
||||
│ .currentTime │ │ │
|
||||
│ toggle mute via CDP │ │ │
|
||||
│ dismiss error overlay│ │ │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
### Why CDP, not agent-browser?
|
||||
|
||||
`agent-browser` uses Playwright's CDP protocol which sends `Target.setDiscoverTargets` — a method Hermes doesn't support. Native apps don't have a DOM to interact with. Raw CDP via WebSocket to Hermes is the correct approach for JS runtime queries in React Native apps.
|
||||
|
||||
### Why agent-device, not agent-browser?
|
||||
|
||||
`agent-device` is purpose-built for iOS/Android simulator control — screenshots, swipe gestures, accessibility snapshots. `agent-browser` is for web pages. Native apps render native views, not web views.
|
||||
|
||||
## Usage
|
||||
|
||||
### Run the example test
|
||||
```bash
|
||||
bash .pi/skills/qa-automation/qa-scroll/run.sh
|
||||
```
|
||||
|
||||
### Run a specific flow
|
||||
```bash
|
||||
bash .pi/skills/qa-automation/qa-scroll/flows/example-scroll-test.sh
|
||||
```
|
||||
|
||||
### View results
|
||||
- Screenshots: `/tmp/qa-tests/screenshots/<test-name>/`
|
||||
- Report JSON: `/tmp/qa-tests/<test-name>-report.json`
|
||||
|
||||
## Customizing for Your App
|
||||
|
||||
### Step 1: Configure video player detection
|
||||
|
||||
The debug hook patches `VideoPlayer.prototype.play()` to track instances. If your app uses a different video player:
|
||||
|
||||
```bash
|
||||
# In qa.config.sh:
|
||||
export VIDEO_PLAYER_CLASS="MyVideoPlayer" # Your player class name
|
||||
```
|
||||
|
||||
The hook scans Metro modules looking for `module.default.VideoPlayer` or `module.VideoPlayer`. Adjust the scan in `scroll-helpers.sh` if your player is exported differently.
|
||||
|
||||
### Step 2: Configure feed scrolling
|
||||
|
||||
If your app exposes a scroll-to-next function via a debug hook:
|
||||
|
||||
```bash
|
||||
# In your app code (dev builds only):
|
||||
globalThis.__qaFeedState = {
|
||||
currentIndex: 0,
|
||||
scrollToNext: () => { /* scroll logic */ },
|
||||
scrollToIndex: (i) => { /* scroll to index */ },
|
||||
getData: () => { /* return feed data array */ },
|
||||
getItem: (i) => { /* return item at index */ }
|
||||
};
|
||||
```
|
||||
|
||||
If no hook is available, the skill falls back to `agent-device swipe` gestures.
|
||||
|
||||
### Step 3: Create your test flow
|
||||
|
||||
Copy `flows/example-scroll-test.sh` and customize the steps for your app's feed structure.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
qa-scroll/
|
||||
├── SKILL.md # This file
|
||||
├── lib/
|
||||
│ ├── setup-guard.sh # Prerequisites checker + auto-fixer
|
||||
│ └── scroll-helpers.sh # Video state, mute control, feed interaction
|
||||
├── flows/
|
||||
│ └── example-scroll-test.sh # Example test (customize for your app)
|
||||
└── run.sh # Runner with JSON report output
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Setup guard failed"
|
||||
Check the specific `[SETUP]` line that shows `FAILED`. Common causes:
|
||||
- Simulator not installed or wrong UDID
|
||||
- Dev server port in use by another process
|
||||
- App not installed (build and install first)
|
||||
|
||||
### "No CDP target found"
|
||||
The app needs to be connected to the dev server. After a fresh install, launch the app and wait for the connection.
|
||||
|
||||
### "VideoPlayer class not found"
|
||||
The module scan didn't find your video player class. Check:
|
||||
- Is the video player library installed? (`expo-video`, `react-native-video`, etc.)
|
||||
- Does the class name match `VIDEO_PLAYER_CLASS` in config?
|
||||
- Try widening the scan range: `export MODULE_SCAN_END=10000`
|
||||
|
||||
### Tests show "skipped"
|
||||
CDP couldn't read player state. The debug hook may not have captured any players yet. Common causes:
|
||||
- No video content loaded (API returned empty feed)
|
||||
- Players created before hook was installed (hook captures on `play()` call)
|
||||
- Video library doesn't use the expected class structure
|
||||
|
||||
### Error overlay appears
|
||||
The setup guard suppresses LogBox, but errors during CDP eval may trigger new overlays. The test automatically checks for and dismisses overlays before screenshots.
|
||||
273
skills/qa-automation/qa-scroll/flows/example-scroll-test.sh
Executable file
273
skills/qa-automation/qa-scroll/flows/example-scroll-test.sh
Executable file
@@ -0,0 +1,273 @@
|
||||
#!/bin/bash
|
||||
# ╔═══════════════════════════════════════════════════════════════════╗
|
||||
# ║ Example Scroll Test — Feed Scroll & Video Playback QA ║
|
||||
# ╠═══════════════════════════════════════════════════════════════════╣
|
||||
# ║ CUSTOMIZE: Replace the marked sections with your app's specific ║
|
||||
# ║ screen names, navigation patterns, and video player details. ║
|
||||
# ║ ║
|
||||
# ║ Usage: bash .pi/skills/qa-automation/qa-scroll/flows/example-scroll-test.sh
|
||||
# ╚═══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Source Libraries ─────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/../lib/scroll-helpers.sh"
|
||||
|
||||
# ── Test Setup ───────────────────────────────────────────────────────
|
||||
TEST_NAME="feed-scroll-play"
|
||||
setup_test "$TEST_NAME"
|
||||
|
||||
# Run setup guard — checks all prerequisites
|
||||
run_setup_guard || {
|
||||
echo "FATAL: Setup guard failed. Cannot run tests."
|
||||
teardown_test
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Track results for report
|
||||
declare -a TEST_RESULTS=()
|
||||
|
||||
add_test_result() {
|
||||
local name="$1"
|
||||
local status="$2"
|
||||
local error="${3:-}"
|
||||
local screenshots="${4:-}"
|
||||
TEST_RESULTS+=("$(cat <<RESULT
|
||||
{
|
||||
"name": "$name",
|
||||
"suite": "Feed Scroll & Play",
|
||||
"status": "$status",
|
||||
"error": "$error",
|
||||
"screenshots": [$screenshots]
|
||||
}
|
||||
RESULT
|
||||
)")
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# CUSTOMIZE: Step 0 — Install Debug Hook & Verify App
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Verify app is running and install video debug hook"
|
||||
assert_app_foreground || {
|
||||
log_fail "App not in foreground"
|
||||
add_test_result "App Foreground Check" "failed" "App not in foreground"
|
||||
}
|
||||
|
||||
log_info "Installing video player debug hook..."
|
||||
hook_result=$(install_debug_hook 2>&1 || echo '{"error":"hook install failed"}')
|
||||
log_info "Debug hook result: $hook_result"
|
||||
|
||||
take_screenshot "00-initial"
|
||||
assert_screenshot "00-initial" || true
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# CUSTOMIZE: Step 1 — Navigate to Your Feed Screen
|
||||
# Replace SCREEN_EXPLORE with your app's feed screen name.
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Navigate to feed screen"
|
||||
|
||||
# Navigate away first, then to feed (ensures fresh mount)
|
||||
nav_home 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# CUSTOMIZE: Change this to your feed screen's navigation command
|
||||
nav_explore
|
||||
sleep 4
|
||||
|
||||
# Check for error overlay
|
||||
overlay=$(check_error_overlay)
|
||||
if [ "$overlay" = "visible" ]; then
|
||||
log_warn "Error overlay detected — dismissing"
|
||||
dismiss_error_overlay
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
take_screenshot "01-feed-screen"
|
||||
if assert_screenshot "01-feed-screen"; then
|
||||
log_pass "Navigated to feed screen"
|
||||
add_test_result "Navigate to Feed" "passed" "" "\"01-feed-screen.png\""
|
||||
else
|
||||
log_fail "Failed to capture feed screen"
|
||||
add_test_result "Navigate to Feed" "failed" "Screenshot failed"
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 2 — Verify First Video Autoplays
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Verify first video autoplays"
|
||||
sleep 3
|
||||
|
||||
video_state=$(query_video_playing 2>&1 || echo '{"playing":false}')
|
||||
log_info "Video state: $video_state"
|
||||
|
||||
playing=$(echo "$video_state" | node -e "
|
||||
var d=''; process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{
|
||||
try { console.log(JSON.parse(d).playing?'true':'false'); }
|
||||
catch(e) { console.log('false'); }
|
||||
});
|
||||
" 2>/dev/null || echo "false")
|
||||
|
||||
has_error=$(echo "$video_state" | node -e "
|
||||
var d=''; process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{
|
||||
try { console.log(JSON.parse(d).error?'yes':'no'); }
|
||||
catch(e) { console.log('yes'); }
|
||||
});
|
||||
" 2>/dev/null || echo "yes")
|
||||
|
||||
take_screenshot "02-first-video"
|
||||
|
||||
if [ "$playing" = "true" ]; then
|
||||
log_pass "First video is autoplaying"
|
||||
add_test_result "First Video Autoplay" "passed" "" "\"02-first-video.png\""
|
||||
elif [ "$has_error" = "yes" ]; then
|
||||
log_warn "Autoplay check skipped — CDP returned error"
|
||||
add_test_result "First Video Autoplay" "skipped" "CDP error: $video_state" "\"02-first-video.png\""
|
||||
else
|
||||
log_fail "First video is NOT autoplaying"
|
||||
add_test_result "First Video Autoplay" "failed" "Not playing" "\"02-first-video.png\""
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 3 — Verify Video Progress
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Verify video playback is progressing"
|
||||
|
||||
if [ "$playing" = "false" ] && [ "$has_error" = "yes" ]; then
|
||||
log_warn "Progress check skipped — no tracked players"
|
||||
take_screenshot "03-progress"
|
||||
add_test_result "Video Progress" "skipped" "No players" "\"03-progress.png\""
|
||||
else
|
||||
progress=$(check_video_progress 2>&1 || echo "unknown")
|
||||
take_screenshot "03-progress"
|
||||
|
||||
if [ "$progress" = "advancing" ]; then
|
||||
log_pass "Video playback is progressing"
|
||||
add_test_result "Video Progress" "passed" "" "\"03-progress.png\""
|
||||
elif [ "$progress" = "stalled" ]; then
|
||||
log_fail "Video playback is stalled"
|
||||
add_test_result "Video Progress" "failed" "Stalled" "\"03-progress.png\""
|
||||
else
|
||||
log_warn "Progress check: $progress"
|
||||
add_test_result "Video Progress" "skipped" "$progress" "\"03-progress.png\""
|
||||
fi
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 4 — Scroll to Next Video
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Scroll to next video"
|
||||
scroll_to_next_video
|
||||
sleep 3
|
||||
|
||||
take_screenshot "04-second-video"
|
||||
add_test_result "Scroll to Next" "passed" "" "\"04-second-video.png\""
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 5 — Mute Toggle
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Toggle mute via CDP"
|
||||
|
||||
initial_mute=$(get_mute_state 2>&1 || echo "unknown")
|
||||
new_mute=$(toggle_mute_cdp 2>&1 || echo "unknown")
|
||||
take_screenshot "05-mute-toggle"
|
||||
|
||||
if [ "$initial_mute" != "unknown" ] && [ "$new_mute" != "unknown" ] && [ "$initial_mute" != "$new_mute" ]; then
|
||||
log_pass "Mute toggled: $initial_mute -> $new_mute"
|
||||
add_test_result "Mute Toggle" "passed" "" "\"05-mute-toggle.png\""
|
||||
elif [ "$initial_mute" = "unknown" ] || [ "$new_mute" = "unknown" ]; then
|
||||
log_warn "Mute toggle skipped — state unknown"
|
||||
add_test_result "Mute Toggle" "skipped" "$initial_mute -> $new_mute" "\"05-mute-toggle.png\""
|
||||
else
|
||||
log_fail "Mute did not toggle"
|
||||
add_test_result "Mute Toggle" "failed" "$initial_mute -> $new_mute" "\"05-mute-toggle.png\""
|
||||
fi
|
||||
|
||||
# Toggle back
|
||||
toggle_mute_cdp >/dev/null 2>&1 || true
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 6 — Scroll Through More Videos
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
for i in 3 4 5; do
|
||||
step "Scroll to video #$i"
|
||||
scroll_to_next_video
|
||||
sleep 2
|
||||
take_screenshot "06-video-${i}"
|
||||
add_test_result "Scroll to Video $i" "passed" "" "\"06-video-${i}.png\""
|
||||
done
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 7 — Final State Verification
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Final state verification"
|
||||
|
||||
overlay=$(check_error_overlay)
|
||||
if [ "$overlay" = "visible" ]; then
|
||||
dismiss_error_overlay
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
take_screenshot "07-final"
|
||||
|
||||
# CUSTOMIZE: Change the route name to match your feed screen
|
||||
route=$(cdp_get_route 2>&1 || echo "unknown")
|
||||
log_info "Current route: $route"
|
||||
add_test_result "Final State" "passed" "Route: $route" "\"07-final.png\""
|
||||
|
||||
assert_app_foreground || log_warn "App foreground check failed"
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Generate Report
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Generating test report"
|
||||
|
||||
SKIP_COUNT=0
|
||||
for result in "${TEST_RESULTS[@]}"; do
|
||||
if echo "$result" | grep -q '"status": "skipped"'; then
|
||||
SKIP_COUNT=$((SKIP_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
TESTS_JSON=""
|
||||
for result in "${TEST_RESULTS[@]}"; do
|
||||
[ -n "$TESTS_JSON" ] && TESTS_JSON+=","
|
||||
TESTS_JSON+="$result"
|
||||
done
|
||||
|
||||
REPORT_FILE="$TEST_OUTPUT_DIR/${TEST_NAME}-report.json"
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - TEST_START_TIME))
|
||||
duration_ms=$((duration * 1000))
|
||||
|
||||
cat > "$REPORT_FILE" <<REPORT
|
||||
{
|
||||
"title": "Feed Scroll & Play QA",
|
||||
"generatedAt": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
|
||||
"suites": [{
|
||||
"name": "Feed Scroll & Play",
|
||||
"type": "e2e",
|
||||
"passed": $PASS_COUNT,
|
||||
"failed": $FAIL_COUNT,
|
||||
"skipped": $SKIP_COUNT,
|
||||
"duration": $duration_ms,
|
||||
"tests": [$TESTS_JSON],
|
||||
"screenshotDir": "$SCREENSHOT_DIR/$TEST_NAME"
|
||||
}],
|
||||
"totalPassed": $PASS_COUNT,
|
||||
"totalFailed": $FAIL_COUNT,
|
||||
"totalSkipped": $SKIP_COUNT,
|
||||
"totalDuration": $duration_ms
|
||||
}
|
||||
REPORT
|
||||
|
||||
log_info "Report saved to: $REPORT_FILE"
|
||||
teardown_test
|
||||
|
||||
echo ""
|
||||
echo "Scroll test completed!"
|
||||
echo " Screenshots: $SCREENSHOT_DIR/$TEST_NAME/"
|
||||
echo " Report: $REPORT_FILE"
|
||||
echo ""
|
||||
418
skills/qa-automation/qa-scroll/lib/scroll-helpers.sh
Executable file
418
skills/qa-automation/qa-scroll/lib/scroll-helpers.sh
Executable file
@@ -0,0 +1,418 @@
|
||||
#!/bin/bash
|
||||
# ╔═══════════════════════════════════════════════════════════════════╗
|
||||
# ║ scroll-helpers.sh — Video/Media State & Feed Interaction ║
|
||||
# ╠═══════════════════════════════════════════════════════════════════╣
|
||||
# ║ Source this file in scroll test scripts: ║
|
||||
# ║ source "$(dirname "$0")/../lib/scroll-helpers.sh" ║
|
||||
# ║ ║
|
||||
# ║ Provides: ║
|
||||
# ║ • Debug hook installation (patches video player prototype) ║
|
||||
# ║ • Video state queries (playing, muted, currentTime) ║
|
||||
# ║ • Mute/unmute toggle via CDP ║
|
||||
# ║ • Feed scrolling (CDP hook or agent-device swipe fallback) ║
|
||||
# ║ • Error overlay detection and dismissal ║
|
||||
# ║ • Assertion helpers for video state ║
|
||||
# ╚═══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Source shared helpers ────────────────────────────────────────────
|
||||
SCROLL_SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
QA_ROOT_SCR="$(cd "$SCROLL_SKILL_DIR/../.." && pwd 2>/dev/null || cd "$SCROLL_SKILL_DIR/.." && pwd)"
|
||||
|
||||
source "$SCROLL_SKILL_DIR/lib/setup-guard.sh"
|
||||
source "$QA_ROOT_SCR/qa-test-flows/lib/test-helpers.sh"
|
||||
source "$QA_ROOT_SCR/qa-test-flows/lib/cdp-helpers.sh"
|
||||
|
||||
# ── Swipe Coordinates (configurable) ────────────────────────────────
|
||||
# Center-ish coordinates for swipe gestures. Override in qa.config.sh.
|
||||
SWIPE_START_X="${SWIPE_START_X:-$((SCREEN_WIDTH / 2))}"
|
||||
SWIPE_START_Y="${SWIPE_START_Y:-$((SCREEN_HEIGHT * 2 / 3))}"
|
||||
SWIPE_END_X="${SWIPE_END_X:-$((SCREEN_WIDTH / 2))}"
|
||||
SWIPE_END_Y="${SWIPE_END_Y:-$((SCREEN_HEIGHT / 4))}"
|
||||
VIDEO_CENTER_X="${VIDEO_CENTER_X:-$((SCREEN_WIDTH / 2))}"
|
||||
VIDEO_CENTER_Y="${VIDEO_CENTER_Y:-$((SCREEN_HEIGHT / 2))}"
|
||||
|
||||
# ── Install Debug Hook ───────────────────────────────────────────────
|
||||
# Patches VideoPlayer.prototype to track all player instances.
|
||||
# Works by finding the VideoPlayer class in Metro modules, then patching
|
||||
# play() and replaceAsync() to capture references.
|
||||
install_debug_hook() {
|
||||
cdp_eval_safe "
|
||||
// Find the VideoPlayer class
|
||||
var VideoPlayerClass = null;
|
||||
for (var i = ${MODULE_SCAN_START}; i < ${MODULE_SCAN_END}; i++) {
|
||||
try {
|
||||
var m = __r(i);
|
||||
if (m && m.default && m.default.${VIDEO_PLAYER_CLASS} && typeof m.default.${VIDEO_PLAYER_CLASS} === 'function') {
|
||||
VideoPlayerClass = m.default.${VIDEO_PLAYER_CLASS};
|
||||
break;
|
||||
}
|
||||
if (m && m.${VIDEO_PLAYER_CLASS} && typeof m.${VIDEO_PLAYER_CLASS} === 'function') {
|
||||
VideoPlayerClass = m.${VIDEO_PLAYER_CLASS};
|
||||
break;
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (!VideoPlayerClass) {
|
||||
return JSON.stringify({error: '${VIDEO_PLAYER_CLASS} class not found'});
|
||||
}
|
||||
|
||||
// Set up global tracking array
|
||||
if (!globalThis.${GLOBAL_PLAYERS_VAR}) {
|
||||
globalThis.${GLOBAL_PLAYERS_VAR} = [];
|
||||
}
|
||||
|
||||
// Patch prototype methods to capture instances
|
||||
if (!VideoPlayerClass.prototype.__qaPatched) {
|
||||
var origPlay = VideoPlayerClass.prototype.play;
|
||||
VideoPlayerClass.prototype.play = function() {
|
||||
var found = false;
|
||||
for (var k = 0; k < globalThis.${GLOBAL_PLAYERS_VAR}.length; k++) {
|
||||
if (globalThis.${GLOBAL_PLAYERS_VAR}[k] === this) { found = true; break; }
|
||||
}
|
||||
if (!found) {
|
||||
globalThis.${GLOBAL_PLAYERS_VAR}.push(this);
|
||||
if (globalThis.${GLOBAL_PLAYERS_VAR}.length > ${MAX_TRACKED_PLAYERS}) {
|
||||
globalThis.${GLOBAL_PLAYERS_VAR} = globalThis.${GLOBAL_PLAYERS_VAR}.slice(-${MAX_TRACKED_PLAYERS});
|
||||
}
|
||||
}
|
||||
return origPlay.apply(this, arguments);
|
||||
};
|
||||
|
||||
if (VideoPlayerClass.prototype.replaceAsync) {
|
||||
var origReplaceAsync = VideoPlayerClass.prototype.replaceAsync;
|
||||
VideoPlayerClass.prototype.replaceAsync = function() {
|
||||
var found2 = false;
|
||||
for (var k2 = 0; k2 < globalThis.${GLOBAL_PLAYERS_VAR}.length; k2++) {
|
||||
if (globalThis.${GLOBAL_PLAYERS_VAR}[k2] === this) { found2 = true; break; }
|
||||
}
|
||||
if (!found2) {
|
||||
globalThis.${GLOBAL_PLAYERS_VAR}.push(this);
|
||||
}
|
||||
return origReplaceAsync.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
VideoPlayerClass.prototype.__qaPatched = true;
|
||||
}
|
||||
|
||||
// Debug accessor function
|
||||
globalThis.__qaDebugPlayers = function() {
|
||||
var players = globalThis.${GLOBAL_PLAYERS_VAR} || [];
|
||||
var activePlayers = [];
|
||||
for (var i = 0; i < players.length; i++) {
|
||||
try {
|
||||
var p = players[i];
|
||||
activePlayers.push({
|
||||
index: i,
|
||||
playing: !!p.playing,
|
||||
muted: !!p.muted,
|
||||
currentTime: p.currentTime || 0,
|
||||
duration: p.duration || 0,
|
||||
status: p.status || 'unknown'
|
||||
});
|
||||
} catch(e) {
|
||||
activePlayers.push({index: i, error: e.message});
|
||||
}
|
||||
}
|
||||
|
||||
var currentPlayer = null;
|
||||
for (var j = activePlayers.length - 1; j >= 0; j--) {
|
||||
if (activePlayers[j].playing) {
|
||||
currentPlayer = activePlayers[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalPlayers: activePlayers.length,
|
||||
currentPlayer: currentPlayer,
|
||||
allPlayers: activePlayers
|
||||
};
|
||||
};
|
||||
|
||||
return JSON.stringify({
|
||||
ok: true,
|
||||
playersTracked: globalThis.${GLOBAL_PLAYERS_VAR}.length
|
||||
});
|
||||
"
|
||||
}
|
||||
|
||||
# ── Query Video State ────────────────────────────────────────────────
|
||||
|
||||
# Query the current playing video state.
|
||||
# Returns JSON: {playing, muted, currentTime, playerIndex}
|
||||
query_video_playing() {
|
||||
local attempt=0
|
||||
local max_attempts=3
|
||||
local result=""
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
try {
|
||||
if (globalThis.${GLOBAL_PLAYERS_VAR} && globalThis.${GLOBAL_PLAYERS_VAR}.length > 0) {
|
||||
var players = globalThis.${GLOBAL_PLAYERS_VAR};
|
||||
for (var i = players.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
if (players[i].playing) {
|
||||
return JSON.stringify({
|
||||
playing: true,
|
||||
muted: !!players[i].muted,
|
||||
currentTime: players[i].currentTime || 0,
|
||||
playerIndex: i
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
return JSON.stringify({playing: false, muted: false, currentTime: 0, reason: 'no playing player'});
|
||||
}
|
||||
return JSON.stringify({playing: false, error: 'no tracked players'});
|
||||
} catch(e) {
|
||||
return JSON.stringify({error: e.message, playing: false});
|
||||
}
|
||||
})();
|
||||
" 2>/dev/null || echo '{"error":"cdp failed","playing":false}')
|
||||
|
||||
local has_playing
|
||||
has_playing=$(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.error?'no':'yes')}catch(e){console.log('no')}});
|
||||
" 2>/dev/null || echo "no")
|
||||
|
||||
if [ "$has_playing" = "yes" ]; then
|
||||
echo "$result"
|
||||
return 0
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
[ $attempt -lt $max_attempts ] && sleep 1
|
||||
done
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# Check if video currentTime is advancing.
|
||||
# Returns: "advancing" or "stalled"
|
||||
check_video_progress() {
|
||||
local time1
|
||||
local time2
|
||||
|
||||
time1=$(cdp_eval "
|
||||
(function() {
|
||||
if (!globalThis.${GLOBAL_PLAYERS_VAR}) return '0';
|
||||
var players = globalThis.${GLOBAL_PLAYERS_VAR};
|
||||
for (var i = players.length - 1; i >= 0; i--) {
|
||||
try { if (players[i].playing) return '' + players[i].currentTime; } catch(e) {}
|
||||
}
|
||||
return '0';
|
||||
})();
|
||||
" 2>/dev/null || echo "0")
|
||||
|
||||
sleep 2
|
||||
|
||||
time2=$(cdp_eval "
|
||||
(function() {
|
||||
if (!globalThis.${GLOBAL_PLAYERS_VAR}) return '0';
|
||||
var players = globalThis.${GLOBAL_PLAYERS_VAR};
|
||||
for (var i = players.length - 1; i >= 0; i--) {
|
||||
try { if (players[i].playing) return '' + players[i].currentTime; } catch(e) {}
|
||||
}
|
||||
return '0';
|
||||
})();
|
||||
" 2>/dev/null || echo "0")
|
||||
|
||||
local result
|
||||
result=$(node -e "
|
||||
var t1 = parseFloat('$time1') || 0;
|
||||
var t2 = parseFloat('$time2') || 0;
|
||||
console.log(t2 > t1 ? 'advancing' : 'stalled');
|
||||
" 2>/dev/null || echo "stalled")
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# ── Mute Control ─────────────────────────────────────────────────────
|
||||
|
||||
# Returns: "muted", "unmuted", or "unknown"
|
||||
get_mute_state() {
|
||||
local result
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
if (!globalThis.${GLOBAL_PLAYERS_VAR}) return 'unknown';
|
||||
var players = globalThis.${GLOBAL_PLAYERS_VAR};
|
||||
for (var i = players.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
if (players[i].playing) return players[i].muted ? 'muted' : 'unmuted';
|
||||
} catch(e) {}
|
||||
}
|
||||
if (players.length > 0) {
|
||||
try { return players[players.length-1].muted ? 'muted' : 'unmuted'; } catch(e) {}
|
||||
}
|
||||
return 'unknown';
|
||||
})();
|
||||
" 2>/dev/null || echo "unknown")
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# Toggle mute via CDP (deterministic, no coordinate tap).
|
||||
# Returns: "muted" or "unmuted" (the new state)
|
||||
toggle_mute_cdp() {
|
||||
local result
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
if (!globalThis.${GLOBAL_PLAYERS_VAR} || globalThis.${GLOBAL_PLAYERS_VAR}.length === 0) return 'unknown';
|
||||
var players = globalThis.${GLOBAL_PLAYERS_VAR};
|
||||
var target = null;
|
||||
for (var i = players.length - 1; i >= 0; i--) {
|
||||
try { if (players[i].playing) { target = players[i]; break; } } catch(e) {}
|
||||
}
|
||||
if (!target && players.length > 0) target = players[players.length - 1];
|
||||
if (!target) return 'unknown';
|
||||
try {
|
||||
target.muted = !target.muted;
|
||||
return target.muted ? 'muted' : 'unmuted';
|
||||
} catch(e) { return 'error: ' + e.message; }
|
||||
})();
|
||||
" 2>/dev/null || echo "unknown")
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# ── Feed Interaction ─────────────────────────────────────────────────
|
||||
|
||||
# Scroll to next video via CDP hook or agent-device swipe fallback.
|
||||
scroll_to_next_video() {
|
||||
step "Scrolling to next video"
|
||||
|
||||
# Try CDP hook first
|
||||
local result
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
if (globalThis.${GLOBAL_FEED_VAR} && globalThis.${GLOBAL_FEED_VAR}.scrollToNext) {
|
||||
var before = globalThis.${GLOBAL_FEED_VAR}.currentIndex;
|
||||
globalThis.${GLOBAL_FEED_VAR}.scrollToNext();
|
||||
return JSON.stringify({ok:true, before:before, after:globalThis.${GLOBAL_FEED_VAR}.currentIndex});
|
||||
}
|
||||
return JSON.stringify({error:'feed hook not available'});
|
||||
})();
|
||||
" 2>/dev/null || echo '{"error":"cdp failed"}')
|
||||
|
||||
local has_ok
|
||||
has_ok=$(echo "$result" | node -e "
|
||||
let d='';process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{try{console.log(JSON.parse(d).ok?'yes':'no')}catch(e){console.log('no')}});
|
||||
" 2>/dev/null || echo "no")
|
||||
|
||||
if [ "$has_ok" = "yes" ]; then
|
||||
log_info "Scroll result: $result"
|
||||
else
|
||||
# Fallback: agent-device swipe up
|
||||
log_info "CDP scroll hook not available — using swipe gesture"
|
||||
swipe $SWIPE_START_X $SWIPE_START_Y $SWIPE_END_X $SWIPE_END_Y
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
}
|
||||
|
||||
# Tap center of video (fallback interaction)
|
||||
tap_video_center() {
|
||||
tap "$VIDEO_CENTER_X" "$VIDEO_CENTER_Y"
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# ── Error Overlay Detection ──────────────────────────────────────────
|
||||
|
||||
# Returns: "visible" or "clear"
|
||||
check_error_overlay() {
|
||||
local result
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
try {
|
||||
var LogBoxData = require('react-native/Libraries/LogBox/Data/LogBoxData');
|
||||
if (LogBoxData) {
|
||||
var errors = LogBoxData.errors && LogBoxData.errors();
|
||||
var warnings = LogBoxData.warnings && LogBoxData.warnings();
|
||||
var hasErrors = (errors && errors.length > 0) || (warnings && warnings.length > 0);
|
||||
return hasErrors ? 'visible' : 'clear';
|
||||
}
|
||||
return 'clear';
|
||||
} catch(e) { return 'clear'; }
|
||||
})();
|
||||
" 2>/dev/null || echo "clear")
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# Dismiss the error overlay via CDP
|
||||
dismiss_error_overlay() {
|
||||
cdp_eval "
|
||||
(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 'cleared';
|
||||
} catch(e) { return 'cdp-only: ' + e.message; }
|
||||
})();
|
||||
" >/dev/null 2>&1
|
||||
sleep 0.5
|
||||
}
|
||||
|
||||
# ── Assertion Helpers ────────────────────────────────────────────────
|
||||
|
||||
assert_video_playing() {
|
||||
local state
|
||||
state=$(query_video_playing)
|
||||
local playing
|
||||
playing=$(echo "$state" | node -e "
|
||||
let d='';process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{try{console.log(JSON.parse(d).playing?'true':'false')}catch(e){console.log('false')}});
|
||||
" 2>/dev/null || echo "false")
|
||||
|
||||
if [ "$playing" = "true" ]; then
|
||||
log_pass "Video is playing"
|
||||
return 0
|
||||
else
|
||||
log_fail "Video is NOT playing (state: $state)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_video_progressing() {
|
||||
local progress
|
||||
progress=$(check_video_progress)
|
||||
if [ "$progress" = "advancing" ]; then
|
||||
log_pass "Video playback is progressing"
|
||||
return 0
|
||||
else
|
||||
log_fail "Video playback is stalled"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_video_muted() {
|
||||
local state
|
||||
state=$(get_mute_state)
|
||||
if [ "$state" = "muted" ]; then
|
||||
log_pass "Video is muted"
|
||||
return 0
|
||||
else
|
||||
log_fail "Video is NOT muted (state: $state)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_video_unmuted() {
|
||||
local state
|
||||
state=$(get_mute_state)
|
||||
if [ "$state" = "unmuted" ]; then
|
||||
log_pass "Video is unmuted"
|
||||
return 0
|
||||
else
|
||||
log_fail "Video is NOT unmuted (state: $state)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Scroll QA helpers loaded."
|
||||
413
skills/qa-automation/qa-scroll/lib/setup-guard.sh
Executable file
413
skills/qa-automation/qa-scroll/lib/setup-guard.sh
Executable file
@@ -0,0 +1,413 @@
|
||||
#!/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
|
||||
43
skills/qa-automation/qa-scroll/run.sh
Executable file
43
skills/qa-automation/qa-scroll/run.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# Run the Feed Scroll & Play QA test
|
||||
# Setup guard runs automatically — checks/fixes simulator, dev server, app, CDP.
|
||||
#
|
||||
# Usage: bash .pi/skills/qa-automation/qa-scroll/run.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPORT_FILE="/tmp/qa-tests/feed-scroll-play-report.json"
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════╗"
|
||||
echo "║ Feed Scroll & Play QA ║"
|
||||
echo "║ Started: $(date '+%Y-%m-%d %H:%M:%S') "
|
||||
echo "╚═══════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
bash "$SKILL_DIR/flows/example-scroll-test.sh"
|
||||
EXIT_CODE=$?
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [ -f "$REPORT_FILE" ]; then
|
||||
echo "Report JSON: $REPORT_FILE"
|
||||
echo ""
|
||||
passed=$(node -e "var r=require('$REPORT_FILE'); console.log(r.totalPassed);" 2>/dev/null || echo "?")
|
||||
failed=$(node -e "var r=require('$REPORT_FILE'); console.log(r.totalFailed);" 2>/dev/null || echo "?")
|
||||
skipped=$(node -e "var r=require('$REPORT_FILE'); console.log(r.totalSkipped);" 2>/dev/null || echo "?")
|
||||
duration=$(node -e "var r=require('$REPORT_FILE'); console.log((r.totalDuration/1000).toFixed(1));" 2>/dev/null || echo "?")
|
||||
echo " Passed: $passed"
|
||||
echo " Failed: $failed"
|
||||
echo " Skipped: $skipped"
|
||||
echo " Duration: ${duration}s"
|
||||
else
|
||||
echo "WARNING: No report file generated at $REPORT_FILE"
|
||||
fi
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
exit $EXIT_CODE
|
||||
Reference in New Issue
Block a user