import AppKit import Testing @testable import Conjure @Test @MainActor func missingColorConfigurationUsesSunsetWithoutReadingGeneratedTheme() throws { let directory = try colorConfigurationDirectory() defer { try? FileManager.default.removeItem(at: directory) } try Data("not JSON".utf8).write(to: directory.appendingPathComponent("theme.json")) let loaded = try ColorScheme.load(from: directory) expectSameColors(loaded, ColorScheme.sunset) try Data("{}".utf8).write(to: directory.appendingPathComponent("config.json")) expectSameColors(try ColorScheme.load(from: directory), ColorScheme.sunset) try Data(#"{"theme":"sunset"}"#.utf8).write(to: directory.appendingPathComponent("config.json")) expectSameColors(try ColorScheme.load(from: directory), ColorScheme.sunset) } @Test @MainActor func partialManualColorsOverrideSunsetWithoutChangingOtherFields() throws { let directory = try colorConfigurationDirectory() defer { try? FileManager.default.removeItem(at: directory) } try Data(##"{"colors":{"accent":"#aB1234","error":"#123456"}}"##.utf8) .write(to: directory.appendingPathComponent("config.json")) var expected = ColorScheme.sunset expected.accent = NSColor(srgbRed: 171.0 / 255, green: 18.0 / 255, blue: 52.0 / 255, alpha: 1) expected.error = NSColor(srgbRed: 18.0 / 255, green: 52.0 / 255, blue: 86.0 / 255, alpha: 1) expectSameColors(try ColorScheme.load(from: directory), expected) } @Test @MainActor func tintColorsLayerBetweenDefaultsAndManualOverrides() throws { let directory = try colorConfigurationDirectory() defer { try? FileManager.default.removeItem(at: directory) } try Data(##"{"theme":"tint","colors":{"accent":"#ffffff","text":"#000000"}}"##.utf8) .write(to: directory.appendingPathComponent("config.json")) try Data(##"{"colors":{"accent":"#ff0000","background":"#123456","glow":"#00ff00"}}"##.utf8) .write(to: directory.appendingPathComponent("theme.json")) var expected = ColorScheme.sunset expected.accent = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) expected.text = NSColor(srgbRed: 0, green: 0, blue: 0, alpha: 1) expected.background = NSColor(srgbRed: 18.0 / 255, green: 52.0 / 255, blue: 86.0 / 255, alpha: 1) expected.glow = NSColor(srgbRed: 0, green: 1, blue: 0, alpha: 1) expectSameColors(try ColorScheme.load(from: directory), expected) } @Test @MainActor func malformedColorConfigurationReportsItsPathAndReason() throws { let directory = try colorConfigurationDirectory() defer { try? FileManager.default.removeItem(at: directory) } let config = directory.appendingPathComponent("config.json") let invalid: [(String, String)] = [ (##"{"colors":{"accent":"#12345678"}}"##, "colors.accent"), (##"{"colors":{"accent":"#fff"}}"##, "colors.accent"), (##"{"colors":{"accent":"#12gg56"}}"##, "colors.accent"), (##"{"colors":{"accent":"#123456"}}"##, "colors.accent"), (#"{"colors":{"accent":"123456"}}"#, "colors.accent"), (#"{"colors":{"accent":" #123456"}}"#, "colors.accent"), (#"{"colors":{"accent":42}}"#, "colors.accent"), (#"{"colors":{"accent":null}}"#, "colors.accent"), (##"{"colors":{"accnet":"#123456"}}"##, "colors.accnet"), (#"{"opacity":0.5}"#, "opacity"), (#"{"colors":[]}"#, "colors must be an object"), (#"{"colors":null}"#, "colors must be an object"), (#"{"theme":"unknown"}"#, "theme"), (#"{"theme":true}"#, "theme"), (#"{"theme":null}"#, "theme"), ("{", "Invalid JSON"), ("[]", "JSON object"), ("null", "JSON object"), ] for (json, reason) in invalid { try Data(json.utf8).write(to: config) expectColorSchemeFailure(in: directory, path: config, reason: reason) } } @Test @MainActor func selectedTintRequiresAnExistingValidGeneratedTheme() throws { let directory = try colorConfigurationDirectory() defer { try? FileManager.default.removeItem(at: directory) } let theme = directory.appendingPathComponent("theme.json") try Data(##"{"theme":"tint","colors":{"accent":"#ffffff"}}"##.utf8) .write(to: directory.appendingPathComponent("config.json")) expectColorSchemeFailure(in: directory, path: theme) let invalid: [(String, String)] = [ ("not JSON", "Invalid JSON"), ("[]", "JSON object"), (#"{"theme":"sunset"}"#, "theme"), (##"{"colors":{"accent":"#12345678"}}"##, "colors.accent"), (##"{"colors":{"glwo":"#123456"}}"##, "colors.glwo"), (#"{"colors":false}"#, "colors must be an object"), ] for (json, reason) in invalid { try Data(json.utf8).write(to: theme) expectColorSchemeFailure(in: directory, path: theme, reason: reason) } try Data(#"{"colors":{}}"#.utf8).write(to: theme) var expected = ColorScheme.sunset expected.accent = NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) expectSameColors(try ColorScheme.load(from: directory), expected) } private func colorConfigurationDirectory() throws -> URL { let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) return directory } @MainActor private func expectSameColors(_ actual: ColorScheme, _ expected: ColorScheme) { #expect(actual.background == expected.background) #expect(actual.text == expected.text) #expect(actual.secondaryText == expected.secondaryText) #expect(actual.accent == expected.accent) #expect(actual.glow == expected.glow) #expect(actual.keyBackground == expected.keyBackground) #expect(actual.error == expected.error) } @MainActor private func expectColorSchemeFailure(in directory: URL, path: URL, reason: String? = nil) { do { _ = try ColorScheme.load(from: directory) Issue.record("Expected invalid color configuration to throw: \(path.path)") } catch { #expect(error is any LocalizedError) #expect(error.localizedDescription.contains(path.path)) if let reason { #expect(error.localizedDescription.contains(reason)) } } }