codeburn/mac/Tests/CodeBurnMenubarTests/StatusItemContextMenuPolicyTests.swift
Frenky Harry S. Sinaga 8e8b58626a
test(mac): lock status-item context menu policy (#802)
Extract StatusItemContextMenuPolicy (event mask, debounce, presentation
mode) and unit-test it so rightMouseUp + statusItemMenu cannot regress
to the flash/scroll-jump paths without a failing test.
2026-07-24 19:27:25 +07:00

87 lines
3.3 KiB
Swift

import AppKit
import XCTest
@testable import CodeBurnMenubar
/// Locks the right-click menu policy that fixed flash + scroll-jump (#802).
/// Does not drive a real NSStatusItem that needs manual/AppKit integration.
final class StatusItemContextMenuPolicyTests: XCTestCase {
func testPresentEventMaskIsRightMouseUpNotDown() {
// Presenting on mouse-down lets the matching up dismiss the menu (flash).
XCTAssertEqual(
StatusItemContextMenuPolicy.presentEventMask,
NSEvent.EventTypeMask.rightMouseUp
)
XCTAssertNotEqual(
StatusItemContextMenuPolicy.presentEventMask,
NSEvent.EventTypeMask.rightMouseDown
)
// Mask must include up and must not include down (single-bit masks here).
XCTAssertTrue(StatusItemContextMenuPolicy.presentEventMask.contains(.rightMouseUp))
XCTAssertFalse(StatusItemContextMenuPolicy.presentEventMask.contains(.rightMouseDown))
}
func testPresentationUsesStatusItemMenuNotManualPopUp() {
// Manual popUp tracks against a point while the cursor sits on the status
// item above the menu scroll chevron / Today-row jump on mouse move.
XCTAssertEqual(
StatusItemContextMenuPolicy.presentation,
.statusItemMenu
)
XCTAssertNotEqual(
StatusItemContextMenuPolicy.presentation,
.manualPopUp
)
}
func testDebounceAcceptsFirstPresent() {
var last = Date.distantPast
let now = Date(timeIntervalSince1970: 1_000)
XCTAssertTrue(
StatusItemContextMenuPolicy.acceptPresent(now: now, lastPresentedAt: &last)
)
XCTAssertEqual(last, now)
}
func testDebounceRejectsWithinWindow() {
let t0 = Date(timeIntervalSince1970: 1_000)
var last = t0
// Just inside the 0.3s window
let t1 = t0.addingTimeInterval(0.299)
XCTAssertFalse(
StatusItemContextMenuPolicy.acceptPresent(now: t1, lastPresentedAt: &last)
)
XCTAssertEqual(last, t0, "reject must not advance lastPresentedAt")
}
func testDebounceAcceptsAfterWindow() {
let t0 = Date(timeIntervalSince1970: 1_000)
var last = t0
let t1 = t0.addingTimeInterval(StatusItemContextMenuPolicy.presentDebounceSeconds + 0.001)
XCTAssertTrue(
StatusItemContextMenuPolicy.acceptPresent(now: t1, lastPresentedAt: &last)
)
XCTAssertEqual(last, t1)
}
func testDebounceBoundaryIsStrictlyGreaterThan() {
// Gate uses `>` not `>=`: exactly debounce seconds later is still rejected.
let t0 = Date(timeIntervalSince1970: 1_000)
var last = t0
let exact = t0.addingTimeInterval(StatusItemContextMenuPolicy.presentDebounceSeconds)
XCTAssertFalse(
StatusItemContextMenuPolicy.acceptPresent(now: exact, lastPresentedAt: &last)
)
XCTAssertEqual(last, t0)
}
func testCustomDebounceOverride() {
var last = Date(timeIntervalSince1970: 0)
let now = Date(timeIntervalSince1970: 0.5)
XCTAssertFalse(
StatusItemContextMenuPolicy.acceptPresent(now: now, lastPresentedAt: &last, debounce: 1.0)
)
XCTAssertTrue(
StatusItemContextMenuPolicy.acceptPresent(now: now, lastPresentedAt: &last, debounce: 0.4)
)
}
}