// ThemeTests.swift // Unit tests for TerminalTheme, ThemeColor, ThemeStore, and FontStore. // // These tests exercise built-in theme validity and store selection logic. // No UIKit rendering or simulator is required for the pure-struct tests; // ThemeStore / FontStore tests require @MainActor and a run loop. import XCTest @testable import piRemote final class ThemeTests: XCTestCase { // ========================================================================= // MARK: 1. TerminalTheme — built-in theme invariants // ========================================================================= func testDark_hasExactly16AnsiColors() { XCTAssertEqual(TerminalTheme.dark.ansiColors.count, 16, ".dark must have exactly 16 ANSI color entries") } func testGitHub_hasExactly16AnsiColors() { XCTAssertEqual(TerminalTheme.github.ansiColors.count, 16, ".github must have exactly 16 ANSI color entries") } // ========================================================================= // MARK: 2. ThemeColor values are in the 0–255 range // ========================================================================= // ThemeColor stores UInt8 components, so they are structurally bounded to // 0–255. These tests make the contract explicit and guard against future // refactors that might widen the type. func testDark_allAnsiColorComponents_inRange() { for (index, color) in TerminalTheme.dark.ansiColors.enumerated() { assertColorInRange(color, label: "dark[\(index)]") } assertColorInRange(TerminalTheme.dark.foreground, label: "dark.foreground") assertColorInRange(TerminalTheme.dark.background, label: "dark.background") assertColorInRange(TerminalTheme.dark.cursor, label: "dark.cursor") } func testGitHub_allAnsiColorComponents_inRange() { for (index, color) in TerminalTheme.github.ansiColors.enumerated() { assertColorInRange(color, label: "github[\(index)]") } assertColorInRange(TerminalTheme.github.foreground, label: "github.foreground") assertColorInRange(TerminalTheme.github.background, label: "github.background") assertColorInRange(TerminalTheme.github.cursor, label: "github.cursor") } private func assertColorInRange(_ color: ThemeColor, label: String) { // UInt8 is intrinsically 0–255; the assertions below make the expectation readable. XCTAssertLessThanOrEqual(color.r, 255, "\(label).r out of range") XCTAssertLessThanOrEqual(color.g, 255, "\(label).g out of range") XCTAssertLessThanOrEqual(color.b, 255, "\(label).b out of range") } // ========================================================================= // MARK: 3. .dark and .github are distinct themes // ========================================================================= func testDarkAndGitHub_haveDistinctIds() { XCTAssertNotEqual(TerminalTheme.dark.id, TerminalTheme.github.id, "Built-in themes must have unique IDs") } func testDarkAndGitHub_haveDistinctBackgrounds() { // "Dark (Hacker)" has a pure-black background; GitHub Dark does not. XCTAssertNotEqual(TerminalTheme.dark.background, TerminalTheme.github.background, "Background colors must differ between dark and github themes") } func testDarkAndGitHub_areNotEqual() { XCTAssertNotEqual(TerminalTheme.dark, TerminalTheme.github, ".dark and .github must be distinct themes") } // ========================================================================= // MARK: 4. TerminalTheme identities // ========================================================================= func testDark_hasExpectedId() { XCTAssertEqual(TerminalTheme.dark.id, "dark") } func testGitHub_hasExpectedId() { XCTAssertEqual(TerminalTheme.github.id, "github") } func testDark_backgroundIsBlack() { let bg = TerminalTheme.dark.background XCTAssertEqual(bg.r, 0x00, "Dark background R must be 0x00") XCTAssertEqual(bg.g, 0x00, "Dark background G must be 0x00") XCTAssertEqual(bg.b, 0x00, "Dark background B must be 0x00") } func testGitHub_backgroundMatchesSpec() { // GitHub Dark background: #0d1117 let bg = TerminalTheme.github.background XCTAssertEqual(bg.r, 0x0D, "GitHub background R must be 0x0D") XCTAssertEqual(bg.g, 0x11, "GitHub background G must be 0x11") XCTAssertEqual(bg.b, 0x17, "GitHub background B must be 0x17") } // ========================================================================= // MARK: 5. TerminalTheme.toSwiftTermColor conversion // ========================================================================= func testToSwiftTermColor_white_mapsTo65535() { // 255 × 257 = 65535 (= UInt16.max) let white = ThemeColor(r: 255, g: 255, b: 255) let stColor = TerminalTheme.dark.toSwiftTermColor(white) XCTAssertEqual(stColor.red, 65535) XCTAssertEqual(stColor.green, 65535) XCTAssertEqual(stColor.blue, 65535) } func testToSwiftTermColor_black_mapsToZero() { let black = ThemeColor(r: 0, g: 0, b: 0) let stColor = TerminalTheme.dark.toSwiftTermColor(black) XCTAssertEqual(stColor.red, 0) XCTAssertEqual(stColor.green, 0) XCTAssertEqual(stColor.blue, 0) } func testSwiftTermAnsiColors_has16Entries() { XCTAssertEqual(TerminalTheme.dark.swiftTermAnsiColors.count, 16) XCTAssertEqual(TerminalTheme.github.swiftTermAnsiColors.count, 16) } // ========================================================================= // MARK: 6. ThemeStore — selection and persistence // ========================================================================= @MainActor func testThemeStore_available_containsBothBuiltins() { let store = ThemeStore.shared let ids = store.available.map(\.id) XCTAssertTrue(ids.contains("dark"), "Available themes must include 'dark'") XCTAssertTrue(ids.contains("github"), "Available themes must include 'github'") } @MainActor func testThemeStore_select_updatesCurrent() { let store = ThemeStore.shared let before = store.current // Select whichever theme is NOT currently active. let next = store.available.first(where: { $0.id != before.id })! store.select(next) XCTAssertEqual(store.current.id, next.id, "select(_:) must update current immediately") // Restore original selection so we don't bleed state into other tests. store.select(before) } // ========================================================================= // MARK: 7. TerminalTheme Codable round-trip // ========================================================================= func testTerminalTheme_codable_roundTrips() throws { let theme = TerminalTheme.dark let data = try JSONEncoder().encode(theme) let loaded = try JSONDecoder().decode(TerminalTheme.self, from: data) XCTAssertEqual(loaded, theme, "TerminalTheme must survive a JSON round-trip") } }