feat: configure launcher color scheme
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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)) }
|
||||
}
|
||||
}
|
||||
@@ -280,8 +280,8 @@ func inlineCodeContainerIsLimitedToItsMarkAndDisappearsWhenToggledOff() throws {
|
||||
let storage = try #require(editor.textStorage)
|
||||
let body = (editor.string as NSString).range(of: "some *literal* code 🦊")
|
||||
for index in body.location..<NSMaxRange(body) {
|
||||
#expect(storage.attribute(.foregroundColor, at: index, effectiveRange: nil) as? NSColor == SunsetPalette.accent)
|
||||
#expect(storage.attribute(.backgroundColor, at: index, effectiveRange: nil) as? NSColor == SunsetPalette.selectionFill)
|
||||
#expect(storage.attribute(.foregroundColor, at: index, effectiveRange: nil) as? NSColor == ThemePalette.accent)
|
||||
#expect(storage.attribute(.backgroundColor, at: index, effectiveRange: nil) as? NSColor == ThemePalette.selectionFill)
|
||||
}
|
||||
for token in ["Before", "after", "not inline"] {
|
||||
let index = (editor.string as NSString).range(of: token).location
|
||||
@@ -295,5 +295,5 @@ func inlineCodeContainerIsLimitedToItsMarkAndDisappearsWhenToggledOff() throws {
|
||||
#expect(editor.string == before)
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.inlineCode, at: body.location, effectiveRange: nil) as? Bool != true)
|
||||
#expect(storage.attribute(.backgroundColor, at: body.location, effectiveRange: nil) == nil)
|
||||
#expect(storage.attribute(.foregroundColor, at: body.location, effectiveRange: nil) as? NSColor == SunsetPalette.text)
|
||||
#expect(storage.attribute(.foregroundColor, at: body.location, effectiveRange: nil) as? NSColor == ThemePalette.text)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func uninstallFilteringPreservesDiscoveryStateAndCheckedFiles() throws {
|
||||
controller.applyDiscoveryResult(.failure(ScanFailure()))
|
||||
let failure = status.stringValue
|
||||
#expect(failure.contains(ScanFailure().localizedDescription))
|
||||
#expect(status.textColor == .systemRed)
|
||||
#expect(status.textColor == ThemePalette.error)
|
||||
filterBy("different")
|
||||
#expect(status.stringValue == failure)
|
||||
#expect(!remove.isEnabled)
|
||||
@@ -46,7 +46,7 @@ func uninstallFilteringPreservesDiscoveryStateAndCheckedFiles() throws {
|
||||
controller.applyDiscoveryResult(.success([candidate]))
|
||||
filterBy("")
|
||||
let counts = status.stringValue
|
||||
#expect(status.textColor == SunsetPalette.secondaryText)
|
||||
#expect(status.textColor == ThemePalette.secondaryText)
|
||||
#expect(remove.isEnabled)
|
||||
#expect(table.numberOfRows == 1)
|
||||
filterBy("no-match")
|
||||
@@ -72,7 +72,7 @@ func shortcutModifierGuidanceDoesNotAppearAsAnError() throws {
|
||||
$0.action == NSSelectorFromString("beginRecording")
|
||||
})
|
||||
let error = try #require(views.compactMap { $0 as? NSTextField }.first {
|
||||
$0.accessibilityLabel() == "Shortcut feedback"
|
||||
$0.accessibilityLabel() == "Settings feedback"
|
||||
})
|
||||
let status = try #require(views.compactMap { $0 as? NSTextField }.first {
|
||||
$0.accessibilityLabel() == "Global shortcut registration status"
|
||||
@@ -85,5 +85,5 @@ func shortcutModifierGuidanceDoesNotAppearAsAnError() throws {
|
||||
context: nil, characters: "", charactersIgnoringModifiers: "", isARepeat: false, keyCode: 55))
|
||||
recorder.flagsChanged(with: modifier)
|
||||
#expect(error.stringValue.isEmpty)
|
||||
#expect(status.textColor == SunsetPalette.secondaryText)
|
||||
#expect(status.textColor == ThemePalette.secondaryText)
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ func translationExecutionRecoversFromCredentialAndRequestFailures() async throws
|
||||
tool.translate() // No stored key.
|
||||
#expect(requests.inputs.isEmpty)
|
||||
#expect(button.isEnabled)
|
||||
#expect(status.textColor == .systemRed)
|
||||
#expect(status.textColor == ThemePalette.error)
|
||||
#expect(!openSettings.isHidden)
|
||||
field.stringValue = "typed-key"
|
||||
tool.translate() // Typing in Settings must not implicitly save on Translate.
|
||||
@@ -296,7 +296,7 @@ func translationExecutionRecoversFromCredentialAndRequestFailures() async throws
|
||||
#expect(requests.inputs[0].key == "typed-key")
|
||||
requests.pending[0].resume(throwing: CredentialFailure())
|
||||
try await waitForTranslation { button.isEnabled }
|
||||
#expect(status.textColor == .systemRed)
|
||||
#expect(status.textColor == ThemePalette.error)
|
||||
keys.denied = true
|
||||
forget.performClick(nil)
|
||||
#expect(keys.value == "typed-key")
|
||||
@@ -315,7 +315,7 @@ func translationExecutionRecoversFromCredentialAndRequestFailures() async throws
|
||||
try await waitForTranslation { requests.pending.count == 3 }
|
||||
requests.pending[2].resume(returning: "成功")
|
||||
try await waitForTranslation { output.string == "成功" }
|
||||
#expect(status.textColor != .systemRed)
|
||||
#expect(status.textColor != ThemePalette.error)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
|
||||
Reference in New Issue
Block a user