ReplayControllerTests.swift (3210B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 /// Derived scrub state on `ReplayController` — the glue between a loaded 7 /// `ReplayTimeline` and the grid override the finish banner renders. 8 @Suite("Replay controller") 9 @MainActor 10 struct ReplayControllerTests { 11 12 private func entry(seq: Int64, at seconds: TimeInterval, row: Int, col: Int, letter: String) -> JournalValue { 13 JournalValue( 14 seq: seq, 15 timestamp: Date(timeIntervalSince1970: seconds), 16 position: GridPosition(row: row, col: col), 17 state: JournalCellState(letter: letter, mark: .none, cellAuthorID: "a"), 18 actingAuthorID: "a", 19 kind: .input, 20 targetSeq: nil, 21 batchID: nil, 22 prevSeqAtCell: nil, 23 direction: nil 24 ) 25 } 26 27 private func timeline() -> ReplayTimeline { 28 ReplayTimeline(merging: [[ 29 entry(seq: 0, at: 10, row: 0, col: 0, letter: "A"), 30 entry(seq: 1, at: 20, row: 0, col: 1, letter: "B"), 31 ]]) 32 } 33 34 @Test("a ready load parks the head at the end with no override") 35 func readyParksAtEnd() async { 36 let controller = ReplayController() 37 let tl = timeline() 38 await controller.load { .ready(tl) } 39 40 #expect(controller.isScrubbable) 41 #expect(controller.position == 2) // hard-right = finished grid 42 #expect(controller.gridOverride == nil) // at rest, render the live grid 43 #expect(controller.cursor == nil) 44 } 45 46 @Test("scrubbing left reconstructs the grid and the playhead") 47 func scrubbingRewinds() async { 48 let controller = ReplayController() 49 let tl = timeline() 50 await controller.load { .ready(tl) } 51 52 controller.position = 1 53 #expect(controller.gridOverride?[GridPosition(row: 0, col: 0)]?.letter == "A") 54 #expect(controller.gridOverride?[GridPosition(row: 0, col: 1)] == nil) 55 #expect(controller.cursor == GridPosition(row: 0, col: 0)) 56 57 controller.position = 0 58 #expect(controller.gridOverride?.isEmpty == true) 59 #expect(controller.cursor == nil) 60 } 61 62 @Test("a waiting load is not scrubbable") 63 func waitingNotScrubbable() async { 64 let controller = ReplayController() 65 await controller.load { .waiting(missing: 2) } 66 67 #expect(!controller.isScrubbable) 68 #expect(controller.gridOverride == nil) 69 if case .waiting(let missing) = controller.status { 70 #expect(missing == 2) 71 } else { 72 Issue.record("expected .waiting, got \(controller.status)") 73 } 74 } 75 76 @Test("load is idempotent once ready, but retry re-opens it") 77 func retryReloads() async { 78 let controller = ReplayController() 79 await controller.load { .ready(timeline()) } 80 81 // A second load while ready is a no-op (loader must not run). 82 await controller.load { 83 Issue.record("loader ran while already ready") 84 return .unavailable 85 } 86 #expect(controller.isScrubbable) 87 88 controller.retry() 89 await controller.load { .waiting(missing: 1) } 90 #expect(!controller.isScrubbable) 91 } 92 }