import AppKit import ConjureKit import Testing @testable import Conjure @Test @MainActor func livePaletteRepaintsNotesWithoutChangingContentSelectionOrUndo() throws { _ = NSApplication.shared let originalPalette = ThemePalette.loaded defer { ThemePalette.loaded = originalPalette NotificationCenter.default.post(name: ThemePalette.didChangeNotification, object: nil) } let directory = try liveViewThemeDirectory() defer { try? FileManager.default.removeItem(at: directory) } ThemePalette.reload(from: directory) let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 500, height: 300)) editor.isRichText = true let delegate = LiveThemeTextDelegate() editor.delegate = delegate let document = NSMutableAttributedString(string: "plain code\nlet answer = 42\n") let inline = NSRange(location: 6, length: 4) let code = (document.string as NSString).paragraphRange(for: NSRange(location: 11, length: 0)) document.addAttribute(NoteAttributes.inlineCode, value: true, range: inline) document.addAttribute(NoteAttributes.bold, value: true, range: NSRange(location: 0, length: 5)) 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: 0, length: 0)) let selection = NSRange(location: 2, length: 3) editor.setSelectedRange(selection) let contents = editor.string let grouping = editor.undoManager?.groupingLevel let oldAccent = editor.insertionPointColor let codeColor = editor.textStorage?.attribute(.foregroundColor, at: code.location + 1, effectiveRange: nil) as? NSColor let boldFont = editor.textStorage?.attribute(.font, at: 1, effectiveRange: nil) as? NSFont let changeCount = delegate.changeCount try writeLiveViewTheme(in: directory) #expect(ThemePalette.reload(from: directory)) #expect(editor.insertionPointColor == ThemePalette.accent) #expect(editor.insertionPointColor != oldAccent) #expect(editor.selectedTextAttributes[.backgroundColor] as? NSColor == ThemePalette.selectionFill) #expect(editor.textStorage?.attribute(.foregroundColor, at: 1, effectiveRange: nil) as? NSColor == ThemePalette.text) #expect(editor.textStorage?.attribute(.foregroundColor, at: inline.location + 1, effectiveRange: nil) as? NSColor == ThemePalette.accent) #expect(editor.textStorage?.attribute(.backgroundColor, at: inline.location + 1, effectiveRange: nil) as? NSColor == ThemePalette.selectionFill) #expect(editor.textStorage?.attribute(.backgroundColor, at: code.location + 1, effectiveRange: nil) as? NSColor == ThemePalette.selectionFill) #expect(editor.textStorage?.attribute(.foregroundColor, at: code.location + 1, effectiveRange: nil) as? NSColor == codeColor) #expect(editor.textStorage?.attribute(.font, at: 1, effectiveRange: nil) as? NSFont == boldFont) #expect(editor.textStorage?.attribute(NoteAttributes.inlineCode, at: inline.location + 1, effectiveRange: nil) as? Bool == true) #expect(editor.textStorage?.attribute(NoteAttributes.language, at: code.location + 1, effectiveRange: nil) as? String == "swift") #expect(editor.string == contents) #expect(editor.selectedRange() == selection) #expect(editor.undoManager?.groupingLevel == grouping) #expect(delegate.changeCount == changeCount) editor.endInsertionSession() #expect(editor.undoManager?.canUndo == true) editor.undoManager?.undo() #expect(editor.string == document.string) } @Test @MainActor func livePaletteRepaintsLauncherAndFavoriteBadgesWithoutResettingSearch() throws { _ = NSApplication.shared let originalPalette = ThemePalette.loaded defer { ThemePalette.loaded = originalPalette NotificationCenter.default.post(name: ThemePalette.didChangeNotification, object: nil) } let directory = try liveViewThemeDirectory() defer { try? FileManager.default.removeItem(at: directory) } ThemePalette.reload(from: directory) let suite = "Conjure.LiveThemeViews.\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: suite)) defer { defaults.removePersistentDomain(forName: suite) } 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) let views = liveThemedDescendants(content) let search = try #require(views.compactMap { $0 as? NSSearchField }.first) let table = try #require(views.compactMap { $0 as? NSTableView }.first) search.stringValue = "notes" launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) let rows = table.numberOfRows let selected = table.selectedRowIndexes let placeholder = search.placeholderAttributedString?.string window.makeFirstResponder(search) let fieldEditor = try #require(search.currentEditor() as? NSTextView) fieldEditor.setSelectedRange(NSRange(location: 1, length: 2)) let selection = fieldEditor.selectedRange() let pill = try #require(views.compactMap { $0 as? ActionPill }.first) let oldBorder = pill.layer?.borderColor let cell = ApplicationCell(frame: .zero) cell.kindLabel.attributedStringValue = ApplicationCell.favoriteCommandKind cell.setNumberHint("3", animated: false) let numberHint = try #require(liveThemedDescendants(cell.numberHint).compactMap { $0 as? NSTextField }.first) try writeLiveViewTheme(in: directory) #expect(ThemePalette.reload(from: directory)) #expect(search.stringValue == "notes") #expect(search.placeholderAttributedString?.string == placeholder) #expect(table.numberOfRows == rows) #expect(table.selectedRowIndexes == selected) #expect(search.currentEditor() === fieldEditor) #expect(fieldEditor.selectedRange() == selection) #expect(search.textColor == ThemePalette.text) #expect(fieldEditor.insertionPointColor == ThemePalette.accent) #expect(search.placeholderAttributedString?.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor == ThemePalette.secondaryText) #expect(pill.layer?.borderColor == ThemePalette.accent.withAlphaComponent(0.35).cgColor) #expect(pill.layer?.borderColor != oldBorder) #expect(cell.kindLabel.attributedStringValue.attribute(.foregroundColor, at: cell.kindLabel.attributedStringValue.length - 1, effectiveRange: nil) as? NSColor == ThemePalette.secondaryText) #expect(!cell.numberHint.isHidden) #expect(numberHint.stringValue == "3") #expect(numberHint.layer?.backgroundColor == ThemePalette.keyBackground.cgColor) } private func liveViewThemeDirectory() throws -> URL { let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) return directory } private func writeLiveViewTheme(in directory: URL) throws { try Data(""" [colors] background = "#ffffff" text = "#112233" secondaryText = "#445566" accent = "#1234ab" glow = "#abcdef" keyBackground = "#778899" error = "#aa0011" """.utf8).write(to: directory.appendingPathComponent("config.toml")) } @MainActor private func liveThemedDescendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap { liveThemedDescendants($0) } } @MainActor private final class LiveThemeTextDelegate: NSObject, NSTextViewDelegate { var changeCount = 0 func textDidChange(_ notification: Notification) { changeCount += 1 } }