RecentChangesTests.swift (4053B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 @Suite("RecentChanges over the ledger") 7 struct RecentChangesTests { 8 9 /// One ledger row: a cell's recorded letter, its author, and when the letter 10 /// last changed. An empty `letter` models a clear; a `nil` author models a 11 /// row with no recorded writer (dropped by the reduction). 12 private func change( 13 _ row: Int, 14 _ col: Int, 15 _ letter: String, 16 author: String?, 17 at: Date 18 ) -> PeerChange { 19 PeerChange( 20 position: GridPosition(row: row, col: col), 21 letter: letter, 22 authorID: author, 23 changedAt: at 24 ) 25 } 26 27 private let cutoff = Date(timeIntervalSinceReferenceDate: 1_000) 28 private var before: Date { cutoff.addingTimeInterval(-60) } 29 private var after: Date { cutoff.addingTimeInterval(60) } 30 31 @Test("A peer's fill after the cutoff is included, attributed to the peer") 32 func peerFillIncluded() { 33 let entries = [change(1, 2, "A", author: "bob", at: after)] 34 let cells = RecentChanges.changedCells(in: entries, since: cutoff, excludingAuthor: "alice") 35 #expect(cells == [GridPosition(row: 1, col: 2): "bob"]) 36 } 37 38 @Test("My own change is excluded even though it is newer than the cutoff") 39 func ownChangeExcluded() { 40 let entries = [change(0, 0, "X", author: "alice", at: after)] 41 let cells = RecentChanges.changedCells(in: entries, since: cutoff, excludingAuthor: "alice") 42 #expect(cells.isEmpty) 43 } 44 45 @Test("A peer's clear after the cutoff is included and attributed to the clearer") 46 func peerClearIncluded() { 47 let entries = [change(3, 4, "", author: "bob", at: after)] 48 let cells = RecentChanges.changedCells(in: entries, since: cutoff, excludingAuthor: "alice") 49 #expect(cells == [GridPosition(row: 3, col: 4): "bob"]) 50 } 51 52 @Test("A change at or before the cutoff is excluded") 53 func preCutoffExcluded() { 54 let entries = [ 55 change(0, 0, "A", author: "bob", at: before), 56 change(1, 1, "B", author: "carol", at: cutoff), 57 ] 58 let cells = RecentChanges.changedCells(in: entries, since: cutoff, excludingAuthor: "alice") 59 #expect(cells.isEmpty) 60 } 61 62 @Test("A row with no recorded author is dropped") 63 func noAuthorDropped() { 64 let entries = [change(2, 2, "A", author: nil, at: after)] 65 let cells = RecentChanges.changedCells(in: entries, since: cutoff, excludingAuthor: "alice") 66 #expect(cells.isEmpty) 67 } 68 69 @Test("Empty entries produce no changes") 70 func emptyEntries() { 71 let cells = RecentChanges.changedCells(in: [], since: cutoff, excludingAuthor: "alice") 72 #expect(cells.isEmpty) 73 } 74 75 // MARK: - Per-author counts (the banner reduction) 76 77 @Test("Counts and the cell map describe the same set: fills add, clears clear") 78 func countsMatchCells() { 79 let entries = [ 80 change(0, 0, "A", author: "bob", at: after), 81 change(0, 1, "B", author: "bob", at: after), 82 change(1, 0, "C", author: "carol", at: after), 83 change(2, 0, "", author: "carol", at: after), 84 ] 85 let changes = RecentChanges.changes(in: entries, since: cutoff, excludingAuthor: "alice") 86 #expect(changes.counts["bob"] == RecentChanges.Count(added: 2, cleared: 0)) 87 #expect(changes.counts["carol"] == RecentChanges.Count(added: 1, cleared: 1)) 88 // Every counted change is a bordered cell, and vice versa. 89 let addedAndCleared = changes.counts.values.reduce(0) { $0 + $1.added + $1.cleared } 90 #expect(addedAndCleared == changes.cells.count) 91 } 92 93 @Test("No changes after the cutoff yields empty counts") 94 func emptyCounts() { 95 let entries = [change(0, 0, "A", author: "bob", at: before)] 96 let changes = RecentChanges.changes(in: entries, since: cutoff, excludingAuthor: "alice") 97 #expect(changes.counts.isEmpty) 98 #expect(changes.cells.isEmpty) 99 } 100 }