listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

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 }