Initial FocusFixer implementation
Menu-bar utility that fixes cross-display keyboard focus loss: a listen-only CGEvent tap locates the window under the cursor and reasserts app activation + AX focus on it, with debounce, tap auto-recovery, and an Accessibility permission gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import XCTest
|
||||
@testable import FocusFixer
|
||||
|
||||
final class DebounceTests: XCTestCase {
|
||||
func testFirstEventIsAlwaysProcessed() {
|
||||
var clockValue: TimeInterval = 0
|
||||
var debouncer = Debouncer(interval: 0.15, clock: { clockValue })
|
||||
XCTAssertTrue(debouncer.shouldProcess())
|
||||
}
|
||||
|
||||
func testEventWithinIntervalIsSuppressed() {
|
||||
var clockValue: TimeInterval = 0
|
||||
var debouncer = Debouncer(interval: 0.15, clock: { clockValue })
|
||||
XCTAssertTrue(debouncer.shouldProcess())
|
||||
|
||||
clockValue += 0.05
|
||||
XCTAssertFalse(debouncer.shouldProcess())
|
||||
}
|
||||
|
||||
func testEventAfterIntervalIsProcessed() {
|
||||
var clockValue: TimeInterval = 0
|
||||
var debouncer = Debouncer(interval: 0.15, clock: { clockValue })
|
||||
XCTAssertTrue(debouncer.shouldProcess())
|
||||
|
||||
clockValue += 0.2
|
||||
XCTAssertTrue(debouncer.shouldProcess())
|
||||
}
|
||||
|
||||
func testAcceptedEventResetsTheWindow() {
|
||||
var clockValue: TimeInterval = 0
|
||||
var debouncer = Debouncer(interval: 0.15, clock: { clockValue })
|
||||
XCTAssertTrue(debouncer.shouldProcess())
|
||||
|
||||
clockValue += 0.2
|
||||
XCTAssertTrue(debouncer.shouldProcess())
|
||||
|
||||
clockValue += 0.05
|
||||
XCTAssertFalse(debouncer.shouldProcess())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import XCTest
|
||||
@testable import FocusFixer
|
||||
|
||||
final class PreferencesTests: XCTestCase {
|
||||
private var suiteName: String!
|
||||
private var defaults: UserDefaults!
|
||||
private var preferences: Preferences!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
suiteName = "com.local.focusfixer.tests.\(UUID().uuidString)"
|
||||
defaults = UserDefaults(suiteName: suiteName)
|
||||
preferences = Preferences(defaults: defaults)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
preferences = nil
|
||||
defaults = nil
|
||||
suiteName = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testDefaultsToEnabled() {
|
||||
XCTAssertTrue(preferences.isEnabled)
|
||||
}
|
||||
|
||||
func testDefaultDebounceIntervalIs150ms() {
|
||||
XCTAssertEqual(preferences.debounceInterval, 0.15, accuracy: 0.0001)
|
||||
}
|
||||
|
||||
func testExclusionListMatching() {
|
||||
XCTAssertFalse(preferences.isExcluded(bundleID: "com.apple.finder"))
|
||||
|
||||
preferences.setExcluded(true, bundleID: "com.apple.finder")
|
||||
XCTAssertTrue(preferences.isExcluded(bundleID: "com.apple.finder"))
|
||||
|
||||
preferences.setExcluded(false, bundleID: "com.apple.finder")
|
||||
XCTAssertFalse(preferences.isExcluded(bundleID: "com.apple.finder"))
|
||||
}
|
||||
|
||||
func testExclusionListRequiresExactMatch() {
|
||||
preferences.setExcluded(true, bundleID: "com.local.focusfixer.helper")
|
||||
XCTAssertFalse(preferences.isExcluded(bundleID: "com.local.focusfixer"))
|
||||
XCTAssertTrue(preferences.isExcluded(bundleID: "com.local.focusfixer.helper"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import XCTest
|
||||
@testable import FocusFixer
|
||||
|
||||
final class WindowLocatorTests: XCTestCase {
|
||||
func testReturnsOwnerPIDWhenPointInsideWindow() {
|
||||
let windows = [
|
||||
WindowInfo(ownerPID: 111, layer: 0, bounds: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
]
|
||||
XCTAssertEqual(WindowLocator.ownerPID(at: CGPoint(x: 50, y: 50), in: windows), 111)
|
||||
}
|
||||
|
||||
func testIgnoresNonZeroLayerWindows() {
|
||||
let windows = [
|
||||
WindowInfo(ownerPID: 999, layer: 25, bounds: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
]
|
||||
XCTAssertNil(WindowLocator.ownerPID(at: CGPoint(x: 50, y: 50), in: windows))
|
||||
}
|
||||
|
||||
func testFallsThroughToNextWindowWhenTopWindowIsWrongLayer() {
|
||||
let windows = [
|
||||
WindowInfo(ownerPID: 1, layer: 25, bounds: CGRect(x: 0, y: 0, width: 100, height: 100)),
|
||||
WindowInfo(ownerPID: 2, layer: 0, bounds: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
]
|
||||
XCTAssertEqual(WindowLocator.ownerPID(at: CGPoint(x: 50, y: 50), in: windows), 2)
|
||||
}
|
||||
|
||||
func testReturnsFrontmostWindowInZOrder() {
|
||||
// Overlapping layer-0 windows: front-to-back order (as returned by
|
||||
// CGWindowListCopyWindowInfo) determines which owner wins.
|
||||
let windows = [
|
||||
WindowInfo(ownerPID: 1, layer: 0, bounds: CGRect(x: 0, y: 0, width: 200, height: 200)),
|
||||
WindowInfo(ownerPID: 2, layer: 0, bounds: CGRect(x: 0, y: 0, width: 200, height: 200))
|
||||
]
|
||||
XCTAssertEqual(WindowLocator.ownerPID(at: CGPoint(x: 50, y: 50), in: windows), 1)
|
||||
}
|
||||
|
||||
func testEmptyWindowListReturnsNil() {
|
||||
XCTAssertNil(WindowLocator.ownerPID(at: CGPoint(x: 50, y: 50), in: []))
|
||||
}
|
||||
|
||||
func testPointOutsideAllWindowsReturnsNil() {
|
||||
let windows = [
|
||||
WindowInfo(ownerPID: 1, layer: 0, bounds: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
]
|
||||
XCTAssertNil(WindowLocator.ownerPID(at: CGPoint(x: 500, y: 500), in: windows))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user