import AppKit import CodexBarCore import Testing @testable import CodexBar @MainActor @Suite(.serialized) struct StatusMenuSwitcherTrackingTests { @Test func `switcher rebuild scheduler runs during menu tracking exactly once`() { var runCount = 0 ProviderSwitcherTrackingRunLoopScheduler.schedule { runCount += 1 } CFRunLoopRunInMode( CFRunLoopMode(RunLoop.Mode.eventTracking.rawValue as CFString), 0.1, true) #expect(runCount == 1) CFRunLoopRunInMode(.defaultMode, 0.1, true) #expect(runCount == 1) } @Test func `pointer switch defers structural menu rebuild until mouse up`() async throws { let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled let previousMenuRefresh = StatusItemController.menuRefreshEnabled StatusItemController.menuCardRenderingEnabled = false StatusItemController.setMenuRefreshEnabledForTesting(true) defer { StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering StatusItemController.setMenuRefreshEnabledForTesting(previousMenuRefresh) } let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true settings.selectedMenuProvider = .codex settings.mergedMenuLastSelectedWasOverview = false let registry = ProviderRegistry.shared for provider in UsageProvider.allCases { guard let metadata = registry.metadata[provider] else { continue } settings.setProviderEnabled( provider: provider, metadata: metadata, enabled: provider == .codex || provider == .claude) } let fetcher = UsageFetcher() let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) let controller = StatusItemController( store: store, settings: settings, account: fetcher.loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: .system, menuRefreshEnabled: false) defer { controller.releaseStatusItemsForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) controller.menuRefreshEnabledOverrideForTesting = true controller.openMenus[ObjectIdentifier(menu)] = menu let switcher = try #require(menu.items.first?.view as? ProviderSwitcherView) let mouseDown = try #require(switcher._test_mouseDownEvent(buttonTag: 2)) let mouseUp = try #require(switcher._test_mouseUpEvent(buttonTag: 2)) var rebuildCount = 0 controller._test_openMenuRebuildObserver = { _ in rebuildCount += 1 } defer { controller._test_openMenuRebuildObserver = nil } #expect(controller.handleProviderSwitcherTrackingEvent(mouseDown, menu: menu)) #expect(settings.selectedMenuProvider == .codex) #expect(controller.providerSwitcherPointerInteractionMenuID == ObjectIdentifier(menu)) #expect(controller.pendingProviderSwitcherPointerRebuild == nil) for _ in 0..<20 { await Task.yield() } #expect(rebuildCount == 0) #expect(controller.handleProviderSwitcherTrackingEvent(mouseUp, menu: menu)) #expect(settings.selectedMenuProvider == .claude) for _ in 0..<100 where rebuildCount == 0 { await Task.yield() try? await Task.sleep(for: .milliseconds(5)) } #expect(rebuildCount == 1) #expect(controller.providerSwitcherPointerInteractionMenuID == nil) #expect(controller.pendingProviderSwitcherPointerRebuild == nil) } @Test func `pointer switch cancels when mouse up leaves pressed segment`() throws { let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled let previousMenuRefresh = StatusItemController.menuRefreshEnabled StatusItemController.menuCardRenderingEnabled = false StatusItemController.setMenuRefreshEnabledForTesting(true) defer { StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering StatusItemController.setMenuRefreshEnabledForTesting(previousMenuRefresh) } let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true settings.selectedMenuProvider = .codex settings.mergedMenuLastSelectedWasOverview = false let registry = ProviderRegistry.shared for provider in UsageProvider.allCases { guard let metadata = registry.metadata[provider] else { continue } settings.setProviderEnabled( provider: provider, metadata: metadata, enabled: provider == .codex || provider == .claude) } let fetcher = UsageFetcher() let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) let controller = StatusItemController( store: store, settings: settings, account: fetcher.loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: .system, menuRefreshEnabled: false) defer { controller.releaseStatusItemsForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) controller.menuRefreshEnabledOverrideForTesting = true controller.openMenus[ObjectIdentifier(menu)] = menu let switcher = try #require(menu.items.first?.view as? ProviderSwitcherView) let mouseDown = try #require(switcher._test_mouseDownEvent(buttonTag: 2)) let mouseUpElsewhere = try #require(switcher._test_mouseUpEvent(buttonTag: 1)) var rebuildCount = 0 controller._test_openMenuRebuildObserver = { _ in rebuildCount += 1 } defer { controller._test_openMenuRebuildObserver = nil } #expect(controller.handleProviderSwitcherTrackingEvent(mouseDown, menu: menu)) #expect(controller.handleProviderSwitcherTrackingEvent(mouseUpElsewhere, menu: menu)) #expect(settings.selectedMenuProvider == .codex) #expect(rebuildCount == 0) #expect(controller.providerSwitcherPointerInteractionMenuID == nil) #expect(controller.pendingProviderSwitcherPointerRebuild == nil) } @Test func `unrelated mouse up remains available to normal menu items`() throws { let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true let fetcher = UsageFetcher() let controller = StatusItemController( store: UsageStore( fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings), settings: settings, account: fetcher.loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: .system) defer { controller.releaseStatusItemsForTesting() } let menu = NSMenu() let switcher = ProviderSwitcherView( providers: [.codex, .claude], selected: .provider(.codex), includesOverview: true, width: 320, showsIcons: false, iconProvider: { _ in NSImage() }, weeklyRemainingProvider: { _ in nil }, onSelect: { _ in }) let item = NSMenuItem() item.view = switcher menu.addItem(item) let unrelatedMouseUp = try #require(switcher._test_mouseUpEvent(buttonTag: 1)) #expect(!controller.handleProviderSwitcherTrackingEvent(unrelatedMouseUp, menu: menu)) } private func makeSettings() -> SettingsStore { let suite = "StatusMenuSwitcherTrackingTests-\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defaults.removePersistentDomain(forName: suite) return SettingsStore( userDefaults: defaults, configStore: testConfigStore(suiteName: suite), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) } }