import AppKit import ConjureKit import Testing @testable import Conjure @Test @MainActor func launcherPositionKeepsTheDraggedCenterAcrossCommandsSizesAndRelaunch() throws { _ = NSApplication.shared let suite = "LauncherPosition.\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: suite)) let directory = FileManager.default.temporaryDirectory.appendingPathComponent(suite) defer { defaults.removePersistentDomain(forName: suite) try? FileManager.default.removeItem(at: directory) } func makeLauncher() -> LauncherWindowController { LauncherWindowController( history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }), defaults: defaults, notesDirectory: directory) } func center(_ frame: CGRect) -> CGPoint { CGPoint(x: frame.midX, y: frame.midY) } let launcher = makeLauncher() let window = try #require(launcher.window as? LauncherWindow) defer { launcher.dismiss(restoringFocus: false) } let display = try #require(window.screen).visibleFrame let initialCenter = CGPoint(x: display.midX.rounded(.down), y: display.midY.rounded(.down)) window.setFrameOrigin(CGPoint(x: initialCenter.x - window.frame.width / 2, y: initialCenter.y - window.frame.height / 2)) // Open from the launcher rather than presenting again (which restores the saved position). func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) } let content = try #require(window.contentView) let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first) search.stringValue = "notes" launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) #expect(launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))) try #require(center(window.frame) == initialCenter) let draggedCenter = CGPoint(x: initialCenter.x + 30, y: initialCenter.y - 20) window.setFrameOrigin(CGPoint(x: draggedCenter.x - window.frame.width / 2, y: draggedCenter.y - window.frame.height / 2)) window.dragEnded?() // Back must use the moved Notes center, not the launcher frame captured on entry. let back = try #require(descendants(content).compactMap { $0 as? NSButton }.first { $0.accessibilityLabel() == "Back to applications" }) back.performClick(nil) #expect(center(window.frame) == draggedCenter) for command: Application.Command in [.emoji, .translate, .notes] { launcher.present(command: command) #expect(center(window.frame) == draggedCenter) } var resized = window.frame resized.size = CGSize(width: 620, height: 330) resized.origin = CGPoint(x: draggedCenter.x - resized.width / 2, y: draggedCenter.y - resized.height / 2) window.setFrame(resized, display: true) window.dragEnded?() launcher.dismiss(restoringFocus: false) let reopened = makeLauncher() defer { reopened.dismiss(restoringFocus: false) } for command: Application.Command in [.notes, .emoji, .translate] { reopened.present(command: command) #expect(center(try #require(reopened.window).frame) == draggedCenter) } // A compact Notes window can fit where the hidden launcher-sized frame cannot. reopened.present(command: .notes) let reopenedWindow = try #require(reopened.window as? LauncherWindow) let edgeCenter = CGPoint(x: display.minX + 320, y: initialCenter.y) reopenedWindow.setFrameOrigin(CGPoint(x: edgeCenter.x - reopenedWindow.frame.width / 2, y: edgeCenter.y - reopenedWindow.frame.height / 2)) reopenedWindow.dragEnded?() reopened.dismiss(restoringFocus: false) reopened.present(command: .notes) #expect(center(reopenedWindow.frame) == edgeCenter) }