crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

TipStoreTests.swift (4268B)


      1 import Foundation
      2 import Testing
      3 
      4 @testable import Crossmate
      5 
      6 @Suite("TipStore")
      7 @MainActor
      8 struct TipStoreTests {
      9 
     10     private let catalog = [
     11         Tip(id: "one", title: "One", body: "First"),
     12         Tip(id: "two", title: "Two", body: "Second"),
     13         Tip(id: "three", title: "Three", body: "Third"),
     14     ]
     15 
     16     private func makeStore(
     17         defaults: UserDefaults? = nil
     18     ) -> (store: TipStore, defaults: UserDefaults) {
     19         // Fresh UserDefaults suite per test to avoid cross-test pollution.
     20         let defaults = defaults ?? UserDefaults(suiteName: "test-\(UUID().uuidString)")!
     21         return (TipStore(defaults: defaults, catalog: catalog), defaults)
     22     }
     23 
     24     @Test("currentTip returns the first catalog tip by default")
     25     func firstTipInitially() {
     26         let (store, _) = makeStore()
     27         #expect(store.currentTip()?.id == "one")
     28     }
     29 
     30     @Test("Dismissing a tip advances to the next undismissed one")
     31     func dismissAdvances() {
     32         let (store, _) = makeStore()
     33         store.markDismissed("one")
     34         #expect(store.currentTip()?.id == "two")
     35         store.markDismissed("two")
     36         #expect(store.currentTip()?.id == "three")
     37     }
     38 
     39     @Test("Once every tip is dismissed there is nothing to surface")
     40     func allDismissed() {
     41         let (store, _) = makeStore()
     42         for tip in catalog { store.markDismissed(tip.id) }
     43         #expect(store.currentTip() == nil)
     44     }
     45 
     46     @Test("Disabling tips suppresses the banner regardless of dismissals")
     47     func disableSuppresses() {
     48         let (store, _) = makeStore()
     49         store.disable()
     50         #expect(store.isDisabled)
     51         #expect(store.currentTip() == nil)
     52         // Re-enabling brings back the next undismissed tip.
     53         store.enable()
     54         #expect(!store.isDisabled)
     55         #expect(store.currentTip()?.id == "one")
     56     }
     57 
     58     @Test("Dismissals and the opt-out flag persist across instances")
     59     func persistsAcrossInstances() {
     60         let (store, defaults) = makeStore()
     61         store.markDismissed("one")
     62         store.disable()
     63 
     64         let (reloaded, _) = makeStore(defaults: defaults)
     65         #expect(reloaded.isDisabled)
     66         reloaded.enable()
     67         #expect(reloaded.currentTip()?.id == "two")
     68     }
     69 
     70     @Test("Tips scoped to other platforms are filtered out")
     71     func platformScoping() {
     72         let catalog = [
     73             Tip(id: "both", title: "Both", body: "Everywhere"),
     74             Tip(id: "ipad", title: "iPad", body: "iPad only", only: [.iPad]),
     75             Tip(id: "iphone", title: "iPhone", body: "iPhone only", only: [.iPhone]),
     76         ]
     77         let defaults = UserDefaults(suiteName: "test-\(UUID().uuidString)")!
     78         let onPhone = TipStore(defaults: defaults, catalog: catalog, platform: .iPhone)
     79         #expect(onPhone.visibleTips.map(\.id) == ["both", "iphone"])
     80         // The first applicable tip skips the iPad-only one.
     81         #expect(onPhone.currentTip()?.id == "both")
     82         onPhone.markDismissed("both")
     83         #expect(onPhone.currentTip()?.id == "iphone")
     84 
     85         let onPad = TipStore(defaults: defaults, catalog: catalog, platform: .iPad)
     86         #expect(onPad.visibleTips.map(\.id) == ["both", "ipad"])
     87     }
     88 
     89     @Test("Tip announcement ids round-trip so the banner can recognise a tip")
     90     func announcementIDRoundTrips() {
     91         let id = Tip.announcementID(for: "welcome")
     92         #expect(Tip.tipID(fromAnnouncementID: id) == "welcome")
     93         // A non-tip announcement id is not mistaken for a tip.
     94         #expect(Tip.tipID(fromAnnouncementID: "reset-database-error") == nil)
     95     }
     96 
     97     @Test("A tip's live banner is a lowest-severity, manually dismissable global")
     98     func liveAnnouncementShape() {
     99         let tip = catalog[0]
    100         let announcement = tip.liveAnnouncement()
    101         #expect(announcement.scope == .global)
    102         #expect(announcement.severity == .tip)
    103         #expect(announcement.dismissal == .manual)
    104         #expect(announcement.title == "One")
    105         #expect(announcement.body == "First")
    106     }
    107 
    108     @Test("A tip's archive banner has no close control")
    109     func archiveAnnouncementHasNoCloseControl() {
    110         // The banner shows a ✕ only for `.manual`; the archive uses `.sticky`.
    111         #expect(catalog[0].archiveAnnouncement().dismissal == .sticky)
    112     }
    113 }