Advertisement

← poker-tutor

Sources/PokerTests/TextRoleTests.swift

5.9 KB Download file
import Foundation
import TestKit
import PokerCore
import PokerUI

/// Every piece of type in the app plays a role, and a role has a size that the text-size setting
/// multiplies. Ken, 2026-09-12: "The font on my game seem very small. Let's make them bigger and
/// make sure that all of the text is consistent in terms of how font size and use of the font size
/// selector." The first half of that is the role table; the second half is the sweep below, which
/// reads every screen's source and refuses a view that reaches past the roles for a small number.
public func textRoleTests(_ t: TestRun) {
    t.suite("every size of type has a role") { t in

        t.test("the floors hold at normal and above") {
            // A phone at arm's length: body 14, captions 12, headings 16, titles 20. These are the
            // numbers Ken's "very small" was measured against — the old My game ran 9 to 12.
            for scale in [TextScale.normal, .large, .extraLarge] {
                t.expect(scale.size(TextRole.body.base) >= 14,
                         "\(scale.name): body is \(scale.size(TextRole.body.base))")
                t.expect(scale.size(TextRole.caption.base) >= 12,
                         "\(scale.name): caption is \(scale.size(TextRole.caption.base))")
                t.expect(scale.size(TextRole.numericSmall.base) >= 13,
                         "\(scale.name): small numbers are \(scale.size(TextRole.numericSmall.base))")
                t.expect(scale.size(TextRole.heading.base) >= 16)
                t.expect(scale.size(TextRole.title.base) >= 20)
            }
        }

        t.test("the roles keep their order at every setting") {
            // A caption can never come out larger than the body beside it, whatever the setting.
            for scale in TextScale.allCases {
                let caption = scale.size(TextRole.caption.base)
                let small = scale.size(TextRole.numericSmall.base)
                let body = scale.size(TextRole.body.base)
                let heading = scale.size(TextRole.heading.base)
                let title = scale.size(TextRole.title.base)
                t.expect(caption <= small && small <= body && body < heading && heading < title,
                         "\(scale.name): \(caption) / \(small) / \(body) / \(heading) / \(title)")
                t.equal(scale.size(TextRole.numeric.base), body, "numbers read at body size")
            }
        }

        t.test("every role grows with the setting") {
            for role in TextRole.allCases {
                let sizes = TextScale.allCases.map { role.isCapped ? $0.fittedSize(role.base) : $0.size(role.base) }
                for (smaller, larger) in zip(sizes, sizes.dropFirst()) {
                    t.expect(larger >= smaller, "\(role) went from \(smaller) to \(larger)")
                }
                t.expect(sizes.last! > sizes.first!, "\(role) never grew at all")
            }
        }

        t.test("only the felt is capped") {
            t.equal(TextRole.allCases.filter(\.isCapped), [.felt])
        }

        t.test("no screen reaches past the roles for a small size") {
            // The sweep: a view may ask a role for MORE than its base (a display number at 46, a
            // title at 22) and never less, so a literal below the floor, or a helper that used to
            // take one, is a screen drifting back to 9-point type. The allowed exceptions are
            // words in fixed boxes on the felt (capped), card faces and chips (sized off the card),
            // icons (not type), and the one glyph drawn as a symbol.
            let sources = BannedWriting.swiftFiles().filter { $0.path.contains("/PokerUI/") }
            t.expect(sources.count > 15, "found only \(sources.count) screen files")
            let allowed: [String] = [
                "capped: true",                 // the felt, the tab bar, the seat boxes
                "metrics.titleSize",            // screen titles, 22 on a phone
                "Text(\"♠\")", "Text(\"♣\")",   // the app's own mark on the sidebar
                "cardSize", "size * ", "chip",  // card faces and chips size off the card
                "Symbol(",                      // icons are not type
                "func ", "///", "//",           // the helpers' own definitions and comments
            ]
            let literalBody = try! NSRegularExpression(pattern: #"\.body\(\s*\d"#)
            let literalHeading = try! NSRegularExpression(pattern: #"\.heading\(\s*\d"#)
            let labelUncapped = try! NSRegularExpression(pattern: #"\)\.label\((?![^)]*capped: true)"#)
            let smallNumeric = try! NSRegularExpression(pattern: #"\.numeric\(\s*(\d|1[0-3])\b"#)
            let rawFont = try! NSRegularExpression(pattern: #"\.font\(\.system\(size:\s*\d"#)
            let glossaryLiteral = try! NSRegularExpression(pattern: #"GlossaryText\([^\n]*size:\s*\d"#)
            let checks = [literalBody, literalHeading, labelUncapped, smallNumeric, rawFont, glossaryLiteral]
            var offenders: [String] = []
            for file in sources {
                guard let text = try? String(contentsOf: file, encoding: .utf8) else { continue }
                for (number, line) in text.components(separatedBy: "\n").enumerated() {
                    guard !allowed.contains(where: { line.contains($0) }) else { continue }
                    let range = NSRange(line.startIndex..., in: line)
                    if checks.contains(where: { $0.firstMatch(in: line, range: range) != nil }) {
                        offenders.append("\(file.lastPathComponent):\(number + 1): \(line.trimmingCharacters(in: .whitespaces))")
                    }
                }
            }
            t.expect(offenders.isEmpty, "type set below its role:\n" + offenders.joined(separator: "\n"))
        }
    }
}