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

555 lines
23 KiB
Swift

import AppKit
import CodexBarCore
import Testing
@testable import CodexBar
@MainActor
extension StatusMenuTests {
final class HighlightProbeView: NSView, MenuCardHighlighting {
private(set) var states: [Bool] = []
func setHighlighted(_ highlighted: Bool) {
self.states.append(highlighted)
}
}
@Test
func `menu highlight updates only previous and current custom rows`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = NSMenu()
let firstView = HighlightProbeView()
let secondView = HighlightProbeView()
let thirdView = HighlightProbeView()
let first = NSMenuItem()
first.view = firstView
first.isEnabled = true
let second = NSMenuItem()
second.view = secondView
second.isEnabled = true
let third = NSMenuItem()
third.view = thirdView
third.isEnabled = true
menu.addItem(first)
menu.addItem(second)
menu.addItem(third)
controller.menu(menu, willHighlight: first)
controller.menu(menu, willHighlight: second)
controller.menu(menu, willHighlight: second)
#expect(firstView.states == [true, false])
#expect(secondView.states == [true])
#expect(thirdView.states.isEmpty)
}
@Test
func `native highlight preserves coalesced baseline resync until pointer leaves native rows`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
let key = ObjectIdentifier(menu)
controller.cancelMenuWork(key)
controller.openMenus[key] = menu
let planUsage = NSMenuItem(title: "Plan Usage", action: nil, keyEquivalent: "")
planUsage.isEnabled = true
let cost = NSMenuItem(title: "Cost", action: nil, keyEquivalent: "")
cost.isEnabled = true
menu.addItem(planUsage)
menu.addItem(cost)
controller.menu(menu, willHighlight: planUsage)
#expect(controller.highlightedMenuItems[key] === planUsage)
#expect(controller.isNativeMenuItemHighlighted(in: menu))
controller.lastMenuAdjunctReadinessSignature = "stale-baseline"
controller.menuSession.invalidate(allowsStaleContent: false, requiresRebuild: true)
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in rebuildCount += 1 }
defer { controller._test_openMenuRebuildObserver = nil }
controller.scheduleOpenMenuRebuildIfStillVisible(
menu,
provider: .codex,
resyncReadinessBaselineAfterRebuild: true)
controller.scheduleOpenMenuRebuildIfStillVisible(menu, provider: .codex)
for _ in 0..<20 where controller.nativeHighlightDeferredMenuRebuilds[key] == nil {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] != nil)
#expect(controller.pendingMenuBaselineResyncs.contains(key))
#expect(controller.menuNeedsRefresh(menu))
controller.menu(menu, willHighlight: cost)
for _ in 0..<20 {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] != nil)
controller.menu(menu, willHighlight: nil)
for _ in 0..<20 where rebuildCount == 0 {
await Task.yield()
}
#expect(rebuildCount == 1)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] == nil)
#expect(!controller.pendingMenuBaselineResyncs.contains(key))
#expect(!controller.menuNeedsRefresh(menu))
#expect(controller.lastMenuAdjunctReadinessSignature == controller.menuAdjunctReadinessSignature())
}
@Test
func `native highlight preserves explicit rebuild even when menu is already fresh`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.populateMenu(menu, provider: .codex)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
defer { controller.menuDidClose(menu) }
let nativeItem = NSMenuItem(title: "Plan Usage", action: nil, keyEquivalent: "")
nativeItem.isEnabled = true
menu.addItem(nativeItem)
controller.menu(menu, willHighlight: nativeItem)
#expect(!controller.menuNeedsRefresh(menu))
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in rebuildCount += 1 }
defer { controller._test_openMenuRebuildObserver = nil }
controller.scheduleOpenMenuRebuildIfStillVisible(menu, provider: .claude)
for _ in 0..<20 where controller.nativeHighlightDeferredMenuRebuilds[key] == nil {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key]?.provider == .claude)
#expect(!controller.menuNeedsRefresh(menu))
controller.menu(menu, willHighlight: nil)
for _ in 0..<20 where rebuildCount == 0 {
await Task.yield()
}
#expect(rebuildCount == 1)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] == nil)
#expect(!controller.menuNeedsRefresh(menu))
}
@Test
func `hosted submenu close resumes deferred explicit rebuild on fresh parent`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let parent = controller.makeMenu()
controller.populateMenu(parent, provider: .codex)
controller.markMenuFresh(parent)
let parentKey = ObjectIdentifier(parent)
controller.openMenus[parentKey] = parent
defer { controller.menuDidClose(parent) }
let nativeItem = NSMenuItem(title: "Plan Usage", action: nil, keyEquivalent: "")
nativeItem.isEnabled = true
parent.addItem(nativeItem)
controller.menu(parent, willHighlight: nativeItem)
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { menu in
if menu === parent {
rebuildCount += 1
}
}
defer { controller._test_openMenuRebuildObserver = nil }
controller.scheduleOpenMenuRebuildIfStillVisible(parent, provider: .claude)
for _ in 0..<20 where controller.nativeHighlightDeferredMenuRebuilds[parentKey] == nil {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.nativeHighlightDeferredMenuRebuilds[parentKey]?.provider == .claude)
#expect(!controller.menuNeedsRefresh(parent))
let submenu = controller.makeHostedSubviewPlaceholderMenu(
chartID: StatusItemController.costHistoryChartID,
provider: .codex)
let submenuKey = ObjectIdentifier(submenu)
controller.openMenus[submenuKey] = submenu
controller.menu(parent, willHighlight: nil)
for _ in 0..<20 {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.nativeHighlightDeferredMenuRebuilds[parentKey]?.provider == .claude)
#expect(!controller.menuNeedsRefresh(parent))
controller.menuDidClose(submenu)
for _ in 0..<40 where rebuildCount == 0 {
await Task.yield()
}
#expect(controller.openMenus[submenuKey] == nil)
#expect(rebuildCount == 1)
#expect(controller.nativeHighlightDeferredMenuRebuilds[parentKey] == nil)
#expect(!controller.menuNeedsRefresh(parent))
}
@Test
func `hosted submenu close keeps explicit rebuild ahead of dirty parent refresh`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let parent = controller.makeMenu()
controller.populateMenu(parent, provider: .codex)
controller.markMenuFresh(parent)
let parentKey = ObjectIdentifier(parent)
controller.openMenus[parentKey] = parent
defer { controller.menuDidClose(parent) }
let nativeItem = NSMenuItem(title: "Plan Usage", action: nil, keyEquivalent: "")
nativeItem.isEnabled = true
parent.addItem(nativeItem)
controller.menu(parent, willHighlight: nativeItem)
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { menu in
if menu === parent {
rebuildCount += 1
}
}
defer { controller._test_openMenuRebuildObserver = nil }
controller.scheduleOpenMenuRebuildIfStillVisible(parent, provider: .claude)
for _ in 0..<20 where controller.nativeHighlightDeferredMenuRebuilds[parentKey] == nil {
await Task.yield()
}
let submenu = controller.makeHostedSubviewPlaceholderMenu(
chartID: StatusItemController.costHistoryChartID,
provider: .codex)
controller.openMenus[ObjectIdentifier(submenu)] = submenu
controller.menuSession.invalidate(allowsStaleContent: false, requiresRebuild: true)
#expect(controller.menuNeedsRefresh(parent))
controller.menuDidClose(submenu)
for _ in 0..<40 {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.nativeHighlightDeferredMenuRebuilds[parentKey]?.provider == .claude)
#expect(controller.menuNeedsRefresh(parent))
controller.menu(parent, willHighlight: nil)
for _ in 0..<40 where rebuildCount == 0 {
await Task.yield()
}
#expect(rebuildCount == 1)
#expect(controller.nativeHighlightDeferredMenuRebuilds[parentKey] == nil)
#expect(!controller.menuNeedsRefresh(parent))
}
@Test
func `hosted submenu close preserves pending parent baseline resync`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let parent = controller.makeMenu()
controller.populateMenu(parent, provider: .codex)
controller.markMenuFresh(parent)
let parentKey = ObjectIdentifier(parent)
controller.openMenus[parentKey] = parent
defer { controller.menuDidClose(parent) }
let submenu = controller.makeHostedSubviewPlaceholderMenu(
chartID: StatusItemController.costHistoryChartID,
provider: .codex)
let submenuKey = ObjectIdentifier(submenu)
controller.openMenus[submenuKey] = submenu
controller.lastMenuAdjunctReadinessSignature = "stale-baseline"
controller.menuSession.invalidate(allowsStaleContent: false, requiresRebuild: true)
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { menu in
if menu === parent {
rebuildCount += 1
}
}
defer { controller._test_openMenuRebuildObserver = nil }
controller.scheduleOpenMenuRebuildIfStillVisible(
parent,
provider: .codex,
resyncReadinessBaselineAfterRebuild: true)
for _ in 0..<20 where controller.openMenuRebuildTasks[parentKey] != nil {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.pendingMenuBaselineResyncs.contains(parentKey))
#expect(controller.menuNeedsRefresh(parent))
controller.menuDidClose(submenu)
for _ in 0..<40 where rebuildCount == 0 {
await Task.yield()
}
#expect(controller.openMenus[submenuKey] == nil)
#expect(rebuildCount == 1)
#expect(!controller.pendingMenuBaselineResyncs.contains(parentKey))
#expect(!controller.menuNeedsRefresh(parent))
#expect(controller.lastMenuAdjunctReadinessSignature == controller.menuAdjunctReadinessSignature())
}
@Test
func `menu close clears native highlight deferral and pending baseline resync`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
let nativeItem = NSMenuItem(title: "Settings", action: nil, keyEquivalent: "")
nativeItem.isEnabled = true
menu.addItem(nativeItem)
controller.menu(menu, willHighlight: nativeItem)
controller.menuSession.invalidate(allowsStaleContent: false, requiresRebuild: true)
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in rebuildCount += 1 }
defer { controller._test_openMenuRebuildObserver = nil }
controller.scheduleOpenMenuRebuildIfStillVisible(
menu,
provider: .codex,
resyncReadinessBaselineAfterRebuild: true)
for _ in 0..<20 where controller.nativeHighlightDeferredMenuRebuilds[key] == nil {
await Task.yield()
}
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] != nil)
#expect(controller.pendingMenuBaselineResyncs.contains(key))
controller.menuDidClose(menu)
for _ in 0..<10 {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.openMenus[key] == nil)
#expect(controller.highlightedMenuItems[key] == nil)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] == nil)
#expect(!controller.pendingMenuBaselineResyncs.contains(key))
#expect(controller.openMenuRebuildTasks[key] == nil)
#expect(controller.openMenuRebuildRequests.tokens[key] == nil)
}
@Test
func `hosted native highlight defers signature changing refresh until pointer leaves`() async throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = true
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let submenu = NSMenu()
#expect(controller.appendStatusComponentsItem(
to: submenu,
provider: .codex,
width: StatusItemController.menuCardBaseWidth))
let key = ObjectIdentifier(submenu)
controller.openMenus[key] = submenu
defer { controller.menuDidClose(submenu) }
let originalLink = try #require(submenu.items.last)
#expect(originalLink.title == L("Open Status Page"))
#expect(originalLink.view == nil)
#expect(originalLink.isEnabled)
controller.menu(submenu, willHighlight: originalLink)
store.statusComponents[.codex] = [
ProviderStatusComponent(
id: "api",
name: "API",
indicator: .none,
status: "operational"),
]
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { menu in
if menu === submenu {
rebuildCount += 1
}
}
defer { controller._test_openMenuRebuildObserver = nil }
controller.refreshOpenMenusAllowingParentRebuild()
for _ in 0..<20 where controller.nativeHighlightDeferredMenuRebuilds[key] == nil {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] != nil)
#expect(submenu.items.count == 1)
#expect(submenu.items.first === originalLink)
controller.menu(submenu, willHighlight: nil)
for _ in 0..<20 where rebuildCount == 0 {
await Task.yield()
}
#expect(rebuildCount == 1)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] == nil)
#expect(submenu.items.count == 3)
#expect(submenu.items.last !== originalLink)
#expect(submenu.items.last?.title == L("Open Status Page"))
}
@Test
func `custom highlight does not defer open menu rebuild`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._cancelPlanUtilizationHistoryLoadForTesting()
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
let key = ObjectIdentifier(menu)
controller.cancelMenuWork(key)
controller.openMenus[key] = menu
let customItem = NSMenuItem()
customItem.view = HighlightProbeView()
customItem.isEnabled = true
menu.addItem(customItem)
controller.menu(menu, willHighlight: customItem)
controller.menuSession.invalidate(allowsStaleContent: false, requiresRebuild: true)
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in rebuildCount += 1 }
defer { controller._test_openMenuRebuildObserver = nil }
controller.rebuildOpenMenuIfStillVisible(menu, provider: .codex)
#expect(rebuildCount == 1)
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] == nil)
#expect(!controller.menuNeedsRefresh(menu))
}
}