210 lines
8.5 KiB
Swift
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())
|
|
}
|
|
}
|