CellAccessibilityDescriberTests.swift (10491B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 @Suite("CellAccessibilityDescriber") 7 struct CellAccessibilityDescriberTests { 8 9 /// 3x3 grid with a centre block: 10 /// A B C 11 /// D # E 12 /// F G H 13 /// Words: 1 Across ABC, 3 Across FGH, 1 Down ADF, 2 Down CEH. 14 /// D (1,0) and E (1,2) each belong to exactly one word. 15 private func makePuzzle() throws -> Puzzle { 16 Puzzle(xd: try XD.parse(""" 17 Title: Describer Fixture 18 19 20 ABC 21 D#E 22 FGH 23 24 25 A1. Top row ~ ABC 26 A3. Bottom row ~ FGH 27 D1. Left column ~ ADF 28 D2. Right column ~ CEH 29 """)) 30 } 31 32 private func describer( 33 authorNames: [String: String] = [:] 34 ) throws -> CellAccessibilityDescriber { 35 CellAccessibilityDescriber(puzzle: try makePuzzle(), authorNames: authorNames) 36 } 37 38 // MARK: - Labels 39 40 @Test("A cell in crossing words reads both memberships, across first") 41 func labelForCrossingCell() throws { 42 let label = try describer().label(atRow: 0, atCol: 0) 43 #expect(label == "1 Across, letter 1 of 3; 1 Down, letter 1 of 3") 44 } 45 46 @Test("A cell in a single word reads only that membership") 47 func labelForDownOnlyCell() throws { 48 // D at (1,0): its across run is length 1 (blocked to the right), so 49 // only the down membership is spoken. 50 let label = try describer().label(atRow: 1, atCol: 0) 51 #expect(label == "1 Down, letter 2 of 3") 52 } 53 54 @Test("Letter position counts from the word start, not the grid edge") 55 func labelCountsWithinWord() throws { 56 let label = try describer().label(atRow: 2, atCol: 1) 57 #expect(label == "3 Across, letter 2 of 3") 58 } 59 60 // MARK: - Values 61 62 @Test("An unfilled square reads as Empty") 63 func valueForEmptySquare() throws { 64 #expect(try describer().value(for: Square()) == "Empty") 65 } 66 67 @Test("A plain pen entry reads just the letter") 68 func valueForPenEntry() throws { 69 var square = Square() 70 square.entry = "R" 71 square.mark = .pen(checked: nil) 72 #expect(try describer().value(for: square) == "R") 73 } 74 75 @Test("A pencil entry reads as draft, matching the keyboard's term") 76 func valueForPencilEntry() throws { 77 var square = Square() 78 square.entry = "R" 79 square.mark = .pencil(checked: nil) 80 #expect(try describer().value(for: square) == "R, draft") 81 } 82 83 @Test("Revealed and checked-wrong marks are spoken") 84 func valueForMarkedSquares() throws { 85 var revealed = Square() 86 revealed.entry = "R" 87 revealed.mark = .revealed 88 #expect(try describer().value(for: revealed) == "R, revealed") 89 90 var wrong = Square() 91 wrong.entry = "R" 92 wrong.mark = .pen(checked: .wrong) 93 #expect(try describer().value(for: wrong) == "R, incorrect") 94 } 95 96 @Test("Author attribution is spoken when names are provided") 97 func valueWithAuthorAttribution() throws { 98 var square = Square() 99 square.entry = "R" 100 square.letterAuthorID = "alice-id" 101 102 let named = try describer(authorNames: ["alice-id": "Alice"]) 103 #expect(named.value(for: square) == "R, filled by Alice") 104 105 // Solo games / hidden annotations pass no names: no attribution. 106 #expect(try describer().value(for: square) == "R") 107 } 108 109 // MARK: - Clue announcements 110 111 private func emptySquares() -> [[Square]] { 112 Array(repeating: Array(repeating: Square(), count: 3), count: 3) 113 } 114 115 @Test("An untouched word announces clue and length, omitting a zero fill count") 116 func clueAnnouncementForEmptyWord() throws { 117 let text = try describer().clueAnnouncement( 118 atRow: 0, atCol: 0, direction: .across, squares: emptySquares() 119 ) 120 #expect(text == "1 Across: Top row. 3 letters") 121 } 122 123 @Test("A partially filled word announces its fill count") 124 func clueAnnouncementForPartialWord() throws { 125 var squares = emptySquares() 126 squares[0][1].entry = "B" 127 let text = try describer().clueAnnouncement( 128 atRow: 0, atCol: 0, direction: .across, squares: squares 129 ) 130 #expect(text == "1 Across: Top row. 3 letters, 1 filled") 131 } 132 133 @Test("A complete word announces all filled") 134 func clueAnnouncementForCompleteWord() throws { 135 var squares = emptySquares() 136 squares[0][0].entry = "A" 137 squares[0][1].entry = "B" 138 squares[0][2].entry = "C" 139 let text = try describer().clueAnnouncement( 140 atRow: 0, atCol: 0, direction: .across, squares: squares 141 ) 142 #expect(text == "1 Across: Top row. 3 letters, all filled") 143 } 144 145 @Test("Announcements resolve from any cell in the word, in either direction") 146 func clueAnnouncementFromMidWord() throws { 147 let text = try describer().clueAnnouncement( 148 atRow: 1, atCol: 0, direction: .down, squares: emptySquares() 149 ) 150 #expect(text == "1 Down: Left column. 3 letters") 151 } 152 153 @Test("A cell with no word in the direction announces nothing") 154 func clueAnnouncementForWordlessDirection() throws { 155 // D at (1,0) has no across word (blocked to the right). 156 let text = try describer().clueAnnouncement( 157 atRow: 1, atCol: 0, direction: .across, squares: emptySquares() 158 ) 159 #expect(text == nil) 160 } 161 162 @Test("Clue labels pair number, direction, and text") 163 func clueLabelWording() throws { 164 #expect(try describer().clueLabel(number: 2, direction: .down) == "2 Down: Right column") 165 #expect(try describer().clueLabel(number: 2, direction: .across) == nil) 166 } 167 168 // MARK: - Peer fill announcements 169 170 @Test("A single peer fill names its word") 171 func peerFillSingleSquare() throws { 172 var squares = emptySquares() 173 squares[0][1].entry = "B" 174 let text = try describer().peerFillAnnouncement( 175 playerName: "Alice", 176 positions: [GridPosition(row: 0, col: 1)], 177 squares: squares 178 ) 179 #expect(text == "Alice filled a square in 1 Across") 180 } 181 182 @Test("A burst inside one open word counts its squares") 183 func peerFillBurstInWord() throws { 184 var squares = emptySquares() 185 squares[0][0].entry = "A" 186 squares[0][1].entry = "B" 187 let text = try describer().peerFillAnnouncement( 188 playerName: "Alice", 189 positions: [GridPosition(row: 0, col: 0), GridPosition(row: 0, col: 1)], 190 squares: squares 191 ) 192 #expect(text == "Alice filled 2 squares in 1 Across") 193 } 194 195 @Test("A burst that completes a word announces the word itself") 196 func peerFillCompletesWord() throws { 197 var squares = emptySquares() 198 squares[0][0].entry = "A" 199 squares[0][1].entry = "B" 200 squares[0][2].entry = "C" 201 let text = try describer().peerFillAnnouncement( 202 playerName: "Bob", 203 positions: [GridPosition(row: 0, col: 0), GridPosition(row: 0, col: 1), 204 GridPosition(row: 0, col: 2)], 205 squares: squares 206 ) 207 #expect(text == "Bob filled 1 Across") 208 } 209 210 @Test("A burst spanning words falls back to a square count") 211 func peerFillAcrossWords() throws { 212 var squares = emptySquares() 213 squares[0][0].entry = "A" 214 squares[2][2].entry = "H" 215 let text = try describer().peerFillAnnouncement( 216 playerName: "Carol", 217 positions: [GridPosition(row: 0, col: 0), GridPosition(row: 2, col: 2)], 218 squares: squares 219 ) 220 #expect(text == "Carol filled 2 squares") 221 } 222 223 // MARK: - Check / reveal / completion announcements 224 225 @Test("Single-square checks read a bare verdict") 226 func checkSingleSquare() throws { 227 let d = try describer() 228 let cell = try makePuzzle().cells[0][0] 229 230 var squares = emptySquares() 231 #expect(d.checkAnnouncement(for: [cell], squares: squares) == "Nothing to check") 232 233 squares[0][0].entry = "A" 234 squares[0][0].mark = .pen(checked: .right) 235 #expect(d.checkAnnouncement(for: [cell], squares: squares) == "Correct") 236 237 squares[0][0].mark = .pen(checked: .wrong) 238 #expect(d.checkAnnouncement(for: [cell], squares: squares) == "Incorrect") 239 } 240 241 @Test("Word checks count mistakes and unfilled squares") 242 func checkWholeWord() throws { 243 let puzzle = try makePuzzle() 244 let d = try describer() 245 let word = puzzle.wordCells(atRow: 0, col: 0, direction: .across) 246 247 var squares = emptySquares() 248 squares[0][0].entry = "A" 249 squares[0][0].mark = .pen(checked: .right) 250 squares[0][1].entry = "X" 251 squares[0][1].mark = .pen(checked: .wrong) 252 #expect(d.checkAnnouncement(for: word, squares: squares) == "1 square incorrect") 253 254 squares[0][1].mark = .pen(checked: .right) 255 #expect(d.checkAnnouncement(for: word, squares: squares) == "No mistakes, 1 square empty") 256 257 squares[0][2].entry = "C" 258 squares[0][2].mark = .pen(checked: .right) 259 #expect(d.checkAnnouncement(for: word, squares: squares) == "All correct") 260 } 261 262 @Test("Reveals speak the revealed letter") 263 func revealSpeaksLetter() throws { 264 var squares = emptySquares() 265 squares[1][2].entry = "E" 266 squares[1][2].mark = .revealed 267 #expect(try describer().revealAnnouncement(atRow: 1, atCol: 2, squares: squares) == "Revealed E") 268 #expect(try describer().revealAnnouncement(atRow: 0, atCol: 0, squares: squares) == nil) 269 } 270 271 @Test("Completion states announce solved and filled-with-errors, never incomplete") 272 func completionAnnouncements() throws { 273 let d = try describer() 274 #expect(d.completionAnnouncement(for: .incomplete) == nil) 275 #expect(d.completionAnnouncement(for: .filledWithErrors) == "The grid is full, but something isn't right") 276 #expect(d.completionAnnouncement(for: .solved) == "Puzzle solved. Congratulations!") 277 } 278 279 @Test("Qualifiers compose in a stable order: letter, mark, author") 280 func valueComposesQualifiers() throws { 281 var square = Square() 282 square.entry = "R" 283 square.mark = .pencil(checked: .wrong) 284 square.letterAuthorID = "bob-id" 285 286 let value = try describer(authorNames: ["bob-id": "Bob"]).value(for: square) 287 #expect(value == "R, draft, incorrect, filled by Bob") 288 } 289 }