PlatformCoverageTests.swift

4.9 KB · raw

1import AppKit2import Testing34@testable import Haven56@MainActor7private final class PopoverStub: NSPopover {8  var stubShown = false9  var showCount = 010  var closeCount = 011  override var isShown: Bool { stubShown }12  override func show(13    relativeTo positioningRect: NSRect, of positioningView: NSView,14    preferredEdge: NSRectEdge15  ) {16    stubShown = true17    showCount += 118  }19  override func performClose(_ sender: Any?) {20    stubShown = false21    closeCount += 122  }23}2425@MainActor26private final class OpenPanelStub: NSOpenPanel {27  var completion: ((NSApplication.ModalResponse) -> Void)?2829  override func begin(30    completionHandler handler: @escaping (NSApplication.ModalResponse) -> Void31  ) {32    completion = handler33  }34}3536@Suite("AppKit adapters", .serialized)37@MainActor38struct PlatformCoverageTests {39  @Test("Application delegate creates the status item and menu controller")40  func appDelegateLaunch() {41    let delegate = AppDelegate()4243    delegate.applicationDidFinishLaunching(44      Notification(name: NSApplication.didFinishLaunchingNotification)45    )4647    #expect(delegate.statusItem != nil)48    #expect(delegate.menuController != nil)49    #expect(delegate.statusItem.button?.image != nil)50  }5152  @Test("Menu controller exercises popover and modal lifecycle")53  func menuControllerLifecycle() {54    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)55    let popover = PopoverStub()56    let controller = MenuController(statusItem: statusItem, popover: popover)5758    #expect(MenuController.shared === controller)59    controller.beginModalPresentation()60    #expect(controller.popover.behavior == .applicationDefined)61    controller.endModalPresentation()62    #expect(controller.popover.behavior == .transient)6364    popover.stubShown = true65    controller.endModalPresentation()66    controller.eventMonitor?.handler(nil)67    popover.stubShown = true68    controller.togglePopover(nil)6970    controller.showPopover(sender: nil)71    controller.togglePopover(nil)72    controller.togglePopover(nil)73    controller.closePopover(sender: nil)74    controller.togglePopover(nil)75    controller.closePopover(sender: nil)76    #expect(popover.showCount > 0)77    #expect(popover.closeCount > 0)7879    NSStatusBar.system.removeStatusItem(statusItem)80  }8182  @Test("Event monitor start and stop are idempotent")83  func eventMonitorLifecycle() {84    var eventSeen = false85    let monitor = EventMonitor(mask: [.leftMouseDown]) { _ in eventSeen = true }8687    monitor.start()88    monitor.stop()89    monitor.stop()9091    #expect(!eventSeen)92  }9394  @Test("Repeated start does not leak AppKit event monitors")95  func eventMonitorRepeatedStartDoesNotLeak() {96    final class RegistrationCounter {97      var adds = 098      var removes = 099    }100    let counter = RegistrationCounter()101    let opaqueHandle = NSObject()102    let monitor = EventMonitor(103      mask: [.leftMouseDown],104      handler: { _ in },105      addMonitor: { _, _ in106        counter.adds += 1107        return opaqueHandle108      },109      removeMonitor: { _ in counter.removes += 1 }110    )111112    monitor.start()113    monitor.start()114    monitor.start()115    monitor.stop()116    monitor.stop()117118    // The first two extra start() calls must each have removed the previous119    // monitor before installing the next one, so adds and removes stay in120    // lockstep and no AppKit handle outlives its owner.121    #expect(counter.adds == 3)122    #expect(counter.removes == 3)123  }124125  @Test("Screen manager exposes AppKit screen metadata")126  func screenManager() {127    let manager = ScreenManager.shared128    #expect(manager.screens == NSScreen.screens)129    if let screen = manager.screens.first {130      #expect(manager.screenName(for: screen) == screen.localizedName)131    }132  }133134  @Test("Application bootstrap wires the delegate and optional event loop")135  func applicationBootstrap() {136    let delegate = AppDelegate()137    var received: AppDelegate?138    var runs = 0139    HavenApplication.start(140      delegate: delegate, shouldRun: false,141      setDelegate: { received = $0 }, run: { runs += 1 })142    #expect(received === delegate)143    #expect(runs == 0)144145    HavenApplication.start(146      delegate: delegate, shouldRun: true,147      setDelegate: { received = $0 }, run: { runs += 1 })148    #expect(runs == 1)149  }150151  @Test("Folder panel adapter configures directory selection")152  func folderPanel() {153    let directory = URL(fileURLWithPath: "/tmp")154    let configured = AppState.makeFolderPanel(at: directory)155    #expect(!configured.canChooseFiles)156    #expect(configured.canChooseDirectories)157    #expect(configured.canCreateDirectories)158    #expect(!configured.allowsMultipleSelection)159    #expect(configured.directoryURL == directory)160  }161162  @Test("Folder picker adapter forwards cancellation")163  func folderPicker() {164    let panel = OpenPanelStub()165    var result: URL? = URL(fileURLWithPath: "/sentinel")166167    AppState.presentFolderPicker(panel: panel) { result = $0 }168    #expect(panel.completion != nil)169170    panel.completion?(.cancel)171    #expect(result == nil)172  }173}