crossmate

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

GameStoreUnseenMovesTests.swift (5395B)


      1 import CoreData
      2 import Foundation
      3 import Testing
      4 
      5 @testable import Crossmate
      6 
      7 /// Pins down the Date-based unread-badge heuristic on `GameStore`. A shared
      8 /// game gains an unread badge when another author's `MovesEntity` row has a
      9 /// later `updatedAt` than the local user's last open.
     10 @Suite("GameStore unread badge", .serialized)
     11 @MainActor
     12 struct GameStoreUnseenMovesTests {
     13 
     14     private static let localAuthorID = "local-author"
     15     private static let otherAuthorID = "other-author"
     16 
     17     private static let sharedPuzzleSource = """
     18     Title: Test Puzzle
     19     Author: Test
     20 
     21 
     22     ABC
     23     D#E
     24     FGH
     25 
     26 
     27     A1. Across 1 ~ ABC
     28     A4. Across 4 ~ DE
     29     A5. Across 5 ~ FGH
     30     D1. Down 1 ~ ADF
     31     D2. Down 2 ~ BG
     32     D3. Down 3 ~ CEH
     33     """
     34 
     35     private func makeSharedGame(
     36         in ctx: NSManagedObjectContext
     37     ) throws -> (GameEntity, UUID) {
     38         let gameID = UUID()
     39         let xd = try XD.parse(Self.sharedPuzzleSource)
     40         let puzzle = Puzzle(xd: xd)
     41 
     42         let entity = GameEntity(context: ctx)
     43         entity.id = gameID
     44         entity.title = "Shared"
     45         entity.puzzleSource = Self.sharedPuzzleSource
     46         entity.createdAt = Date()
     47         entity.updatedAt = Date()
     48         entity.ckRecordName = "game-\(gameID.uuidString)"
     49         entity.ckZoneName = "game-\(gameID.uuidString)"
     50         entity.ckZoneOwnerName = "_someOtherUser"
     51         entity.databaseScope = 1
     52         // Pre-populate the cached summary fields so `GameSummary.init?` takes
     53         // the fast path and doesn't have to re-parse XD.
     54         entity.populateCachedSummaryFields(from: puzzle)
     55         try ctx.save()
     56         return (entity, gameID)
     57     }
     58 
     59     private func addMovesRow(
     60         for entity: GameEntity,
     61         gameID: UUID,
     62         authorID: String,
     63         updatedAt: Date,
     64         in ctx: NSManagedObjectContext
     65     ) throws {
     66         let row = MovesEntity(context: ctx)
     67         row.game = entity
     68         row.authorID = authorID
     69         row.deviceID = "test-\(authorID)"
     70         row.cells = Data()
     71         row.updatedAt = updatedAt
     72         row.ckRecordName = RecordSerializer.recordName(
     73             forMovesInGame: gameID,
     74             authorID: authorID,
     75             deviceID: "test-\(authorID)"
     76         )
     77         try ctx.save()
     78     }
     79 
     80     @Test("Other-author Moves update marks the shared game unread")
     81     func otherAuthorMoveMarksSharedGameUnread() throws {
     82         let persistence = makeTestPersistence()
     83         let store = makeTestStore(persistence: persistence)
     84         let (entity, gameID) = try makeSharedGame(in: persistence.viewContext)
     85         let updatedAt = Date(timeIntervalSinceNow: -10)
     86         try addMovesRow(
     87             for: entity,
     88             gameID: gameID,
     89             authorID: Self.otherAuthorID,
     90             updatedAt: updatedAt,
     91             in: persistence.viewContext
     92         )
     93 
     94         store.noteIncomingMovesUpdate(
     95             gameIDs: [gameID],
     96             currentAuthorID: Self.localAuthorID
     97         )
     98 
     99         let summary = try #require(GameSummary(entity: entity))
    100         #expect(entity.latestOtherMoveAt == updatedAt)
    101         #expect(entity.lastSeenOtherMoveAt == nil)
    102         #expect(summary.hasUnseenOtherMoves)
    103     }
    104 
    105     @Test("Own Moves update does not mark the shared game unread")
    106     func ownMoveDoesNotMarkSharedGameUnread() throws {
    107         let persistence = makeTestPersistence()
    108         let store = makeTestStore(persistence: persistence)
    109         let (entity, gameID) = try makeSharedGame(in: persistence.viewContext)
    110         try addMovesRow(
    111             for: entity,
    112             gameID: gameID,
    113             authorID: Self.localAuthorID,
    114             updatedAt: Date(),
    115             in: persistence.viewContext
    116         )
    117 
    118         store.noteIncomingMovesUpdate(
    119             gameIDs: [gameID],
    120             currentAuthorID: Self.localAuthorID
    121         )
    122 
    123         let summary = try #require(GameSummary(entity: entity))
    124         #expect(entity.latestOtherMoveAt == nil)
    125         #expect(!summary.hasUnseenOtherMoves)
    126     }
    127 
    128     @Test("Opening a game advances lastSeenOtherMoveAt to latestOtherMoveAt")
    129     func openingGameMarksOtherMovesSeen() throws {
    130         let persistence = makeTestPersistence()
    131         let store = makeTestStore(persistence: persistence)
    132         let ctx = persistence.viewContext
    133         let (entity, _) = try makeSharedGame(in: ctx)
    134         let latest = Date(timeIntervalSinceNow: -10)
    135         entity.latestOtherMoveAt = latest
    136         try ctx.save()
    137 
    138         _ = try store.loadGame(id: entity.id!)
    139 
    140         #expect(entity.lastSeenOtherMoveAt == latest)
    141         let summary = try #require(GameSummary(entity: entity))
    142         #expect(!summary.hasUnseenOtherMoves)
    143     }
    144 
    145     @Test("Opening a stale CmVer game reparses source and records current CmVer")
    146     func openingStaleCmVerGameReparsesSource() throws {
    147         let persistence = makeTestPersistence()
    148         let store = makeTestStore(persistence: persistence)
    149         let ctx = persistence.viewContext
    150         let (entity, _) = try makeSharedGame(in: ctx)
    151         entity.puzzleCmVersion = 0
    152         entity.gridWidth = 0
    153         entity.gridHeight = 0
    154         entity.blockMask = nil
    155         try ctx.save()
    156 
    157         _ = try store.loadGame(id: entity.id!)
    158 
    159         #expect(entity.puzzleCmVersion == Int64(XD.currentCmVersion))
    160         #expect(entity.gridWidth == 3)
    161         #expect(entity.gridHeight == 3)
    162         #expect(entity.blockMask?.count == 9)
    163     }
    164 }