crossmate

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

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 }