fix(ios): iOS 13.0 compatibility and test app UI unification

Fixed iOS 13.0 compatibility issue in test harness by replacing Logger
(iOS 14+) with os_log (iOS 13+). Fixed build script to correctly detect
and sync Capacitor config from App subdirectory. Unified both Android
and iOS test app UIs to use www/index.html as the canonical source.

Changes:
- DailyNotificationBackgroundTaskTestHarness: Replace Logger with os_log
  for iOS 13.0 deployment target compatibility
- build-ios-test-app.sh: Fix Capacitor sync path detection to check
  both current directory and App/ subdirectory for config files
- test-apps: Update both Android and iOS test apps to use www/index.html
  as the canonical UI source for consistency

This ensures the plugin builds on iOS 13.0+ and both test apps provide
the same testing experience across platforms.
This commit is contained in:
Matthew
2025-11-18 21:25:14 -08:00
parent b74d38056f
commit 9f26588331
4 changed files with 1152 additions and 901 deletions

View File

@@ -46,16 +46,16 @@ class DailyNotificationBackgroundTaskTestHarness {
// MARK: - Structured Logging
/// Swift Logger categories for structured logging
static let pluginLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "plugin")
static let fetchLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "fetch")
static let schedulerLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "scheduler")
static let storageLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "storage")
/// OSLog categories for structured logging (iOS 13.0+ compatible)
private static let pluginLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "plugin")
private static let fetchLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "fetch")
private static let schedulerLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "scheduler")
private static let storageLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "storage")
/// Log telemetry snapshot for validation
static func logTelemetrySnapshot(prefix: String = "DNP-") {
// Phase 2: Capture telemetry counters from structured logs
fetchLogger.info("Telemetry snapshot: \(prefix)prefetch_scheduled_total, \(prefix)prefetch_executed_total, \(prefix)prefetch_success_total")
os_log("[DNP-FETCH] Telemetry snapshot: %{public}@prefetch_scheduled_total, %{public}@prefetch_executed_total, %{public}@prefetch_success_total", log: fetchLog, type: .info, prefix, prefix, prefix)
}
// MARK: - Registration
@@ -106,16 +106,16 @@ class DailyNotificationBackgroundTaskTestHarness {
do {
try BGTaskScheduler.shared.submit(request)
fetchLogger.info("[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=\(String(describing: request.earliestBeginDate)))")
os_log("[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=%{public}@)", log: fetchLog, type: .info, String(describing: request.earliestBeginDate))
return true
} catch {
// Handle simulator limitation (Code=1 is expected on simulator)
if let nsError = error as NSError?, nsError.domain == "BGTaskSchedulerErrorDomain", nsError.code == 1 {
fetchLogger.warning("[DNP-FETCH] BGTask scheduling failed on simulator (expected): Code=1 notPermitted")
os_log("[DNP-FETCH] BGTask scheduling failed on simulator (expected): Code=1 notPermitted", log: fetchLog, type: .default)
print("[DNP-FETCH] NOTE: BGTaskScheduler doesn't work on simulator - this is expected. Use Xcode → Debug → Simulate Background Fetch for testing.")
return false
}
fetchLogger.error("[DNP-FETCH] Failed to schedule BGAppRefreshTask: \(error.localizedDescription)")
os_log("[DNP-FETCH] Failed to schedule BGAppRefreshTask: %{public}@", log: fetchLog, type: .error, error.localizedDescription)
print("[DNP-FETCH] Failed to schedule BGAppRefreshTask: \(error)")
return false
}
@@ -130,7 +130,7 @@ class DailyNotificationBackgroundTaskTestHarness {
// In real implementation, check if this request matches the notificationId
// For now, cancel all prefetch tasks (Phase 1: single notification)
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: prefetchTaskIdentifier)
fetchLogger.info("[DNP-FETCH] Cancelled existing pending task for notificationId=\(notificationId)")
os_log("[DNP-FETCH] Cancelled existing pending task for notificationId=%{public}@", log: fetchLog, type: .info, notificationId)
}
}
}
@@ -149,7 +149,7 @@ class DailyNotificationBackgroundTaskTestHarness {
semaphore.wait()
if pendingCount > 1 {
fetchLogger.warning("[DNP-FETCH] WARNING: Multiple pending tasks detected (\(pendingCount)) - expected 1")
os_log("[DNP-FETCH] WARNING: Multiple pending tasks detected (%d) - expected 1", log: fetchLog, type: .default, pendingCount)
return false
}
@@ -203,7 +203,7 @@ class DailyNotificationBackgroundTaskTestHarness {
/// 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) {
fetchLogger.info("[DNP-FETCH] BGTask handler invoked (task.identifier=\(task.identifier))")
os_log("[DNP-FETCH] BGTask handler invoked (task.identifier=%{public}@)", log: fetchLog, type: .info, task.identifier)
// STEP 1: Schedule the next task IMMEDIATELY (Apple best practice)
// This ensures continuity even if app is terminated shortly after
@@ -219,14 +219,14 @@ class DailyNotificationBackgroundTaskTestHarness {
// STEP 4: Set expiration handler (called if iOS terminates task early, ~30 seconds)
task.expirationHandler = {
fetchLogger.warning("[DNP-FETCH] Task expired - cancelling operations")
os_log("[DNP-FETCH] Task expired - cancelling operations", log: fetchLog, type: .default)
operation.cancel()
// Ensure task is marked complete even on expiration
if !taskCompleted {
taskCompleted = true
task.setTaskCompleted(success: false)
fetchLogger.info("[DNP-FETCH] Task marked complete (success=false) due to expiration")
os_log("[DNP-FETCH] Task marked complete (success=false) due to expiration", log: fetchLog, type: .info)
}
}
@@ -239,9 +239,9 @@ class DailyNotificationBackgroundTaskTestHarness {
if !taskCompleted {
taskCompleted = true
task.setTaskCompleted(success: success)
fetchLogger.info("[DNP-FETCH] Task marked complete (success=\(success))")
os_log("[DNP-FETCH] Task marked complete (success=%{public}@)", log: fetchLog, type: .info, success ? "true" : "false")
} else {
fetchLogger.warning("[DNP-FETCH] WARNING: Attempted to complete task twice")
os_log("[DNP-FETCH] WARNING: Attempted to complete task twice", log: fetchLog, type: .default)
}
}
@@ -262,12 +262,12 @@ class DailyNotificationBackgroundTaskTestHarness {
class PrefetchOperation: Operation {
var isFailed = false
private let fetchLogger = Logger(subsystem: "com.timesafari.dailynotification", category: "fetch")
private static let fetchLog = OSLog(subsystem: "com.timesafari.dailynotification", category: "fetch")
override func main() {
if isCancelled { return }
fetchLogger.info("[DNP-FETCH] PrefetchOperation: starting fetch...")
os_log("[DNP-FETCH] PrefetchOperation: starting fetch...", log: Self.fetchLog, type: .info)
// Simulate some work
// In real implementation, this would be:
@@ -287,11 +287,11 @@ class PrefetchOperation: Operation {
let success = true // Replace with actual fetch result
if success {
fetchLogger.info("[DNP-FETCH] PrefetchOperation: fetch success")
os_log("[DNP-FETCH] PrefetchOperation: fetch success", log: Self.fetchLog, type: .info)
// In real implementation: persist cache here before completion
} else {
isFailed = true
fetchLogger.error("[DNP-FETCH] PrefetchOperation: fetch failed")
os_log("[DNP-FETCH] PrefetchOperation: fetch failed", log: Self.fetchLog, type: .error)
}
}
}