Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
166
skills/qa-automation/qa-state-persistence/SKILL.md
Normal file
166
skills/qa-automation/qa-state-persistence/SKILL.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: qa-state-persistence
|
||||
description: >
|
||||
QA test skill for verifying UI state persistence across navigation. Tests that
|
||||
state changes (likes, bookmarks, cart items, form inputs) survive scrolling away
|
||||
and returning. Uses CDP + agent-device dual-driver architecture. Works with any
|
||||
React Native app that uses list-based feeds or scrollable content.
|
||||
Invoke when user says "test state persistence", "test like state", "verify bookmark persists",
|
||||
"QA the state", "test data survives scroll", "verify UI state", or any task requiring
|
||||
state persistence verification across navigation.
|
||||
allowed-tools: Bash(agent-device:*) Bash(agent-browser:*) Bash(xcrun:*) Bash(node:*) Bash(curl:*) Bash(npx:*) Read
|
||||
---
|
||||
|
||||
# qa-state-persistence
|
||||
|
||||
QA test skill for verifying **UI state persistence** across navigation. Tests that state changes (likes, bookmarks, cart additions, form inputs) survive scrolling away and returning. Uses the dual-driver architecture (CDP + agent-device).
|
||||
|
||||
## What It Tests
|
||||
|
||||
| Test | Method | Assertion |
|
||||
|------|--------|-----------|
|
||||
| Navigate to feed | CDP navigation | Route is on feed screen |
|
||||
| Record item identity | CDP feed data query | Item ID and initial state captured |
|
||||
| Verify initial state | CDP property query | State property is in expected initial value |
|
||||
| Mutate state | CDP data mutation | Property flips (e.g., `isLiked: false → true`) |
|
||||
| Scroll away (N items) | CDP scroll or agent-device swipe | Feed index advances |
|
||||
| Scroll back | CDP `scrollToIndex(0)` | Feed index returns to 0 |
|
||||
| **State persisted** | CDP property query | **KEY ASSERTION: property still has mutated value** |
|
||||
| Cleanup | CDP data mutation | Restore original state |
|
||||
| Final route check | CDP `cdp_get_route` | Still on feed screen |
|
||||
|
||||
## Configuration
|
||||
|
||||
Set your app-specific values in `qa.config.sh`:
|
||||
|
||||
```bash
|
||||
# What property to test
|
||||
export STATE_PROPERTY="isLiked" # Property name to toggle
|
||||
export STATE_COUNTER_PROPERTY="likesCount" # Associated counter (optional)
|
||||
export STATE_SCROLL_COUNT=5 # How many items to scroll past
|
||||
|
||||
# Feed screen
|
||||
export SCREEN_EXPLORE="ExploreScreen" # Your feed screen name
|
||||
|
||||
# Feed debug hook (set up in your app's dev build)
|
||||
export GLOBAL_FEED_VAR="__qaFeedState" # Global variable name
|
||||
```
|
||||
|
||||
### Setting Up the Feed Debug Hook
|
||||
|
||||
In your app's feed component (dev builds only), expose:
|
||||
|
||||
```javascript
|
||||
// In your feed component (e.g., ExploreFeed.tsx):
|
||||
if (__DEV__) {
|
||||
globalThis.__qaFeedState = {
|
||||
currentIndex: currentIndex,
|
||||
scrollToNext: () => flatListRef.current?.scrollToIndex({ index: currentIndex + 1 }),
|
||||
scrollToIndex: (i) => flatListRef.current?.scrollToIndex({ index: i }),
|
||||
getData: () => feedData, // Return the full data array
|
||||
getItem: (i) => feedData[i], // Return item at index
|
||||
dataLength: feedData.length,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CDP (Hermes Runtime) agent-device (Simulator)
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ navigate to tab │ │ screenshot capture │
|
||||
│ install state hook │ │ tap fallback (if CDP │
|
||||
│ query item property │ │ mutation fails) │
|
||||
│ mutate item property │ │ swipe fallback (if │
|
||||
│ scrollToNext() │ │ scroll hook absent) │
|
||||
│ scrollToIndex(0) │ │ │
|
||||
│ read feed data │ │ │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Run the example test
|
||||
```bash
|
||||
bash .pi/skills/qa-automation/qa-state-persistence/run.sh
|
||||
```
|
||||
|
||||
### View results
|
||||
- Screenshots: `/tmp/qa-tests/screenshots/<test-name>/`
|
||||
- Report JSON: `/tmp/qa-tests/<test-name>-report.json`
|
||||
|
||||
## Test Flow Diagram
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Navigate to │
|
||||
│ Feed Screen │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Record item #0 │
|
||||
│ (state=initial) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Mutate state │
|
||||
│ (state=changed) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Scroll away Nx │
|
||||
│ (index → N) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Scroll back to 0 │
|
||||
│ (index → 0) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ ★ VERIFY: state │
|
||||
│ persisted! │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Cleanup (restore │
|
||||
│ original state) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
qa-state-persistence/
|
||||
├── SKILL.md # This file
|
||||
├── lib/
|
||||
│ └── state-helpers.sh # State query, mutation, scroll-to-index
|
||||
├── flows/
|
||||
│ └── example-state-test.sh # Example test (customize for your app)
|
||||
└── run.sh # Runner with JSON report output
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Feed hook not available"
|
||||
The `__qaFeedState` global isn't set. Ensure:
|
||||
- Your feed component sets it up in `__DEV__` mode
|
||||
- The feed screen is mounted (navigate to it first)
|
||||
- The hook name matches `GLOBAL_FEED_VAR` in config
|
||||
|
||||
### "Could not read feed data"
|
||||
`getData()` or `getItem()` failed. Possible causes:
|
||||
- Component hasn't fully mounted yet (increase settle time)
|
||||
- Feed data structure changed
|
||||
- Hook was set up before data loaded
|
||||
|
||||
### "scrollToIndex not available"
|
||||
The hook doesn't expose `scrollToIndex`. The test falls back to repeated swipe-down gestures.
|
||||
|
||||
### "State lost after scroll"
|
||||
**This is the real failure the test catches.** If state doesn't persist, investigate:
|
||||
- List item recycling (FlatList/FlashList virtualization)
|
||||
- State management (local vs global state)
|
||||
- Cache invalidation during scroll
|
||||
- Component unmount/remount cycles
|
||||
299
skills/qa-automation/qa-state-persistence/flows/example-state-test.sh
Executable file
299
skills/qa-automation/qa-state-persistence/flows/example-state-test.sh
Executable file
@@ -0,0 +1,299 @@
|
||||
#!/bin/bash
|
||||
# ╔═══════════════════════════════════════════════════════════════════╗
|
||||
# ║ Example State Persistence Test ║
|
||||
# ╠═══════════════════════════════════════════════════════════════════╣
|
||||
# ║ Tests: mutate state → scroll away → scroll back → verify state ║
|
||||
# ║ ║
|
||||
# ║ CUSTOMIZE: Set STATE_PROPERTY and SCREEN_EXPLORE in qa.config.sh ║
|
||||
# ║ ║
|
||||
# ║ Usage: bash .pi/skills/qa-automation/qa-state-persistence/flows/example-state-test.sh
|
||||
# ╚═══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Source Libraries ─────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/../lib/state-helpers.sh"
|
||||
|
||||
# ── Test Setup ───────────────────────────────────────────────────────
|
||||
TEST_NAME="state-persistence"
|
||||
setup_test "$TEST_NAME"
|
||||
|
||||
run_setup_guard || {
|
||||
echo "FATAL: Setup guard failed."
|
||||
teardown_test
|
||||
exit 1
|
||||
}
|
||||
|
||||
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": "State Persistence",
|
||||
"status": "$status",
|
||||
"error": "$error",
|
||||
"screenshots": [$screenshots]
|
||||
}
|
||||
RESULT
|
||||
)")
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 0 — Install Hooks
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Install debug hooks"
|
||||
assert_app_foreground || add_test_result "App Check" "failed" "Not foreground"
|
||||
|
||||
hook_result=$(install_debug_hook 2>&1 || echo '{"error":"failed"}')
|
||||
log_info "Video hook: $hook_result"
|
||||
|
||||
take_screenshot "00-initial"
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 1 — Navigate to Feed
|
||||
# CUSTOMIZE: Change nav_explore to your feed navigation
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Navigate to feed screen"
|
||||
nav_home 2>/dev/null || true
|
||||
sleep 1
|
||||
nav_explore
|
||||
sleep 4
|
||||
|
||||
overlay=$(check_error_overlay)
|
||||
[ "$overlay" = "visible" ] && dismiss_error_overlay && sleep 1
|
||||
|
||||
# Reset to index 0
|
||||
current_idx=$(get_current_feed_index)
|
||||
if [ "$current_idx" != "0" ] && [ "$current_idx" != "-1" ]; then
|
||||
scroll_to_index 0
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
# Install state hook after feed is mounted
|
||||
state_hook_result=$(install_state_debug_hook 2>&1 || echo '{"error":"failed"}')
|
||||
log_info "State hook: $state_hook_result"
|
||||
|
||||
take_screenshot "01-feed"
|
||||
add_test_result "Navigate to Feed" "passed" "" "\"01-feed.png\""
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 2 — Record Item Identity
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Record first item identity"
|
||||
|
||||
first_item=$(query_item_state 0 2>&1 || echo '{"error":"query failed"}')
|
||||
log_info "First item: $first_item"
|
||||
|
||||
item_id=$(echo "$first_item" | node -e "
|
||||
let d='';process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{try{console.log(JSON.parse(d).id||'unknown')}catch(e){console.log('unknown')}});
|
||||
" 2>/dev/null || echo "unknown")
|
||||
|
||||
has_ok=$(echo "$first_item" | 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_pass "Item captured: $item_id"
|
||||
add_test_result "Record Identity" "passed" ""
|
||||
else
|
||||
# Retry with re-installed hook
|
||||
install_state_debug_hook >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
first_item=$(query_item_state 0 2>&1 || echo '{"error":"retry failed"}')
|
||||
has_ok=$(echo "$first_item" | 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
|
||||
add_test_result "Record Identity" "passed" "Retry succeeded"
|
||||
else
|
||||
add_test_result "Record Identity" "skipped" "Could not read data"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 3 — Verify Initial State
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Verify initial state (${STATE_PROPERTY} should be false)"
|
||||
|
||||
initial_value=$(echo "$first_item" | node -e "
|
||||
let d='';process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{try{console.log(JSON.parse(d).${STATE_PROPERTY}?'true':'false')}catch(e){console.log('unknown')}});
|
||||
" 2>/dev/null || echo "unknown")
|
||||
|
||||
take_screenshot "02-initial-state"
|
||||
|
||||
if [ "$initial_value" = "true" ]; then
|
||||
log_warn "State is already true — toggling off for clean baseline"
|
||||
toggle_item_property_cdp >/dev/null 2>&1
|
||||
sleep 2
|
||||
add_test_result "Initial State" "passed" "Reset to false"
|
||||
elif [ "$initial_value" = "false" ]; then
|
||||
log_pass "Clean baseline (${STATE_PROPERTY}=false)"
|
||||
add_test_result "Initial State" "passed" "" "\"02-initial-state.png\""
|
||||
else
|
||||
add_test_result "Initial State" "skipped" "Unknown" "\"02-initial-state.png\""
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 4 — Mutate State
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Mutate state (set ${STATE_PROPERTY}=true)"
|
||||
|
||||
toggle_result=$(toggle_item_property_cdp 2>&1 || echo '{"error":"failed"}')
|
||||
log_info "Toggle result: $toggle_result"
|
||||
take_screenshot "03-after-mutate"
|
||||
|
||||
after_value=$(query_item_state 0 2>&1 || echo '{"error":"failed"}')
|
||||
after_prop=$(echo "$after_value" | node -e "
|
||||
let d='';process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{try{console.log(JSON.parse(d).${STATE_PROPERTY}?'true':'false')}catch(e){console.log('unknown')}});
|
||||
" 2>/dev/null || echo "unknown")
|
||||
|
||||
if [ "$after_prop" = "true" ]; then
|
||||
log_pass "State mutated successfully"
|
||||
add_test_result "Mutate State" "passed" "" "\"03-after-mutate.png\""
|
||||
else
|
||||
log_warn "Could not verify mutation: $after_prop"
|
||||
add_test_result "Mutate State" "skipped" "Verification failed" "\"03-after-mutate.png\""
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 5 — Scroll Away
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
for i in $(seq 1 $STATE_SCROLL_COUNT); do
|
||||
step "Scroll to item #$i"
|
||||
scroll_to_next_video
|
||||
sleep 2
|
||||
take_screenshot "04-scroll-${i}"
|
||||
add_test_result "Scroll to Item $i" "passed" "" "\"04-scroll-${i}.png\""
|
||||
done
|
||||
|
||||
# Verify we scrolled away
|
||||
away_index=$(get_current_feed_index)
|
||||
log_info "Current index: $away_index"
|
||||
take_screenshot "05-scrolled-away"
|
||||
add_test_result "Scrolled Away" "passed" "Index: $away_index" "\"05-scrolled-away.png\""
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 6 — Scroll Back
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Scroll back to first item"
|
||||
scroll_to_index 0
|
||||
sleep 3
|
||||
|
||||
back_index=$(get_current_feed_index)
|
||||
take_screenshot "06-scrolled-back"
|
||||
|
||||
if [ "$back_index" = "0" ]; then
|
||||
log_pass "Back at first item"
|
||||
add_test_result "Scroll Back" "passed" "" "\"06-scrolled-back.png\""
|
||||
else
|
||||
scroll_back_to_start
|
||||
sleep 2
|
||||
add_test_result "Scroll Back" "passed" "Used fallback" "\"06-scrolled-back.png\""
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 7 — KEY ASSERTION: State Persisted
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "★ KEY ASSERTION: Verify ${STATE_PROPERTY} persisted"
|
||||
|
||||
# Re-install hook (fiber tree may have shifted)
|
||||
install_state_debug_hook >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
|
||||
persist_state=$(query_item_state 0 2>&1 || echo '{"error":"failed"}')
|
||||
persist_value=$(echo "$persist_state" | node -e "
|
||||
let d='';process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{try{console.log(JSON.parse(d).${STATE_PROPERTY}?'true':'false')}catch(e){console.log('unknown')}});
|
||||
" 2>/dev/null || echo "unknown")
|
||||
|
||||
take_screenshot "07-persisted"
|
||||
|
||||
if [ "$persist_value" = "true" ]; then
|
||||
log_pass "★ STATE PERSISTED! ${STATE_PROPERTY} is still true after scrolling away and back"
|
||||
add_test_result "State Persisted" "passed" "" "\"07-persisted.png\""
|
||||
elif [ "$persist_value" = "unknown" ]; then
|
||||
log_warn "Could not verify — CDP state read failed"
|
||||
add_test_result "State Persisted" "skipped" "CDP failed" "\"07-persisted.png\""
|
||||
else
|
||||
log_fail "★ STATE LOST! ${STATE_PROPERTY} is false after scrolling back"
|
||||
add_test_result "State Persisted" "failed" "${STATE_PROPERTY}=false after scroll" "\"07-persisted.png\""
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Step 8 — Cleanup
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Cleanup: restore original state"
|
||||
|
||||
if [ "$persist_value" = "true" ]; then
|
||||
toggle_item_property_cdp >/dev/null 2>&1 || true
|
||||
sleep 1
|
||||
log_pass "State restored"
|
||||
add_test_result "Cleanup" "passed" ""
|
||||
else
|
||||
log_info "No cleanup needed"
|
||||
add_test_result "Cleanup" "passed" "Not needed"
|
||||
fi
|
||||
|
||||
take_screenshot "08-final"
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Generate Report
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
step "Generating report"
|
||||
|
||||
SKIP_COUNT=0
|
||||
for r in "${TEST_RESULTS[@]}"; do
|
||||
echo "$r" | grep -q '"status": "skipped"' && SKIP_COUNT=$((SKIP_COUNT + 1))
|
||||
done
|
||||
|
||||
TESTS_JSON=""
|
||||
for r in "${TEST_RESULTS[@]}"; do
|
||||
[ -n "$TESTS_JSON" ] && TESTS_JSON+=","
|
||||
TESTS_JSON+="$r"
|
||||
done
|
||||
|
||||
REPORT_FILE="$TEST_OUTPUT_DIR/${TEST_NAME}-report.json"
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - TEST_START_TIME))
|
||||
|
||||
cat > "$REPORT_FILE" <<REPORT
|
||||
{
|
||||
"title": "State Persistence QA",
|
||||
"generatedAt": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
|
||||
"suites": [{
|
||||
"name": "State Persistence (${STATE_PROPERTY})",
|
||||
"type": "e2e",
|
||||
"passed": $PASS_COUNT,
|
||||
"failed": $FAIL_COUNT,
|
||||
"skipped": $SKIP_COUNT,
|
||||
"duration": $((duration * 1000)),
|
||||
"tests": [$TESTS_JSON],
|
||||
"screenshotDir": "$SCREENSHOT_DIR/$TEST_NAME"
|
||||
}],
|
||||
"totalPassed": $PASS_COUNT,
|
||||
"totalFailed": $FAIL_COUNT,
|
||||
"totalSkipped": $SKIP_COUNT,
|
||||
"totalDuration": $((duration * 1000))
|
||||
}
|
||||
REPORT
|
||||
|
||||
log_info "Report: $REPORT_FILE"
|
||||
teardown_test
|
||||
|
||||
echo ""
|
||||
echo "State persistence test completed!"
|
||||
echo " Screenshots: $SCREENSHOT_DIR/$TEST_NAME/"
|
||||
echo " Report: $REPORT_FILE"
|
||||
echo ""
|
||||
290
skills/qa-automation/qa-state-persistence/lib/state-helpers.sh
Executable file
290
skills/qa-automation/qa-state-persistence/lib/state-helpers.sh
Executable file
@@ -0,0 +1,290 @@
|
||||
#!/bin/bash
|
||||
# ╔═══════════════════════════════════════════════════════════════════╗
|
||||
# ║ state-helpers.sh — State Persistence Inspection & Mutation ║
|
||||
# ╠═══════════════════════════════════════════════════════════════════╣
|
||||
# ║ Source this file in state persistence test scripts: ║
|
||||
# ║ source "$(dirname "$0")/../lib/state-helpers.sh" ║
|
||||
# ║ ║
|
||||
# ║ Provides: ║
|
||||
# ║ • State debug hook installation ║
|
||||
# ║ • Item state queries (any property at any index) ║
|
||||
# ║ • CDP-first state mutation with tap fallback ║
|
||||
# ║ • Feed index tracking and scroll-to-index ║
|
||||
# ║ • Assertion helpers for property values ║
|
||||
# ╚═══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Source shared helpers (chains to test-helpers + cdp-helpers) ─────
|
||||
STATE_SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCROLL_SKILL_DIR="$(cd "$STATE_SKILL_DIR/../qa-scroll" && pwd)"
|
||||
|
||||
source "$SCROLL_SKILL_DIR/lib/scroll-helpers.sh"
|
||||
|
||||
# ── Install State Debug Hook ────────────────────────────────────────
|
||||
# Sets up a global accessor for reading feed item properties.
|
||||
install_state_debug_hook() {
|
||||
cdp_eval_safe "
|
||||
globalThis.__qaItemState = function(targetIndex) {
|
||||
try {
|
||||
var feed = globalThis.${GLOBAL_FEED_VAR};
|
||||
if (!feed) return JSON.stringify({error: '${GLOBAL_FEED_VAR} not available', method: 'none'});
|
||||
|
||||
var idx = (targetIndex !== undefined && targetIndex !== null) ? targetIndex : feed.currentIndex;
|
||||
|
||||
// Method 1: getItem() directly
|
||||
if (typeof feed.getItem === 'function') {
|
||||
var item = feed.getItem(idx);
|
||||
if (item && typeof item === 'object') {
|
||||
return JSON.stringify({
|
||||
ok: true,
|
||||
method: 'getItem',
|
||||
index: idx,
|
||||
${STATE_PROPERTY}: !!item.${STATE_PROPERTY},
|
||||
${STATE_COUNTER_PROPERTY}: item.${STATE_COUNTER_PROPERTY} || 0,
|
||||
id: item.id || item._id || 'unknown',
|
||||
title: (item.description || item.title || item.name || '').substring(0, 50)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: getData() array
|
||||
if (typeof feed.getData === 'function') {
|
||||
var data = feed.getData();
|
||||
if (data && idx < data.length && data[idx]) {
|
||||
var it = data[idx];
|
||||
return JSON.stringify({
|
||||
ok: true,
|
||||
method: 'getData',
|
||||
index: idx,
|
||||
${STATE_PROPERTY}: !!it.${STATE_PROPERTY},
|
||||
${STATE_COUNTER_PROPERTY}: it.${STATE_COUNTER_PROPERTY} || 0,
|
||||
id: it.id || it._id || 'unknown',
|
||||
title: (it.description || it.title || it.name || '').substring(0, 50)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
error: 'feed methods not available',
|
||||
method: 'none',
|
||||
hasGetItem: typeof feed.getItem === 'function',
|
||||
hasGetData: typeof feed.getData === 'function',
|
||||
dataLength: feed.dataLength,
|
||||
currentIndex: feed.currentIndex
|
||||
});
|
||||
} catch(e) {
|
||||
return JSON.stringify({error: e.message, method: 'exception'});
|
||||
}
|
||||
};
|
||||
|
||||
var feed = globalThis.${GLOBAL_FEED_VAR};
|
||||
return JSON.stringify({
|
||||
ok: true,
|
||||
hookInstalled: 'itemState',
|
||||
hasGetItem: !!(feed && typeof feed.getItem === 'function'),
|
||||
hasGetData: !!(feed && typeof feed.getData === 'function'),
|
||||
dataLength: feed ? feed.dataLength : 0
|
||||
});
|
||||
"
|
||||
}
|
||||
|
||||
# ── Query Item State ─────────────────────────────────────────────────
|
||||
# Returns JSON: {ok, STATE_PROPERTY, STATE_COUNTER_PROPERTY, id, ...}
|
||||
query_item_state() {
|
||||
local target_index="${1:-}"
|
||||
local index_arg=""
|
||||
[ -n "$target_index" ] && index_arg="$target_index"
|
||||
|
||||
local attempt=0
|
||||
local max_attempts=3
|
||||
local result=""
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
try {
|
||||
if (globalThis.__qaItemState) {
|
||||
return globalThis.__qaItemState($index_arg);
|
||||
}
|
||||
return JSON.stringify({error: 'state hook not installed'});
|
||||
} catch(e) {
|
||||
return JSON.stringify({error: e.message});
|
||||
}
|
||||
})();
|
||||
" 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{const o=JSON.parse(d);console.log(o.ok?'yes':'no')}catch(e){console.log('no')}});
|
||||
" 2>/dev/null || echo "no")
|
||||
|
||||
if [ "$has_ok" = "yes" ]; then
|
||||
echo "$result"
|
||||
return 0
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
[ $attempt -lt $max_attempts ] && sleep 1
|
||||
done
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# ── Feed Index ───────────────────────────────────────────────────────
|
||||
|
||||
get_current_feed_index() {
|
||||
local result
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
if (globalThis.${GLOBAL_FEED_VAR}) {
|
||||
return '' + (globalThis.${GLOBAL_FEED_VAR}.currentIndex || 0);
|
||||
}
|
||||
return '-1';
|
||||
})();
|
||||
" 2>/dev/null || echo "-1")
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
scroll_to_index() {
|
||||
local target_index="$1"
|
||||
step "Scrolling to feed index $target_index"
|
||||
local result
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
if (globalThis.${GLOBAL_FEED_VAR} && globalThis.${GLOBAL_FEED_VAR}.scrollToIndex) {
|
||||
var before = globalThis.${GLOBAL_FEED_VAR}.currentIndex;
|
||||
globalThis.${GLOBAL_FEED_VAR}.scrollToIndex($target_index);
|
||||
return JSON.stringify({ok:true, before:before, after:$target_index});
|
||||
}
|
||||
return JSON.stringify({error:'scrollToIndex not available'});
|
||||
})();
|
||||
" 2>/dev/null || echo '{"error":"cdp failed"}')
|
||||
log_info "Scroll to index result: $result"
|
||||
sleep 3
|
||||
}
|
||||
|
||||
scroll_back_to_start() {
|
||||
local current_index
|
||||
current_index=$(get_current_feed_index)
|
||||
|
||||
if [ "$current_index" = "-1" ] || [ "$current_index" = "0" ]; then
|
||||
log_info "Already at start or unknown index"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try scrollToIndex first
|
||||
local result
|
||||
result=$(cdp_eval "
|
||||
(function() {
|
||||
if (globalThis.${GLOBAL_FEED_VAR} && globalThis.${GLOBAL_FEED_VAR}.scrollToIndex) {
|
||||
globalThis.${GLOBAL_FEED_VAR}.scrollToIndex(0);
|
||||
return 'ok';
|
||||
}
|
||||
return 'no_hook';
|
||||
})();
|
||||
" 2>/dev/null || echo "error")
|
||||
|
||||
if [ "$result" = "ok" ]; then
|
||||
log_info "Scrolled to index 0 via scrollToIndex"
|
||||
sleep 3
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback: swipe down N times
|
||||
log_info "Using swipe-down fallback ($current_index times)"
|
||||
local i=0
|
||||
while [ $i -lt "$current_index" ]; do
|
||||
swipe $SWIPE_END_X $SWIPE_END_Y $SWIPE_START_X $SWIPE_START_Y
|
||||
sleep 2
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
# ── State Mutation ───────────────────────────────────────────────────
|
||||
|
||||
# Toggle a boolean property on the current feed item via CDP.
|
||||
toggle_item_property_cdp() {
|
||||
local current_index
|
||||
current_index=$(get_current_feed_index)
|
||||
|
||||
local result
|
||||
result=$(cdp_eval_safe "
|
||||
var feed = globalThis.${GLOBAL_FEED_VAR};
|
||||
if (!feed || typeof feed.getItem !== 'function') {
|
||||
return JSON.stringify({error: 'feed not available'});
|
||||
}
|
||||
|
||||
var item = feed.getItem($current_index);
|
||||
if (!item) {
|
||||
return JSON.stringify({error: 'no item at index $current_index'});
|
||||
}
|
||||
|
||||
try {
|
||||
var oldValue = !!item.${STATE_PROPERTY};
|
||||
item.${STATE_PROPERTY} = !oldValue;
|
||||
if (item.${STATE_COUNTER_PROPERTY} !== undefined) {
|
||||
item.${STATE_COUNTER_PROPERTY} = oldValue
|
||||
? (item.${STATE_COUNTER_PROPERTY} - 1)
|
||||
: (item.${STATE_COUNTER_PROPERTY} + 1);
|
||||
}
|
||||
return JSON.stringify({ok: true, was: oldValue, now: !oldValue, id: item.id});
|
||||
} catch(e) {
|
||||
return JSON.stringify({error: e.message});
|
||||
}
|
||||
" 2>/dev/null || echo '{"error":"cdp failed"}')
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# ── Assertion Helpers ────────────────────────────────────────────────
|
||||
|
||||
# Assert that item at index has the property set to true
|
||||
assert_item_property_true() {
|
||||
local target_index="${1:-}"
|
||||
local state
|
||||
state=$(query_item_state "$target_index")
|
||||
|
||||
local value
|
||||
value=$(echo "$state" | node -e "
|
||||
let d='';process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{try{const o=JSON.parse(d);console.log(o.${STATE_PROPERTY}?'true':'false')}catch(e){console.log('unknown')}});
|
||||
" 2>/dev/null || echo "unknown")
|
||||
|
||||
if [ "$value" = "true" ]; then
|
||||
log_pass "Item ${STATE_PROPERTY} is true"
|
||||
return 0
|
||||
elif [ "$value" = "unknown" ]; then
|
||||
log_warn "Could not determine ${STATE_PROPERTY} state"
|
||||
return 2
|
||||
else
|
||||
log_fail "Item ${STATE_PROPERTY} is false (expected true)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Assert that item at index has the property set to false
|
||||
assert_item_property_false() {
|
||||
local target_index="${1:-}"
|
||||
local state
|
||||
state=$(query_item_state "$target_index")
|
||||
|
||||
local value
|
||||
value=$(echo "$state" | node -e "
|
||||
let d='';process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{try{const o=JSON.parse(d);console.log(o.${STATE_PROPERTY}?'true':'false')}catch(e){console.log('unknown')}});
|
||||
" 2>/dev/null || echo "unknown")
|
||||
|
||||
if [ "$value" = "false" ]; then
|
||||
log_pass "Item ${STATE_PROPERTY} is false (as expected)"
|
||||
return 0
|
||||
elif [ "$value" = "unknown" ]; then
|
||||
log_warn "Could not determine ${STATE_PROPERTY} state"
|
||||
return 2
|
||||
else
|
||||
log_fail "Item ${STATE_PROPERTY} is true (expected false)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "State persistence helpers loaded."
|
||||
43
skills/qa-automation/qa-state-persistence/run.sh
Executable file
43
skills/qa-automation/qa-state-persistence/run.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# Run the State Persistence QA test
|
||||
# Setup guard runs automatically.
|
||||
#
|
||||
# Usage: bash .pi/skills/qa-automation/qa-state-persistence/run.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPORT_FILE="/tmp/qa-tests/state-persistence-report.json"
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════╗"
|
||||
echo "║ State Persistence QA ║"
|
||||
echo "║ Started: $(date '+%Y-%m-%d %H:%M:%S') "
|
||||
echo "╚═══════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
bash "$SKILL_DIR/flows/example-state-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"
|
||||
fi
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
exit $EXIT_CODE
|
||||
Reference in New Issue
Block a user