Files
steipete--codexbar/Tests/CodexBarTests/StatusMenuSwitcherTrackingTests.swift
T
2026-07-13 12:22:33 +08:00

210 lines
8.5 KiB
Swift

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())
}
}