import AppKit import ConjureKit import Testing @testable import Conjure @Test @MainActor func existingWindowsAndNotesFollowPaletteAppearanceWithoutResettingState() throws { _ = NSApplication.shared let originalPalette = ThemePalette.loaded let originalAppearance = NSApp.appearance defer { NSApp.appearance = originalAppearance ThemePalette.loaded = originalPalette NotificationCenter.default.post(name: ThemePalette.didChangeNotification, object: nil) } let suite = "Conjure.LightAppearanceViews.\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: suite)) let directory = FileManager.default.temporaryDirectory.appendingPathComponent(suite) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) defer { defaults.removePersistentDomain(forName: suite) try? FileManager.default.removeItem(at: directory) } try Data("theme = \"tint\"".utf8).write(to: directory.appendingPathComponent("config.toml")) func writePalette(light: Bool) throws { try Data(""" {"variant":"\(light ? "light" : "dark")","colors":{ "background":"\(light ? "#ffffff" : "#201714")", "text":"\(light ? "#112233" : "#ffe3b5")", "secondaryText":"\(light ? "#667788" : "#d9c7a6")" }} """.utf8).write(to: directory.appendingPathComponent("theme.json")) } func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) } func appearanceName(_ appearance: NSAppearance) -> NSAppearance.Name? { appearance.bestMatch(from: [.aqua, .darkAqua]) } try writePalette(light: false) ThemePalette.reload(from: directory) NSApp.appearance = NSAppearance(named: .aqua) let launcher = LauncherWindowController( history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }), defaults: defaults, notesDirectory: directory.appendingPathComponent("notes")) let window = try #require(launcher.window) let content = try #require(window.contentView) defer { window.orderOut(nil) } #expect(appearanceName(window.effectiveAppearance) == .darkAqua) let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first) search.stringValue = "notes" launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:))) let editor = try #require(descendants(content).compactMap { $0 as? NotesTextView }.first) let document = NSMutableAttributedString(string: "let answer = 42\nplain") let code = NSRange(location: 0, length: 16) document.addAttribute(NoteAttributes.block, value: "code", range: code) document.addAttribute(NoteAttributes.language, value: "swift", range: code) editor.setDocumentContents(document) editor.beginInsertionSession() editor.insertText("!", replacementRange: NSRange(location: document.length, length: 0)) editor.endInsertionSession() let selection = NSRange(location: 1, length: 2) editor.setSelectedRange(selection) let text = editor.string let undoGrouping = editor.undoManager?.groupingLevel let frame = window.frame let style = window.styleMask let settings = SettingsWindowController(shortcuts: GlobalShortcuts(defaults: defaults) { _ in }) let settingsWindow = try #require(settings.window) defer { settingsWindow.close() } let settingsContent = try #require(settingsWindow.contentView) let destination = try #require(descendants(settingsContent).compactMap { $0 as? NSPopUpButton }.first) destination.selectItem(at: 2) let credential = try #require(descendants(settingsContent).compactMap { $0 as? NSSecureTextField }.first) credential.stringValue = "unsaved-value" let anchor = try #require(descendants(content).compactMap { $0 as? NSButton }.first { $0.title == "Actions" && $0.target is NotesController }) let notes = try #require(anchor.target as? NotesController) let popup = ApplicationActionsController() popup.showLanguages([("en", "English"), ("ja", "Japanese")], selected: "en", source: true, relativeTo: anchor) defer { popup.close(animated: false) } let popupSearch = try #require(descendants(popup.view).compactMap { $0 as? NSSearchField }.first) popupSearch.stringValue = "Eng" popup.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: popupSearch)) let table = try #require(descendants(popup.view).compactMap { $0 as? NSTableView }.first) let rows = table.numberOfRows let selectedRows = table.selectedRowIndexes let material = try #require(popup.view.subviews.compactMap { $0 as? NSVisualEffectView }.first) #expect(appearanceName(editor.effectiveAppearance) == .darkAqua) #expect(appearanceName(settingsWindow.effectiveAppearance) == .darkAqua) #expect(appearanceName(material.effectiveAppearance) == .darkAqua) // Deliberately oppose the light palette: app/system inheritance is not the contract. NSApp.appearance = NSAppearance(named: .darkAqua) for light in [true, false] { try writePalette(light: light) #expect(ThemePalette.reload(from: directory)) let expected: NSAppearance.Name = light ? .aqua : .darkAqua for appearance in [window.effectiveAppearance, content.effectiveAppearance, editor.effectiveAppearance, settingsWindow.effectiveAppearance, settingsContent.effectiveAppearance, popup.view.effectiveAppearance, material.effectiveAppearance] { #expect(appearanceName(appearance) == expected) } #expect(launcher.window === window) #expect(window.frame == frame) #expect(window.styleMask == style) #expect(editor.string == text) #expect(editor.selectedRange() == selection) #expect(editor.undoManager?.groupingLevel == undoGrouping) #expect(editor.undoManager?.canUndo == true) #expect(editor.textStorage?.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) as? String == "code") #expect(editor.textStorage?.attribute(NoteAttributes.language, at: 0, effectiveRange: nil) as? String == "swift") let keyword = try #require((editor.textStorage?.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor)?.usingColorSpace(.sRGB)) let rgb: (CGFloat, CGFloat, CGFloat) = light ? (166, 38, 164) : (198, 120, 221) #expect(abs(keyword.redComponent * 255 - rgb.0) < 1) #expect(abs(keyword.greenComponent * 255 - rgb.1) < 1) #expect(abs(keyword.blueComponent * 255 - rgb.2) < 1) #expect(destination.indexOfSelectedItem == 2) #expect(credential.stringValue == "unsaved-value") #expect(popup.isShown) #expect(popupSearch.stringValue == "Eng") #expect(table.numberOfRows == rows) #expect(table.selectedRowIndexes == selectedRows) #expect(material.layer?.borderColor == ThemePalette.secondaryText.withAlphaComponent(0.22).cgColor) #expect(material.layer?.borderColor?.alpha == 0.22) } popup.close(animated: false) editor.undoManager?.undo() #expect(editor.string == document.string) try writePalette(light: true) #expect(ThemePalette.reload(from: directory)) notes.goBack() #expect(window.styleMask == [.borderless]) #expect(appearanceName(window.effectiveAppearance) == .aqua) #expect(appearanceName(content.effectiveAppearance) == .aqua) #expect(search.stringValue == "notes") }