#!/bin/bash # Phase 1 Testing Script - iOS Interactive Test Runner # Guides through all Phase 1 tests with clear prompts for UI interaction # Adapted from Android test-phase1.sh for iOS testing set -e # Exit on error # Source shared library (if exists) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "${SCRIPT_DIR}/ios-test-lib.sh" ]; then source "${SCRIPT_DIR}/ios-test-lib.sh" fi # Phase 1 specific configuration APP_BUNDLE_ID="com.timesafari.dailynotification.test" SIMULATOR_DEVICE="iPhone 15" LOG_PREFIX="DNP" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Helper functions print_header() { echo "" echo -e "${BLUE}========================================${NC}" echo -e "${BLUE}$1${NC}" echo -e "${BLUE}========================================${NC}" echo "" } print_step() { echo -e "${GREEN}[STEP $1]${NC} $2" echo "" } print_info() { echo -e "${BLUE}ℹ️ $1${NC}" } print_success() { echo -e "${GREEN}✅ $1${NC}" } print_warn() { echo -e "${YELLOW}⚠️ $1${NC}" } print_error() { echo -e "${RED}❌ $1${NC}" } wait_for_user() { echo "" read -p "Press Enter to continue..." echo "" } wait_for_ui_action() { echo "" echo -e "${YELLOW}$1${NC}" echo "" read -p "Press Enter when done..." echo "" } # iOS-specific helper functions get_simulator_id() { # First try to find a booted device matching the name # Extract UUID from line like: " iPhone 15 (6514F1D6-80C2-4D0E-8CB4-6F561C8EA1F1) (Booted)" local booted_id=$(xcrun simctl list devices | grep "${SIMULATOR_DEVICE}" | grep "Booted" | head -1 | sed -E 's/.*\(([A-F0-9-]{36})\).*\(Booted\).*/\1/') if [ -n "${booted_id}" ] && [ "${booted_id}" != "Booted" ]; then echo "${booted_id}" return 0 fi # If no booted device, try available devices local available_id=$(xcrun simctl list devices available | grep "${SIMULATOR_DEVICE}" | head -1 | sed -E 's/.*\(([A-F0-9-]{36})\).*/\1/') if [ -n "${available_id}" ]; then echo "${available_id}" return 0 fi # Last resort: try any device with similar name (handles "iPhone 15" vs "iPhone 15 Pro") # Prefer booted devices local any_id=$(xcrun simctl list devices | grep -i "iphone.*15" | grep "Booted" | head -1 | sed -E 's/.*\(([A-F0-9-]{36})\).*\(Booted\).*/\1/') if [ -n "${any_id}" ] && [ "${any_id}" != "Booted" ]; then echo "${any_id}" return 0 fi # Try any iPhone 15 device (not necessarily booted) any_id=$(xcrun simctl list devices | grep -i "iphone.*15" | head -1 | sed -E 's/.*\(([A-F0-9-]{36})\).*/\1/') if [ -n "${any_id}" ]; then echo "${any_id}" return 0 fi return 1 } get_app_logs() { local device_id=$1 local lines=${2:-50} # Use log show (historical) instead of log stream (live) to avoid hanging # This matches Android's approach of using logcat -d (historical logs) # log stream can block indefinitely waiting for new logs # Remove predicate to catch all logs (plugin logs may not match processImagePath predicate) xcrun simctl spawn "${device_id}" log show --last 2m --style=compact 2>/dev/null | grep -iE "(dailynotification|ios-test-app|App)" | tail -n "${lines}" || echo "" } check_plugin_configured() { print_info "Checking if plugin is configured..." local device_id=$(get_simulator_id) if [ -z "${device_id}" ]; then print_error "Simulator not found: ${SIMULATOR_DEVICE}" return 1 fi # Check multiple ways to determine if app is installed/configured: # 1. Check if app container exists (most reliable for installed apps) local app_container=$(xcrun simctl get_app_container "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || echo "") # 2. Check if app is listed in simulator (installed) local app_listed=$(xcrun simctl listapps "${device_id}" 2>/dev/null | grep -c "${APP_BUNDLE_ID}" || echo "0") app_listed=$(echo "${app_listed}" | tr -d '\n' | head -1) app_listed=${app_listed:-0} # 3. Check if app data directory exists (indicates app has been launched) local data_root="$HOME/Library/Developer/CoreSimulator/Devices/${device_id}/data/Containers/Data/Application" local app_data_exists="" if [ -d "${data_root}" ]; then # Check if any app data directory exists (app has been launched at least once) app_data_exists=$(find "${data_root}" -maxdepth 1 -type d 2>/dev/null | head -1 || echo "") fi # If any check indicates app exists, consider it potentially configured if [ -n "${app_container}" ] || [ "${app_listed}" -gt "0" ] || [ -n "${app_data_exists}" ]; then print_success "App detected (plugin may be configured)" print_info "Please verify in the app UI that you see:" echo " ⚙️ Plugin Settings: ✅ Configured" echo " 🔌 Native Fetcher: ✅ Configured" echo "" echo "If both show ✅, the plugin is configured and you can skip configuration." echo "If not configured, you'll need to click 'Configure Plugin' in the app UI." return 0 else print_info "Plugin not configured (no app data found)" print_info "You will need to click 'Configure Plugin' in the app UI" return 1 fi } ensure_plugin_configured() { if check_plugin_configured; then # Plugin might be configured, but let user verify (matches Android pattern) wait_for_ui_action "Please check the Plugin Status section at the top of the app. If you see: - ⚙️ Plugin Settings: ✅ Configured - 🔌 Native Fetcher: ✅ Configured - 🔔 Notifications: ✅ Granted (or similar) Then the plugin is already configured - just press Enter to continue. If any show ❌ or 'Not configured': - Click 'Request Permissions' if notifications are not granted - Click 'Configure Plugin' if settings/fetcher are not configured - Wait for all to show ✅, then press Enter." # Give a moment for any configuration that just happened sleep 2 print_success "Continuing with tests (plugin configuration verified or skipped)" return 0 else # Plugin definitely needs configuration print_info "Plugin needs configuration" # First ensure permissions ensure_permissions wait_for_ui_action "Click the 'Configure Plugin' button in the app UI. Wait for the status to update: - ⚙️ Plugin Settings: Should change to ✅ Configured - 🔌 Native Fetcher: Should change to ✅ Configured Once both show ✅, press Enter to continue." # Verify configuration completed sleep 2 print_success "Plugin configuration completed (or verified)" fi } check_permissions() { print_info "Checking notification permissions..." # Note: iOS permissions are checked at runtime, not via command line # We can only check if the app has been granted permission by checking logs print_info "iOS notification permissions are checked at runtime." print_info "Please verify in the app UI that notifications are authorized." print_info "If not authorized, you'll need to grant permission in the app." return 0 } ensure_permissions() { if check_permissions; then print_success "Permissions check passed" return 0 else print_info "Notification permissions needed" wait_for_ui_action "In the app UI, click the 'Request Permissions' button. This will show a system permission dialog. Steps: 1. Click 'Request Permissions' button 2. In the system dialog, tap 'Allow' to grant notification permission 3. Return to the app and verify the status shows: - 🔔 Notifications: ✅ Granted (or similar) Once permission is granted, press Enter to continue." return 0 fi } launch_app() { print_info "Launching app on simulator..." local device_id=$(get_simulator_id) if [ -z "${device_id}" ]; then print_error "Simulator not found: ${SIMULATOR_DEVICE}" print_info "Available simulators:" xcrun simctl list devices available | grep "iPhone" | head -5 return 1 fi # Boot simulator if not running local booted=$(xcrun simctl list devices | grep "${device_id}" | grep -c "Booted" 2>/dev/null || echo "0") booted=$(echo "${booted}" | tr -d '\n' | head -1) # Remove newlines and take first value booted=${booted:-0} # Default to 0 if empty if [ "${booted}" -eq "0" ]; then print_info "Booting simulator..." xcrun simctl boot "${device_id}" 2>/dev/null || true sleep 3 fi # Launch app xcrun simctl launch "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || { print_warn "App may already be running or needs to be built first" print_info "Please build and run the app in Xcode first:" echo " 1. Open: test-apps/ios-test-app/ios/App/App.xcworkspace" echo " 2. Select simulator: ${SIMULATOR_DEVICE}" echo " 3. Press Cmd+R to build and run" echo "" wait_for_user } sleep 2 # Give app time to launch print_success "App launched" } get_pending_notifications() { local device_id=$(get_simulator_id) if [ -z "${device_id}" ]; then echo "0" return fi # Note: iOS doesn't provide direct command-line access to pending notifications like Android's dumpsys alarm # We check logs for the explicit pendingCount that the plugin now logs after scheduling # Get logs directly without predicate to catch plugin logs (plugin runs in app process) local logs=$(xcrun simctl spawn "${device_id}" log show --last 2m --style=compact 2>/dev/null | grep -iE "(DailyNotificationScheduler|pendingCount|dailynotification)" | tail -100 || echo "") # Method 1: Look for explicit pendingCount in scheduling logs (most reliable) # The plugin logs: "Notification scheduled successfully for ..., id=..., pendingCount=X" # Match case-insensitive and extract the number after pendingCount= local pending_count=$(echo "${logs}" | grep -iE "pendingCount[=:][[:space:]]*[0-9]+" | tail -1 | grep -oE "[0-9]+" | head -1 || echo "") # Method 2: Look for "pending" count in status responses # The plugin's getNotificationStatus returns "pending": count local status_count=$(echo "${logs}" | grep -E "\"pending\"[[:space:]]*:[[:space:]]*[0-9]+" | tail -1 | grep -oE "[0-9]+" | head -1 || echo "") # Method 3: Count unique notification IDs that were scheduled (fallback) # Extract notification IDs from scheduling logs (format: "id=daily_...") local notification_ids=$(echo "${logs}" | grep -oE "id=[a-zA-Z0-9_.-]+" | sed 's/id=//' | sort -u | wc -l | tr -d ' ') notification_ids=${notification_ids:-0} # Prefer explicit pendingCount from scheduling log (most reliable) if [ -n "${pending_count}" ] && [ "${pending_count}" -ge "0" ] 2>/dev/null && [ "${pending_count}" -le "64" ] 2>/dev/null; then echo "${pending_count}" # Otherwise use status count if available elif [ -n "${status_count}" ] && [ "${status_count}" -ge "0" ] 2>/dev/null && [ "${status_count}" -le "64" ] 2>/dev/null; then echo "${status_count}" # Fallback to counting unique notification IDs elif [ "${notification_ids}" -gt "0" ]; then echo "${notification_ids}" # Default to 0 if nothing found else echo "0" fi } should_run_test() { local test_id=$1 shift local selected_tests=("$@") if [ ${#selected_tests[@]} -eq 0 ]; then return 0 # Run all tests if none specified fi for selected in "${selected_tests[@]}"; do if [ "${selected}" = "${test_id}" ]; then return 0 fi done return 1 } # ============================================ # TEST 0: Daily Rollover (Core Contract Verification) # ============================================ if should_run_test "0" "$@"; then print_header "TEST 0: Daily Rollover Verification" echo "Purpose: Verify that after a notification fires, the next day's" echo " schedule is correctly computed and only ONE notification exists." echo "" echo "Note: This test verifies the core 'one notification per day' contract." echo " It requires either:" echo " 1. Scheduling a notification for 'now + N seconds' and waiting, OR" echo " 2. Manipulating the simulator clock to cross the fire boundary." echo "" wait_for_user print_step "1" "Schedule a test notification for near-future..." launch_app ensure_plugin_configured INITIAL_COUNT=$(get_pending_notifications) print_info "Current pending notifications: ${INITIAL_COUNT}" wait_for_ui_action "In the app UI, schedule a daily notification. For this test, you may want to schedule it for a time very soon (e.g., 1-2 minutes from now) to observe the rollover behavior. This will schedule: - 1 notification (UNUserNotificationCenter) for the specified time - 1 prefetch task (BGTaskScheduler) for 2 minutes before that time" sleep 3 # Give notification time to be registered POST_SCHEDULE_COUNT=$(get_pending_notifications) print_info "Pending notifications after scheduling: ${POST_SCHEDULE_COUNT}" print_step "2" "Manual verification steps..." echo "" echo "To complete this test, you need to:" echo " 1. Wait for the notification to fire (or advance simulator clock)" echo " 2. Check that the plugin:" echo " - Computed the next day's time (24 hours later)" echo " - Scheduled exactly ONE notification for tomorrow" echo " - Did NOT create duplicate notifications" echo " 3. Verify in logs (Xcode Console or Console.app):" echo " - Next run time calculation shows tomorrow's time" echo " - Only one notification scheduled" echo "" echo "Expected log patterns:" echo " DNP-SCHEDULE: Scheduling next daily notification: ... source=ROLLOVER_ON_FIRE" echo " DNP-NOTIFY: Scheduling notification: triggerTime=" echo "" wait_for_ui_action "After the notification fires (or you advance the clock), check the logs and verify: 1. Only ONE notification exists (one per day) 2. The notification time is for tomorrow (24 hours later) 3. No duplicate notifications were created Press Enter when verification is complete." print_success "TEST 0: Daily Rollover Verification - Manual verification required" fi # ============================================ # TEST 1: Cold Start Recovery # ============================================ if should_run_test "1" "$@"; then print_header "TEST 1: Cold Start Recovery" echo "Purpose: Verify that when the app launches after termination," echo " missed notifications are detected and future notifications" echo " are verified/rescheduled." echo "" wait_for_user print_step "1" "Schedule a notification for future time..." launch_app ensure_permissions wait_for_ui_action "In the app UI, schedule a daily notification for a future time (e.g., 1 hour from now). This creates a notification that should persist across app termination." sleep 2 print_step "2" "Terminate app (simulate cold start)..." print_info "Terminating app to simulate cold start scenario..." device_id=$(get_simulator_id) xcrun simctl terminate "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || true print_info "App terminated. Waiting 5 seconds..." sleep 5 print_step "3" "Launch app and verify recovery..." launch_app print_info "Checking logs for recovery activity..." sleep 3 device_id=$(get_simulator_id) logs=$(get_app_logs "${device_id}" 100) if echo "${logs}" | grep -q "DNP-REACTIVATION\|recovery\|missed"; then print_success "Recovery activity detected in logs" else print_warn "No recovery activity detected in logs" print_info "This may indicate recovery is not yet implemented (expected for Phase 1)" fi wait_for_ui_action "Verify in the app UI that: 1. The notification is still scheduled (check 'Scheduled Notifications' screen) 2. Any missed notifications are marked as missed 3. Future notifications are verified/rescheduled Press Enter when verification is complete." print_success "TEST 1: Cold Start Recovery - Manual verification required" fi # ============================================ # TEST 2: Notification Persistence (Swipe from App Switcher) # ============================================ if should_run_test "2" "$@"; then print_header "TEST 2: Notification Persistence (App Termination)" echo "Purpose: Verify that notifications persist when app is terminated" echo " (iOS OS-guaranteed behavior)." echo "" wait_for_user print_step "1" "Schedule a notification..." launch_app ensure_permissions wait_for_ui_action "In the app UI, schedule a daily notification for a future time." sleep 2 print_step "2" "Terminate app..." print_info "Terminating app (simulating swipe from app switcher)..." device_id=$(get_simulator_id) xcrun simctl terminate "${device_id}" "${APP_BUNDLE_ID}" 2>/dev/null || true print_info "App terminated. Waiting 3 seconds..." sleep 3 print_step "3" "Verify notification still exists..." print_info "On iOS, notifications persist automatically (OS-guaranteed)." print_info "The notification should still fire even though the app is terminated." wait_for_ui_action "Verify that: 1. The notification fires at the scheduled time (even though app is terminated) 2. When you tap the notification, the app launches 3. The notification is marked as delivered Press Enter when verification is complete." print_success "TEST 2: Notification Persistence - iOS OS-guaranteed behavior verified" fi # ============================================ # TEST 3: Invalid Data Handling # ============================================ if should_run_test "3" "$@"; then print_header "TEST 3: Invalid Data Handling" echo "Purpose: Verify that the plugin handles invalid data gracefully" echo " without crashing." echo "" wait_for_user print_step "1" "Test invalid notification time..." launch_app ensure_permissions wait_for_ui_action "In the app UI, try to schedule a notification with invalid data: 1. Empty time string 2. Invalid time format (e.g., '25:00' or '12:99') 3. Negative time values The app should show an error message and NOT crash." print_info "Checking logs for error handling..." sleep 2 device_id=$(get_simulator_id) logs=$(get_app_logs "${device_id}" 50) # Debug: Show captured logs if verbose (uncomment to debug) # echo "DEBUG: Captured logs:" # echo "${logs}" # Check for error patterns (case-insensitive): # - error/invalid keywords # - DNP-* error prefixes (plugin error logs) # - invalid_time_format (error code) # - ERROR MESSAGE (Capacitor bridge prefix, if it appears in system logs) if echo "${logs}" | grep -qiE "error|invalid|DNP-.*(error|fail|reject)|invalid_time_format|ERROR MESSAGE"; then print_success "Error handling detected in logs" else print_info "No errors in recent system logs (may indicate graceful handling)" print_warn "Note: Capacitor bridge errors (⚡️ logs in Xcode console) may not appear in system logs." print_info "The plugin uses call.reject() which logs to Xcode console, not system logs." print_info "Verify error handling by checking:" print_info " 1. Xcode console for ⚡️ ERROR MESSAGE logs" print_info " 2. App UI shows error messages (not crashes)" print_info " 3. Valid notifications still work after errors" fi wait_for_ui_action "Verify that: 1. Invalid data is rejected with clear error messages 2. The app does NOT crash 3. Valid notifications can still be scheduled after errors Press Enter when verification is complete." print_success "TEST 3: Invalid Data Handling - Manual verification required" fi # ============================================ # Summary # ============================================ print_header "Phase 1 Testing Complete" echo "All Phase 1 tests have been executed." echo "" echo "Note: iOS recovery features (ReactivationManager) are NOT yet implemented." echo " Tests 1 and 2 will show expected behavior once recovery is implemented." echo "" echo "Next Steps:" echo " 1. Review test results" echo " 2. Check logs for any errors" echo " 3. Implement recovery features (Phase 1 directive)" echo " 4. Re-run tests after implementation" echo ""