mirror of
https://github.com/yattee/yattee.git
synced 2026-07-19 05:42:04 +00:00
Adds an option in Appearance settings to use different accent colors in light and dark mode, with a toggle to keep a single shared color (the default, preserving existing behavior). - New synced settings keys: accentColorDark, customAccentColorDark, useSeparateDarkAccentColor; dark values fall back to the light selection until explicitly set, so no migration is needed - resolvedAccentColor returns a dynamic platform color (UIColor trait provider / NSColor appearance provider) when the toggle is on, so the root tint and all direct readers adapt to light/dark automatically without consumer changes - Accent Color section shows two stacked preset+custom grids with Light/Dark headers when enabled, extracted into AccentColorGrid; CustomAccentColorButton now takes bindings so the macOS color panel edits whichever target was last opened - New DarkAccentColorTests covering sync contract, default-off state, fallbacks, and toggle-off resolution Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
334 lines
11 KiB
Swift
334 lines
11 KiB
Swift
//
|
|
// SettingsTests.swift
|
|
// YatteeTests
|
|
//
|
|
// Tests for settings and preferences types.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import SwiftUI
|
|
@testable import Yattee
|
|
|
|
// MARK: - AppTheme Tests
|
|
|
|
@Suite("AppTheme Tests")
|
|
@MainActor
|
|
struct AppThemeTests {
|
|
|
|
@Test("AppTheme cases")
|
|
func allCases() {
|
|
let cases = AppTheme.allCases
|
|
#expect(cases.contains(.system))
|
|
#expect(cases.contains(.light))
|
|
#expect(cases.contains(.dark))
|
|
}
|
|
|
|
@Test("AppTheme colorScheme mapping")
|
|
func colorSchemeMapping() {
|
|
#expect(AppTheme.system.colorScheme == nil)
|
|
#expect(AppTheme.light.colorScheme == .light)
|
|
#expect(AppTheme.dark.colorScheme == .dark)
|
|
}
|
|
|
|
@Test("AppTheme is Codable")
|
|
func codable() throws {
|
|
for theme in AppTheme.allCases {
|
|
let encoded = try JSONEncoder().encode(theme)
|
|
let decoded = try JSONDecoder().decode(AppTheme.self, from: encoded)
|
|
#expect(theme == decoded)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - AccentColor Tests
|
|
|
|
@Suite("AccentColor Tests")
|
|
@MainActor
|
|
struct AccentColorTests {
|
|
|
|
@Test("AccentColor cases")
|
|
func allCases() {
|
|
let cases = AccentColor.allCases
|
|
#expect(cases.contains(.default))
|
|
#expect(cases.contains(.red))
|
|
#expect(cases.contains(.blue))
|
|
#expect(cases.contains(.green))
|
|
#expect(cases.contains(.purple))
|
|
}
|
|
|
|
@Test("AccentColor is Codable")
|
|
func codable() throws {
|
|
for color in AccentColor.allCases {
|
|
let encoded = try JSONEncoder().encode(color)
|
|
let decoded = try JSONDecoder().decode(AccentColor.self, from: encoded)
|
|
#expect(color == decoded)
|
|
}
|
|
}
|
|
|
|
@Test("Presets exclude custom and retired indigo")
|
|
func presetsExcludeHiddenCases() {
|
|
#expect(!AccentColor.presets.contains(.custom))
|
|
#expect(!AccentColor.presets.contains(.indigo))
|
|
#expect(AccentColor.presets.count == AccentColor.allCases.count - 2)
|
|
}
|
|
|
|
@Test("Retired indigo still decodes and resolves")
|
|
func retiredIndigoStillWorks() throws {
|
|
let decoded = try JSONDecoder().decode(AccentColor.self, from: Data("\"indigo\"".utf8))
|
|
#expect(decoded == .indigo)
|
|
#expect(AccentColor(rawValue: "indigo") == .indigo)
|
|
}
|
|
}
|
|
|
|
// MARK: - Dark Accent Color Tests
|
|
|
|
@Suite("Dark Accent Color Tests", .serialized)
|
|
@MainActor
|
|
struct DarkAccentColorTests {
|
|
|
|
private static let touchedKeys = [
|
|
SettingsKey.accentColor,
|
|
.customAccentColor,
|
|
.accentColorDark,
|
|
.customAccentColorDark,
|
|
.useSeparateDarkAccentColor,
|
|
]
|
|
|
|
private func withCleanDefaults(_ body: (SettingsManager) throws -> Void) rethrows {
|
|
let removeAll = {
|
|
for key in Self.touchedKeys {
|
|
UserDefaults.standard.removeObject(forKey: key.rawValue)
|
|
}
|
|
}
|
|
removeAll()
|
|
defer { removeAll() }
|
|
try body(SettingsManager())
|
|
}
|
|
|
|
@Test("New keys are part of the settings key set and sync globally")
|
|
func keysSyncContract() {
|
|
for key in [SettingsKey.accentColorDark, .customAccentColorDark, .useSeparateDarkAccentColor] {
|
|
#expect(SettingsKey.allCases.contains(key))
|
|
#expect(!key.isLocalOnly)
|
|
#expect(!key.isPlatformSpecific)
|
|
}
|
|
}
|
|
|
|
@Test("Separate dark accent color is disabled by default")
|
|
func separateDarkDisabledByDefault() {
|
|
withCleanDefaults { settings in
|
|
#expect(settings.useSeparateDarkAccentColor == false)
|
|
}
|
|
}
|
|
|
|
@Test("Dark accent color falls back to light selection until set")
|
|
func darkFallsBackToLight() {
|
|
withCleanDefaults { settings in
|
|
settings.accentColor = .green
|
|
#expect(settings.accentColorDark == .green)
|
|
|
|
settings.accentColorDark = .purple
|
|
#expect(settings.accentColorDark == .purple)
|
|
#expect(settings.accentColor == .green)
|
|
}
|
|
}
|
|
|
|
@Test("Custom dark accent color falls back to light custom color until set")
|
|
func customDarkFallsBackToLight() {
|
|
withCleanDefaults { settings in
|
|
settings.customAccentColor = Color(hex: "#336699")!
|
|
#expect(settings.customAccentColorDark.toHexString() == "#336699")
|
|
|
|
settings.customAccentColorDark = Color(hex: "#993366")!
|
|
#expect(settings.customAccentColorDark.toHexString() == "#993366")
|
|
#expect(settings.customAccentColor.toHexString() == "#336699")
|
|
}
|
|
}
|
|
|
|
@Test("Resolved accent color equals light resolution when toggle is off")
|
|
func resolvedMatchesLightWhenToggleOff() {
|
|
withCleanDefaults { settings in
|
|
settings.accentColor = .teal
|
|
settings.accentColorDark = .red
|
|
settings.useSeparateDarkAccentColor = false
|
|
#expect(settings.resolvedAccentColor == AccentColor.teal.color)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - VideoQuality Tests
|
|
|
|
@Suite("VideoQuality Tests")
|
|
@MainActor
|
|
struct VideoQualityTests {
|
|
|
|
@Test("VideoQuality cases")
|
|
func allCases() {
|
|
let cases = VideoQuality.allCases
|
|
#expect(cases.contains(.auto))
|
|
#expect(cases.contains(.hd4k))
|
|
#expect(cases.contains(.hd1440p))
|
|
#expect(cases.contains(.hd1080p))
|
|
#expect(cases.contains(.hd720p))
|
|
#expect(cases.contains(.sd480p))
|
|
#expect(cases.contains(.sd360p))
|
|
}
|
|
|
|
@Test("VideoQuality raw values")
|
|
func rawValues() {
|
|
#expect(VideoQuality.auto.rawValue == "auto")
|
|
#expect(VideoQuality.hd4k.rawValue == "4k")
|
|
#expect(VideoQuality.hd1440p.rawValue == "1440p")
|
|
#expect(VideoQuality.hd1080p.rawValue == "1080p")
|
|
}
|
|
|
|
@Test("VideoQuality recommendedForPlatform returns valid quality")
|
|
func recommendedForPlatform() {
|
|
let recommended = VideoQuality.recommendedForPlatform
|
|
#expect(VideoQuality.allCases.contains(recommended))
|
|
}
|
|
|
|
@Test("VideoQuality is Codable")
|
|
func codable() throws {
|
|
for quality in VideoQuality.allCases {
|
|
let encoded = try JSONEncoder().encode(quality)
|
|
let decoded = try JSONDecoder().decode(VideoQuality.self, from: encoded)
|
|
#expect(quality == decoded)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SponsorBlockCategory Tests
|
|
|
|
@Suite("SponsorBlockCategory Tests")
|
|
@MainActor
|
|
struct SponsorBlockCategoryTests {
|
|
|
|
@Test("SponsorBlockCategory cases")
|
|
func allCases() {
|
|
let cases = SponsorBlockCategory.allCases
|
|
#expect(cases.contains(.sponsor))
|
|
#expect(cases.contains(.selfpromo))
|
|
#expect(cases.contains(.interaction))
|
|
#expect(cases.contains(.intro))
|
|
#expect(cases.contains(.outro))
|
|
#expect(cases.contains(.preview))
|
|
#expect(cases.contains(.musicOfftopic))
|
|
#expect(cases.contains(.filler))
|
|
#expect(cases.contains(.highlight))
|
|
}
|
|
|
|
@Test("SponsorBlockCategory display names are not empty")
|
|
func displayNames() {
|
|
// Display names use localized strings, so we just verify they're not empty
|
|
for category in SponsorBlockCategory.allCases {
|
|
#expect(!category.displayName.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test("SponsorBlockCategory raw values")
|
|
func rawValues() {
|
|
#expect(SponsorBlockCategory.sponsor.rawValue == "sponsor")
|
|
#expect(SponsorBlockCategory.musicOfftopic.rawValue == "music_offtopic")
|
|
}
|
|
|
|
@Test("SponsorBlockCategory defaultEnabled set")
|
|
func defaultEnabled() {
|
|
let defaults = SponsorBlockCategory.defaultEnabled
|
|
#expect(defaults.contains(.sponsor))
|
|
#expect(defaults.contains(.selfpromo))
|
|
#expect(defaults.contains(.interaction))
|
|
#expect(defaults.contains(.intro))
|
|
#expect(defaults.contains(.outro))
|
|
#expect(!defaults.contains(.preview))
|
|
#expect(!defaults.contains(.musicOfftopic))
|
|
#expect(!defaults.contains(.filler))
|
|
}
|
|
|
|
@Test("SponsorBlockCategory is Codable")
|
|
func codable() throws {
|
|
for category in SponsorBlockCategory.allCases {
|
|
let encoded = try JSONEncoder().encode(category)
|
|
let decoded = try JSONDecoder().decode(SponsorBlockCategory.self, from: encoded)
|
|
#expect(category == decoded)
|
|
}
|
|
}
|
|
|
|
@Test("SponsorBlockCategory Set is Codable")
|
|
func setCodable() throws {
|
|
let categories: Set<SponsorBlockCategory> = [.sponsor, .intro, .outro]
|
|
let encoded = try JSONEncoder().encode(categories)
|
|
let decoded = try JSONDecoder().decode(Set<SponsorBlockCategory>.self, from: encoded)
|
|
#expect(categories == decoded)
|
|
}
|
|
}
|
|
|
|
// MARK: - UserAgentGenerator Tests
|
|
|
|
@Suite("UserAgentGenerator Tests")
|
|
struct UserAgentGeneratorTests {
|
|
|
|
@Test("Random user agent is not empty")
|
|
func randomNotEmpty() {
|
|
let ua = UserAgentGenerator.generateRandom()
|
|
#expect(!ua.isEmpty)
|
|
}
|
|
|
|
@Test("Random user agent starts with Mozilla")
|
|
func randomStartsWithMozilla() {
|
|
for _ in 0..<10 {
|
|
let ua = UserAgentGenerator.generateRandom()
|
|
#expect(ua.hasPrefix("Mozilla/5.0"))
|
|
}
|
|
}
|
|
|
|
@Test("Random user agent contains browser identifier")
|
|
func randomContainsBrowser() {
|
|
// Run multiple times to test randomness
|
|
var containsChrome = false
|
|
var containsFirefox = false
|
|
var containsSafari = false
|
|
var containsEdge = false
|
|
|
|
for _ in 0..<100 {
|
|
let ua = UserAgentGenerator.generateRandom()
|
|
if ua.contains("Chrome/") { containsChrome = true }
|
|
if ua.contains("Firefox/") { containsFirefox = true }
|
|
if ua.contains("Safari/") { containsSafari = true }
|
|
if ua.contains("Edg/") { containsEdge = true }
|
|
}
|
|
|
|
// At least some browser types should appear (probabilistic)
|
|
#expect(containsChrome || containsFirefox || containsSafari || containsEdge)
|
|
}
|
|
|
|
@Test("Default user agent is valid")
|
|
func defaultUserAgent() {
|
|
let ua = UserAgentGenerator.defaultUserAgent
|
|
#expect(!ua.isEmpty)
|
|
#expect(ua.hasPrefix("Mozilla/5.0"))
|
|
}
|
|
|
|
@Test("Random user agent has reasonable length")
|
|
func reasonableLength() {
|
|
for _ in 0..<10 {
|
|
let ua = UserAgentGenerator.generateRandom()
|
|
// User agents are typically 80-200 characters
|
|
#expect(ua.count > 50)
|
|
#expect(ua.count < 300)
|
|
}
|
|
}
|
|
|
|
@Test("Random user agent contains platform info")
|
|
func containsPlatformInfo() {
|
|
for _ in 0..<20 {
|
|
let ua = UserAgentGenerator.generateRandom()
|
|
// Should contain Windows, Macintosh, or similar
|
|
let containsPlatform = ua.contains("Windows") || ua.contains("Macintosh") || ua.contains("Intel Mac")
|
|
#expect(containsPlatform)
|
|
}
|
|
}
|
|
}
|
|
|