204 lines
8.3 KiB
Swift
204 lines
8.3 KiB
Swift
import AppKit
|
|
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
@MainActor
|
|
struct StatusMenuOverviewScrollTests {
|
|
private func makeController(suiteName: String) -> StatusItemController {
|
|
_ = NSApplication.shared
|
|
let settings = SettingsStore(
|
|
configStore: testConfigStore(suiteName: suiteName),
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(
|
|
fetcher: fetcher,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings)
|
|
return StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: .system)
|
|
}
|
|
|
|
private func makeOverviewMenu() -> NSMenu {
|
|
let menu = NSMenu()
|
|
for provider in ["claude", "codex"] {
|
|
let item = NSMenuItem()
|
|
item.representedObject = "\(StatusItemController.overviewRowIdentifierPrefix)\(provider)"
|
|
item.isEnabled = true
|
|
menu.addItem(item)
|
|
}
|
|
return menu
|
|
}
|
|
|
|
private func makeScrollEvent(deltaY: Double, precise: Bool) -> NSEvent? {
|
|
guard let cgEvent = CGEvent(
|
|
scrollWheelEvent2Source: nil,
|
|
units: precise ? .pixel : .line,
|
|
wheelCount: 1,
|
|
wheel1: Int32(deltaY),
|
|
wheel2: 0,
|
|
wheel3: 0)
|
|
else { return nil }
|
|
return NSEvent(cgEvent: cgEvent)
|
|
}
|
|
|
|
@Test
|
|
func `coarse wheel steps move highlight and respect direction`() throws {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-Direction")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = self.makeOverviewMenu()
|
|
|
|
var steps: [OverviewScrollStep] = []
|
|
controller.overviewScrollNavigationHandlerForTesting = { steps.append($0) }
|
|
|
|
let scrollUp = try #require(self.makeScrollEvent(deltaY: 1, precise: false))
|
|
#expect(controller.handleOverviewScrollWheel(scrollUp, menu: menu))
|
|
#expect(steps == [.up])
|
|
|
|
steps = []
|
|
let scrollDown = try #require(self.makeScrollEvent(deltaY: -1, precise: false))
|
|
#expect(controller.handleOverviewScrollWheel(scrollDown, menu: menu))
|
|
#expect(steps == [.down])
|
|
}
|
|
|
|
@Test
|
|
func `navigation targets only overview rows`() {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-Targets")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = self.makeOverviewMenu()
|
|
let refresh = NSMenuItem(title: "Refresh", action: nil, keyEquivalent: "")
|
|
refresh.isEnabled = true
|
|
menu.addItem(refresh)
|
|
let rows = Array(menu.items.prefix(2))
|
|
|
|
#expect(controller.overviewScrollTargetItem(in: menu, step: .down) === rows[0])
|
|
#expect(controller.overviewScrollTargetItem(in: menu, step: .up) === rows[1])
|
|
|
|
controller.highlightedMenuItems[ObjectIdentifier(menu)] = rows[0]
|
|
#expect(controller.overviewScrollTargetItem(in: menu, step: .down) === rows[1])
|
|
#expect(controller.overviewScrollTargetItem(in: menu, step: .up) === rows[0])
|
|
|
|
controller.highlightedMenuItems[ObjectIdentifier(menu)] = rows[1]
|
|
#expect(controller.overviewScrollTargetItem(in: menu, step: .down) === rows[1])
|
|
#expect(controller.overviewScrollTargetItem(in: menu, step: .up) === rows[0])
|
|
|
|
controller.highlightedMenuItems[ObjectIdentifier(menu)] = refresh
|
|
#expect(controller.overviewScrollTargetItem(in: menu, step: .down) === rows[0])
|
|
}
|
|
|
|
@Test
|
|
func `precise trackpad scrolling is passed through to native menu scrolling`() throws {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-Precise")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = self.makeOverviewMenu()
|
|
|
|
var steps: [OverviewScrollStep] = []
|
|
controller.overviewScrollNavigationHandlerForTesting = { steps.append($0) }
|
|
|
|
let scroll = try #require(self.makeScrollEvent(deltaY: 30, precise: true))
|
|
#expect(!controller.handleOverviewScrollWheel(scroll, menu: menu))
|
|
#expect(steps.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `precise trackpad scrolling clears wheel accumulation`() throws {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-PreciseReset")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = self.makeOverviewMenu()
|
|
|
|
var steps: [OverviewScrollStep] = []
|
|
controller.overviewScrollNavigationHandlerForTesting = { steps.append($0) }
|
|
|
|
controller.overviewScrollAccumulatedDelta = 0.5
|
|
let scroll = try #require(self.makeScrollEvent(deltaY: 30, precise: true))
|
|
#expect(!controller.handleOverviewScrollWheel(scroll, menu: menu))
|
|
#expect(steps.isEmpty)
|
|
#expect(controller.overviewScrollAccumulatedDelta == 0)
|
|
}
|
|
|
|
@Test
|
|
func `coarse wheel lines step immediately`() throws {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-Wheel")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = self.makeOverviewMenu()
|
|
|
|
var steps: [OverviewScrollStep] = []
|
|
controller.overviewScrollNavigationHandlerForTesting = { steps.append($0) }
|
|
|
|
let wheelNotch = try #require(self.makeScrollEvent(deltaY: -1, precise: false))
|
|
#expect(controller.handleOverviewScrollWheel(wheelNotch, menu: menu))
|
|
#expect(steps == [.down])
|
|
}
|
|
|
|
@Test
|
|
func `fast flick is capped per event`() throws {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-Cap")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = self.makeOverviewMenu()
|
|
|
|
var steps: [OverviewScrollStep] = []
|
|
controller.overviewScrollNavigationHandlerForTesting = { steps.append($0) }
|
|
|
|
let flick = try #require(self.makeScrollEvent(deltaY: 500, precise: false))
|
|
#expect(controller.handleOverviewScrollWheel(flick, menu: menu))
|
|
#expect(steps == [.up, .up, .up])
|
|
}
|
|
|
|
@Test
|
|
func `precise flick is passed through instead of being capped into highlight jumps`() throws {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-PreciseFlick")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = self.makeOverviewMenu()
|
|
|
|
var steps: [OverviewScrollStep] = []
|
|
controller.overviewScrollNavigationHandlerForTesting = { steps.append($0) }
|
|
|
|
let flick = try #require(self.makeScrollEvent(deltaY: 500, precise: true))
|
|
#expect(!controller.handleOverviewScrollWheel(flick, menu: menu))
|
|
#expect(steps.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `open submenu suspends scroll navigation`() throws {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-Submenu")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = self.makeOverviewMenu()
|
|
let submenu = NSMenu()
|
|
controller.openMenus[ObjectIdentifier(menu)] = menu
|
|
controller.openMenus[ObjectIdentifier(submenu)] = submenu
|
|
defer { controller.openMenus.removeAll() }
|
|
|
|
var steps: [OverviewScrollStep] = []
|
|
controller.overviewScrollNavigationHandlerForTesting = { steps.append($0) }
|
|
|
|
let scroll = try #require(self.makeScrollEvent(deltaY: 1, precise: false))
|
|
#expect(!controller.handleOverviewScrollWheel(scroll, menu: menu))
|
|
#expect(steps.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `menus without overview rows ignore scrolling`() throws {
|
|
let controller = self.makeController(suiteName: "OverviewScroll-NonOverview")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
let menu = NSMenu()
|
|
menu.addItem(NSMenuItem(title: "Refresh", action: nil, keyEquivalent: ""))
|
|
|
|
var steps: [OverviewScrollStep] = []
|
|
controller.overviewScrollNavigationHandlerForTesting = { steps.append($0) }
|
|
|
|
let scroll = try #require(self.makeScrollEvent(deltaY: 1, precise: false))
|
|
#expect(!controller.handleOverviewScrollWheel(scroll, menu: menu))
|
|
#expect(steps.isEmpty)
|
|
#expect(!menu.items.isEmpty)
|
|
}
|
|
}
|