crossmate

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

PuzzleNotificationTextTests.swift (11030B)


      1 import Foundation
      2 import Testing
      3 
      4 @testable import Crossmate
      5 
      6 @Suite("Puzzle notification text")
      7 struct PuzzleNotificationTextTests {
      8     @Test("Puzzle title includes subtitle separated by an en dash")
      9     func titleIncludesSubtitle() {
     10         let date = Calendar(identifier: .gregorian).date(from: DateComponents(year: 2001, month: 1, day: 1))
     11 
     12         let title = PuzzleNotificationText.title("Saturday Puzzle", publisher: nil, date: date)
     13 
     14         #expect(title.contains("Saturday Puzzle – "))
     15         #expect(title.contains("2001"))
     16     }
     17 
     18     @Test("Legacy join ping is system-only")
     19     func legacyJoinPingIsSystemOnly() {
     20         let ping = Ping(
     21             recordName: "ping-test-1",
     22             gameID: UUID(),
     23             authorID: "alice",
     24             deviceID: "device-a",
     25             playerName: "Alice",
     26             puzzleTitle: "Saturday Puzzle – 1 January 2001",
     27             kind: .join,
     28             payload: nil,
     29             addressee: nil
     30         )
     31 
     32         #expect(InviteCoordinator.bodyText(for: ping) == "system-only ping should not be presented")
     33     }
     34 
     35     @Test("nudgeBody names the nudger and puzzle")
     36     func nudgeBodyNamesNudger() {
     37         #expect(PuzzleNotificationText.nudgeBody(playerName: "Alice", puzzleTitle: "Saturday Puzzle")
     38             == "Alice nudged you to play the puzzle 'Saturday Puzzle'")
     39         // Empty name and title fall back to neutral wording.
     40         #expect(PuzzleNotificationText.nudgeBody(playerName: "", puzzleTitle: "")
     41             == "A player nudged you to play the puzzle")
     42     }
     43 
     44     @Test("joinBody names the joiner and puzzle")
     45     func joinBodyNamesJoiner() {
     46         #expect(PuzzleNotificationText.joinBody(playerName: "Alice", puzzleTitle: "Saturday Puzzle")
     47             == "Alice joined the puzzle 'Saturday Puzzle'")
     48         // Empty name and title fall back to neutral wording.
     49         #expect(PuzzleNotificationText.joinBody(playerName: "", puzzleTitle: "")
     50             == "A player joined the puzzle")
     51     }
     52 
     53     @Test("completionBody distinguishes a solve from a resignation")
     54     func completionBodySolveVsResign() {
     55         #expect(PuzzleNotificationText.completionBody(
     56             playerName: "Alice", puzzleTitle: "X", resigned: false
     57         ) == "Alice solved the puzzle 'X'")
     58         #expect(PuzzleNotificationText.completionBody(
     59             playerName: "Alice", puzzleTitle: "X", resigned: true
     60         ) == "Alice resigned the puzzle 'X'.")
     61     }
     62 
     63     @Test("pauseBody combines fills and clears counts when both are non-zero")
     64     func pauseBodyFillsAndClears() {
     65         let body = PuzzleNotificationText.pauseBody(
     66             playerName: "Alice",
     67             puzzleTitle: "Saturday Puzzle",
     68             fills: 3,
     69             clears: 2,
     70             checks: 0,
     71             reveals: 0
     72         )
     73 
     74         #expect(body == "Alice filled 3 letters and cleared 2 letters in the puzzle 'Saturday Puzzle'")
     75     }
     76 
     77     @Test("pauseBody pluralises only multi-letter counts")
     78     func pauseBodyPluralisation() {
     79         let filled = PuzzleNotificationText.pauseBody(
     80             playerName: "Alice",
     81             puzzleTitle: "X",
     82             fills: 1,
     83             clears: 0,
     84             checks: 0,
     85             reveals: 0
     86         )
     87         let cleared = PuzzleNotificationText.pauseBody(
     88             playerName: "Alice",
     89             puzzleTitle: "X",
     90             fills: 0,
     91             clears: 1,
     92             checks: 0,
     93             reveals: 0
     94         )
     95 
     96         #expect(filled == "Alice filled 1 letter in the puzzle 'X'")
     97         #expect(cleared == "Alice cleared 1 letter in the puzzle 'X'")
     98     }
     99 
    100     @Test("pauseBody reports check and reveal gestures, folded into one clause")
    101     func pauseBodyGestures() {
    102         let checkOnly = PuzzleNotificationText.pauseBody(
    103             playerName: "Alice",
    104             puzzleTitle: "X",
    105             fills: 0,
    106             clears: 0,
    107             checks: 1,
    108             reveals: 0
    109         )
    110         let both = PuzzleNotificationText.pauseBody(
    111             playerName: "Alice",
    112             puzzleTitle: "X",
    113             fills: 0,
    114             clears: 0,
    115             checks: 2,
    116             reveals: 1
    117         )
    118 
    119         #expect(checkOnly == "Alice ran 1 check in the puzzle 'X'")
    120         #expect(both == "Alice ran 2 checks and 1 reveal in the puzzle 'X'")
    121     }
    122 
    123     @Test("pauseBody joins all four clauses with an Oxford comma")
    124     func pauseBodyAllClauses() {
    125         let body = PuzzleNotificationText.pauseBody(
    126             playerName: "Alice",
    127             puzzleTitle: "X",
    128             fills: 5,
    129             clears: 2,
    130             checks: 1,
    131             reveals: 1
    132         )
    133 
    134         #expect(body == "Alice filled 5 letters, cleared 2 letters and ran 1 check and 1 reveal in the puzzle 'X'")
    135     }
    136 
    137     @Test("pauseBody falls back to no-edits wording when every count is zero")
    138     func pauseBodyZeroCounts() {
    139         let body = PuzzleNotificationText.pauseBody(
    140             playerName: "Alice",
    141             puzzleTitle: "Saturday Puzzle",
    142             fills: 0,
    143             clears: 0,
    144             checks: 0,
    145             reveals: 0
    146         )
    147 
    148         #expect(body == "Alice stopped solving the puzzle 'Saturday Puzzle'.")
    149     }
    150 
    151     @Test("pauseBody substitutes a default name and a generic suffix when missing")
    152     func pauseBodyMissingNameAndTitle() {
    153         let body = PuzzleNotificationText.pauseBody(
    154             playerName: "",
    155             puzzleTitle: "",
    156             fills: 2,
    157             clears: 0,
    158             checks: 0,
    159             reveals: 0
    160         )
    161 
    162         #expect(body == "A player filled 2 letters in the puzzle")
    163     }
    164 
    165     @Test("coalescedBody reuses the full pause wording for a single contributor")
    166     func coalescedBodySingle() {
    167         let body = PuzzleNotificationText.coalescedBody(
    168             puzzleTitle: "Saturday Puzzle",
    169             contributors: [
    170                 .init(authorID: "a", name: "Alice", fills: 5, clears: 2, checks: 0, reveals: 0)
    171             ]
    172         )
    173 
    174         #expect(body == "Alice filled 5 letters and cleared 2 letters in the puzzle 'Saturday Puzzle'")
    175     }
    176 
    177     @Test("coalescedBody describes multiple contributors in full, joined by semicolons")
    178     func coalescedBodyMultiple() {
    179         let body = PuzzleNotificationText.coalescedBody(
    180             puzzleTitle: "X",
    181             contributors: [
    182                 .init(authorID: "a", name: "Alice", fills: 5, clears: 1, checks: 0, reveals: 0),
    183                 .init(authorID: "b", name: "Bob", fills: 3, clears: 0, checks: 0, reveals: 0)
    184             ]
    185         )
    186 
    187         #expect(body == "Alice filled 5 letters and cleared 1 letter; Bob filled 3 letters in the puzzle 'X'")
    188     }
    189 
    190     @Test("coalescedBody reports each contributor's help gestures in full")
    191     func coalescedBodyGestures() {
    192         let body = PuzzleNotificationText.coalescedBody(
    193             puzzleTitle: "X",
    194             contributors: [
    195                 .init(authorID: "a", name: "Alice", fills: 0, clears: 0, checks: 2, reveals: 0),
    196                 .init(authorID: "b", name: "Bob", fills: 0, clears: 0, checks: 0, reveals: 1)
    197             ]
    198         )
    199 
    200         #expect(body == "Alice ran 2 checks; Bob ran 1 reveal in the puzzle 'X'")
    201     }
    202 
    203     @Test("coalescedBody is nil with no contributors")
    204     func coalescedBodyEmpty() {
    205         #expect(PuzzleNotificationText.coalescedBody(puzzleTitle: "X", contributors: []) == nil)
    206     }
    207 
    208     @Test("Invite body names the inviter and puzzle")
    209     func inviteBody() {
    210         let ping = Ping(
    211             recordName: "ping-test-2",
    212             gameID: UUID(),
    213             authorID: "alice",
    214             deviceID: "device-a",
    215             playerName: "Alice",
    216             puzzleTitle: "Saturday Puzzle – 1 January 2001",
    217             kind: .invite,
    218             payload: nil,
    219             addressee: "bob"
    220         )
    221 
    222         #expect(InviteCoordinator.bodyText(for: ping) == "Alice invited you to the puzzle 'Saturday Puzzle – 1 January 2001'")
    223     }
    224 
    225     @Test("Decline body names the decliner and puzzle")
    226     func declineBody() {
    227         let ping = Ping(
    228             recordName: "ping-test-3",
    229             gameID: UUID(),
    230             authorID: "bob",
    231             deviceID: "device-b",
    232             playerName: "Bob",
    233             puzzleTitle: "Saturday Puzzle – 1 January 2001",
    234             kind: .decline,
    235             payload: nil,
    236             addressee: "alice"
    237         )
    238 
    239         #expect(InviteCoordinator.bodyText(for: ping) == "Bob declined your invitation to the puzzle 'Saturday Puzzle – 1 January 2001'")
    240     }
    241 }
    242 
    243 @Suite("Notification preference muting")
    244 struct NotificationMutedKindsTests {
    245     @Test("All toggles on mutes nothing")
    246     func allOnMutesNothing() {
    247         let muted = AccountPushCoordinator.mutedPushKinds(
    248             nudges: true,
    249             joins: true,
    250             pauses: true,
    251             completions: true
    252         )
    253 
    254         #expect(muted.isEmpty)
    255     }
    256 
    257     @Test("Each toggle maps to its push kinds")
    258     func togglesMapToKinds() {
    259         #expect(AccountPushCoordinator.mutedPushKinds(
    260             nudges: false,
    261             joins: true,
    262             pauses: true,
    263             completions: true
    264         ) == ["nudge"])
    265         #expect(AccountPushCoordinator.mutedPushKinds(
    266             nudges: true,
    267             joins: false,
    268             pauses: true,
    269             completions: true
    270         ) == ["join"])
    271         #expect(AccountPushCoordinator.mutedPushKinds(
    272             nudges: true,
    273             joins: true,
    274             pauses: false,
    275             completions: true
    276         ) == ["pause"])
    277         #expect(AccountPushCoordinator.mutedPushKinds(
    278             nudges: true,
    279             joins: true,
    280             pauses: true,
    281             completions: false
    282         ) == ["win", "resign"])
    283     }
    284 
    285     @Test("Muted kinds never include background or invite kinds")
    286     func neverMutesBackgroundKinds() {
    287         let muted = AccountPushCoordinator.mutedPushKinds(
    288             nudges: false,
    289             joins: false,
    290             pauses: false,
    291             completions: false
    292         )
    293 
    294         #expect(muted == ["join", "nudge", "pause", "win", "resign"])
    295         #expect(!muted.contains("replay"))
    296         #expect(!muted.contains("accountJoined"))
    297         #expect(!muted.contains("accountSeen"))
    298     }
    299 
    300     @Test("Publish delivery summary formats the worker counts")
    301     func deliverySummaryFormatsCounts() {
    302         let body = Data(#"{"delivered":2,"removed":0,"muted":1,"failed":0}"#.utf8)
    303 
    304         #expect(PushClient.deliverySummary(from: body) == " (delivered=2 muted=1 removed=0 failed=0)")
    305     }
    306 
    307     @Test("Publish delivery summary tolerates a pre-muted worker response")
    308     func deliverySummaryOldWorker() {
    309         let body = Data(#"{"delivered":3,"removed":1,"failed":0}"#.utf8)
    310 
    311         #expect(PushClient.deliverySummary(from: body) == " (delivered=3 removed=1 failed=0)")
    312     }
    313 
    314     @Test("Publish delivery summary is empty for an unparseable body")
    315     func deliverySummaryUnparseable() {
    316         #expect(PushClient.deliverySummary(from: Data()) == "")
    317         #expect(PushClient.deliverySummary(from: Data("ok".utf8)) == "")
    318     }
    319 }