AccentColorTests.swift (5361B)
1 import Foundation 2 import SwiftUI 3 import Testing 4 5 #if os(macOS) 6 @testable import Listless 7 #else 8 @testable import Listless_iOS 9 #endif 10 11 @Suite("AccentColor") 12 struct AccentColorTests { 13 14 // MARK: - ColorTheme properties 15 16 @Test("displayOrder sorts themes alphabetically by displayName") 17 func displayOrderSorting() { 18 let order = ColorTheme.displayOrder 19 let names = order.map(\.displayName) 20 21 #expect(names == names.sorted()) 22 } 23 24 @Test("All themes have unique display names") 25 func uniqueDisplayNames() { 26 let names = ColorTheme.allCases.map(\.displayName) 27 28 #expect(Set(names).count == names.count) 29 } 30 31 @Test("displayOrder contains all cases") 32 func displayOrderContainsAll() { 33 #expect(Set(ColorTheme.displayOrder) == Set(ColorTheme.allCases)) 34 } 35 36 // MARK: - itemColor 37 38 @Test( 39 "Single item returns top color for all themes", 40 arguments: ColorTheme.allCases 41 ) 42 func singleItemReturnsTopColor(theme: ColorTheme) { 43 let singleColor = itemColor(forIndex: 0, total: 1, theme: theme) 44 // For a single item, itemColor returns the top HSB directly. 45 // We can't inspect Color internals, but we can verify it doesn't crash 46 // and returns a non-nil Color. 47 #expect(type(of: singleColor) == Color.self) 48 } 49 50 @Test("First item of many matches top color") 51 func firstItemMatchesTop() { 52 let topColor = itemColor(forIndex: 0, total: 1, theme: .pilbara) 53 let firstOfMany = itemColor(forIndex: 0, total: 10, theme: .pilbara) 54 55 // Both should be the top color since progress=0 yields top for both paths. 56 #expect(topColor.description == firstOfMany.description) 57 } 58 59 @Test("Last item of many uses bottom color") 60 func lastItemUsesBottom() { 61 // progress = 1.0 → interpolates from mid to bottom with progress=1.0 → bottom 62 let lastColor = itemColor(forIndex: 9, total: 10, theme: .pilbara) 63 #expect(type(of: lastColor) == Color.self) 64 } 65 66 @Test("Middle item of odd count uses mid color") 67 func middleItemUsesMidColor() { 68 // For total=3, index=1 → progress = 0.5 → exactly at mid 69 let midColor = itemColor(forIndex: 1, total: 3, theme: .pilbara) 70 #expect(type(of: midColor) == Color.self) 71 } 72 73 @Test( 74 "Gradient produces distinct colors for each position", 75 arguments: ColorTheme.allCases 76 ) 77 func gradientProducesDistinctColors(theme: ColorTheme) { 78 let total = 5 79 let colors = (0..<total).map { 80 itemColor(forIndex: $0, total: total, theme: theme).description 81 } 82 // Adjacent colors should differ (gradient is continuous but not flat). 83 for i in 0..<(colors.count - 1) { 84 #expect(colors[i] != colors[i + 1]) 85 } 86 } 87 88 @Test("Different themes produce different colors for same position") 89 func differentThemesDiffer() { 90 let pilbaraColor = itemColor(forIndex: 0, total: 5, theme: .pilbara).description 91 let collaroyColor = itemColor(forIndex: 0, total: 5, theme: .collaroy).description 92 93 #expect(pilbaraColor != collaroyColor) 94 } 95 96 // MARK: - Edge cases / out-of-range inputs 97 98 @Test("total=0 returns top color without crashing") 99 func totalZero() { 100 // total <= 1 hits the guard, so total=0 should behave like total=1. 101 let color = itemColor(forIndex: 0, total: 0, theme: .pilbara) 102 let topColor = itemColor(forIndex: 0, total: 1, theme: .pilbara) 103 #expect(color.description == topColor.description) 104 } 105 106 @Test("Negative total returns top color without crashing") 107 func negativeTotalDoesNotCrash() { 108 let color = itemColor(forIndex: 0, total: -1, theme: .pilbara) 109 let topColor = itemColor(forIndex: 0, total: 1, theme: .pilbara) 110 #expect(color.description == topColor.description) 111 } 112 113 @Test("Negative index produces a color without crashing") 114 func negativeIndex() { 115 // Extrapolates beyond the gradient — should not crash. 116 let color = itemColor(forIndex: -1, total: 5, theme: .pilbara) 117 #expect(type(of: color) == Color.self) 118 } 119 120 @Test("Index beyond total produces a color without crashing") 121 func indexBeyondTotal() { 122 // Extrapolates beyond the gradient — should not crash. 123 let color = itemColor(forIndex: 10, total: 5, theme: .pilbara) 124 #expect(type(of: color) == Color.self) 125 } 126 127 @Test("Very large total does not crash") 128 func veryLargeTotal() { 129 let color = itemColor(forIndex: 500, total: 1000, theme: .collaroy) 130 #expect(type(of: color) == Color.self) 131 } 132 133 // MARK: - cachedItemColor 134 135 @Test("Cached color returns same result as uncached") 136 @MainActor 137 func cachedMatchesUncached() { 138 let uncached = itemColor(forIndex: 2, total: 5, theme: .pilbara).description 139 let cached = cachedItemColor(forIndex: 2, total: 5, theme: .pilbara).description 140 141 #expect(cached == uncached) 142 } 143 144 @Test("Repeated cached calls return consistent results") 145 @MainActor 146 func cachedConsistency() { 147 let first = cachedItemColor(forIndex: 1, total: 4, theme: .collaroy).description 148 let second = cachedItemColor(forIndex: 1, total: 4, theme: .collaroy).description 149 150 #expect(first == second) 151 } 152 }