import SwiftUI import Testing @testable import Kanban /// **`BoardThemeFilters`** and **`BoardThemeMode`** — the Theme tab's shared filter state and its /// Solid color / Pattern picker: the default-tone rule, the pure filters → `FacetsRecipe` assembly, /// and solid colour's seed-independence, pinned the way the popover's other pure seams are /// (`BoardInfoMetrics`, `BoardDiskFootprint` in `BoardInfoTabTests`). Everything else about the /// carousel — the strip's layout, the placeholder chip, the chevron paging, the apply gesture — is /// SwiftUI and deliberately untested; `FacetsGeneratorTests` and `GeneratedBackgroundTests` already /// cover the generator and the write paths this feeds. @Suite("Board popover ▸ Theme tab filters") struct BoardThemeFiltersTests { @Test("Dark system appearance opens on Tone Dark, light opens on Tone Light") func defaultToneFollowsTheSystem() { #expect(BoardThemeFilters.defaultTone(colorScheme: .dark) == .dark) #expect(BoardThemeFilters.defaultTone(colorScheme: .light) == .light) } @Test("The opening state is mono, medium, mid — only tone varies with the system") func initialStateIsTheReviewedDefaults() { let light = BoardThemeFilters.initial(colorScheme: .light) #expect(light.tone == .light) #expect(light.colors == .mono) #expect(light.mesh == .medium) #expect(light.saturation == .mid) let dark = BoardThemeFilters.initial(colorScheme: .dark) #expect(dark.tone == .dark) #expect(dark.colors == .mono) #expect(dark.mesh == .medium) #expect(dark.saturation == .mid) } @Test("A hue and a seed assemble the exact recipe the filters describe") func recipeAssemblesEveryAxis() { let filters = BoardThemeFilters(tone: .dark, colors: .trio, mesh: .fine, saturation: .rich) let recipe = filters.recipe(hue: .iris, seed: 0x5EED) #expect(recipe.hue == .iris) #expect(recipe.strategy == .trio) #expect(recipe.density == .fine) #expect(recipe.tone == .dark) #expect(recipe.saturation == .rich) #expect(recipe.seed == 0x5EED) } @Test("Two hues under the same filters and seed differ only in hue") func onlyHueChangesAcrossTheWheel() { let filters = BoardThemeFilters.initial(colorScheme: .light) let sky = filters.recipe(hue: .sky, seed: 42) let rose = filters.recipe(hue: .rose, seed: 42) #expect(sky.hue == .sky) #expect(rose.hue == .rose) #expect(sky.strategy == rose.strategy) #expect(sky.density == rose.density) #expect(sky.tone == rose.tone) #expect(sky.saturation == rose.saturation) #expect(sky.seed == rose.seed) } @Test("The same filters and seed recipe identically — the preview/apply agreement the carousel depends on") func sameInputsRecipeIdentically() { let filters = BoardThemeFilters(tone: .light, colors: .duo, mesh: .coarse, saturation: .soft) #expect(filters.recipe(hue: .forest, seed: 7) == filters.recipe(hue: .forest, seed: 7)) } // MARK: - Mode @Test("Solid color and Pattern are the picker's two cases, labeled as the segmented control shows them") func modeCasesAreTheSegmentedLabels() { #expect(BoardThemeMode.allCases == [.solid, .pattern]) #expect(BoardThemeMode.solid.rawValue == "Solid color") #expect(BoardThemeMode.pattern.rawValue == "Pattern") #expect(BoardThemeMode.solid.id == .solid) #expect(BoardThemeMode.pattern.id == .pattern) } // MARK: - Solid colour derivation /// The Solid color swatch's fill (`BoardThemeTabView.solidColor(_:)`): the filters' own /// tone/saturation level at a hue, un-jittered — the same thing a Pattern swatch's ground colour /// is (`FacetsRecipe.primaryColor`), read directly rather than through a mesh. @Test("The same filters and hue always yield the same solid colour") func solidColorIsDeterministic() { let filters = BoardThemeFilters(tone: .dark, colors: .duo, mesh: .fine, saturation: .rich) let first = filters.recipe(hue: .teal, seed: 0).primaryColor let second = filters.recipe(hue: .teal, seed: 0).primaryColor #expect(first == second) } /// **Seed-independence**: `primaryColor` never reads `seed`, so a Solid swatch clicked at seed /// `0` (`BoardThemeTabView.applySolid`) is exactly what a Pattern swatch's ground colour would be /// at any seed — the fact that lets Solid color skip minting a seed at all. @Test("primaryColor is the same colour whatever the seed") func primaryColorIsSeedIndependent() { let filters = BoardThemeFilters(tone: .light, colors: .trio, mesh: .coarse, saturation: .soft) let seeds: [UInt64] = [0, 1, 42, .max] let colors = seeds.map { filters.recipe(hue: .amber, seed: $0).primaryColor } #expect(Set(colors).count == 1) } /// Different hues under the same filters still land on different solid colours — the carousel /// would otherwise show eight identical swatches. @Test("Different hues yield different solid colours under the same filters") func differentHuesYieldDifferentSolidColors() { let filters = BoardThemeFilters.initial(colorScheme: .light) let sky = filters.recipe(hue: .sky, seed: 0).primaryColor let rose = filters.recipe(hue: .rose, seed: 0).primaryColor #expect(sky != rose) } }