crossmate

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

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 }