A reusable symbol picker — the board's glyph joins its name in the info popover
SymbolPicker: one well at rest, a 6x6 curated grid in a popover (leading well = the level default, clearing the key), and an optional search over the OS's full symbol inventory read from CoreGlyphs metadata. Geometry font-derived off StyleEditorLayout's base, grid enlarged by a deliberate 1.3x. Wired inline with the rename field in the board info popover through the StyleCommand funnel; the Title header retires. Curated set and the search's standing vs 03's full-browser refusal await ratification.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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 ▸ 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user