Fix documentation to address Xcode behavior where 'Simulate Background Fetch' menu item only appears when app is NOT running. Changes: - Add explicit note about Xcode menu item availability - Prioritize LLDB command method when app is running (recommended) - Document three methods: LLDB command, Xcode menu, and UI button - Add troubleshooting section for common issues - Update quick start section to reference LLDB method - Explicitly reference test-apps/ios-test-app path for clarity This resolves confusion when 'Simulate Background Fetch' disappears from Debug menu while app is running. LLDB command method works reliably in all scenarios.
39 KiB
iOS Prefetch Testing Guide
Purpose: How to test background prefetch for DailyNotificationPlugin on iOS (simulator + device)
Version: 1.0.1
Scope: Phase 1 Prefetch MVP
Next Target: Phase 2 (Rolling Window + TTL Telemetry)
Maintainer: Matthew Raymer
Last Updated: 2025-11-15
Status: 🎯 ACTIVE - Testing guide for Phase 1+ implementation
Note: This guide assumes the iOS test app is implemented as described in doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md. The test app is located at test-apps/ios-test-app/.
Glossary: See doc/test-app-ios/IOS_PREFETCH_GLOSSARY.md for terminology definitions.
Android parity: Behavior is aligned with test-apps/android-test-app where platform constraints allow. Timing and BGTask heuristics will differ from Android's exact alarms:
- Android: Exact alarms via AlarmManager / WorkManager
- iOS: Heuristic BGTaskScheduler; no hard guarantee of 5-min prefetch
Non-Goals (Phase 1)
Out of scope for Phase 1 testing:
- ❌ No testing of silent push notifications
- ❌ No testing of advanced rolling-window policies beyond "schedule one daily notification"
- ❌ No guarantees of exact N-minute timing (iOS heuristics only)
- ❌ No testing of multi-day rolling windows (deferred to Phase 2)
Table of Contents
- Goals
- Assumptions
- Simulator Test Plan (Logic-Correctness)
- Real Device Test Plan (Timing & Reliability)
- Log Checklist
- Behavior Classification
- Test Harness Reference
Goals
Verify that the iOS prefetch path actually runs:
- ✅ BGTask is registered
- ✅ BGTask is scheduled
- ✅ Fetch executes
- ✅ Data is persisted & used by the notification
Separate concerns:
- "Does the logic work?" → Test in Simulator
- "Does timing behave?" → Test on Real Device
Time & T-Lead Rules
Critical: These rules prevent bugs when tweaking prefetch timing.
Time Storage
notificationTimeis stored in UTC (not local time)- All internal calculations use UTC timestamps
- Display to users may convert to local time, but storage remains UTC
Prefetch Lead Calculation
- Prefetch lead:
prefetchTime = notificationTime - leadMinutes - Default lead: 5 minutes (300 seconds)
- Minimum lead: 1 minute (60 seconds) - iOS requires at least 1 minute in future
- Maximum lead: No hard limit, but iOS heuristics favor shorter windows
Edge Cases
Negative/Too-Small Lead Times:
- If
notificationTime - leadMinutes < now + 60 seconds:- Cap prefetch at
now + 60 seconds(minimum iOS requirement) - Log:
[DNP-FETCH] Prefetch lead too small, capping at minimum (60s) - Or disable prefetch if notification is too soon
- Cap prefetch at
DST (Daylight Saving Time) Changes:
- UTC storage prevents DST issues
- Test case: Schedule notification at 2:00 AM on DST change day
- Verify: Prefetch and notification times remain correct (UTC-based)
Timezone Changes:
- Test case: User changes timezone between schedule and fire
- Verify: Notification fires at correct UTC time (not local time)
- Log:
[DNP-SCHEDULER] Timezone changed, but UTC schedule unchanged
Test Cases
-
DST Change:
- Schedule notification for 2:00 AM on DST transition day
- Verify prefetch and notification use UTC, not local time
- Check logs show UTC timestamps
-
Timezone Change:
- Schedule notification
- Change device timezone
- Verify notification fires at correct UTC time
- Check logs confirm UTC-based scheduling
-
Too-Small Lead:
- Schedule notification 30 seconds in future
- Verify prefetch is capped at 60 seconds minimum
- Check logs show capping behavior
If You Only Have 30 Minutes
Quick validation checklist (each step links to log verification):
- Build test app (per
IOS_TEST_APP_REQUIREMENTS.md):cd test-apps/ios-test-app # Or from repo root: ./scripts/build-ios-test-app.sh --simulator - Run on iOS Simulator
- Schedule notification 5 minutes in future → See Log Checklist → Notification Scheduled
- Background app (Home button / Cmd+Shift+H)
- Trigger BGTask (see Step 4 below for methods):
- Recommended: Use LLDB command in Xcode console (app must be running)
- Alternative: Stop app, then use Xcode → Debug → Simulate Background Fetch
- See Log Checklist → BGTask Fired
- Confirm logs show: Registration → Scheduling → BGTask handler → Fetch success → Task completed
- Wait for notification time or manually trigger → See Log Checklist → Notification Delivery
- Verify notification uses cached content (check logs for
[DNP-FETCH] Found cached content) → See Log Checklist → Notification Delivery
Assumptions
- Using BGTaskScheduler (
BGAppRefreshTask) for prefetch - App has background modes enabled:
- Background fetch
- Background processing (if using
BGProcessingTask)
- Info.plist has:
BGTaskSchedulerPermittedIdentifiersarray with task identifier:com.timesafari.dailynotification.fetch
- Plugin exposes:
scheduleDailyNotification()method that schedules both prefetch and notification
Architecture at a Glance
Flow Overview:
JS/HTML Test App → Capacitor Bridge → DailyNotificationPlugin (iOS)
↓
BGTaskScheduler (BGAppRefreshTask)
↓
HTTP API (JWT, ETag, etc.)
↓
Cache/DB (UserDefaults/CoreData)
↓
Notification Delivery
(uses cached content if valid,
falls back to live fetch)
Key Components:
- Test App: Provides UI to trigger plugin methods (see
IOS_TEST_APP_REQUIREMENTS.md) - Plugin: Handles scheduling, fetching, caching, notification delivery
- BGTaskScheduler: iOS background execution for prefetch
- Storage: Persists schedules and cached content
- Notification System: Delivers notifications using prefetched or live content
Verification Points:
- BGTask registered at app startup
- BGTask scheduled 5 minutes before notification time
- BGTask executes and fetches content
- Content persisted to cache/DB
- Notification delivery uses cached content
Simulator Test Plan (Logic-Correctness)
Objective: Confirm that when a background task fires, your prefetch code runs end-to-end.
Cross-Reference: Corresponds to IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios → Basic Functionality [SIM+DEV]
1. Harden Logging (One-Time Prep)
Add structured logs at key points:
On app startup:
[DNP-FETCH] Registering BGTaskScheduler task (id=com.timesafari.dailynotification.fetch)
When scheduling:
[DNP-FETCH] Scheduling BGAppRefreshTask (earliestBeginDate=2025-11-14T05:48:00Z)
[DNP-FETCH] Prefetch scheduled 5 minutes before notification (notificationTime=2025-11-14T05:53:00Z)
When BGTask handler fires:
[DNP-FETCH] BGTask handler invoked (task.identifier=com.timesafari.dailynotification.fetch)
Inside prefetch logic:
[DNP-FETCH] Starting fetch (notificationTime=2025-11-14T05:53:00Z, scheduleId=...)
[DNP-FETCH] Fetch success: status=200, items=1, ttl=86400
[DNP-FETCH] Fetch failed: error=..., willRetry=false
When integrating with notifications:
[DNP-FETCH] Using cached content for notification at 2025-11-14T05:53:00Z
[DNP-FETCH] No cached content, falling back to on-demand fetch
2. Start App in Simulator
- Clean build, run on iPhone 15 simulator (or similar)
- Verify console logs:
- ✅
[DNP-FETCH] Registering BGTaskScheduler task...at startup
- ✅
3. Schedule a Notification with Prefetch
- Call plugin's
scheduleDailyNotification()from JS layer - Check logs:
- ✅
[DNP-FETCH] Scheduling BGAppRefreshTask...with sensibleearliestBeginDate - ✅
[DNP-SCHEDULER] Scheduling notification for 2025-11-14T05:53:00Z - ✅ Any DB/state writes for the schedule
- ✅
Note: For a full happy-path log example, see IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios → Basic Functionality.
4. Simulate Background Fetch
Important: Xcode's "Simulate Background Fetch" menu item only appears when the app is NOT running. When the app is running, use the LLDB command method below.
Method 1: LLDB Command (Recommended when app is running)
- Background the app (Home button / Cmd+Shift+H in simulator)
- Open Xcode Debug Console (View → Debug Area → Activate Console, or press Cmd+Shift+Y)
- In LLDB console, paste and execute:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] - Press Enter
Method 2: Xcode Menu (Only when app is NOT running)
- Stop the app (Cmd+.)
- In Xcode menu:
- Debug → Simulate Background Fetch or
- Debug → Simulate Background Refresh
- This will launch the app and trigger the background fetch
Method 3: Test App UI Button (If implemented)
- Use test app UI button: "Simulate BGTask Now" (if available in test app)
Expected logs (in order):
[DNP-FETCH] BGTask handler invoked (task.identifier=...)
[DNP-FETCH] Starting fetch (notificationTime=..., scheduleId=...)
[DNP-FETCH] API call: GET /api/notifications (JWT present, ETag=...)
[DNP-FETCH] Fetch success: status=200, bytes=1234, ttl=86400
[DNP-FETCH] Cached content for scheduleId=...
[DNP-FETCH] Task completed (success=true)
See Sample Prefetch Response & Mapping below for expected API response structure.
Troubleshooting:
- If LLDB command doesn't work, ensure the app is backgrounded first
- If you see "Simulate Background Fetch" in menu when app is running, it may be a different Xcode version - try the LLDB method anyway
- Verify BGTask identifier matches exactly:
com.timesafari.dailynotification.fetch
5. Trigger or Wait for Notification
Option A: Wait for scheduled time
- Wait until scheduled time
- Look for:
[DNP-FETCH] Using cached content for notification at ...[DNP-SCHEDULER] Notification delivered (id=..., title=...)
Option B: Manual trigger
- Use test app UI to trigger notification test path
- Confirm notification shows content derived from prefetch, not fallback
6. Sample Prefetch Response & Mapping
Example API Response:
When prefetch succeeds, the API returns JSON like:
{
"id": "notif-2025-11-15",
"title": "Your daily update",
"body": "Here is today's summary…",
"ttl": 86400,
"scheduled_for": "2025-11-15T05:53:00Z"
}
Response Mapping:
id→ Used for notification identification and deduplicationtitle→ Notification title (displayed to user)body→ Notification body (displayed to user)ttl→ Cache validity check at delivery time (seconds)scheduled_for→ Cross-check withnotificationTimeto ensure alignment
Log Verification:
When you see [DNP-FETCH] Fetch success (status=200, bytes=1234, ttl=86400), the ttl=86400 should match the ttl field in the JSON response. The scheduled_for timestamp should match the notificationTime used in scheduling.
Copy-Paste Commands:
// In test app or test harness:
// View cached payload
let cachedContent = UserDefaults.standard.data(forKey: "DNP_CachedContent_\(scheduleId)")
let json = try? JSONSerialization.jsonObject(with: cachedContent ?? Data())
// Simulate time warp (testing only)
DailyNotificationBackgroundTaskTestHarness.simulateTimeWarp(minutesForward: 60)
// Force reschedule all tasks
DailyNotificationBackgroundTaskTestHarness.forceRescheduleAll()
7. Negative-Path Tests
Network failure:
- Turn off network in Simulator (Settings → Network)
- Trigger background fetch
- Expect:
[DNP-FETCH] Fetch failed: error=NetworkError, willRetry=false- Then either:
- Cached content used if available, or
- Fallback path logged:
[DNP-FETCH] No cached content, falling back to on-demand fetch
Permission denied:
- Deny notification permissions
- Attempt to schedule
- Expect:
[DNP-PLUGIN] notifications_denied for scheduleDailyNotification- Error returned:
{ error: "notifications_denied", ... }
Storage unavailable:
- Simulate DB write failure (test harness can inject this)
- Attempt to cache prefetched content
- Expect:
[DNP-STORAGE] Cache write failed: error=...- Fallback to on-demand fetch at notification time
- Log:
[DNP-FETCH] Storage unavailable, will fetch on-demand
JWT expiration:
- Simulate expired JWT token (test harness can inject this)
- Trigger background fetch
- Expect:
[DNP-FETCH] Fetch failed: error=AuthError, httpStatus=401, willRetry=false- Telemetry:
dnp_prefetch_failure_total{reason="AUTH"} - Log:
[DNP-FETCH] JWT expired, authentication failed
Timezone drift:
- Schedule notification
- Change device timezone (Settings → General → Date & Time)
- Verify:
- Notification fires at correct UTC time (not local time)
- Log:
[DNP-SCHEDULER] Timezone changed, but UTC schedule unchanged - Prefetch timing remains correct (UTC-based)
Corrupted cache:
- Manually tamper with stored cache (invalid JSON or remove entry)
- Wait for notification to fire
- Expect:
[DNP-FETCH] No cached content available, falling back to on-demand fetch- No crash on reading bad cache (error handling wraps cache read)
- Fallback fetch executes successfully
BGTask execution failure:
- Simulate internal failure in BGTask handler (test harness can inject exception)
- Verify expiration handler or completion still gets called
- Expect:
- Task marked complete even on exception (defer guard ensures completion)
- Log:
[DNP-FETCH] Task marked complete (success=false) due to error
Repeated scheduling calls:
- Call
scheduleDailyNotification()multiple times rapidly - Verify no duplicate scheduling for same time
- Expect:
- Only one BGTask is actually submitted (one active task rule)
- Logs show single scheduling, not duplicates
Permission revoked mid-run:
- Schedule and fetch (ensure success)
- Go to Settings and disable notifications before notification fires
- Expect:
- Notification won't show (iOS blocks it)
- Plugin logs absence of delivery (telemetry or log note)
- No crash if user toggles permissions
Known OS Limitations
Critical: These are iOS system limitations, not plugin bugs. If these conditions occur, it is not a plugin bug; confirm logs and document behavior.
BGTaskScheduler Simulator Limitation:
- BGTaskSchedulerErrorDomain Code=1 (notPermitted) is EXPECTED on simulator
- BGTaskScheduler doesn't work reliably on iOS Simulator - this is a known iOS limitation
- Background fetch scheduling will fail on simulator with Code=1 error
- This is NOT a plugin bug - notification scheduling still works correctly
- Prefetch won't run on simulator, but will work on real devices with Background App Refresh enabled
- Error handling logs: "Background fetch scheduling failed (expected on simulator)"
- Testing: Use Xcode → Debug → Simulate Background Fetch for simulator testing
- See also:
doc/directives/0003-iOS-Android-Parity-Directive.mdfor implementation details
iOS may NOT run BGTasks if:
- App has been force-quit by the user (iOS won't run BGTask for force-quit apps)
- Background App Refresh is disabled in Settings → [Your App]
- Device is in Low Power Mode and idle
- Device battery is critically low
- On Simulator: BGTaskScheduler generally doesn't work (Code=1 error is expected)
iOS timing heuristics:
- iOS may delay or batch tasks; prefetch might run much later than
earliestBeginDate(up to 15+ minutes) - iOS adapts BGTask frequency based on user behavior, app usage, and energy constraints
- Notification delivery can drift by ±180 seconds (this is expected, not a bug)
What to do:
- Check logs to confirm plugin logic executed correctly
- Document the OS-imposed delay in test results
- Verify fallback mechanisms work when prefetch is delayed
Real Device Test Plan (Timing & Reliability)
Objective: Confirm that prefetch happens near the intended time (e.g., 5 minutes before) in realistic conditions.
Cross-Reference: Corresponds to IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios → Background Tasks [DEV-ONLY]
1. Install Dev Build on iPhone
- Same BGTask + background modes config as simulator
- Confirm logs visible via:
- Xcode → Devices & Simulators → device → open console, or
os_logaggregator / remote logging
2. Baseline Run
- Open app
- Schedule a notification for 30–60 minutes in the future with prefetch
- Lock the phone and leave it idle, plugged in (best-case for iOS)
3. Monitor Logs
Look for:
- BGTask scheduled with appropriate
earliestBeginDate - Some time before the notification:
- BGTask handler invoked
- Prefetch fetch executes
- At notification fire:
- Notification uses prefetched data
4. Edge Case Scenarios and Environment Conditions
Test different conditions and edge cases:
| Scenario / Condition | Test Strategy | Expected Outcome & Plugin Behavior |
|---|---|---|
| Background Refresh Off | Disable Background App Refresh in iOS Settings, schedule notification, leave device idle | BGTask will not run (iOS suppresses it). Plugin logs warning (notPermitted). Notification appears using live fetch at fire time. No crash or hang. Telemetry records system failure reason. |
| Low Power Mode On | Enable Low Power Mode, schedule notification, idle device (battery vs plugged) | iOS may delay or skip BGTask. If skipped: plugin falls back gracefully. If runs later: notification uses cached data if valid. |
| App Force-Quit | Schedule notification, kill app (swipe up), keep device idle until notification time | iOS won't run BGTask (app force-quit). No prefetch occurs. Notification fires with fallback fetch. Plugin detects on next launch that prefetch didn't happen. |
| Device Timezone Change | Schedule notification for 12:00 UTC, change device timezone before it fires | Notification fires at correct UTC time (not local). Logs show UTC timestamp unchanged. Prefetch runs at UTC-5min regardless of timezone. |
| DST Transition | Schedule notification on DST change day (e.g., just after "lost hour") | Prefetch and notification align correctly in UTC. No double-firing or misses. Logs show consistent UTC times. |
| Multi-Day Scheduling (Phase 2) | Schedule two daily notifications (today and tomorrow) | Plugin handles multiple schedules. Prefetch today's content today, schedules BGTask for tomorrow. After first day, second day's prefetch still occurs. |
| Device Reboot | Schedule notification, reboot device before it fires, launch app | Schedule persists. BGTask reschedules after reboot (or app detects on launch). Startup log shows hasPendingSchedules=true. Notification still delivered. |
Additional Variations:
- Device on battery vs plugged in
- App force-quit vs just backgrounded
- Multiple days in a row: iOS will adapt its heuristics; see if prefetch becomes more or less reliable
- Low Power Mode: May delay/disable background tasks
- Background App Refresh disabled: Should fail gracefully
5. Success Criteria
In "good" conditions (plugged in, WiFi, not force-quit):
- ✅ Prefetch runs at least once before the notification in the majority of tests
- ✅ Failure modes are logged clearly:
- So you know when timing failed because of iOS, not your code
- ✅ For at least one full test cycle, logs and telemetry counts confirm that the sequence: scheduled → executed → success → used is coherent
Acceptable outcomes:
- Prefetch runs within 15 minutes of
earliestBeginDate(iOS heuristic window) - If prefetch misses, fallback to on-demand fetch works
- All failures are logged with clear reasons
Test Run Result Template
Use this template when recording test runs (especially on real devices):
Date:
Device model:
iOS version:
App build / commit:
Lead time (T-Lead - see glossary):
Scenario ID(s) tested:
Conditions: (plugged vs battery, Wi-Fi vs LTE, Low Power Mode on/off, Background App Refresh on/off)
Outcome summary:
- Prefetch scheduled at:
- BGTask executed at:
- Notification fired at:
- Cached content used: yes/no
- Actual execution time vs scheduled: (e.g., scheduled 5min before, executed 2min before = within heuristics)
Telemetry counters:
- dnp_prefetch_scheduled_total:
- dnp_prefetch_executed_total:
- dnp_prefetch_success_total:
- dnp_prefetch_used_for_notification_total:
State verification:
- Content hash match: (fetch hash vs delivery hash)
- Schedule hash match: (scheduling hash vs execution hash)
- Cache persistence: (verified after notification)
Failures observed (if any) and key log lines:
Notes / follow-ups:
Persistent Test Artifacts:
Test app can save this summary to file for later review. Access via "Export Test Results" button or UserDefaults key DNP_TestRunArtifact.
Log Checklist
When everything is wired correctly, one full cycle should produce:
1. App Launch
Expected logs:
[DNP-FETCH] Registering BGTaskScheduler task (id=com.timesafari.dailynotification.fetch)
[DNP-PLUGIN] Startup complete (hasPendingSchedules=true|false)
If you see registration but not startup:
- Check plugin initialization in AppDelegate
- Verify Capacitor plugin registration
- Check for initialization errors in logs
If you don't see registration:
- Check Info.plist has
BGTaskSchedulerPermittedIdentifiers - Verify task registered in AppDelegate before app finishes launching
- Check for registration errors in logs
2. Notification Scheduled (from JS / plugin)
Expected logs:
[DNP-FETCH] Scheduling prefetch for notification at 2025-11-14T05:53:00Z
[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=2025-11-14T05:48:00Z)
[DNP-SCHEDULER] Scheduling notification for 2025-11-14T05:53:00Z
[DNP-STORAGE] Persisted schedule to DB (id=..., type=DAILY, ...)
Note: For a full happy-path log example, see IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios → Basic Functionality.
If you see scheduling but not BGTask scheduled:
- Check BGTaskScheduler submission succeeded
- Verify
earliestBeginDateis at least 1 minute in future - Check for BGTaskScheduler errors in logs
- On Simulator: BGTaskSchedulerErrorDomain Code=1 (notPermitted) is expected - see "Known OS Limitations"
If you see BGTask scheduled but not persisted:
- Check database write operations
- Verify storage layer is initialized
- Check for storage errors in logs
3. BGTask Fired (simulator or device)
Expected logs:
[DNP-FETCH] BGTask handler invoked (id=com.timesafari.dailynotification.fetch)
[DNP-FETCH] Resolved next notification needing content (time=..., scheduleId=...)
[DNP-FETCH] Starting fetch from <URL> (notificationTime=..., jwtPresent=true)
[DNP-FETCH] Fetch success (status=200, bytes=1234, ttl=86400)
[DNP-FETCH] Cached content for scheduleId=...
[DNP-FETCH] Task completed (success=true)
If you see "Registering BGTaskScheduler task" but never see "BGTask handler invoked" on device:
- Check Background App Refresh enabled in Settings → [Your App]
- Verify app not force-quit (iOS won't run BGTask for force-quit apps)
- Check Info.plist identifiers match code exactly (case-sensitive)
- Verify task was actually scheduled (check
earliestBeginDatein logs) - On Simulator: Use Xcode → Debug → Simulate Background Fetch
If you see handler invoked but not fetch starting:
- Check notification resolution logic
- Verify schedule exists in database
- Check for resolution errors in logs
If you see fetch starting but not success:
- Check network connectivity
- Verify API endpoint is accessible
- Check JWT/authentication
- Look for HTTP error codes in logs
4. Notification Delivery
Expected logs:
[DNP-SCHEDULER] Preparing content for notification at 2025-11-14T05:53:00Z
[DNP-FETCH] Found cached content (age=300s, source=prefetch)
[DNP-SCHEDULER] Notification built (title=..., body=..., id=...)
[DNP-SCHEDULER] Notification scheduled/delivered
If you see "Preparing content" but not "Found cached content":
- Check if prefetch actually ran (see BGTask logs)
- Verify cache/DB read operations
- Check TTL validation (content may have expired)
- Look for fallback to on-demand fetch
If you see "Found cached content" but not "Notification built":
- Check notification building logic
- Verify notification permissions granted
- Check for notification construction errors
5. Failure Cases
Expected logs:
[DNP-FETCH] Fetch failed (error=NetworkError, httpStatus=0, willRetry=false)
[DNP-FETCH] No cached content available, falling back to on-demand fetch
[DNP-FETCH] Task completed (success=false, reason=NETWORK)
If you see fetch failures repeatedly:
- Check network connectivity
- Verify API endpoint is correct
- Check authentication (JWT) is valid
- Look for specific error reasons in logs (NETWORK, AUTH, SYSTEM)
If you see "No cached content" but prefetch should have run:
- Check if BGTask actually executed (see BGTask logs)
- Verify cache/DB write operations succeeded
- Check for cache expiration (TTL may have passed)
- Verify fallback to on-demand fetch works
If you can walk through a log and trace all steps like this, you're in good shape.
Behavior Classification
Testable matrix of deterministic vs heuristic behavior:
| Bucket | Component / Method | Deterministic? | Test on | Notes |
|---|---|---|---|---|
| A | BGTaskScheduler.shared.register |
Yes | Sim + Dev | Registration must always log & succeed/fail deterministically |
| A | configure(), getLastNotification(), cancelAllNotifications(), getNotificationStatus(), updateSettings() |
Yes | Sim + Dev | Logic & I/O-only, no timing dependencies |
| A | checkPermissionStatus(), requestNotificationPermissions() |
Yes | Sim + Dev | Permission state reading and requests, deterministic |
| A | isChannelEnabled(channelId?), openChannelSettings(channelId?) |
Yes | Sim + Dev | Channel status and settings (iOS: app-wide, not per-channel) |
| A | getBatteryStatus(), getPowerState(), getRollingWindowStats() |
Yes | Sim + Dev | State reading, deterministic |
| A | testJWTGeneration(), testEndorserAPI() |
Yes | Sim + Dev | API call logic, deterministic |
| A | Fetch function logic (HTTP calls, DB writes, JSON parsing) | Yes | Sim + Dev | Code path is deterministic |
| A | Error handling, retry logic, fallbacks | Yes | Sim + Dev | Logic is deterministic |
| A | Log emission / structured logging | Yes | Sim + Dev | Logging is deterministic |
| A | Notification building logic (given data) | Yes | Sim + Dev | Title/body/payload construction is deterministic |
| A | TTL validation at delivery time | Yes | Sim + Dev | Validation logic is deterministic |
| B | scheduleDailyNotification() |
Logic yes, timing no | Sim + Dev | Use logs to verify scheduling but not run time |
| B | maintainRollingWindow() |
Logic yes, timing no | Sim + Dev | Logic deterministic, but when iOS allows execution is heuristic |
| B | BGTaskScheduler scheduling (earliestBeginDate) |
Logic yes, timing no | Sim + Dev | earliestBeginDate is a hint, not a guarantee |
| B | Notification trigger + prefetch relationship | Logic yes, timing no | Sim + Dev | "If cached content exists, use it" is deterministic; whether prefetch ran in time is heuristic |
| C | Time between earliestBeginDate and task execution |
No | Device | Purely heuristic, iOS controls when tasks run |
| C | Background task execution timing (BGTaskScheduler) | No | Device | iOS heuristics control execution timing |
| C | Notification delivery timing (UNUserNotificationCenter) | No | Device | iOS controls delivery timing (±180s tolerance) |
| C | Reboot recovery detection (uptime comparison) | No | Device | May vary based on system state |
Bucket Summary:
- 🟢 Bucket A (Deterministic): Test in Simulator and Device - Logic correctness
- 🟡 Bucket B (Partially Deterministic): Test flow in Simulator, timing on Device
- 🔴 Bucket C (Heuristic): Test on Real Device only - Timing and reliability
Test Harness Reference
File: ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift
Purpose: Use this harness if the plugin code is broken or being refactored; it isolates BGTask behavior from plugin wiring.
What it demonstrates:
- Task registration
- Task scheduling
- Task handler implementation
- Expiration handling
- Completion reporting
Usage:
- Reference implementation when building actual prefetch logic
- Standalone testing of BGTaskScheduler behavior
- Debugging BGTask registration/scheduling issues
- See file for detailed usage examples and testing instructions
Integration with Main Directive
This testing guide supports:
- Phase 1: Core prefetch functionality testing
- Phase 2: Advanced features testing (rolling window, TTL)
- Phase 3: TimeSafari integration testing (JWT, ETag)
- Validation Matrix: Cross-platform feature validation
Telemetry and Monitoring
Expected Counters (even if not yet implemented):
Define counters you expect the runtime to emit:
dnp_prefetch_scheduled_total- Total prefetch tasks scheduleddnp_prefetch_executed_total- Total prefetch tasks executeddnp_prefetch_success_total- Total successful prefetch executionsdnp_prefetch_failure_total{reason="NETWORK|AUTH|SYSTEM"}- Total failed prefetch executions by reasondnp_prefetch_used_for_notification_total- Total notifications using prefetched content
Telemetry Pipeline:
These counters MUST be emitted via the same pipeline as Android (e.g., structlog → rsyslog → Prometheus/Influx/Loki). If telemetry is not yet wired on iOS, mark tests that rely on counters as P2 and fall back to log inspection for Phase 1.
Telemetry JSON Schema (Phase 2 Ready):
Reserve placeholder JSON fields for telemetry events in the log schema:
{
"event": "prefetch_success",
"scheduleId": "notif-2025-11-15",
"duration_ms": 2432,
"ttl": 86400,
"timestamp": "2025-11-15T05:48:32Z",
"telemetry": {
"dnp_prefetch_scheduled_total": 1,
"dnp_prefetch_executed_total": 1,
"dnp_prefetch_success_total": 1
}
}
Even if not wired yet, this ensures Phase 2 code can emit compatible structured logs.
Telemetry Validation:
Use optional console log validation:
// In test harness or plugin
logTelemetrySnapshot(prefix: "DNP-")
This captures telemetry counters from structured logs for Phase 2 validation. Verify increment patterns (scheduled → executed → success → used).
Structured Log Output (JSON):
Log important events in structured format for automated parsing:
Success Event:
{
"event": "prefetch_success",
"id": "notif-123",
"ttl": 86400,
"fetchDurationMs": 1200,
"scheduledFor": "2025-11-15T05:53:00Z",
"timestamp": "2025-11-15T05:48:32Z"
}
Failure Event:
{
"event": "prefetch_failure",
"reason": "NETWORK",
"scheduledFor": "2025-11-15T05:53:00Z",
"timestamp": "2025-11-15T05:48:32Z"
}
Cycle Complete Summary:
{
"event": "prefetch_cycle_complete",
"id": "notif-123",
"fetched": true,
"usedCached": true,
"timestamp": "2025-11-15T05:53:00Z"
}
These structured logs can be parsed by validation scripts and analytics backends.
Success Criteria:
- For at least one full test cycle, logs and telemetry counts confirm that the sequence: scheduled → executed → success → used is coherent
- Counters increment as expected through the prefetch lifecycle
- Failure counters provide clear reason categorization
Test Campaign Sign-Off Checklist (Phase 1)
Use this checklist to verify Phase 1 prefetch testing is complete:
- Simulator: one full happy-path cycle verified (Schedule → BGTask → Fetch → Delivery)
- Device: at least one "best-case" run (plugged in, Wi-Fi, not force-quit) where prefetch runs before notification and cached content is used
- At least one failure path logged (NETWORK or PERMISSIONS) and handled gracefully
- Time & T-Lead edge-case tests run (too-small lead, timezone change) or explicitly deferred
- Telemetry counters either wired and validated, or marked P2 with log-based fallback documented
See also:
doc/directives/0003-iOS-Android-Parity-Directive.md- Main implementation directivedoc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md- Test app setup requirements
Glossary
See: doc/test-app-ios/IOS_PREFETCH_GLOSSARY.md for complete terminology definitions.
Quick Reference:
- BGTaskScheduler – iOS framework for background task scheduling (see glossary)
- T-Lead – Lead time between prefetch and notification (see glossary)
- Bucket A/B/C – Deterministic vs heuristic classification (see glossary)
- UTC – Coordinated Universal Time for timestamp storage (see glossary)
Status: 🎯 READY FOR USE
Next Steps: Use this guide when implementing and testing Phase 1+ prefetch functionality
Automated Testing Strategies
Unit Tests (Swift)
Write unit tests for plugin logic that can be tested in isolation:
Time Calculations:
- Test prefetchTime calculation:
prefetchTime = notificationTime - leadMinutes - Test edge conditions: exactly 60s lead, less than 60s lead (should cap at 60s)
- Test timezone handling: UTC storage vs local display
TTL Validation:
- Test cache validity check with simulated clock
- Test just-before-expiry vs just-after-expiry
- Test TTL expiration at notification delivery time
JSON Mapping:
- Test API response parsing with sample JSON
- Verify all fields map correctly (id, title, body, ttl, scheduled_for)
- Test error handling for malformed JSON
Permission Check Flow:
- Mock UNUserNotificationCenter returning .denied
- Verify plugin returns proper error (
notifications_denied) - Verify BGTask is not scheduled when permission denied
BGTask Scheduling Logic:
- Abstract scheduling method for dependency injection
- Inject fake BGTaskScheduler to verify correct identifier and earliestBeginDate
- Test one active task rule (cancel existing before scheduling new)
Integration Tests
Xcode UI Tests:
- Launch test app, tap "Schedule Notification"
- Background app (simulate Home button)
- Use debug simulation command to trigger BGTask
- Bring app to foreground and verify UI/logs indicate success
Log Sequence Validation:
- Run app in simulator via CLI
- Use simctl or LLDB to trigger events
- Collect logs and analyze with script
- Assert all expected log lines appear in order
Mocking and Dependency Injection:
- Network calls: Use URLProtocol to intercept HTTP and return canned responses
- Notification scheduling: Provide dummy UNUserNotificationCenter
- BGTask invocation: Call handler method directly with dummy BGTask object
- Time travel: Use injectable time source (Clock.now) for TTL/multi-day tests
Coverage of BGTask Expiration
Test expiration handler invocation:
- Set up BGTask that intentionally sleeps 40+ seconds on real device
- Monitor logs for "expirationHandler invoked" message
- Validate cancellation logic works correctly
Validation and Verification Enhancements
Structured Logging and Automated Log Analysis
Distinct Log Markers:
- Continue using prefixes:
[DNP-FETCH],[DNP-SCHEDULER],[DNP-PLUGIN],[DNP-STORAGE] - Log concise summary line at end of successful cycle:
[DNP-PLUGIN] Prefetch cycle complete (id=XYZ, fetched=true, usedCached=true)
- This gives one line to grep for overall success
Log Validation Script:
Develop validate-ios-logs.sh to parse device logs and verify sequence automatically:
#!/bin/bash
# validate-ios-logs.sh - Validates prefetch log sequence
LOG_FILE="${1:-device.log}"
# Expected sequence markers
REGISTRATION="Registering BGTaskScheduler task"
SCHEDULING="BGAppRefreshTask scheduled"
HANDLER="BGTask handler invoked"
FETCH_START="Starting fetch"
FETCH_SUCCESS="Fetch success"
TASK_COMPLETE="Task completed"
NOTIFICATION="Notification delivered"
# Check sequence
if grep -q "$REGISTRATION" "$LOG_FILE" && \
grep -q "$SCHEDULING" "$LOG_FILE" && \
grep -q "$HANDLER" "$LOG_FILE" && \
grep -q "$FETCH_START" "$LOG_FILE" && \
grep -q "$FETCH_SUCCESS" "$LOG_FILE" && \
grep -q "$TASK_COMPLETE" "$LOG_FILE" && \
grep -q "$NOTIFICATION" "$LOG_FILE"; then
echo "✅ Log sequence validated: All steps present"
exit 0
else
echo "❌ Log sequence incomplete: Missing steps"
exit 1
fi
Usage:
grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-logs.sh
Enhanced Verification Signals
Telemetry Counters:
Monitor telemetry counters for prefetch operations:
dnp_prefetch_scheduled_total- Increment when BGTask successfully scheduleddnp_prefetch_executed_total- Increment when BGTask handler runsdnp_prefetch_success_total- Increment on successful fetchdnp_prefetch_failure_total{reason="NETWORK|AUTH|SYSTEM"}- Increment on failurednp_prefetch_used_for_notification_total- Increment when notification uses cached content
State Integrity Checks:
Content Hash Verification:
- Compute MD5 hash of cached content after fetch
- Log:
[DNP-FETCH] Cached content MD5: abcdef - At notification time, verify hash matches:
[DNP-FETCH] Using cached content MD5: abcdef - Mismatch indicates content changed or wasn't used properly
Schedule Hash Verification:
- Hash scheduling data (notificationTime + lead + scheduleId)
- Print hash when scheduling and when executing
- Confirm BGTask is executing for correct schedule
Persistence Verification:
- After notification fires, verify cache state (empty if cleared, present if within TTL)
- Flag any discrepancy in logs
Persistent Test Artifacts:
Test app saves summary of each test run to file:
{
"testRun": {
"date": "2025-11-15T10:00:00Z",
"device": "iPhone 15 Pro",
"iosVersion": "17.1",
"outcome": {
"prefetchScheduledAt": "2025-11-15T05:48:00Z",
"bgTaskExecutedAt": "2025-11-15T05:50:00Z",
"notificationFiredAt": "2025-11-15T05:53:00Z",
"cachedContentUsed": true,
"errors": []
},
"telemetry": {
"scheduled": 1,
"executed": 1,
"success": 1,
"used": 1
}
}
}
Access via test app UI button "Export Test Results" or UserDefaults key DNP_TestRunArtifact.
Phase 2 Forward Plan
Planned enhancements for Phase 2:
- Implement rolling window validation
- Integrate Prometheus metrics collector
- Add automated CI pipeline for simulator validation
- Verify TTL and cache invalidation logic
- Wire telemetry counters to production pipeline
- Add automated log sequence validation
- Implement persistent schedule snapshot for post-run verification
- Add in-app log viewer/export for QA use
- Test multi-day scenarios with varying TTL values
- Validate content reuse across days when TTL allows
See also: doc/directives/0003-iOS-Android-Parity-Directive.md for Phase 2 implementation details.
Changelog (high-level)
- 2025-11-15 — Initial Phase 1 version (prefetch MVP, Android parity)