docs(ios): add comprehensive testing guide and refine iOS parity directive
Add iOS prefetch testing guide with detailed procedures, log checklists, and behavior classification. Enhance iOS test app requirements with security constraints, sign-off checklists, and changelog structure. Update main directive with testing strategy and method behavior mapping. Changes: - Add IOS_PREFETCH_TESTING.md with simulator/device test plans, log diagnostics, telemetry expectations, and test run templates - Add DailyNotificationBackgroundTaskTestHarness.swift as reference implementation for BGTaskScheduler testing - Enhance IOS_TEST_APP_REQUIREMENTS.md with security/privacy constraints, review checklists, CI hints, and glossary cross-links - Update 0003-iOS-Android-Parity-Directive.md with testing strategy section, method behavior classification, and validation matrix updates All documents now include changelog stubs, cross-references, and completion criteria for Phase 1 implementation and testing.
This commit is contained in:
@@ -792,6 +792,46 @@ A "successful run" is defined as: BGTask handler invoked, content fetch complete
|
|||||||
|
|
||||||
## Validation & QA Plan
|
## Validation & QA Plan
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
|
||||||
|
**Comprehensive testing guide:** See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for detailed testing procedures, log checklists, and behavior classification.
|
||||||
|
|
||||||
|
**Key Testing Principles:**
|
||||||
|
- **Simulator:** Test logic correctness (deterministic behavior)
|
||||||
|
- **Real Device:** Test timing and reliability (heuristic behavior)
|
||||||
|
- **Log-Driven:** Use structured logging to trace full execution paths
|
||||||
|
- **Failure Modes:** Test negative paths (network failures, permission denials)
|
||||||
|
|
||||||
|
**Behavior Classification:**
|
||||||
|
- **🟢 Deterministic:** Code logic, error handling, data processing (test in simulator)
|
||||||
|
- **🟡 Partially Deterministic:** Scheduling logic works, but timing is heuristic (test flow in simulator, timing on device)
|
||||||
|
- **🔴 Heuristic:** Background execution timing, notification delivery timing (test on real device only)
|
||||||
|
|
||||||
|
### Method Behavior Classification
|
||||||
|
|
||||||
|
**🟢 Deterministic Methods (Test in Simulator):**
|
||||||
|
- `configure()` - Configuration logic
|
||||||
|
- `getLastNotification()` - Data retrieval
|
||||||
|
- `cancelAllNotifications()` - Cancellation logic
|
||||||
|
- `getNotificationStatus()` - Status calculation
|
||||||
|
- `updateSettings()` - Settings update logic
|
||||||
|
- `getBatteryStatus()` - Battery state reading
|
||||||
|
- `getPowerState()` - Power state reading
|
||||||
|
- `getRollingWindowStats()` - Statistics calculation
|
||||||
|
- `testJWTGeneration()` - JWT generation logic
|
||||||
|
- `testEndorserAPI()` - API call logic
|
||||||
|
|
||||||
|
**🟡 Partially Deterministic Methods (Test Flow in Simulator, Timing on Device):**
|
||||||
|
- `scheduleDailyNotification()` - Scheduling logic is deterministic, but iOS delivery timing is heuristic
|
||||||
|
- `maintainRollingWindow()` - Logic is deterministic, but when iOS allows execution is heuristic
|
||||||
|
|
||||||
|
**🔴 Heuristic Methods (Test on Real Device Only):**
|
||||||
|
- Background task execution timing (BGTaskScheduler) - iOS controls when tasks run
|
||||||
|
- Notification delivery timing (UNUserNotificationCenter) - iOS controls delivery timing
|
||||||
|
- Reboot recovery detection (uptime comparison) - May vary based on system state
|
||||||
|
|
||||||
|
**See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for detailed testing procedures for each behavior class.**
|
||||||
|
|
||||||
### Unit Tests
|
### Unit Tests
|
||||||
|
|
||||||
- [ ] Test all new methods match Android behavior
|
- [ ] Test all new methods match Android behavior
|
||||||
@@ -811,12 +851,13 @@ A "successful run" is defined as: BGTask handler invoked, content fetch complete
|
|||||||
|
|
||||||
### Manual Testing
|
### Manual Testing
|
||||||
|
|
||||||
- [ ] Test on iOS Simulator (primary)
|
- [ ] Test on iOS Simulator (primary) - Logic correctness
|
||||||
- [ ] Test on physical iOS devices (validation)
|
- [ ] Test on physical iOS devices (validation) - Timing and reliability
|
||||||
- [ ] Test Background App Refresh settings
|
- [ ] Test Background App Refresh settings
|
||||||
- [ ] Test notification permissions
|
- [ ] Test notification permissions
|
||||||
- [ ] Test battery optimization scenarios
|
- [ ] Test battery optimization scenarios
|
||||||
- [ ] Test app lifecycle events
|
- [ ] Test app lifecycle events
|
||||||
|
- [ ] Test prefetch execution (see `doc/test-app-ios/IOS_PREFETCH_TESTING.md`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1049,6 +1090,7 @@ scripts/
|
|||||||
- `ios/Plugin/DailyNotificationDatabase.swift` - Database (CoreData)
|
- `ios/Plugin/DailyNotificationDatabase.swift` - Database (CoreData)
|
||||||
- `ios/Plugin/DailyNotificationRollingWindow.swift` - Rolling window
|
- `ios/Plugin/DailyNotificationRollingWindow.swift` - Rolling window
|
||||||
- `ios/Plugin/DailyNotificationTTLEnforcer.swift` - TTL enforcement
|
- `ios/Plugin/DailyNotificationTTLEnforcer.swift` - TTL enforcement
|
||||||
|
- `ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift` - BGTaskScheduler test harness (reference implementation)
|
||||||
- `ios/Plugin/DailyNotificationETagManager.swift` - ETag management
|
- `ios/Plugin/DailyNotificationETagManager.swift` - ETag management
|
||||||
- `ios/Plugin/DailyNotificationBackgroundTaskManager.swift` - Background tasks
|
- `ios/Plugin/DailyNotificationBackgroundTaskManager.swift` - Background tasks
|
||||||
|
|
||||||
@@ -1061,6 +1103,7 @@ scripts/
|
|||||||
- `doc/implementation-roadmap.md` - Implementation roadmap
|
- `doc/implementation-roadmap.md` - Implementation roadmap
|
||||||
- `doc/INTEGRATION_CHECKLIST.md` - Integration checklist
|
- `doc/INTEGRATION_CHECKLIST.md` - Integration checklist
|
||||||
- `doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md` - iOS test app detailed requirements (must be created in Phase 1)
|
- `doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md` - iOS test app detailed requirements (must be created in Phase 1)
|
||||||
|
- `doc/test-app-ios/IOS_PREFETCH_TESTING.md` - Comprehensive iOS prefetch testing guide (testing procedures, log checklists, behavior classification)
|
||||||
- `README.md` - Project documentation
|
- `README.md` - Project documentation
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1447,6 +1490,7 @@ scripts/
|
|||||||
|---------|-----------------|----------------|-------------|-------|
|
|---------|-----------------|----------------|-------------|-------|
|
||||||
| Daily notification | Exact ±0s | ±180s tolerance¹ | Test case: verify **observed** delivery within 3 min window | 1 |
|
| Daily notification | Exact ±0s | ±180s tolerance¹ | Test case: verify **observed** delivery within 3 min window | 1 |
|
||||||
| Daily prefetch | Worker at exact time | BGTask within 15 min window | Logs + UI: check fetch timestamp | 1 |
|
| Daily prefetch | Worker at exact time | BGTask within 15 min window | Logs + UI: check fetch timestamp | 1 |
|
||||||
|
| Prefetch execution | Deterministic logic | Heuristic timing (device-only) | Simulator: logic correctness; Device: timing verification (see `IOS_PREFETCH_TESTING.md`) | 1 |
|
||||||
| Rolling window | 2 days ahead | iOS 64 limit enforced | Stats call: verify pending count ≤ 64 | 2 |
|
| Rolling window | 2 days ahead | iOS 64 limit enforced | Stats call: verify pending count ≤ 64 | 2 |
|
||||||
| TTL enforcement | Discard stale at fire | Same: discard stale at fire | Deliver-time check: verify TTL validation | 2 |
|
| TTL enforcement | Discard stale at fire | Same: discard stale at fire | Deliver-time check: verify TTL validation | 2 |
|
||||||
| Reboot recovery | BOOT_COMPLETED | Uptime comparison | App launch: verify reschedule after reboot | 2 |
|
| Reboot recovery | BOOT_COMPLETED | Uptime comparison | App launch: verify reschedule after reboot | 2 |
|
||||||
@@ -1513,12 +1557,20 @@ scripts/
|
|||||||
- Verify task registered in AppDelegate before app finishes launching
|
- Verify task registered in AppDelegate before app finishes launching
|
||||||
- **Simulator-only debugging trick:** Use LLDB command to manually trigger: `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"]`
|
- **Simulator-only debugging trick:** Use LLDB command to manually trigger: `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"]`
|
||||||
- Note: This is for simulator testing only, not available in production
|
- Note: This is for simulator testing only, not available in production
|
||||||
|
- **Testing Guide:** See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for comprehensive testing procedures
|
||||||
|
|
||||||
2. **Notifications Not Delivering:**
|
2. **Notifications Not Delivering:**
|
||||||
- Check notification permissions: `UNUserNotificationCenter.current().getNotificationSettings()`
|
- Check notification permissions: `UNUserNotificationCenter.current().getNotificationSettings()`
|
||||||
- Verify notification scheduled: `UNUserNotificationCenter.current().getPendingNotificationRequests()`
|
- Verify notification scheduled: `UNUserNotificationCenter.current().getPendingNotificationRequests()`
|
||||||
- Check notification category registered
|
- Check notification category registered
|
||||||
|
|
||||||
|
3. **Prefetch Not Executing:**
|
||||||
|
- Verify BGTask scheduled: Check logs for `[DNP-FETCH] BGAppRefreshTask scheduled`
|
||||||
|
- Check Background App Refresh enabled in Settings
|
||||||
|
- In Simulator: Use Xcode → Debug → Simulate Background Fetch
|
||||||
|
- On Device: Check logs via Xcode Devices console
|
||||||
|
- **Testing Guide:** See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for detailed troubleshooting
|
||||||
|
|
||||||
3. **Build Failures:**
|
3. **Build Failures:**
|
||||||
- Ensure CocoaPods installed: `pod install`
|
- Ensure CocoaPods installed: `pod install`
|
||||||
- Check Capacitor plugin path in `capacitor.plugins.json`
|
- Check Capacitor plugin path in `capacitor.plugins.json`
|
||||||
|
|||||||
644
doc/test-app-ios/IOS_PREFETCH_TESTING.md
Normal file
644
doc/test-app-ios/IOS_PREFETCH_TESTING.md
Normal file
@@ -0,0 +1,644 @@
|
|||||||
|
# iOS Prefetch Testing Guide
|
||||||
|
|
||||||
|
**Purpose:** How to test background prefetch for DailyNotificationPlugin on iOS (simulator + device)
|
||||||
|
|
||||||
|
**Plugin Target:** DailyNotificationPlugin v3.x (iOS)
|
||||||
|
**Phase:** Phase 1 – Prefetch MVP
|
||||||
|
**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`.
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
1. [Goals](#goals)
|
||||||
|
2. [Assumptions](#assumptions)
|
||||||
|
3. [Simulator Test Plan (Logic-Correctness)](#simulator-test-plan)
|
||||||
|
4. [Real Device Test Plan (Timing & Reliability)](#real-device-test-plan)
|
||||||
|
5. [Log Checklist](#log-checklist)
|
||||||
|
6. [Behavior Classification](#behavior-classification)
|
||||||
|
7. [Test Harness Reference](#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
|
||||||
|
|
||||||
|
- **`notificationTime` is 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
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
1. **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
|
||||||
|
|
||||||
|
2. **Timezone Change:**
|
||||||
|
- Schedule notification
|
||||||
|
- Change device timezone
|
||||||
|
- Verify notification fires at correct UTC time
|
||||||
|
- Check logs confirm UTC-based scheduling
|
||||||
|
|
||||||
|
3. **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):
|
||||||
|
|
||||||
|
1. Build test app (per `IOS_TEST_APP_REQUIREMENTS.md`)
|
||||||
|
2. Run on iOS Simulator
|
||||||
|
3. Schedule notification 5 minutes in future → **See Log Checklist → Notification Scheduled**
|
||||||
|
4. Background app (Home button / Cmd+Shift+H)
|
||||||
|
5. Xcode → Debug → Simulate Background Fetch → **See Log Checklist → BGTask Fired**
|
||||||
|
6. Confirm logs show: Registration → Scheduling → BGTask handler → Fetch success → Task completed
|
||||||
|
7. Wait for notification time or manually trigger → **See Log Checklist → Notification Delivery**
|
||||||
|
8. 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:
|
||||||
|
- `BGTaskSchedulerPermittedIdentifiers` array 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.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
1. Clean build, run on iPhone 15 simulator (or similar)
|
||||||
|
2. Verify console logs:
|
||||||
|
- ✅ `[DNP-FETCH] Registering BGTaskScheduler task...` at startup
|
||||||
|
|
||||||
|
### 3. Schedule a Notification with Prefetch
|
||||||
|
|
||||||
|
1. Call plugin's `scheduleDailyNotification()` from JS layer
|
||||||
|
2. Check logs:
|
||||||
|
- ✅ `[DNP-FETCH] Scheduling BGAppRefreshTask...` with sensible `earliestBeginDate`
|
||||||
|
- ✅ `[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
|
||||||
|
|
||||||
|
With app running:
|
||||||
|
|
||||||
|
1. **Background the app** (Home button / Cmd+Shift+H)
|
||||||
|
2. In Xcode menu:
|
||||||
|
- **Debug → Simulate Background Fetch** or
|
||||||
|
- **Debug → Simulate Background Refresh**
|
||||||
|
|
||||||
|
**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.**
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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 deduplication
|
||||||
|
- `title` → Notification title (displayed to user)
|
||||||
|
- `body` → Notification body (displayed to user)
|
||||||
|
- `ttl` → Cache validity check at delivery time (seconds)
|
||||||
|
- `scheduled_for` → Cross-check with `notificationTime` to 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.
|
||||||
|
|
||||||
|
### 7. Negative-Path Tests
|
||||||
|
|
||||||
|
**Network failure:**
|
||||||
|
1. Turn off network in Simulator (Settings → Network)
|
||||||
|
2. Trigger background fetch
|
||||||
|
3. 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:**
|
||||||
|
1. Deny notification permissions
|
||||||
|
2. Attempt to schedule
|
||||||
|
3. Expect:
|
||||||
|
- `[DNP-PLUGIN] notifications_denied for scheduleDailyNotification`
|
||||||
|
- Error returned: `{ error: "notifications_denied", ... }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### 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_log` aggregator / remote logging
|
||||||
|
|
||||||
|
### 2. Baseline Run
|
||||||
|
|
||||||
|
1. Open app
|
||||||
|
2. Schedule a notification for **30–60 minutes in the future** with prefetch
|
||||||
|
3. 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. Variations
|
||||||
|
|
||||||
|
Test different conditions:
|
||||||
|
|
||||||
|
- **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):
|
||||||
|
|
||||||
|
```text
|
||||||
|
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
|
||||||
|
Failures observed (if any) and key log lines:
|
||||||
|
Notes / follow-ups:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 `earliestBeginDate` is at least 1 minute in future
|
||||||
|
- Check for BGTaskScheduler errors in logs
|
||||||
|
|
||||||
|
**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 `earliestBeginDate` in 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 | `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 scheduled
|
||||||
|
- `dnp_prefetch_executed_total` - Total prefetch tasks executed
|
||||||
|
- `dnp_prefetch_success_total` - Total successful prefetch executions
|
||||||
|
- `dnp_prefetch_failure_total{reason="NETWORK|AUTH|SYSTEM"}` - Total failed prefetch executions by reason
|
||||||
|
- `dnp_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.
|
||||||
|
|
||||||
|
**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 directive
|
||||||
|
- `doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md` - Test app setup requirements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
**BGTaskScheduler** – iOS framework for scheduling background tasks (BGAppRefreshTask / BGProcessingTask). Provides heuristic-based background execution, not exact timing guarantees.
|
||||||
|
|
||||||
|
**UNUserNotificationCenter** – iOS notification framework for scheduling and delivering user notifications. Handles permission requests and notification delivery.
|
||||||
|
|
||||||
|
**T-Lead** – The lead time between prefetch and notification fire, e.g., 5 minutes. Prefetch is scheduled at `notificationTime - T-Lead`.
|
||||||
|
|
||||||
|
**Bucket A/B/C** – Deterministic vs heuristic classification used in Behavior Classification:
|
||||||
|
- **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
|
||||||
|
|
||||||
|
**UTC** – Coordinated Universal Time. All internal timestamps are stored in UTC to avoid DST and timezone issues.
|
||||||
|
|
||||||
|
**earliestBeginDate** – The earliest time iOS may execute a BGTask. This is a hint, not a guarantee; iOS may run the task later based on heuristics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** 🎯 **READY FOR USE**
|
||||||
|
**Next Steps:** Use this guide when implementing and testing Phase 1+ prefetch functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog (high-level)
|
||||||
|
|
||||||
|
- 2025-11-15 — Initial Phase 1 version (prefetch MVP, Android parity)
|
||||||
|
|
||||||
@@ -1,16 +1,58 @@
|
|||||||
# iOS Test App Requirements
|
# iOS Test App Requirements
|
||||||
|
|
||||||
|
**Purpose:** What the iOS test app must provide so that the testing guide can be executed with parity vs Android
|
||||||
|
|
||||||
|
**Plugin Target:** DailyNotificationPlugin v3.x (iOS)
|
||||||
|
**Phase:** Phase 1 – Prefetch MVP
|
||||||
**Status:** 📋 **REQUIRED FOR PHASE 1**
|
**Status:** 📋 **REQUIRED FOR PHASE 1**
|
||||||
**Date:** 2025-01-XX
|
**Date:** 2025-11-15
|
||||||
**Author:** Matthew Raymer
|
**Author:** Matthew Raymer
|
||||||
**Directive Reference:** `doc/directives/0003-iOS-Android-Parity-Directive.md`
|
**Directive Reference:** `doc/directives/0003-iOS-Android-Parity-Directive.md`
|
||||||
|
|
||||||
|
**Note:** This app exists to support the prefetch testing scenarios in `doc/test-app-ios/IOS_PREFETCH_TESTING.md`.
|
||||||
|
|
||||||
|
**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 (see glossary); no hard guarantee of 5-min prefetch
|
||||||
|
|
||||||
|
**Glossary:** See glossary in `IOS_PREFETCH_TESTING.md` for terminology.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document defines the requirements for the iOS test app (`test-apps/ios-test-app/`) that must be created as part of Phase 1 implementation. The iOS test app must provide UI parity with the Android test app (`test-apps/android-test-app/`) while respecting iOS-specific constraints and capabilities.
|
This document defines the requirements for the iOS test app (`test-apps/ios-test-app/`) that must be created as part of Phase 1 implementation. The iOS test app must provide UI parity with the Android test app (`test-apps/android-test-app/`) while respecting iOS-specific constraints and capabilities.
|
||||||
|
|
||||||
|
## Non-Goals (Phase 1)
|
||||||
|
|
||||||
|
**Out of scope for Phase 1:**
|
||||||
|
|
||||||
|
- ❌ No full UX polish (color, branding)
|
||||||
|
- ❌ No localization / accessibility guarantees (text only for internal QA)
|
||||||
|
- ❌ No production signing / App Store deployment
|
||||||
|
- ❌ No advanced UI features beyond basic functionality testing
|
||||||
|
|
||||||
|
## Security & Privacy Constraints
|
||||||
|
|
||||||
|
**Critical requirements for test app implementation:**
|
||||||
|
|
||||||
|
- Test app MUST use **non-production** endpoints and credentials
|
||||||
|
- JWT / API keys used here are **test-only** and may be rotated or revoked at any time
|
||||||
|
- Logs MUST NOT include real user PII (names, emails, phone numbers)
|
||||||
|
- Any screenshots or shared logs should be scrubbed of secrets before external sharing
|
||||||
|
- Test app should clearly indicate it's a development/testing build (not production)
|
||||||
|
|
||||||
|
## If You Only Have 30 Minutes
|
||||||
|
|
||||||
|
Quick setup checklist:
|
||||||
|
|
||||||
|
1. Copy HTML/JS from Android test app (`test-apps/android-test-app/app/src/main/assets/public/index.html`)
|
||||||
|
2. Wire plugin into Capacitor (`capacitor.config.json`)
|
||||||
|
3. Add Info.plist keys (BGTask identifiers, background modes, notification permissions)
|
||||||
|
4. Build/run (`./scripts/build-ios-test-app.sh --simulator` or Xcode)
|
||||||
|
5. Press buttons: Check Plugin Status → Request Permissions → Schedule Test Notification
|
||||||
|
6. See logs with prefixes `DNP-PLUGIN`, `DNP-FETCH`, `DNP-SCHEDULER`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## UI Parity Requirements
|
## UI Parity Requirements
|
||||||
@@ -54,6 +96,20 @@ The test app UI must support:
|
|||||||
- Show pending notification count
|
- Show pending notification count
|
||||||
- Display error messages if any
|
- Display error messages if any
|
||||||
|
|
||||||
|
### UI Elements to Plugin Methods Mapping
|
||||||
|
|
||||||
|
| UI Element / Button | Plugin Method / API Call | Notes |
|
||||||
|
|---------------------|-------------------------|-------|
|
||||||
|
| "Check Plugin Status" | `DailyNotification.configure()` or status call | Verify plugin load & config |
|
||||||
|
| "Check Permissions" | `getNotificationStatus()` | Maps to permission state |
|
||||||
|
| "Request Permissions" | `requestPermissions()` | Drives iOS UNUserNotificationCenter |
|
||||||
|
| "Schedule Test Notification" | `scheduleDailyNotification()` | Should schedule prefetch + notify |
|
||||||
|
| "Show Last Notification" | `getLastNotification()` | Uses deterministic path (Bucket A) |
|
||||||
|
| "Cancel All Notifications" | `cancelAllNotifications()` | Uses deterministic path (Bucket A) |
|
||||||
|
| "Get Notification Status" | `getNotificationStatus()` | Uses deterministic path (Bucket A) |
|
||||||
|
|
||||||
|
**See `IOS_PREFETCH_TESTING.md` Behavior Classification for deterministic vs heuristic methods.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## iOS Permissions Configuration
|
## iOS Permissions Configuration
|
||||||
@@ -64,10 +120,11 @@ The test app's `Info.plist` **MUST** include:
|
|||||||
|
|
||||||
```xml
|
```xml
|
||||||
<!-- Background Task Identifiers -->
|
<!-- Background Task Identifiers -->
|
||||||
|
<!-- These identifiers MUST match exactly the values used in IOS_PREFETCH_TESTING.md and the plugin Swift code, otherwise BGTaskScheduler (see glossary) will silently fail -->
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>com.timesafari.dailynotification.fetch</string>
|
<string>com.timesafari.dailynotification.fetch</string> <!-- Prefetch task (BGAppRefreshTask - see glossary) -->
|
||||||
<string>com.timesafari.dailynotification.notify</string>
|
<string>com.timesafari.dailynotification.notify</string> <!-- Notification maintenance task (if applicable) -->
|
||||||
</array>
|
</array>
|
||||||
|
|
||||||
<!-- Background Modes -->
|
<!-- Background Modes -->
|
||||||
@@ -121,12 +178,29 @@ Use the build script:
|
|||||||
./scripts/build-ios-test-app.sh --device
|
./scripts/build-ios-test-app.sh --device
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Future CI Integration (Optional)
|
||||||
|
|
||||||
|
**Note for Phase 2+:** Consider adding `xcodebuild`-based CI job that:
|
||||||
|
- Builds `test-apps/ios-test-app` for simulator
|
||||||
|
- Runs a minimal UI test that:
|
||||||
|
- Launches app
|
||||||
|
- Calls `configure()` and `getNotificationStatus()`
|
||||||
|
- This ensures test app remains buildable as plugin evolves
|
||||||
|
|
||||||
|
**Phase 1:** Manual testing only; CI integration is out of scope.
|
||||||
|
|
||||||
### Build Requirements
|
### Build Requirements
|
||||||
|
|
||||||
|
**Required Tools:**
|
||||||
- **Xcode:** 15.0 or later
|
- **Xcode:** 15.0 or later
|
||||||
- **macOS:** 13.0 (Ventura) or later
|
- **macOS:** 13.0 (Ventura) or later
|
||||||
- **iOS Deployment Target:** iOS 15.0 or later
|
- **iOS Deployment Target:** iOS 15.0 or later
|
||||||
- **CocoaPods:** Must run `pod install` before first build
|
- **CocoaPods:** >= 1.13 (must run `pod install` before first build)
|
||||||
|
- **Node.js:** 20.x (recommended)
|
||||||
|
- **npm:** Latest stable (comes with Node.js)
|
||||||
|
- **Xcode Command Line Tools:** Must run `xcode-select --install` if not already installed
|
||||||
|
|
||||||
|
**Note:** Mismatched versions are **out of scope** for Phase 1 support. Use recommended versions to avoid compatibility issues.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -167,6 +241,8 @@ npx cap sync ios
|
|||||||
|
|
||||||
## Debugging Strategy
|
## Debugging Strategy
|
||||||
|
|
||||||
|
**When executing the scenarios in `IOS_PREFETCH_TESTING.md`, use the following commands while looking for logs with prefixes `DNP-PLUGIN`, `DNP-FETCH`, `DNP-SCHEDULER`.**
|
||||||
|
|
||||||
### Xcode Debugger
|
### Xcode Debugger
|
||||||
|
|
||||||
**Check Pending Notifications:**
|
**Check Pending Notifications:**
|
||||||
@@ -193,21 +269,27 @@ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWith
|
|||||||
**Key Log Prefixes:**
|
**Key Log Prefixes:**
|
||||||
- `DNP-PLUGIN:` - Main plugin operations
|
- `DNP-PLUGIN:` - Main plugin operations
|
||||||
- `DNP-FETCH:` - Background fetch operations
|
- `DNP-FETCH:` - Background fetch operations
|
||||||
- `DNP-FETCH-SCHEDULE:` - BGTask scheduling
|
- `DNP-SCHEDULER:` - Scheduling operations
|
||||||
- `DailyNotificationStorage:` - Storage operations
|
- `DNP-STORAGE:` - Storage operations
|
||||||
- `DailyNotificationScheduler:` - Scheduling operations
|
|
||||||
|
|
||||||
### Common Debugging Scenarios
|
### Common Debugging Scenarios
|
||||||
|
|
||||||
|
**Scenario: BGTask not running when expected → follow this checklist:**
|
||||||
|
|
||||||
1. **BGTask Not Running:**
|
1. **BGTask Not Running:**
|
||||||
- Check Info.plist has `BGTaskSchedulerPermittedIdentifiers`
|
- Check Info.plist has `BGTaskSchedulerPermittedIdentifiers`
|
||||||
- Verify task registered in AppDelegate
|
- Verify identifiers match code exactly (case-sensitive)
|
||||||
|
- Verify task registered in AppDelegate before app finishes launching
|
||||||
|
- Check Background App Refresh enabled in Settings → [Your App]
|
||||||
|
- Verify app not force-quit (iOS won't run BGTask for force-quit apps)
|
||||||
- Use simulator-only LLDB command to manually trigger
|
- Use simulator-only LLDB command to manually trigger
|
||||||
|
- **See also:** `IOS_PREFETCH_TESTING.md – Log Checklist: Section 3`
|
||||||
|
|
||||||
2. **Notifications Not Delivering:**
|
2. **Notifications Not Delivering:**
|
||||||
- Check notification permissions
|
- Check notification permissions: `UNUserNotificationCenter.current().getNotificationSettings()`
|
||||||
- Verify notification scheduled
|
- Verify notification scheduled: `UNUserNotificationCenter.current().getPendingNotificationRequests()`
|
||||||
- Check notification category registered
|
- Check notification category registered
|
||||||
|
- **See also:** `IOS_PREFETCH_TESTING.md – Log Checklist: Section 4`
|
||||||
|
|
||||||
3. **Build Failures:**
|
3. **Build Failures:**
|
||||||
- Run `pod install`
|
- Run `pod install`
|
||||||
@@ -278,56 +360,172 @@ test-apps/ios-test-app/
|
|||||||
|
|
||||||
### Basic Functionality
|
### Basic Functionality
|
||||||
|
|
||||||
1. **Plugin Registration**
|
**Happy Path Example:**
|
||||||
|
|
||||||
|
When everything is working correctly, your first run should produce logs like this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[DNP-PLUGIN] configure() called
|
||||||
|
[DNP-PLUGIN] status = ready
|
||||||
|
[DNP-PLUGIN] requestPermissions() → granted
|
||||||
|
[DNP-SCHEDULER] scheduleDailyNotification() → notificationTime=2025-11-15T05:53:00Z
|
||||||
|
[DNP-FETCH] Scheduling prefetch for notification at 2025-11-15T05:53:00Z
|
||||||
|
[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=2025-11-15T05:48:00Z)
|
||||||
|
[DNP-SCHEDULER] Scheduling notification for 2025-11-15T05:53:00Z
|
||||||
|
[DNP-STORAGE] Persisted schedule to DB (id=..., type=DAILY, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
**If your first run doesn't look roughly like this, fix that before proceeding to BGTask tests.**
|
||||||
|
|
||||||
|
1. **Plugin Registration** `[P1-Core][SIM+DEV]`
|
||||||
- Launch app
|
- Launch app
|
||||||
- Verify plugin status shows "Plugin is loaded and ready!"
|
- Verify plugin status shows "Plugin is loaded and ready!"
|
||||||
|
- **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Step 2`
|
||||||
|
|
||||||
2. **Permission Management**
|
2. **Permission Management** `[P1-Core][SIM+DEV]`
|
||||||
- Check permissions
|
- Check permissions
|
||||||
- Request permissions
|
- Request permissions
|
||||||
- Verify permissions granted
|
- Verify permissions granted
|
||||||
|
- **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Step 6 (Negative-Path Tests)`
|
||||||
|
|
||||||
3. **Notification Scheduling**
|
3. **Notification Scheduling** `[P1-Prefetch][SIM+DEV]`
|
||||||
- Schedule test notification
|
- Schedule test notification
|
||||||
- Verify notification scheduled
|
- Verify notification scheduled
|
||||||
- Wait for notification to appear
|
- Wait for notification to appear
|
||||||
|
- **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Steps 3-5`
|
||||||
|
|
||||||
### Background Tasks
|
### Background Tasks
|
||||||
|
|
||||||
1. **BGTask Scheduling**
|
**Sim vs Device Behavior:**
|
||||||
|
|
||||||
|
- `[SIM+DEV]` – can be fully tested on simulator and device (logic correctness)
|
||||||
|
- `[DEV-ONLY]` – timing / heuristic behavior, must be verified on real hardware
|
||||||
|
|
||||||
|
1. **BGTask Scheduling** `[P1-Prefetch][SIM+DEV]`
|
||||||
- Schedule notification with prefetch
|
- Schedule notification with prefetch
|
||||||
- Verify BGTask scheduled 5 minutes before notification
|
- Verify BGTask scheduled 5 minutes before notification
|
||||||
- Manually trigger BGTask (simulator only)
|
- Manually trigger BGTask (simulator only)
|
||||||
- Verify content fetched
|
- Verify content fetched
|
||||||
|
- **Note:** Logic and logs on sim; real timing on device
|
||||||
|
- **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Steps 3-4`
|
||||||
|
|
||||||
2. **BGTask Miss Detection**
|
2. **BGTask Miss Detection** `[P1-Prefetch][DEV-ONLY]`
|
||||||
- Schedule notification
|
- Schedule notification
|
||||||
- Wait 15+ minutes
|
- Wait 15+ minutes
|
||||||
- Launch app
|
- Launch app
|
||||||
- Verify BGTask rescheduled
|
- Verify BGTask rescheduled
|
||||||
|
- **Note:** Only meaningful on device (heuristic timing)
|
||||||
|
- **See also:** `IOS_PREFETCH_TESTING.md – Real Device Test Plan: Step 4`
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
1. **Permission Denied**
|
1. **Permission Denied** `[P1-Core]`
|
||||||
- Deny notification permissions
|
- Deny notification permissions
|
||||||
- Try to schedule notification
|
- Try to schedule notification
|
||||||
- Verify error returned
|
- Verify error returned
|
||||||
|
- **See also:** `IOS_PREFETCH_TESTING.md – Simulator Test Plan: Step 6 (Negative-Path Tests)`
|
||||||
|
|
||||||
2. **Invalid Parameters**
|
2. **Invalid Parameters** `[P1-Core]`
|
||||||
- Try to schedule with invalid time format
|
- Try to schedule with invalid time format
|
||||||
- Verify error returned
|
- Verify error returned
|
||||||
|
|
||||||
|
### Advanced Features `[P2-Advanced]`
|
||||||
|
|
||||||
|
1. **Rolling Window** `[P2-Advanced]`
|
||||||
|
- Schedule multiple notifications
|
||||||
|
- Verify rolling window maintenance
|
||||||
|
- Check notification limits (64 max on iOS)
|
||||||
|
|
||||||
|
2. **TTL Enforcement** `[P2-Advanced]`
|
||||||
|
- Schedule notification with prefetch
|
||||||
|
- Wait for TTL to expire
|
||||||
|
- Verify stale content discarded at delivery
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Developer/Test Harness Features (Optional but Recommended)
|
||||||
|
|
||||||
|
Since this test app is for internal testing, it can expose more tools:
|
||||||
|
|
||||||
|
### Dev-Only Toggles
|
||||||
|
|
||||||
|
1. **Force Schedule Notification N Minutes from Now**
|
||||||
|
- Bypass normal scheduling UI
|
||||||
|
- Directly call `scheduleDailyNotification()` with calculated time
|
||||||
|
- Useful for quick testing scenarios
|
||||||
|
|
||||||
|
2. **Force "Prefetch-Only" Task**
|
||||||
|
- Trigger BGTask without scheduling notification
|
||||||
|
- Useful for testing prefetch logic in isolation
|
||||||
|
- Display raw JSON returned from API (last fetched payload)
|
||||||
|
|
||||||
|
3. **Display Raw API Response**
|
||||||
|
- Show last fetched payload as JSON
|
||||||
|
- Useful for debugging API responses
|
||||||
|
- Can be referenced from `IOS_PREFETCH_TESTING.md` as shortcuts for specific scenarios
|
||||||
|
|
||||||
|
4. **Manual BGTask Trigger (Dev Build Only)**
|
||||||
|
- Button to manually trigger BGTask (simulator only)
|
||||||
|
- Wraps the LLDB command in UI for convenience
|
||||||
|
|
||||||
|
These features can then be referenced from `IOS_PREFETCH_TESTING.md` as shortcuts for specific test scenarios.
|
||||||
|
|
||||||
|
## Risks & Gotchas
|
||||||
|
|
||||||
|
**Common pitfalls when working with the test app:**
|
||||||
|
|
||||||
|
1. **Accidentally editing shared HTML/JS in iOS test app instead of Android canonical source**
|
||||||
|
- Always edit Android test app HTML/JS first, then copy to iOS
|
||||||
|
- Keep iOS test app HTML/JS as a copy, not the source of truth
|
||||||
|
|
||||||
|
2. **Forgetting `npx cap sync ios` after plugin or asset changes**
|
||||||
|
- Run `npx cap sync ios` after any plugin code changes
|
||||||
|
- Run `npx cap sync ios` after any web asset changes
|
||||||
|
- Check `capacitor.config.json` is up to date
|
||||||
|
|
||||||
|
3. **Running with stale Pods**
|
||||||
|
- Run `pod repo update` periodically
|
||||||
|
- Run `pod install` after dependency changes
|
||||||
|
- Clean build folder (Cmd+Shift+K) if Pods seem stale
|
||||||
|
|
||||||
|
4. **Changing BGTask identifiers in one place but not the other**
|
||||||
|
- Info.plist `BGTaskSchedulerPermittedIdentifiers` must match Swift code exactly (case-sensitive)
|
||||||
|
- Check both places when updating identifiers
|
||||||
|
- See `IOS_PREFETCH_TESTING.md` for identifier requirements
|
||||||
|
|
||||||
|
5. **Mismatched tooling versions**
|
||||||
|
- Use recommended versions (Node 20.x, CocoaPods >= 1.13, Xcode 15.0+)
|
||||||
|
- Mismatched versions are out of scope for Phase 1 support
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- **Directive:** `doc/directives/0003-iOS-Android-Parity-Directive.md`
|
- **Directive:** `doc/directives/0003-iOS-Android-Parity-Directive.md`
|
||||||
|
- **Testing Guide:** `doc/test-app-ios/IOS_PREFETCH_TESTING.md` - Comprehensive prefetch testing procedures
|
||||||
- **Android Test App:** `test-apps/android-test-app/`
|
- **Android Test App:** `test-apps/android-test-app/`
|
||||||
- **Build Script:** `scripts/build-ios-test-app.sh`
|
- **Build Script:** `scripts/build-ios-test-app.sh`
|
||||||
- **Testing Guide:** `doc/IOS_PHASE1_TESTING_GUIDE.md`
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Review & Sign-Off Checklist (Phase 1)
|
||||||
|
|
||||||
|
**Use this checklist to verify Phase 1 iOS test app is complete:**
|
||||||
|
|
||||||
|
- [ ] Test app builds on simulator with recommended toolchain (Node 20.x, CocoaPods >= 1.13, Xcode 15.0+)
|
||||||
|
- [ ] Test app builds on at least one real device
|
||||||
|
- [ ] All UI buttons map to plugin methods as per the **UI Elements to Plugin Methods Mapping** table
|
||||||
|
- [ ] Happy-path log sequence matches the example in **Testing Scenarios → Basic Functionality**
|
||||||
|
- [ ] BGTask identifiers are consistent (Info.plist ↔ Swift ↔ docs)
|
||||||
|
- [ ] Risks & Gotchas section has been read and acknowledged by the implementer
|
||||||
|
- [ ] Security & Privacy Constraints have been followed (non-production endpoints, no PII in logs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog (high-level)
|
||||||
|
|
||||||
|
- 2025-11-15 — Initial Phase 1 version (prefetch MVP, Android parity)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Status:** 📋 **REQUIRED FOR PHASE 1**
|
**Status:** 📋 **REQUIRED FOR PHASE 1**
|
||||||
**Last Updated:** 2025-01-XX
|
**Last Updated:** 2025-11-15
|
||||||
|
|
||||||
|
|||||||
218
ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift
Normal file
218
ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
//
|
||||||
|
// DailyNotificationBackgroundTaskTestHarness.swift
|
||||||
|
// DailyNotificationPlugin
|
||||||
|
//
|
||||||
|
// Test harness for BGTaskScheduler prefetch functionality
|
||||||
|
// Reference implementation demonstrating task registration, scheduling, and handling
|
||||||
|
//
|
||||||
|
// See: doc/test-app-ios/IOS_PREFETCH_TESTING.md for testing procedures
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import BackgroundTasks
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Minimal BGTaskScheduler test harness for DailyNotificationPlugin prefetch testing
|
||||||
|
///
|
||||||
|
/// This is a reference implementation demonstrating:
|
||||||
|
/// - Task registration
|
||||||
|
/// - Task scheduling
|
||||||
|
/// - Task handler implementation
|
||||||
|
/// - Expiration handling
|
||||||
|
/// - Completion reporting
|
||||||
|
///
|
||||||
|
/// **Usage:**
|
||||||
|
/// - Reference this when implementing actual prefetch logic in `DailyNotificationBackgroundTaskManager.swift`
|
||||||
|
/// - Use in test app for debugging BGTaskScheduler behavior
|
||||||
|
/// - See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for comprehensive testing guide
|
||||||
|
///
|
||||||
|
/// **Info.plist Requirements:**
|
||||||
|
/// ```xml
|
||||||
|
/// <key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
/// <array>
|
||||||
|
/// <string>com.timesafari.dailynotification.fetch</string>
|
||||||
|
/// </array>
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// **Background Modes (Xcode Capabilities):**
|
||||||
|
/// - Background fetch
|
||||||
|
/// - Background processing (if using BGProcessingTask)
|
||||||
|
class DailyNotificationBackgroundTaskTestHarness {
|
||||||
|
|
||||||
|
// MARK: - Constants
|
||||||
|
|
||||||
|
static let prefetchTaskIdentifier = "com.timesafari.dailynotification.fetch"
|
||||||
|
|
||||||
|
// MARK: - Registration
|
||||||
|
|
||||||
|
/// Register BGTaskScheduler task handler
|
||||||
|
///
|
||||||
|
/// Call this in AppDelegate.application(_:didFinishLaunchingWithOptions:)
|
||||||
|
/// before app finishes launching.
|
||||||
|
static func registerBackgroundTasks() {
|
||||||
|
BGTaskScheduler.shared.register(
|
||||||
|
forTaskWithIdentifier: prefetchTaskIdentifier,
|
||||||
|
using: nil
|
||||||
|
) { task in
|
||||||
|
// This closure is called when the task is launched by the system
|
||||||
|
handlePrefetchTask(task: task as! BGAppRefreshTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[DNP-FETCH] Registered BGTaskScheduler with id=\(prefetchTaskIdentifier)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Scheduling
|
||||||
|
|
||||||
|
/// Schedule a BGAppRefreshTask for prefetch
|
||||||
|
///
|
||||||
|
/// - Parameter earliestOffsetSeconds: Seconds from now when task can begin
|
||||||
|
/// - Returns: true if scheduling succeeded, false otherwise
|
||||||
|
@discardableResult
|
||||||
|
static func schedulePrefetchTask(earliestOffsetSeconds: TimeInterval) -> Bool {
|
||||||
|
let request = BGAppRefreshTaskRequest(identifier: prefetchTaskIdentifier)
|
||||||
|
request.earliestBeginDate = Date(timeIntervalSinceNow: earliestOffsetSeconds)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try BGTaskScheduler.shared.submit(request)
|
||||||
|
print("[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=\(String(describing: request.earliestBeginDate)))")
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("[DNP-FETCH] Failed to schedule BGAppRefreshTask: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Handler
|
||||||
|
|
||||||
|
/// Handle BGAppRefreshTask execution
|
||||||
|
///
|
||||||
|
/// This is called by the system when the background task is launched.
|
||||||
|
/// Replace PrefetchOperation with your actual prefetch logic.
|
||||||
|
private static func handlePrefetchTask(task: BGAppRefreshTask) {
|
||||||
|
print("[DNP-FETCH] BGTask handler invoked (task.identifier=\(task.identifier))")
|
||||||
|
|
||||||
|
// Schedule the next one early, so that there's always a pending task
|
||||||
|
// In real implementation, calculate next schedule based on notification time
|
||||||
|
schedulePrefetchTask(earliestOffsetSeconds: 60 * 30) // 30 minutes later, for example
|
||||||
|
|
||||||
|
// Define the work
|
||||||
|
let queue = OperationQueue()
|
||||||
|
queue.maxConcurrentOperationCount = 1
|
||||||
|
|
||||||
|
let operation = PrefetchOperation()
|
||||||
|
|
||||||
|
// Set expiration handler
|
||||||
|
// Called if iOS decides to end the task early (typically ~30 seconds)
|
||||||
|
task.expirationHandler = {
|
||||||
|
print("[DNP-FETCH] Task expired")
|
||||||
|
operation.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set completion handler
|
||||||
|
operation.completionBlock = {
|
||||||
|
let success = !operation.isCancelled
|
||||||
|
print("[DNP-FETCH] Task completionBlock (success=\(success))")
|
||||||
|
task.setTaskCompleted(success: success)
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.addOperation(operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Prefetch Operation
|
||||||
|
|
||||||
|
/// Simple Operation example for testing
|
||||||
|
///
|
||||||
|
/// Replace this with your actual prefetch logic:
|
||||||
|
/// - HTTP fetch from TimeSafari API
|
||||||
|
/// - JWT signing
|
||||||
|
/// - ETag validation
|
||||||
|
/// - Content caching
|
||||||
|
/// - Error handling
|
||||||
|
class PrefetchOperation: Operation {
|
||||||
|
|
||||||
|
override func main() {
|
||||||
|
if isCancelled { return }
|
||||||
|
|
||||||
|
print("[DNP-FETCH] PrefetchOperation: starting fake fetch...")
|
||||||
|
|
||||||
|
// Simulate some work
|
||||||
|
// In real implementation, this would be:
|
||||||
|
// - Make HTTP request
|
||||||
|
// - Parse response
|
||||||
|
// - Cache content
|
||||||
|
// - Update database
|
||||||
|
Thread.sleep(forTimeInterval: 2)
|
||||||
|
|
||||||
|
if isCancelled { return }
|
||||||
|
|
||||||
|
print("[DNP-FETCH] PrefetchOperation: finished fake fetch.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AppDelegate Integration Example
|
||||||
|
|
||||||
|
/*
|
||||||
|
Example integration in AppDelegate.swift:
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import BackgroundTasks
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
|
||||||
|
// Register background tasks BEFORE app finishes launching
|
||||||
|
DailyNotificationBackgroundTaskTestHarness.registerBackgroundTasks()
|
||||||
|
|
||||||
|
// Schedule initial task (for testing)
|
||||||
|
DailyNotificationBackgroundTaskTestHarness.schedulePrefetchTask(earliestOffsetSeconds: 5 * 60) // 5 minutes
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// MARK: - Testing in Simulator
|
||||||
|
|
||||||
|
/*
|
||||||
|
To test in simulator:
|
||||||
|
|
||||||
|
1. Run app in Xcode
|
||||||
|
2. Background the app (Home button / Cmd+Shift+H)
|
||||||
|
3. In Xcode menu:
|
||||||
|
- Debug → Simulate Background Fetch, or
|
||||||
|
- Debug → Simulate Background Refresh
|
||||||
|
4. Check console logs for [DNP-FETCH] messages
|
||||||
|
|
||||||
|
Expected logs:
|
||||||
|
- [DNP-FETCH] Registered BGTaskScheduler with id=...
|
||||||
|
- [DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=...)
|
||||||
|
- [DNP-FETCH] BGTask handler invoked (task.identifier=...)
|
||||||
|
- [DNP-FETCH] PrefetchOperation: starting fake fetch...
|
||||||
|
- [DNP-FETCH] PrefetchOperation: finished fake fetch.
|
||||||
|
- [DNP-FETCH] Task completionBlock (success=true)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// MARK: - Testing on Real Device
|
||||||
|
|
||||||
|
/*
|
||||||
|
To test on real device:
|
||||||
|
|
||||||
|
1. Install app on iPhone
|
||||||
|
2. Enable Background App Refresh in Settings → [Your App]
|
||||||
|
3. Schedule a notification with prefetch
|
||||||
|
4. Lock device and leave idle (plugged in for best results)
|
||||||
|
5. Monitor logs via:
|
||||||
|
- Xcode → Devices & Simulators → device → open console
|
||||||
|
- Or os_log aggregator / remote logging
|
||||||
|
|
||||||
|
Note: Real device timing is heuristic, not deterministic.
|
||||||
|
iOS will run the task when it determines it's appropriate,
|
||||||
|
not necessarily at the exact earliestBeginDate.
|
||||||
|
*/
|
||||||
|
|
||||||
Reference in New Issue
Block a user