PlatformCoverageTests.swift

5.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    // Pre-launch: the lifecycle-safe storage is genuinely absent, not a44    // force-unwrap waiting to trap.45    #expect(delegate.currentStatusItem == nil)46    #expect(delegate.currentMenuController == nil)4748    delegate.applicationDidFinishLaunching(49      Notification(name: NSApplication.didFinishLaunchingNotification)50    )5152    #expect(delegate.currentStatusItem != nil)53    #expect(delegate.currentMenuController != nil)54    #expect(delegate.currentStatusItem?.button?.image != nil)5556    // invalidate() drops the lifecycle-safe accessors back to nil without57    // surfacing any trap when callers ask again.58    delegate.invalidate()59    #expect(delegate.currentStatusItem == nil)60    #expect(delegate.currentMenuController == nil)61  }6263  @Test("Menu controller exercises popover and modal lifecycle")64  func menuControllerLifecycle() {65    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)66    let popover = PopoverStub()67    let controller = MenuController(statusItem: statusItem, popover: popover)6869    #expect(MenuController.shared === controller)70    controller.beginModalPresentation()71    #expect(controller.popover.behavior == .applicationDefined)72    controller.endModalPresentation()73    #expect(controller.popover.behavior == .transient)7475    popover.stubShown = true76    controller.endModalPresentation()77    controller.eventMonitor?.handler(nil)78    popover.stubShown = true79    controller.togglePopover(nil)8081    controller.showPopover(sender: nil)82    controller.togglePopover(nil)83    controller.togglePopover(nil)84    controller.closePopover(sender: nil)85    controller.togglePopover(nil)86    controller.closePopover(sender: nil)87    #expect(popover.showCount > 0)88    #expect(popover.closeCount > 0)8990    NSStatusBar.system.removeStatusItem(statusItem)91  }9293  @Test("Event monitor start and stop are idempotent")94  func eventMonitorLifecycle() {95    var eventSeen = false96    let monitor = EventMonitor(mask: [.leftMouseDown]) { _ in eventSeen = true }9798    monitor.start()99    monitor.stop()100    monitor.stop()101102    #expect(!eventSeen)103  }104105  @Test("Repeated start does not leak AppKit event monitors")106  func eventMonitorRepeatedStartDoesNotLeak() {107    final class RegistrationCounter {108      var adds = 0109      var removes = 0110    }111    let counter = RegistrationCounter()112    let opaqueHandle = NSObject()113    let monitor = EventMonitor(114      mask: [.leftMouseDown],115      handler: { _ in },116      addMonitor: { _, _ in117        counter.adds += 1118        return opaqueHandle119      },120      removeMonitor: { _ in counter.removes += 1 }121    )122123    monitor.start()124    monitor.start()125    monitor.start()126    monitor.stop()127    monitor.stop()128129    // The first two extra start() calls must each have removed the previous130    // monitor before installing the next one, so adds and removes stay in131    // lockstep and no AppKit handle outlives its owner.132    #expect(counter.adds == 3)133    #expect(counter.removes == 3)134  }135136  @Test("Screen manager exposes AppKit screen metadata")137  func screenManager() {138    let manager = ScreenManager.shared139    #expect(manager.screens == NSScreen.screens)140    if let screen = manager.screens.first {141      #expect(manager.screenName(for: screen) == screen.localizedName)142    }143  }144145  @Test("Application bootstrap wires the delegate and optional event loop")146  func applicationBootstrap() {147    let delegate = AppDelegate()148    var received: AppDelegate?149    var runs = 0150    HavenApplication.start(151      delegate: delegate, shouldRun: false,152      setDelegate: { received = $0 }, run: { runs += 1 })153    #expect(received === delegate)154    #expect(runs == 0)155156    HavenApplication.start(157      delegate: delegate, shouldRun: true,158      setDelegate: { received = $0 }, run: { runs += 1 })159    #expect(runs == 1)160  }161162  @Test("Application bootstrap installs the accessory activation policy")163  func applicationBootstrapInstallsAccessoryPolicy() {164    let delegate = AppDelegate()165    var installed: NSApplication.ActivationPolicy?166    let choice = HavenApplication.start(167      delegate: delegate, shouldRun: false,168      setDelegate: { _ in }, run: {},169      setActivationPolicy: { installed = $0 })170171    #expect(installed == .accessory)172    #expect(choice.policy == .accessory)173  }174175  @Test("Folder panel adapter configures directory selection")176  func folderPanel() {177    let directory = URL(fileURLWithPath: "/tmp")178    let configured = AppState.makeFolderPanel(at: directory)179    #expect(!configured.canChooseFiles)180    #expect(configured.canChooseDirectories)181    #expect(configured.canCreateDirectories)182    #expect(!configured.allowsMultipleSelection)183    #expect(configured.directoryURL == directory)184  }185186  @Test("Folder picker adapter forwards cancellation")187  func folderPicker() {188    let panel = OpenPanelStub()189    var result: URL? = URL(fileURLWithPath: "/sentinel")190191    AppState.presentFolderPicker(panel: panel) { result = $0 }192    #expect(panel.completion != nil)193194    panel.completion?(.cancel)195    #expect(result == nil)196  }197}