Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

536 lines
21 KiB
Swift

import Carbon.HIToolbox
import XCTest
import MacToolsPluginKit
@testable import MacTools
@testable import WindowSwitcherPlugin
@MainActor
private final class WindowSwitcherMemoryStorage: PluginStorage {
var values: [String: Any] = [:]
func object(forKey key: String) -> Any? {
values[key]
}
func data(forKey key: String) -> Data? {
values[key] as? Data
}
func string(forKey key: String) -> String? {
values[key] as? String
}
func stringArray(forKey key: String) -> [String]? {
values[key] as? [String]
}
func integer(forKey key: String) -> Int {
values[key] as? Int ?? 0
}
func bool(forKey key: String) -> Bool {
values[key] as? Bool ?? false
}
func set(_ value: Any?, forKey key: String) {
values[key] = value
}
func removeObject(forKey key: String) {
values.removeValue(forKey: key)
}
func migrateValueIfNeeded(fromLegacyKey legacyKey: String, to key: String) {
guard values[key] == nil, let value = values[legacyKey] else {
return
}
values[key] = value
values.removeValue(forKey: legacyKey)
}
}
@MainActor
final class WindowSwitcherPluginTests: XCTestCase {
func testMetadataIdentifiesPlugin() {
let plugin = makePlugin()
XCTAssertEqual(plugin.metadata.id, "window-switcher")
XCTAssertEqual(plugin.metadata.title, "窗口切换")
XCTAssertNil(plugin.primaryPanel)
}
func testDefaultConfigurationIsEnabledKeyWindowMode() {
let store = WindowSwitcherStore(storage: WindowSwitcherMemoryStorage())
XCTAssertTrue(store.configuration.isEnabled)
XCTAssertEqual(store.configuration.mode, .keyWindow)
XCTAssertEqual(store.configuration.sortMode, .recentUse)
}
func testModePersists() {
let storage = WindowSwitcherMemoryStorage()
let store = WindowSwitcherStore(storage: storage)
store.setMode(.directCycle)
let loaded = WindowSwitcherStore(storage: storage)
XCTAssertEqual(loaded.configuration.mode, .directCycle)
}
func testSortModePersists() {
let storage = WindowSwitcherMemoryStorage()
let store = WindowSwitcherStore(storage: storage)
store.setSortMode(.fixed)
let loaded = WindowSwitcherStore(storage: storage)
XCTAssertEqual(loaded.configuration.sortMode, .fixed)
}
func testLegacyConfigurationDefaultsToEnabled() throws {
let storage = WindowSwitcherMemoryStorage()
let data = try XCTUnwrap(#"{"mode":"directCycle"}"#.data(using: .utf8))
storage.set(data, forKey: "configuration")
let store = WindowSwitcherStore(storage: storage)
XCTAssertTrue(store.configuration.isEnabled)
XCTAssertEqual(store.configuration.mode, .directCycle)
XCTAssertEqual(store.configuration.sortMode, .recentUse)
}
func testEnabledStatePersists() {
let storage = WindowSwitcherMemoryStorage()
let store = WindowSwitcherStore(storage: storage)
store.setEnabled(false)
let loaded = WindowSwitcherStore(storage: storage)
XCTAssertFalse(loaded.configuration.isEnabled)
}
func testObsoleteShortcutAssignmentsAreDiscarded() throws {
let storage = WindowSwitcherMemoryStorage()
storage.set(
try JSONEncoder().encode(["bundle:com.apple.Safari": "s"]),
forKey: "shortcut-assignments"
)
let store = WindowSwitcherStore(storage: storage)
XCTAssertEqual(store.shortcutBindings.manual, [:])
XCTAssertEqual(store.shortcutBindings.automatic, [:])
XCTAssertNil(storage.data(forKey: "shortcut-assignments"))
XCTAssertNil(storage.data(forKey: "shortcut-bindings"))
}
func testShortcutDefinitionDefaultsToCommandTab() {
let plugin = makePlugin()
let definition = plugin.shortcutDefinitions.first
XCTAssertEqual(definition?.id, "switcher")
XCTAssertEqual(definition?.defaultBinding?.keyCode, UInt16(kVK_Tab))
XCTAssertEqual(definition?.defaultBinding?.modifiers, .command)
XCTAssertEqual(definition?.isRequired, true)
}
func testShortcutDefinitionUsesEventTapScope() throws {
let plugin = makePlugin()
let definition = try XCTUnwrap(plugin.shortcutDefinitions.first)
switch definition.scope {
case .whilePluginActive:
break
case .global:
XCTFail("WindowSwitcher uses its event tap for shortcut handling.")
}
}
func testPermissionRequirementUsesAccessibility() {
let plugin = makePlugin(accessibilityTrusted: false)
XCTAssertEqual(plugin.permissionRequirements.map(\.id), ["accessibility"])
XCTAssertFalse(plugin.permissionState(for: "accessibility").isGranted)
}
func testOverlayMaskUsesSharedRoundedGeometry() {
let image = WindowSwitcherOverlayMetrics.roundedMaskImage()
let radius = WindowSwitcherOverlayMetrics.cornerRadius
XCTAssertEqual(image.size, NSSize(width: radius * 2 + 1, height: radius * 2 + 1))
XCTAssertEqual(image.capInsets.top, radius)
XCTAssertEqual(image.capInsets.left, radius)
XCTAssertEqual(image.capInsets.bottom, radius)
XCTAssertEqual(image.capInsets.right, radius)
XCTAssertEqual(image.resizingMode, .stretch)
}
func testOverlayIconGeometryUsesCompactHoverWithQuitButtonOutside() {
XCTAssertEqual(
WindowSwitcherOverlayMetrics.iconHighlightSize,
WindowSwitcherOverlayMetrics.iconSize
+ WindowSwitcherOverlayMetrics.iconHighlightPadding * 2
)
XCTAssertLessThan(
WindowSwitcherOverlayMetrics.iconHighlightSize,
WindowSwitcherOverlayMetrics.iconLayoutSize
)
let contentTopOffset = WindowSwitcherOverlayMetrics.shortcutHeight
+ WindowSwitcherOverlayMetrics.shortcutContentSpacing
let center = WindowSwitcherOverlayMetrics.quitButtonCenter(
tileWidth: WindowSwitcherOverlayMetrics.keyWindowTileWidth,
contentTopOffset: contentTopOffset
)
let highlightTop = contentTopOffset
+ (WindowSwitcherOverlayMetrics.iconLayoutSize
- WindowSwitcherOverlayMetrics.iconHighlightSize) / 2
let cornerCenter = CGPoint(
x: WindowSwitcherOverlayMetrics.keyWindowTileWidth / 2
+ WindowSwitcherOverlayMetrics.iconHighlightSize / 2
- WindowSwitcherOverlayMetrics.iconHighlightCornerRadius,
y: highlightTop + WindowSwitcherOverlayMetrics.iconHighlightCornerRadius
)
XCTAssertEqual(
hypot(center.x - cornerCenter.x, center.y - cornerCenter.y),
WindowSwitcherOverlayMetrics.iconHighlightCornerRadius
+ WindowSwitcherOverlayMetrics.quitButtonSize / 2
+ WindowSwitcherOverlayMetrics.quitButtonGap,
accuracy: 0.001
)
XCTAssertEqual(
WindowSwitcherOverlayMetrics.directTileHeight,
WindowSwitcherOverlayMetrics.directTopSpacing
+ WindowSwitcherOverlayMetrics.tileContentHeight
)
XCTAssertEqual(
WindowSwitcherOverlayMetrics.keyWindowTileHeight,
WindowSwitcherOverlayMetrics.shortcutHeight
+ WindowSwitcherOverlayMetrics.shortcutContentSpacing
+ WindowSwitcherOverlayMetrics.tileContentHeight
)
}
func testShortcutAssignmentUsesLettersThenDigitsThenCommandKeys() {
let entries = (0..<73).map { index in
makeEntry(index: index, appName: "\(index)")
}
let assigned = WindowSwitcherShortcutAssignment.assignShortcuts(to: entries)
XCTAssertEqual(assigned[0].shortcutToken, "f")
XCTAssertEqual(assigned[1].shortcutToken, "j")
XCTAssertEqual(assigned[25].shortcutToken, "y")
XCTAssertEqual(assigned[26].shortcutToken, "1")
XCTAssertEqual(assigned[35].shortcutToken, "0")
XCTAssertEqual(assigned[36].shortcutToken, "cmd+f")
XCTAssertEqual(assigned[71].shortcutToken, "cmd+0")
XCTAssertNil(assigned[72].shortcutToken)
}
func testShortcutAssignmentPrefersApplicationInitials() {
let entries = [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Finder", bundleIdentifier: "com.apple.finder"),
]
let assigned = WindowSwitcherShortcutAssignment.assignShortcuts(to: entries)
XCTAssertEqual(assigned[0].shortcutToken, "s")
XCTAssertEqual(assigned[1].shortcutToken, "f")
}
func testShortcutAssignmentKeepsStoredTokensAcrossOrderChanges() {
let entries = [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Slack", bundleIdentifier: "com.tinyspeck.slackmacgap"),
]
let first = WindowSwitcherShortcutAssignment.assignShortcuts(
to: entries,
bindingState: WindowSwitcherShortcutBindingState()
)
let reordered = [
makeEntry(index: 1, appName: "Slack", bundleIdentifier: "com.tinyspeck.slackmacgap"),
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
]
let second = WindowSwitcherShortcutAssignment.assignShortcuts(
to: reordered,
bindingState: first.bindingState
)
XCTAssertEqual(first.entries[0].shortcutToken, "s")
XCTAssertEqual(first.entries[1].shortcutToken, "f")
XCTAssertEqual(second.entries[0].shortcutToken, "f")
XCTAssertEqual(second.entries[1].shortcutToken, "s")
}
func testShortcutAssignmentReleasesAbsentApplicationTokens() {
let entries = [
makeEntry(index: 0, appName: "Sketch", bundleIdentifier: "com.bohemiancoding.sketch3"),
]
let stored = ["bundle:com.apple.Safari": "s"]
let result = WindowSwitcherShortcutAssignment.assignShortcuts(
to: entries,
bindingState: WindowSwitcherShortcutBindingState(automatic: stored)
)
XCTAssertEqual(result.entries[0].shortcutToken, "s")
XCTAssertNil(result.bindingState.automatic["bundle:com.apple.Safari"])
XCTAssertEqual(result.bindingState.automatic["bundle:com.bohemiancoding.sketch3"], "s")
}
func testShortcutAssignmentKeepsCommandKeyWhenListShrinks() {
let entries = [
makeEntry(index: 0, appName: "Numbers", bundleIdentifier: "com.apple.Numbers"),
]
let stored = ["bundle:com.apple.Numbers": "cmd+f"]
let result = WindowSwitcherShortcutAssignment.assignShortcuts(
to: entries,
bindingState: WindowSwitcherShortcutBindingState(automatic: stored)
)
XCTAssertEqual(result.entries[0].shortcutToken, "cmd+f")
XCTAssertEqual(result.bindingState.automatic["bundle:com.apple.Numbers"], "cmd+f")
}
func testManualShortcutTakesPriorityOverConflictingAutomaticShortcut() {
let entries = [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Finder", bundleIdentifier: "com.apple.finder"),
]
let state = WindowSwitcherShortcutBindingState(
manual: ["bundle:com.apple.Safari": "f"],
automatic: ["bundle:com.apple.finder": "f"]
)
let result = WindowSwitcherShortcutAssignment.assignShortcuts(
to: entries,
bindingState: state
)
XCTAssertEqual(result.entries[0].shortcutToken, "f")
XCTAssertEqual(result.entries[1].shortcutToken, "j")
XCTAssertEqual(result.bindingState.manual["bundle:com.apple.Safari"], "f")
XCTAssertEqual(result.bindingState.automatic["bundle:com.apple.finder"], "j")
}
func testManualShortcutRejectsConflictWithRunningEntry() {
let store = WindowSwitcherStore(storage: WindowSwitcherMemoryStorage())
let entries = store.assignShortcuts(to: [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Finder", bundleIdentifier: "com.apple.finder"),
])
let finderToken = entries[1].shortcutToken
let bindingsBeforeConflict = store.shortcutBindings
let result = store.setManualShortcut(finderToken, for: entries[0].id, in: entries)
guard case .conflict = result else {
return XCTFail("Expected an active shortcut conflict.")
}
XCTAssertEqual(store.shortcutBindings, bindingsBeforeConflict)
XCTAssertEqual(store.assignShortcuts(to: entries), entries)
}
func testManualShortcutPersistsAndSurvivesSortingChanges() {
let storage = WindowSwitcherMemoryStorage()
let store = WindowSwitcherStore(storage: storage)
let entries = store.assignShortcuts(to: [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Finder", bundleIdentifier: "com.apple.finder"),
])
let result = store.setManualShortcut("q", for: entries[0].id, in: entries)
guard case let .updated(updatedEntries) = result else {
return XCTFail("Expected a saved manual shortcut.")
}
XCTAssertEqual(updatedEntries[0].shortcutToken, "q")
let loaded = WindowSwitcherStore(storage: storage)
let reordered = loaded.assignShortcuts(to: [entries[1], entries[0]])
XCTAssertEqual(reordered[0].shortcutToken, entries[1].shortcutToken)
XCTAssertEqual(reordered[1].shortcutToken, "q")
XCTAssertEqual(loaded.shortcutBindings.manual["bundle:com.apple.Safari"], "q")
}
func testManualShortcutSupportsDigitsAndCommandKeys() {
let store = WindowSwitcherStore(storage: WindowSwitcherMemoryStorage())
let entries = store.assignShortcuts(to: [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Finder", bundleIdentifier: "com.apple.finder"),
])
let digitResult = store.setManualShortcut("7", for: entries[0].id, in: entries)
guard case let .updated(digitEntries) = digitResult else {
return XCTFail("Expected a digit manual shortcut.")
}
XCTAssertEqual(digitEntries[0].shortcutToken, "7")
let commandResult = store.setManualShortcut("cmd+q", for: entries[0].id, in: digitEntries)
guard case let .updated(commandEntries) = commandResult else {
return XCTFail("Expected a Command-key manual shortcut.")
}
XCTAssertEqual(commandEntries[0].shortcutToken, "cmd+q")
XCTAssertEqual(store.shortcutBindings.manual["bundle:com.apple.Safari"], "cmd+q")
}
func testManualShortcutRejectsMultiKeyAndUnsupportedInput() {
let store = WindowSwitcherStore(storage: WindowSwitcherMemoryStorage())
let entries = store.assignShortcuts(to: [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
])
for invalidToken in ["ff", "s1", "cmd+ff", "cmd+-", "⌘a", "窗口"] {
guard case .unavailable = store.setManualShortcut(
invalidToken,
for: entries[0].id,
in: entries
) else {
return XCTFail("Expected \(invalidToken) to be rejected.")
}
}
XCTAssertTrue(store.shortcutBindings.manual.isEmpty)
}
func testCommandAndUnmodifiedShortcutsDoNotConflict() throws {
let store = WindowSwitcherStore(storage: WindowSwitcherMemoryStorage())
let entries = store.assignShortcuts(to: [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Finder", bundleIdentifier: "com.apple.finder"),
])
let finderToken = try XCTUnwrap(entries[1].shortcutToken)
XCTAssertTrue(store.hasShortcutConflict(finderToken, for: entries[0].id, in: entries))
XCTAssertFalse(store.hasShortcutConflict("cmd+\(finderToken)", for: entries[0].id, in: entries))
}
func testSelectionShortcutNormalizesStorageAndDisplayValues() throws {
let letter = try XCTUnwrap(WindowSwitcherSelectionShortcut(storageValue: "CMD+Q"))
let digit = try XCTUnwrap(WindowSwitcherSelectionShortcut(storageValue: "7"))
XCTAssertEqual(letter.storageValue, "cmd+q")
XCTAssertEqual(letter.displayValue, "⌘Q")
XCTAssertEqual(digit.storageValue, "7")
XCTAssertEqual(digit.displayValue, "7")
XCTAssertNil(WindowSwitcherSelectionShortcut(storageValue: "qq"))
XCTAssertNil(WindowSwitcherSelectionShortcut(storageValue: "cmd+77"))
}
func testAssignmentRemovesUnsupportedStoredBindings() {
let entries = [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Finder", bundleIdentifier: "com.apple.finder"),
]
let state = WindowSwitcherShortcutBindingState(
manual: ["bundle:com.apple.Safari": "safari"],
automatic: ["bundle:com.apple.finder": "ff"]
)
let result = WindowSwitcherShortcutAssignment.assignShortcuts(
to: entries,
bindingState: state
)
XCTAssertTrue(result.bindingState.manual.isEmpty)
XCTAssertEqual(result.entries.map(\.shortcutToken), ["s", "f"])
XCTAssertEqual(result.bindingState.automatic["bundle:com.apple.Safari"], "s")
XCTAssertEqual(result.bindingState.automatic["bundle:com.apple.finder"], "f")
}
func testClearingManualShortcutRestoresAutomaticShortcut() {
let store = WindowSwitcherStore(storage: WindowSwitcherMemoryStorage())
let entries = store.assignShortcuts(to: [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
])
XCTAssertEqual(entries[0].shortcutToken, "s")
guard case let .updated(customized) = store.setManualShortcut(
"q",
for: entries[0].id,
in: entries
) else {
return XCTFail("Expected a saved manual shortcut.")
}
let result = store.setManualShortcut(nil, for: customized[0].id, in: customized)
guard case let .updated(restored) = result else {
return XCTFail("Expected the manual shortcut to be cleared.")
}
XCTAssertEqual(restored[0].shortcutToken, "s")
XCTAssertTrue(store.shortcutBindings.manual.isEmpty)
XCTAssertEqual(store.shortcutBindings.automatic["bundle:com.apple.Safari"], "s")
}
func testSavedManualShortcutStaysReservedWhenApplicationIsNotRunning() {
let entries = [
makeEntry(index: 0, appName: "Finder", bundleIdentifier: "com.apple.finder"),
]
let state = WindowSwitcherShortcutBindingState(
manual: ["bundle:com.apple.Safari": "f"],
automatic: ["bundle:com.apple.finder": "f"]
)
let result = WindowSwitcherShortcutAssignment.assignShortcuts(
to: entries,
bindingState: state
)
XCTAssertEqual(result.entries[0].shortcutToken, "j")
XCTAssertEqual(result.bindingState.manual["bundle:com.apple.Safari"], "f")
XCTAssertEqual(result.bindingState.automatic["bundle:com.apple.finder"], "j")
}
func testMultiWindowEntriesUseDistinctPersistentBindingIdentities() {
let store = WindowSwitcherStore(storage: WindowSwitcherMemoryStorage())
let entries = store.assignShortcuts(to: [
makeEntry(index: 0, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
makeEntry(index: 1, appName: "Safari", bundleIdentifier: "com.apple.Safari"),
])
let result = store.setManualShortcut("q", for: entries[1].id, in: entries)
guard case let .updated(updated) = result else {
return XCTFail("Expected a saved shortcut for the second window.")
}
XCTAssertEqual(updated[0].shortcutToken, "s")
XCTAssertEqual(updated[1].shortcutToken, "q")
XCTAssertEqual(
store.shortcutBindings.manual["bundle:com.apple.Safari#window:2"],
"q"
)
}
private func makePlugin(accessibilityTrusted: Bool = true) -> WindowSwitcherPlugin {
WindowSwitcherPlugin(
context: PluginRuntimeContext(
pluginID: "window-switcher",
storage: WindowSwitcherMemoryStorage()
),
accessibilityTrusted: { accessibilityTrusted },
requestAccessibilityTrust: { _ in accessibilityTrusted }
)
}
private func makeEntry(
index: Int,
appName: String,
bundleIdentifier: String? = nil
) -> WindowSwitcherAppEntry {
WindowSwitcherAppEntry(
id: "app-\(index)",
processIdentifier: pid_t(index + 100),
bundleIdentifier: bundleIdentifier,
appName: appName,
windowTitle: nil,
icon: nil,
windowElement: nil,
isMinimized: false,
shortcutToken: nil
)
}
}