Files
lanework/KanbanTests/SymbolPickerTests.swift
T
rzen 7ac34651a2 The board's symbol takes a tint — a 4×2 colour row under the picker's glyphs, and the glyph itself moves into the titlebar
iconColor stops being hand-written-only (user-ruled, superseding 03's
"schema yes, control no"): it rides StyleCommand.apply → applyStyle as
the third styled dimension — per-dimension no-op skip, one bracket, one
history step, ExpectedField.iconColor for staleness. The SymbolPicker
grows an opt-in colour row (leading None plus seven Palette.foregrounds
hues, None removes the key); the board popover is its one caller. The
window-title widget now draws the board's resolved glyph in that tint
beside the name. Doc realignment filed on the Redesign board (Minor).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-07 18:36:49 -04:00

105 lines
4.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Testing
@testable import Kanban
/// **The symbol picker's pure seams** (03-board-ui.md § Styling ▸ Controls, the general-purpose
/// picker `SymbolPicker.swift` builds beside the style editor's own curated grid): the curated
/// default set, the full-catalog loader and its cache, and the search filter's AND semantics.
/// SwiftUI rendering — the well grid, the popover's arrow-key navigation — is deliberately untested,
/// exactly as `StyleEditor.swift`'s own wells are.
@Suite("SymbolPicker ▸ the curated default set")
struct SymbolPickerCatalogDefaultSetTests {
@Test("Exactly 36 entries, all unique")
func shape() {
#expect(SymbolPickerCatalog.defaultSet.count == 36)
#expect(Set(SymbolPickerCatalog.defaultSet).count == SymbolPickerCatalog.defaultSet.count)
}
@Test("Every entry is one this system can actually draw — pins the list against typos")
func everyNameResolves() {
let missing = SymbolPickerCatalog.defaultSet.filter { !ItemSymbol.exists($0) }
#expect(missing.isEmpty, "unknown SF Symbol names: \(missing)")
#expect(SymbolPickerCatalog.available.count == SymbolPickerCatalog.defaultSet.count)
}
}
@Suite("SymbolPicker ▸ the colour row")
struct SymbolPickerColorSetTests {
@Test("Seven unique tints — the leading None plus these fills the 4×2 grid exactly")
func shape() {
#expect(SymbolPickerCatalog.colorSet.count
== SymbolPickerLayout.colorColumns * SymbolPickerLayout.colorRows - 1)
#expect(Set(SymbolPickerCatalog.colorSet).count == SymbolPickerCatalog.colorSet.count)
}
@Test("Every entry is a palette name that resolves — pins the list against typos and palette drift")
func everyNameResolves() {
let missing = SymbolPickerCatalog.colorSet.filter { Palette.color(named: $0) == nil }
#expect(missing.isEmpty, "unresolvable palette names: \(missing)")
}
}
@Suite("SymbolPicker ▸ filter")
struct SymbolPickerFilterTests {
@Test("An empty or whitespace-only query returns the input unchanged")
func emptyQueryIsANoOp() {
let symbols = ["star", "flag", "heart"]
#expect(SymbolPickerCatalog.filter("", in: symbols) == symbols)
#expect(SymbolPickerCatalog.filter(" ", in: symbols) == symbols)
#expect(SymbolPickerCatalog.filter("\t\n", in: symbols) == symbols)
}
@Test("A single token matches case-insensitively, as a substring")
func singleTokenSubstring() {
let symbols = ["star", "star.fill", "flag", "flag.checkered"]
#expect(SymbolPickerCatalog.filter("star", in: symbols) == ["star", "star.fill"])
#expect(SymbolPickerCatalog.filter("STAR", in: symbols) == ["star", "star.fill"])
#expect(SymbolPickerCatalog.filter("Fla", in: symbols) == ["flag", "flag.checkered"])
}
@Test("Multiple tokens are an AND — every token must appear somewhere in the name")
func multiTokenIsAnAnd() {
let symbols = ["wrench.and.screwdriver", "screwdriver", "wrench"]
#expect(SymbolPickerCatalog.filter("wrench screw", in: symbols) == ["wrench.and.screwdriver"])
#expect(SymbolPickerCatalog.filter("screw wrench", in: symbols) == ["wrench.and.screwdriver"])
}
@Test("Input order is preserved")
func orderPreserved() {
let symbols = ["zebra.star", "apple.star", "mango.star"]
#expect(SymbolPickerCatalog.filter("star", in: symbols) == symbols)
}
@Test("No match returns an empty array")
func noMatchIsEmpty() {
#expect(SymbolPickerCatalog.filter("xyzzy-nonexistent", in: ["star", "flag"]).isEmpty)
}
}
@Suite("SymbolPicker ▸ the full catalog")
struct SymbolPickerFullCatalogTests {
@Test("The default path returns a sorted, unique, non-empty list containing well-known names")
func defaultPathLoads() {
let catalog = SymbolPickerCatalog.fullCatalog()
#expect(!catalog.isEmpty)
#expect(catalog == catalog.sorted())
#expect(Set(catalog).count == catalog.count)
#expect(catalog.contains("star"))
#expect(catalog.contains("folder"))
}
@Test("Two calls against the default path return identical results — the cache is coherent")
func cacheIsCoherent() {
#expect(SymbolPickerCatalog.fullCatalog() == SymbolPickerCatalog.fullCatalog())
}
@Test("A nonexistent bundle path falls back to the merged curated set, sorted and unique")
func nonexistentPathFallsBack() {
let expected = Set(SymbolPickerCatalog.defaultSet + CuratedSymbols.all).sorted()
#expect(SymbolPickerCatalog.fullCatalog(bundlePath: "/nonexistent") == expected)
}
}