← poker-tutor
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"))
}
}
}