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
105 lines
4.6 KiB
Swift
105 lines
4.6 KiB
Swift
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)
|
||
}
|
||
}
|