crossmate

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

ReplayControlsTests.swift (4249B)


      1 import Foundation
      2 import Testing
      3 
      4 @testable import Crossmate
      5 
      6 /// Derived scrub state on `ReplayControls` — the glue between a loaded
      7 /// `ReplayTimeline` and the grid override the finish banner renders.
      8 @Suite("Replay controls")
      9 @MainActor
     10 struct ReplayControlsTests {
     11 
     12     private func entry(
     13         seq: Int64,
     14         at seconds: TimeInterval,
     15         row: Int,
     16         col: Int,
     17         letter: String,
     18         direction: Puzzle.Direction? = nil
     19     ) -> JournalValue {
     20         JournalValue(
     21             seq: seq,
     22             timestamp: Date(timeIntervalSince1970: seconds),
     23             position: GridPosition(row: row, col: col),
     24             state: JournalCellState(letter: letter, mark: .none, cellAuthorID: "a"),
     25             actingAuthorID: "a",
     26             kind: .input,
     27             targetSeq: nil,
     28             batchID: nil,
     29             prevSeqAtCell: nil,
     30             direction: direction
     31         )
     32     }
     33 
     34     private func timeline() -> ReplayTimeline {
     35         ReplayTimeline(merging: [[
     36             entry(seq: 0, at: 10, row: 0, col: 0, letter: "A", direction: .across),
     37             entry(seq: 1, at: 20, row: 0, col: 1, letter: "B", direction: .down),
     38         ]])
     39     }
     40 
     41     @Test("a ready load parks the head at the end with no override")
     42     func readyParksAtEnd() async {
     43         let controls = ReplayControls()
     44         let tl = timeline()
     45         await controls.load { .ready(tl) }
     46 
     47         #expect(controls.isScrubbable)
     48         #expect(controls.position == 2)         // hard-right = finished grid
     49         #expect(controls.gridOverride == nil)   // at rest, render the live grid
     50         #expect(controls.cursor == nil)
     51     }
     52 
     53     @Test("scrubbing left reconstructs the grid and the playhead")
     54     func scrubbingRewinds() async {
     55         let controls = ReplayControls()
     56         let tl = timeline()
     57         await controls.load { .ready(tl) }
     58 
     59         controls.position = 1
     60         #expect(controls.gridOverride?[GridPosition(row: 0, col: 0)]?.letter == "A")
     61         #expect(controls.gridOverride?[GridPosition(row: 0, col: 1)] == nil)
     62         #expect(controls.cursor == GridPosition(row: 0, col: 0))
     63         #expect(controls.cursorDirection == .across)
     64 
     65         controls.position = 0
     66         #expect(controls.gridOverride?.isEmpty == true)
     67         #expect(controls.cursor == nil)
     68         #expect(controls.cursorDirection == nil)
     69     }
     70 
     71     @Test("a waiting load is not scrubbable")
     72     func waitingNotScrubbable() async {
     73         let controls = ReplayControls()
     74         await controls.load { .waiting(missing: 2) }
     75 
     76         #expect(!controls.isScrubbable)
     77         #expect(controls.gridOverride == nil)
     78         if case .waiting(let missing) = controls.status {
     79             #expect(missing == 2)
     80         } else {
     81             Issue.record("expected .waiting, got \(controls.status)")
     82         }
     83     }
     84 
     85     @Test("load is idempotent once ready, but retry re-opens it")
     86     func retryReloads() async {
     87         let controls = ReplayControls()
     88         await controls.load { .ready(timeline()) }
     89 
     90         // A second load while ready is a no-op (loader must not run).
     91         await controls.load {
     92             Issue.record("loader ran while already ready")
     93             return .unavailable
     94         }
     95         #expect(controls.isScrubbable)
     96 
     97         controls.retry()
     98         await controls.load { .waiting(missing: 1) }
     99         #expect(!controls.isScrubbable)
    100     }
    101 
    102     @Test("playback toggles without losing the selected speed")
    103     func playbackTogglePreservesSelectedSpeed() async {
    104         let controls = ReplayControls()
    105         #expect(!controls.isPlaybackActive)
    106         #expect(controls.selectedPlaybackSpeed == 1)
    107 
    108         controls.togglePlayback()
    109         #expect(controls.isPlaybackActive)
    110 
    111         controls.cycleSelectedPlaybackSpeed()
    112         controls.cycleSelectedPlaybackSpeed()
    113         #expect(controls.selectedPlaybackSpeed == 3)
    114 
    115         controls.pausePlayback()
    116         #expect(!controls.isPlaybackActive)
    117         #expect(controls.selectedPlaybackSpeed == 3)
    118 
    119         controls.togglePlayback()
    120         #expect(controls.isPlaybackActive)
    121         #expect(controls.selectedPlaybackSpeed == 3)
    122 
    123         controls.retry()
    124         #expect(!controls.isPlaybackActive)
    125         #expect(controls.selectedPlaybackSpeed == 1)
    126     }
    127 }