import AppKit import ConjureKit import Testing @testable import Conjure @Test @MainActor func emojiPickerNavigatesCopiesAndRemembersRecentSelections() throws { _ = NSApplication.shared let suite = "ConjureEmoji.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! let pasteboard = NSPasteboard.withUniqueName() defer { defaults.removePersistentDomain(forName: suite) pasteboard.releaseGlobally() } var query = "face" var closed = false var pasteRequests = 0 let picker = EmojiPickerController(defaults: defaults, pasteboard: pasteboard) picker.input = { query } picker.closed = { closed = true } picker.pasteRequested = { #expect(pasteboard.string(forType: .string) == EmojiCatalog.search(query)[1].value) pasteRequests += 1 } _ = picker.view let matches = EmojiCatalog.search(query) #expect(picker.handle(#selector(NSResponder.moveRight(_:)))) #expect(picker.handle(#selector(NSResponder.insertNewline(_:)))) #expect(pasteboard.string(forType: .string) == matches[1].value) #expect(!closed) #expect(pasteRequests == 1) #expect(defaults.stringArray(forKey: "recentEmoji") == [matches[1].value]) closed = false query = "no-such-emoji" picker.inputChanged() picker.copyAndPasteSelection() #expect(!closed) #expect(pasteboard.string(forType: .string) == matches[1].value) #expect(pasteRequests == 1) query = "👩‍💻" picker.inputChanged() picker.copySelection() #expect(pasteboard.string(forType: .string) == "👩‍💻") #expect(pasteRequests == 1) let reopened = EmojiPickerController(defaults: defaults, pasteboard: pasteboard) _ = reopened.view reopened.copySelection() #expect(pasteboard.string(forType: .string) == "👩‍💻") #expect(defaults.stringArray(forKey: "recentEmoji") == ["👩‍💻", matches[1].value]) } @Test(arguments: [false, true], [false, true]) @MainActor func emojiKeyboardNavigationKeepsSelectionVisible(controlKeys: Bool, favorites: Bool) throws { _ = NSApplication.shared let suite = "ConjureEmojiScroll.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! let pasteboard = NSPasteboard.withUniqueName() defer { defaults.removePersistentDomain(forName: suite); pasteboard.releaseGlobally() } let favored = favorites ? Array(EmojiCatalog.all.suffix(3)) : [] defaults.set(favored.map(\.value), forKey: "favoriteEmoji") let ordered = favored + EmojiCatalog.all.filter { emoji in !favored.contains { $0.value == emoji.value } } let picker = EmojiPickerController(defaults: defaults, pasteboard: pasteboard) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 400), styleMask: [.borderless], backing: .buffered, defer: false) window.contentView = picker.view picker.view.layoutSubtreeIfNeeded() let scroll = try #require(picker.view.subviews.compactMap { $0 as? NSScrollView }.first) let grid = try #require(scroll.documentView as? NSCollectionView) func expectVisible() throws { picker.copySelection() let index = try #require(ordered.firstIndex { $0.value == pasteboard.string(forType: .string) }) let path = favorites && index >= favored.count ? IndexPath(item: index - favored.count, section: 1) : IndexPath(item: index, section: 0) let frame = try #require(grid.collectionViewLayout?.layoutAttributesForItem(at: path)?.frame) try #require(scroll.documentVisibleRect.contains(frame)) } for down in [true, false] { for _ in 0..<12 { if controlKeys { #expect(picker.handleControlKey(down ? "j" : "k")) } else { #expect(picker.handle(down ? #selector(NSResponder.moveDown(_:)) : #selector(NSResponder.moveUp(_:)))) } try expectVisible() } } for right in [true, false] { for _ in 0..<80 { if controlKeys { #expect(picker.handleControlKey(right ? "l" : "h")) } else { #expect(picker.handle(right ? #selector(NSResponder.moveRight(_:)) : #selector(NSResponder.moveLeft(_:)))) } try expectVisible() } } } @Test @MainActor func emojiActionsDefaultToPasteAndKeepCopyOnlyAvailable() throws { _ = NSApplication.shared let suite = "ConjureEmojiActions.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! let pasteboard = NSPasteboard.withUniqueName() defer { defaults.removePersistentDomain(forName: suite); pasteboard.releaseGlobally() } let picker = EmojiPickerController(defaults: defaults, pasteboard: pasteboard) picker.input = { "👩‍💻" } var pasteRequests = 0 picker.pasteRequested = { pasteRequests += 1 } let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 400), styleMask: [.borderless], backing: .buffered, defer: false) window.contentView = picker.view picker.showActions() #expect(picker.handle(#selector(NSResponder.insertNewline(_:)))) #expect(pasteboard.string(forType: .string) == "👩‍💻") #expect(pasteRequests == 1) pasteboard.clearContents() picker.showActions() #expect(picker.handle(#selector(NSResponder.moveDown(_:)))) #expect(picker.handle(#selector(NSResponder.insertNewline(_:)))) #expect(pasteboard.string(forType: .string) == "👩‍💻") #expect(pasteRequests == 1) picker.showActions() #expect(picker.handle(#selector(NSResponder.moveDown(_:)))) #expect(picker.handle(#selector(NSResponder.moveDown(_:)))) #expect(picker.handle(#selector(NSResponder.insertNewline(_:)))) #expect(defaults.stringArray(forKey: "favoriteEmoji") == ["👩‍💻"]) #expect(pasteRequests == 1) } @Test @MainActor func closingEmojiRemovesActionsEvenDuringBackAnimation() throws { _ = NSApplication.shared let suite = "ConjureEmojiLifetime.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! let pasteboard = NSPasteboard.withUniqueName() defer { defaults.removePersistentDomain(forName: suite); pasteboard.releaseGlobally() } let picker = EmojiPickerController(defaults: defaults, pasteboard: pasteboard) picker.input = { "👩‍💻" } let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 400), styleMask: [.borderless], backing: .buffered, defer: false) let host = try #require(window.contentView) host.addSubview(picker.view) picker.showActions() let overlay = try #require(host.subviews.first { $0 !== picker.view }) picker.goBack() var closed = false picker.closed = { closed = true } picker.close() #expect(closed) #expect(overlay.superview == nil) } @Test(arguments: ["replace", "dismiss", "copy", "paste"]) @MainActor func commandFullExitRemovesEmojiAndItsActions(_ exit: String) throws { _ = NSApplication.shared let suite = "ConjureCommandExit.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! let pasteboard = NSPasteboard.withUniqueName() defer { defaults.removePersistentDomain(forName: suite); pasteboard.releaseGlobally() } let launcher = LauncherWindowController( history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }), defaults: defaults, pasteboard: pasteboard) let window = try #require(launcher.window) defer { window.orderOut(nil) } let content = try #require(window.contentView) func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) } let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first) search.stringValue = "emoji" launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:))) search.stringValue = "👩‍💻" launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) window.orderFront(nil) let button = try #require(descendants(content).compactMap { $0 as? NSButton }.first { $0.title == "Actions" && $0.target is EmojiPickerController }) let existing = Set(content.subviews.map(ObjectIdentifier.init)) (button.target as? EmojiPickerController)?.showActions() let overlay = try #require(content.subviews.last { !existing.contains(ObjectIdentifier($0)) }) switch exit { case "replace": launcher.present(command: .translate) case "dismiss": window.performClose(nil) case "copy": (button.target as? EmojiPickerController)?.copySelection() default: _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:))) } #expect(overlay.superview == nil) #expect(!descendants(content).contains { $0 is NSCollectionView }) if exit == "replace" { #expect(search.accessibilityLabel() == "English text to translate into Japanese") #expect(search.stringValue.isEmpty) } else { #expect(!window.isVisible) #expect(search.stringValue == "emoji") #expect(search.accessibilityLabel() == "Search applications, calculate, or convert units and currencies") } if exit == "copy" || exit == "paste" { #expect(pasteboard.string(forType: .string) == "👩‍💻") #expect(defaults.stringArray(forKey: "recentEmoji") == ["👩‍💻"]) } } @Test @MainActor func emojiCommandRestoresLauncherSearchOnEscape() throws { _ = NSApplication.shared let suite = "ConjureEmojiCommand.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let history = LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }) let launcher = LauncherWindowController(history: history, defaults: defaults) let content = try #require(launcher.window?.contentView) func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) } let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first) search.stringValue = "emoji" launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:))) let grid = try #require(descendants(content).compactMap { $0 as? NSCollectionView }.first) search.stringValue = ":woman_technologist:" launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) #expect(grid.numberOfItems(inSection: 0) == 1) #expect((grid.dataSource?.collectionView(grid, itemForRepresentedObjectAt: IndexPath(item: 0, section: 0)).view as? NSButton)?.title == "👩‍💻") let actionsButton = try #require(descendants(content).compactMap { $0 as? NSButton }.first { $0.title == "Actions" && $0.target is EmojiPickerController }) (actionsButton.target as? EmojiPickerController)?.showActions() _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:))) #expect(descendants(content).contains { $0 === grid }) #expect(search.stringValue == ":woman_technologist:") let copyButton = try #require(descendants(content).compactMap { $0 as? NSButton }.first { $0.title == "Copy and Paste" }) #expect(copyButton.keyEquivalent == "\r") _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:))) #expect(!descendants(content).contains { $0 === grid }) #expect(search.stringValue == "emoji") let table = try #require(descendants(content).compactMap { $0 as? NSTableView }.first) #expect(table.numberOfRows == 1) } @Test @MainActor func emojiFavoritesPersistAndNavigationCrossesTheSpacingBreak() throws { _ = NSApplication.shared let suite = "ConjureEmojiFavorites.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! let pasteboard = NSPasteboard.withUniqueName() defer { defaults.removePersistentDomain(forName: suite); pasteboard.releaseGlobally() } var query = "👩‍💻" let picker = EmojiPickerController(defaults: defaults, pasteboard: pasteboard) picker.input = { query } picker.view.frame = NSRect(x: 0, y: 0, width: 750, height: 400) picker.toggleFavorite() #expect(defaults.stringArray(forKey: "favoriteEmoji") == ["👩‍💻"]) query = "" picker.inputChanged() picker.view.layoutSubtreeIfNeeded() let grid = try #require(picker.view.subviews.compactMap { $0 as? NSScrollView }.first?.documentView as? NSCollectionView) #expect(grid.numberOfSections == 2) #expect(grid.numberOfItems(inSection: 0) == 1) #expect(picker.handleControlKey("j")) picker.copySelection() #expect(pasteboard.string(forType: .string) == EmojiCatalog.all[0].value) #expect(picker.handleControlKey("k")) picker.copySelection() #expect(pasteboard.string(forType: .string) == "👩‍💻") #expect(picker.handleControlKey("l")) #expect(picker.handleControlKey("l")) #expect(picker.handleControlKey("h")) picker.copySelection() #expect(pasteboard.string(forType: .string) == EmojiCatalog.all[0].value) #expect(picker.handleControlKey("k")) picker.toggleFavorite() #expect(defaults.stringArray(forKey: "favoriteEmoji") == []) #expect(grid.numberOfSections == 1) query = "👩‍💻" picker.inputChanged() picker.toggleFavorite() let reopened = EmojiPickerController(defaults: defaults, pasteboard: pasteboard) _ = reopened.view reopened.copySelection() #expect(pasteboard.string(forType: .string) == "👩‍💻") }