Nudge-Screen

This commit is contained in:
2026-04-20 09:41:18 +02:00
parent a48e857ada
commit 187c3e4fc6
26 changed files with 2542 additions and 229 deletions
+151
View File
@@ -0,0 +1,151 @@
import Testing
@testable import bookstax
import Foundation
// MARK: - DonationPurchaseState Enum
@Suite("DonationPurchaseState Computed Properties")
struct DonationPurchaseStateTests {
@Test("activePurchasingID returns id when purchasing")
func activePurchasingID() {
let state = DonationPurchaseState.purchasing(productID: "donatebook")
#expect(state.activePurchasingID == "donatebook")
}
@Test("activePurchasingID returns nil for non-purchasing states")
@available(iOS 18.0, *)
func activePurchasingIDNil() {
#expect(DonationPurchaseState.idle.activePurchasingID == nil)
#expect(DonationPurchaseState.thankYou(productID: "x").activePurchasingID == nil)
#expect(DonationPurchaseState.pending(productID: "x").activePurchasingID == nil)
#expect(DonationPurchaseState.failed(productID: "x", message: "err").activePurchasingID == nil)
}
@Test("thankYouID returns id when in thankYou state")
func thankYouID() {
let state = DonationPurchaseState.thankYou(productID: "doneatepage")
#expect(state.thankYouID == "doneatepage")
}
@Test("thankYouID returns nil for other states")
func thankYouIDNil() {
#expect(DonationPurchaseState.idle.thankYouID == nil)
#expect(DonationPurchaseState.purchasing(productID: "x").thankYouID == nil)
}
@Test("pendingID returns id when in pending state")
func pendingID() {
let state = DonationPurchaseState.pending(productID: "donateencyclopaedia")
#expect(state.pendingID == "donateencyclopaedia")
}
@Test("pendingID returns nil for other states")
func pendingIDNil() {
#expect(DonationPurchaseState.idle.pendingID == nil)
#expect(DonationPurchaseState.thankYou(productID: "x").pendingID == nil)
}
@Test("errorMessage returns message when failed for matching id")
func errorMessageMatch() {
let state = DonationPurchaseState.failed(productID: "donatebook", message: "Payment declined")
#expect(state.errorMessage(for: "donatebook") == "Payment declined")
}
@Test("errorMessage returns nil for wrong product id")
func errorMessageNoMatch() {
let state = DonationPurchaseState.failed(productID: "donatebook", message: "error")
#expect(state.errorMessage(for: "doneatepage") == nil)
}
@Test("errorMessage returns nil for non-failed states")
func errorMessageNotFailed() {
#expect(DonationPurchaseState.idle.errorMessage(for: "x") == nil)
#expect(DonationPurchaseState.purchasing(productID: "x").errorMessage(for: "x") == nil)
}
@Test("isIdle returns true only for .idle")
func isIdleCheck() {
#expect(DonationPurchaseState.idle.isIdle == true)
#expect(DonationPurchaseState.purchasing(productID: "x").isIdle == false)
#expect(DonationPurchaseState.thankYou(productID: "x").isIdle == false)
#expect(DonationPurchaseState.failed(productID: "x", message: "e").isIdle == false)
}
@Test("Equatable: same purchasing states are equal")
func equatablePurchasing() {
#expect(DonationPurchaseState.purchasing(productID: "a") == DonationPurchaseState.purchasing(productID: "a"))
#expect(DonationPurchaseState.purchasing(productID: "a") != DonationPurchaseState.purchasing(productID: "b"))
}
@Test("Equatable: idle equals idle")
func equatableIdle() {
#expect(DonationPurchaseState.idle == DonationPurchaseState.idle)
#expect(DonationPurchaseState.idle != DonationPurchaseState.purchasing(productID: "x"))
}
}
// MARK: - shouldShowNudge Timing
@Suite("DonationService shouldShowNudge", .serialized)
@MainActor
struct DonationServiceNudgeTests {
private let installKey = "bookstax.installDate"
private let nudgeKey = "bookstax.lastNudgeDate"
private let historyKey = "bookstax.donationHistory"
init() {
// Clean UserDefaults before each test so DonationService reads fresh state
UserDefaults.standard.removeObject(forKey: installKey)
UserDefaults.standard.removeObject(forKey: nudgeKey)
UserDefaults.standard.removeObject(forKey: historyKey)
}
@Test("Within 3-day grace period: nudge should not show")
func gracePeriodPreventsNudge() {
let recentInstall = Date().addingTimeInterval(-(2 * 24 * 3600)) // 2 days ago
UserDefaults.standard.set(recentInstall, forKey: installKey)
#expect(DonationService.shared.shouldShowNudge == false)
}
@Test("After 3-day grace period: nudge should show")
func afterGracePeriodNudgeShows() {
let oldInstall = Date().addingTimeInterval(-(4 * 24 * 3600)) // 4 days ago
UserDefaults.standard.set(oldInstall, forKey: installKey)
#expect(DonationService.shared.shouldShowNudge == true)
}
@Test("No install date recorded: nudge should not show (safe fallback)")
func noInstallDateFallback() {
// No install date stored graceful fallback
#expect(DonationService.shared.shouldShowNudge == false)
}
@Test("Nudge recently dismissed: should not show again")
func recentNudgeHidden() {
let oldInstall = Date().addingTimeInterval(-(90 * 24 * 3600))
let recentNudge = Date().addingTimeInterval(-(10 * 24 * 3600)) // dismissed 10 days ago
UserDefaults.standard.set(oldInstall, forKey: installKey)
UserDefaults.standard.set(recentNudge, forKey: nudgeKey)
#expect(DonationService.shared.shouldShowNudge == false)
}
@Test("Nudge dismissed ~6 months ago: should show again")
func sixMonthsLaterShowAgain() {
let oldInstall = Date().addingTimeInterval(-(200 * 24 * 3600))
let oldNudge = Date().addingTimeInterval(-(183 * 24 * 3600)) // ~6 months ago
UserDefaults.standard.set(oldInstall, forKey: installKey)
UserDefaults.standard.set(oldNudge, forKey: nudgeKey)
#expect(DonationService.shared.shouldShowNudge == true)
}
@Test("Nudge not yet 6 months: should not show")
func beforeSixMonthsHidden() {
let oldInstall = Date().addingTimeInterval(-(200 * 24 * 3600))
let recentNudge = Date().addingTimeInterval(-(100 * 24 * 3600)) // only 100 days
UserDefaults.standard.set(oldInstall, forKey: installKey)
UserDefaults.standard.set(recentNudge, forKey: nudgeKey)
#expect(DonationService.shared.shouldShowNudge == false)
}
}