import AppKit
|
import XCTest
|
@testable import AlignerCore
|
|
final class AlignerCoreTests: XCTestCase {
|
func testAppIdentity() {
|
XCTAssertEqual(AlignerAppInfo.name, "Aligner")
|
XCTAssertEqual(AlignerAppInfo.bundleIdentifier, "com.ar.Aligner")
|
XCTAssertEqual(AlignerAppInfo.minimumMacOSVersion, "26.0")
|
}
|
|
func testRound0OnlyDefinesQuickSwitchEntryPoint() {
|
XCTAssertEqual(EntryPoint.allCases, [.quickSwitch])
|
}
|
|
func testTriggerDefaultsUseAltTabForQuickSwitch() {
|
XCTAssertEqual(TriggerDefaults.quickSwitchShortcut.keyCode, TriggerKeyCode.tab)
|
XCTAssertEqual(TriggerDefaults.quickSwitchShortcut.modifiers, [.option])
|
}
|
|
func testTriggerEventPolicyAllowsFutureCustomQuickSwitchShortcut() {
|
let customShortcut = KeyboardShortcut(keyCode: 12, modifiers: [.control, .option])
|
|
XCTAssertEqual(
|
TriggerEventPolicy.decision(
|
for: customShortcut,
|
configuredQuickSwitch: customShortcut,
|
overlayVisible: false
|
),
|
.quickSwitch(consumeEvent: false)
|
)
|
}
|
|
func testTriggerEventPolicyOnlyConsumesQuickSwitchWhenOverlayVisible() {
|
let shortcut = TriggerDefaults.quickSwitchShortcut
|
|
XCTAssertEqual(
|
TriggerEventPolicy.decision(for: shortcut, overlayVisible: false),
|
.quickSwitch(consumeEvent: false)
|
)
|
XCTAssertEqual(
|
TriggerEventPolicy.decision(for: shortcut, overlayVisible: true),
|
.quickSwitch(consumeEvent: true)
|
)
|
}
|
|
func testTriggerEventPolicyCanConsumeInitialQuickSwitchForRound01FullTakeover() {
|
XCTAssertEqual(
|
TriggerEventPolicy.decision(
|
for: TriggerDefaults.quickSwitchShortcut,
|
overlayVisible: false,
|
consumeInitialQuickSwitch: true
|
),
|
.quickSwitch(consumeEvent: true)
|
)
|
}
|
|
func testTriggerEventPolicyNeverConsumesForceQuit() {
|
XCTAssertEqual(
|
TriggerEventPolicy.decision(for: TriggerDefaults.forceQuitShortcut, overlayVisible: true),
|
.passThrough
|
)
|
XCTAssertEqual(
|
TriggerEventPolicy.decision(
|
for: TriggerDefaults.forceQuitShortcut,
|
overlayVisible: false,
|
consumeInitialQuickSwitch: true
|
),
|
.passThrough
|
)
|
}
|
|
@MainActor
|
func testManualTriggerServiceOnlyFiresWhileRunning() {
|
let service = ManualTriggerService()
|
var fireCount = 0
|
service.onQuickSwitch = {
|
fireCount += 1
|
}
|
|
service.triggerQuickSwitch()
|
XCTAssertEqual(fireCount, 0)
|
|
service.start()
|
service.triggerQuickSwitch()
|
XCTAssertEqual(fireCount, 1)
|
|
service.stop()
|
service.triggerQuickSwitch()
|
XCTAssertEqual(fireCount, 1)
|
}
|
|
func testWindowDisplayPolicyShowsMinimizedWindows() {
|
let app = AlignerApp(bundleIdentifier: "com.apple.TextEdit", name: "TextEdit", category: .documentNotes)
|
let window = AlignerWindow(id: 1, app: app, title: "Notes", isMinimized: true)
|
|
XCTAssertTrue(WindowDisplayPolicy.shouldDisplay(window))
|
}
|
|
func testWindowDisplayPolicyHidesHiddenAppsAndGhostWindows() {
|
let hiddenApp = AlignerApp(bundleIdentifier: "com.example.Hidden", name: "Hidden", category: .generic, isHidden: true)
|
let visibleApp = AlignerApp(bundleIdentifier: "com.example.Visible", name: "Visible", category: .generic)
|
|
XCTAssertFalse(WindowDisplayPolicy.shouldDisplay(AlignerWindow(id: 2, app: hiddenApp, title: "Hidden")))
|
XCTAssertFalse(WindowDisplayPolicy.shouldDisplay(AlignerWindow(id: 3, app: visibleApp, title: "Ghost", isGhost: true)))
|
}
|
|
func testWindowDisplayPolicyKeepsFullscreenWhenRunningAppReportsHidden() {
|
let hiddenApp = AlignerApp(bundleIdentifier: "com.example.Fullscreen", name: "Fullscreen", category: .generic, isHidden: true)
|
let window = AlignerWindow(id: 4, app: hiddenApp, title: "Fullscreen", isFullscreen: true)
|
|
XCTAssertTrue(WindowDisplayPolicy.shouldDisplay(window))
|
}
|
|
func testWindowEnumerationPolicyIncludesRegularStandardWindows() {
|
let window = WindowEnumerationPolicy.window(from: makeWindowRecord())
|
|
XCTAssertEqual(window?.id, 100)
|
XCTAssertEqual(window?.title, "Document")
|
XCTAssertFalse(window?.isMinimized ?? true)
|
}
|
|
func testWindowEnumerationPolicyKeepsMinimizedWindowsDespiteSmallBounds() {
|
let record = makeWindowRecord(size: .zero, isMinimized: true)
|
|
XCTAssertTrue(WindowEnumerationPolicy.shouldInclude(record))
|
XCTAssertTrue(WindowEnumerationPolicy.window(from: record)?.isMinimized ?? false)
|
}
|
|
func testMinimizedWindowFallbackPolicyCreatesSyntheticIDsOnlyForAXOnlyMinimizedWindows() {
|
XCTAssertTrue(MinimizedWindowFallbackPolicy.shouldCreateAXOnlyFallback(hasCGWindowID: false, isMinimized: true))
|
XCTAssertFalse(MinimizedWindowFallbackPolicy.shouldCreateAXOnlyFallback(hasCGWindowID: true, isMinimized: true))
|
XCTAssertFalse(MinimizedWindowFallbackPolicy.shouldCreateAXOnlyFallback(hasCGWindowID: false, isMinimized: false))
|
|
let windowID = MinimizedWindowFallbackPolicy.syntheticWindowID(
|
processIdentifier: 42,
|
windowIndex: 3,
|
title: "Notes",
|
subroleDescription: "standard"
|
)
|
|
XCTAssertTrue(MinimizedWindowFallbackPolicy.isSyntheticWindow(AlignerWindow(
|
id: windowID,
|
app: makeApp(),
|
title: "Notes",
|
identifierSource: .syntheticAX,
|
isMinimized: true
|
)))
|
XCTAssertFalse(MinimizedWindowFallbackPolicy.isSyntheticWindow(AlignerWindow(
|
id: windowID,
|
app: makeApp(),
|
title: "Notes",
|
identifierSource: .cgWindow,
|
isMinimized: true
|
)))
|
XCTAssertEqual(
|
windowID,
|
MinimizedWindowFallbackPolicy.syntheticWindowID(
|
processIdentifier: 42,
|
windowIndex: 3,
|
title: "Notes",
|
subroleDescription: "standard"
|
)
|
)
|
XCTAssertNotEqual(
|
windowID,
|
MinimizedWindowFallbackPolicy.syntheticWindowID(
|
processIdentifier: 42,
|
windowIndex: 4,
|
title: "Notes",
|
subroleDescription: "standard"
|
)
|
)
|
}
|
|
func testMinimizedWindowFallbackPolicyAvoidsOccupiedSyntheticIDCollisions() {
|
let windowID = MinimizedWindowFallbackPolicy.syntheticWindowID(
|
processIdentifier: 42,
|
windowIndex: 3,
|
title: "Notes",
|
subroleDescription: "standard"
|
)
|
let replacement = MinimizedWindowFallbackPolicy.syntheticWindowID(
|
processIdentifier: 42,
|
windowIndex: 3,
|
title: "Notes",
|
subroleDescription: "standard",
|
occupiedWindowIDs: [windowID]
|
)
|
|
XCTAssertGreaterThanOrEqual(replacement, MinimizedWindowFallbackPolicy.syntheticWindowIDMarker)
|
XCTAssertNotEqual(replacement, windowID)
|
}
|
|
func testWindowEnumerationPolicyKeepsFullscreenWindowsDespiteSmallBounds() {
|
let record = makeWindowRecord(size: .zero, isFullscreen: true, spaceIDs: [42])
|
let window = WindowEnumerationPolicy.window(from: record)
|
|
XCTAssertEqual(window?.isFullscreen, true)
|
XCTAssertEqual(window?.spaceIDs, [42])
|
}
|
|
func testWindowEnumerationPolicyKeepsFullscreenWhenRunningAppReportsHidden() {
|
let record = makeWindowRecord(appHidden: true, isFullscreen: true, spaceIDs: [42])
|
let window = WindowEnumerationPolicy.window(from: record)
|
|
XCTAssertEqual(window?.id, 100)
|
XCTAssertEqual(window?.isFullscreen, true)
|
}
|
|
func testWindowEnumerationPolicyExcludesCGOnlyOffscreenGhostWindows() {
|
let record = makeWindowRecord(
|
title: "Closed Document.md",
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(record))
|
XCTAssertNil(WindowEnumerationPolicy.window(from: record))
|
}
|
|
func testWindowEnumerationPolicyKeepsCGOnlyOffscreenWindowsWithKnownSpaceIDs() {
|
let record = makeWindowRecord(
|
title: "Other Space Document.md",
|
hasAXBacking: false,
|
isOnscreen: false,
|
spaceIDs: [5]
|
)
|
|
let window = WindowEnumerationPolicy.window(from: record)
|
|
XCTAssertEqual(window?.title, "Other Space Document.md")
|
XCTAssertEqual(window?.spaceIDs, [5])
|
}
|
|
func testWindowEnumerationPolicyKeepsAXBackedOffscreenWindows() {
|
let record = makeWindowRecord(
|
title: "Other Space Document.md",
|
hasAXBacking: true,
|
isOnscreen: false,
|
spaceIDs: [5]
|
)
|
let window = WindowEnumerationPolicy.window(from: record)
|
|
XCTAssertEqual(window?.title, "Other Space Document.md")
|
}
|
|
func testWindowEnumerationPolicyKeepsOnscreenCGOnlyWindows() {
|
let record = makeWindowRecord(
|
title: "Visible Document.md",
|
hasAXBacking: false,
|
isOnscreen: true,
|
spaceIDs: [5]
|
)
|
let window = WindowEnumerationPolicy.window(from: record)
|
|
XCTAssertEqual(window?.title, "Visible Document.md")
|
}
|
|
func testWindowEnumerationPolicyKeepsKnownCGOnlyOffscreenMainWindows() {
|
let hbuilderApp = AlignerApp(
|
bundleIdentifier: "io.dcloud.HBuilderX",
|
name: "HBuilderX",
|
category: .editorIDE
|
)
|
let record = WindowEnumerationRecord(
|
id: 55_388,
|
app: hbuilderApp,
|
title: "lm-app - HBuilder X 5.07",
|
size: CGSize(width: 1048, height: 796),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
let emptyTitleRecord = WindowEnumerationRecord(
|
id: 55_389,
|
app: hbuilderApp,
|
title: "",
|
size: CGSize(width: 1048, height: 796),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
let tinyRecord = WindowEnumerationRecord(
|
id: 55_390,
|
app: hbuilderApp,
|
title: "HBuilderX Utility",
|
size: CGSize(width: 80, height: 40),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
|
XCTAssertEqual(WindowEnumerationPolicy.window(from: record)?.title, "lm-app - HBuilder X 5.07")
|
XCTAssertNil(WindowEnumerationPolicy.window(from: emptyTitleRecord))
|
XCTAssertNil(WindowEnumerationPolicy.window(from: tinyRecord))
|
}
|
|
func testWindowEnumerationPolicyExcludesClosedShottrCGPlaceholderButKeepsVisibleWindow() {
|
let shottrApp = AlignerApp(
|
bundleIdentifier: "cc.ffitch.shottr",
|
name: "Shottr",
|
category: .generic
|
)
|
let closedPlaceholderRecord = WindowEnumerationRecord(
|
id: 78,
|
app: shottrApp,
|
activationPolicy: .accessory,
|
title: "Shottr",
|
size: CGSize(width: 1710, height: 997),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
let visibleRecord = WindowEnumerationRecord(
|
id: 79,
|
app: shottrApp,
|
activationPolicy: .accessory,
|
title: "Shottr",
|
size: CGSize(width: 1710, height: 997),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: true
|
)
|
let utilityRecord = WindowEnumerationRecord(
|
id: 102,
|
app: shottrApp,
|
activationPolicy: .accessory,
|
title: "Colors",
|
size: CGSize(width: 250, height: 397),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
|
XCTAssertNil(WindowEnumerationPolicy.window(from: closedPlaceholderRecord))
|
XCTAssertEqual(WindowEnumerationPolicy.window(from: visibleRecord)?.title, "Shottr")
|
XCTAssertNil(WindowEnumerationPolicy.window(from: utilityRecord))
|
}
|
|
func testWindowEnumerationPolicyKeepsFinderCGOnlyOffscreenMainWindows() {
|
let finderApp = AlignerApp(
|
bundleIdentifier: "com.apple.finder",
|
name: "访达",
|
category: .finder
|
)
|
let record = WindowEnumerationRecord(
|
id: 83_664,
|
app: finderApp,
|
title: "Screencapture",
|
size: CGSize(width: 920, height: 436),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
let emptyTitleRecord = WindowEnumerationRecord(
|
id: 83_665,
|
app: finderApp,
|
title: "",
|
size: CGSize(width: 920, height: 436),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
let tinyRecord = WindowEnumerationRecord(
|
id: 83_666,
|
app: finderApp,
|
title: "Finder Utility",
|
size: CGSize(width: 80, height: 40),
|
subrole: .standard,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
|
XCTAssertEqual(WindowEnumerationPolicy.window(from: record)?.title, "Screencapture")
|
XCTAssertNil(WindowEnumerationPolicy.window(from: emptyTitleRecord))
|
XCTAssertNil(WindowEnumerationPolicy.window(from: tinyRecord))
|
}
|
|
func testWindowEnumerationPolicyKeepsMinimizedAndFullscreenCGOnlyOffscreenWindows() {
|
let minimized = makeWindowRecord(
|
title: "Minimized Document.md",
|
isMinimized: true,
|
hasAXBacking: false,
|
isOnscreen: false
|
)
|
let fullscreen = makeWindowRecord(
|
title: "Fullscreen Document.md",
|
isFullscreen: true,
|
hasAXBacking: false,
|
isOnscreen: false,
|
spaceIDs: [42]
|
)
|
|
XCTAssertEqual(WindowEnumerationPolicy.window(from: minimized)?.isMinimized, true)
|
XCTAssertEqual(WindowEnumerationPolicy.window(from: fullscreen)?.isFullscreen, true)
|
}
|
|
func testWindowEnumerationPolicyExcludesVMarkClosedDocumentPlaceholders() {
|
let record = WindowEnumerationRecord(
|
id: 29966,
|
app: AlignerApp(bundleIdentifier: "app.vmark", name: "VMark", category: .generic),
|
title: "",
|
identifierSource: .cgWindow,
|
size: CGSize(width: 1434, height: 902),
|
subrole: .standard,
|
spaceIDs: [5]
|
)
|
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(record))
|
XCTAssertNil(WindowEnumerationPolicy.window(from: record))
|
}
|
|
func testWindowEnumerationPolicyKeepsVMarkAXBackedUntitledWindows() {
|
let record = WindowEnumerationRecord(
|
id: 33973,
|
app: AlignerApp(bundleIdentifier: "app.vmark", name: "VMark", category: .generic),
|
title: "",
|
identifierSource: .cgWindow,
|
size: CGSize(width: 1434, height: 902),
|
subrole: .standard,
|
hasAXBacking: true,
|
spaceIDs: [5]
|
)
|
|
let window = WindowEnumerationPolicy.window(from: record)
|
|
XCTAssertEqual(window?.id, 33973)
|
XCTAssertEqual(window?.title, "VMark Untitled")
|
}
|
|
func testWindowEnumerationPolicyKeepsVMarkOnscreenUntitledWindows() {
|
let record = WindowEnumerationRecord(
|
id: 33973,
|
app: AlignerApp(bundleIdentifier: "app.vmark", name: "VMark", category: .generic),
|
title: "",
|
identifierSource: .cgWindow,
|
size: CGSize(width: 1434, height: 902),
|
subrole: .standard,
|
isOnscreen: true,
|
spaceIDs: [5]
|
)
|
|
let window = WindowEnumerationPolicy.window(from: record)
|
|
XCTAssertEqual(window?.id, 33973)
|
XCTAssertEqual(window?.title, "VMark Untitled")
|
}
|
|
func testWindowEnumerationPolicyExcludesNonProductWindows() {
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(id: 0)))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(activationPolicy: .accessory)))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(size: CGSize(width: 100, height: 60))))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(size: CGSize(width: 120, height: 50))))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(subrole: .unknown("AXSystemDialog"))))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(appHidden: true)))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(isGhost: true)))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(isDesktopElement: true)))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(isSystemCritical: true)))
|
XCTAssertFalse(WindowEnumerationPolicy.shouldInclude(makeWindowRecord(isInteractable: false)))
|
}
|
|
func testWindowEnumerationPolicyIncludesDialogsSheetsAndFullscreenWindows() {
|
let dialogRecord = makeWindowRecord(subrole: .dialog, isFullscreen: true, spaceIDs: [42])
|
let sheetRecord = makeWindowRecord(id: 101, title: "Attached Sheet", subrole: .sheet)
|
let untitledRecord = makeWindowRecord(id: 102, title: "")
|
let dialogWindow = WindowEnumerationPolicy.window(from: dialogRecord)
|
let sheetWindow = WindowEnumerationPolicy.window(from: sheetRecord)
|
let untitledWindow = WindowEnumerationPolicy.window(from: untitledRecord)
|
|
XCTAssertEqual(dialogWindow?.isFullscreen, true)
|
XCTAssertEqual(dialogWindow?.spaceIDs, [42])
|
XCTAssertEqual(sheetWindow?.title, "Attached Sheet")
|
XCTAssertEqual(untitledWindow?.title, "Example Untitled")
|
}
|
|
func testWindowServiceProtocolExposesDelegateSubscription() {
|
let service = MockWindowService()
|
let delegate = MockWindowServiceDelegate()
|
|
service.delegate = delegate
|
service.startObserving()
|
|
XCTAssertEqual(delegate.changes, [WindowChange(kind: .opened, windowID: 700)])
|
}
|
|
func testThumbnailPolicyPrefersScreenshotsForAllAppCategories() {
|
XCTAssertEqual(ThumbnailPolicy.preferredStrategy(for: .finder), .screenshotPreferred)
|
XCTAssertEqual(ThumbnailPolicy.preferredStrategy(for: .browser), .screenshotPreferred)
|
XCTAssertEqual(ThumbnailPolicy.preferredStrategy(for: .terminal), .screenshotPreferred)
|
}
|
|
func testSkeletonThumbnailTemplatesCoverAllAppCategories() {
|
let categories = AppCategory.allCases
|
let templates = categories.map(SkeletonThumbnailTemplate.template(for:))
|
|
XCTAssertEqual(templates.map(\.category), categories)
|
XCTAssertTrue(templates.allSatisfy { $0.headerHeight > 0 })
|
XCTAssertTrue(templates.allSatisfy { $0.contentRows > 0 })
|
}
|
|
@MainActor
|
func testNativeSkeletonThumbnailProviderProducesValidImages() {
|
let provider = NativeSkeletonThumbnailProvider()
|
|
for category in AppCategory.allCases {
|
let app = AlignerApp(bundleIdentifier: "com.example.\(category.rawValue)", name: category.rawValue, category: category)
|
let image = provider.skeleton(for: app)
|
|
XCTAssertEqual(image.size, NativeSkeletonThumbnailProvider.defaultSize)
|
XCTAssertTrue(image.isValid)
|
}
|
}
|
|
@MainActor
|
func testNativeSkeletonThumbnailProviderCanOverlayTitle() {
|
let provider = NativeSkeletonThumbnailProvider()
|
let app = AlignerApp(bundleIdentifier: "com.example.Editor", name: "Editor", category: .editorIDE)
|
|
let image = provider.withOverlayTitle("Project Notes", for: app)
|
|
XCTAssertEqual(image.size, NativeSkeletonThumbnailProvider.defaultSize)
|
XCTAssertTrue(image.isValid)
|
}
|
|
func testSpaceEnumerationPolicyMapsCGSSpaceTypes() {
|
XCTAssertEqual(SpaceEnumerationPolicy.spaceType(fromCGSType: 0), .user)
|
XCTAssertEqual(SpaceEnumerationPolicy.spaceType(fromCGSType: 4), .fullscreen)
|
XCTAssertEqual(SpaceEnumerationPolicy.spaceType(fromCGSType: 99), .unknown(99))
|
}
|
|
func testSpaceEnumerationPolicyKeepsFullscreenSpacesWithContinuousIndexes() {
|
let displays = SpaceEnumerationPolicy.normalizedDisplays(from: [
|
SpaceEnumerationRecord(id: 10, cgsType: 0, displayUUID: "display-a"),
|
SpaceEnumerationRecord(id: 11, cgsType: 4, displayUUID: "display-a"),
|
SpaceEnumerationRecord(id: 12, cgsType: 0, displayUUID: "display-a")
|
])
|
|
XCTAssertEqual(displays.count, 1)
|
XCTAssertEqual(displays[0].spaces.map(\.id), [10, 11, 12])
|
XCTAssertEqual(displays[0].spaces.map(\.type), [.user, .fullscreen, .user])
|
XCTAssertEqual(displays[0].spaces.map(\.index), [1, 2, 3])
|
}
|
|
func testSpaceEnumerationPolicyTreatsMainAsSharedDisplayGroup() {
|
let displays = SpaceEnumerationPolicy.normalizedDisplays(from: [
|
SpaceEnumerationRecord(id: 20, cgsType: 0, displayUUID: "Main"),
|
SpaceEnumerationRecord(id: 21, cgsType: 4, displayUUID: "")
|
])
|
|
XCTAssertEqual(displays.count, 1)
|
XCTAssertEqual(displays[0].uuid, SpaceEnumerationPolicy.sharedDisplayUUID)
|
XCTAssertEqual(displays[0].spaces.map(\.displayUUID), [
|
SpaceEnumerationPolicy.sharedDisplayUUID,
|
SpaceEnumerationPolicy.sharedDisplayUUID
|
])
|
}
|
|
func testSpaceEnumerationPolicyMarksKnownPhysicalDisplays() {
|
let displays = SpaceEnumerationPolicy.normalizedDisplays(
|
from: [
|
SpaceEnumerationRecord(id: 30, cgsType: 0, displayUUID: "physical-display"),
|
SpaceEnumerationRecord(id: 31, cgsType: 0, displayUUID: "virtual-display")
|
],
|
physicalDisplayUUIDs: ["physical-display"]
|
)
|
|
XCTAssertEqual(displays.map(\.uuid), ["physical-display", "virtual-display"])
|
XCTAssertEqual(displays.map(\.physical), [true, false])
|
}
|
|
func testBundleIDAppCategorizerUsesBuiltInMappings() {
|
let categorizer = BundleIDAppCategorizer()
|
let chrome = AlignerApp(bundleIdentifier: "com.google.Chrome", name: "Chrome", category: .generic)
|
|
XCTAssertEqual(categorizer.category(for: chrome), .browser)
|
}
|
|
@MainActor
|
func testWorkspaceAppIconProviderUsesApplicationURLIcon() {
|
let resolver = MockApplicationIconResolver()
|
let provider = WorkspaceAppIconProvider(resolver: resolver)
|
let app = AlignerApp(bundleIdentifier: "com.example.Found", name: "Found", category: .generic)
|
|
resolver.applicationURLs[app.bundleIdentifier] = URL(fileURLWithPath: "/Applications/Found.app")
|
|
let icon = provider.icon(for: app)
|
|
XCTAssertTrue(icon === resolver.fileIcon)
|
XCTAssertEqual(resolver.requestedIconPaths, ["/Applications/Found.app"])
|
}
|
|
@MainActor
|
func testWorkspaceAppIconProviderFallsBackWhenApplicationIsMissing() {
|
let resolver = MockApplicationIconResolver()
|
let provider = WorkspaceAppIconProvider(resolver: resolver)
|
let app = AlignerApp(bundleIdentifier: "com.example.Missing", name: "Missing", category: .generic)
|
|
let icon = provider.icon(for: app)
|
|
XCTAssertTrue(icon === resolver.fallbackIcon)
|
XCTAssertTrue(resolver.requestedIconPaths.isEmpty)
|
}
|
|
@MainActor
|
func testWorkspaceAppIconProviderUsesExecutablePathFallback() {
|
let resolver = MockApplicationIconResolver()
|
let provider = WorkspaceAppIconProvider(resolver: resolver)
|
let app = AlignerApp(
|
bundleIdentifier: "com.example.Unregistered",
|
name: "Unregistered",
|
category: .generic,
|
executablePath: "/Applications/Unregistered.app/Contents/MacOS/Unregistered"
|
)
|
|
let icon = provider.icon(for: app)
|
|
XCTAssertTrue(icon === resolver.fileIcon)
|
XCTAssertEqual(resolver.requestedIconPaths, ["/Applications/Unregistered.app/Contents/MacOS/Unregistered"])
|
}
|
|
@MainActor
|
func testWorkspaceAppIconProviderFallsBackWhenResolvedIconIsInvalid() {
|
let resolver = MockApplicationIconResolver(fileIcon: NSImage(size: .zero))
|
let provider = WorkspaceAppIconProvider(resolver: resolver)
|
let app = AlignerApp(bundleIdentifier: "com.example.Invalid", name: "Invalid", category: .generic)
|
|
resolver.applicationURLs[app.bundleIdentifier] = URL(fileURLWithPath: "/Applications/Invalid.app")
|
|
let icon = provider.icon(for: app)
|
|
XCTAssertTrue(icon === resolver.fallbackIcon)
|
XCTAssertEqual(resolver.requestedIconPaths, ["/Applications/Invalid.app"])
|
}
|
|
@MainActor
|
func testScreenCaptureKitScreenshotProviderReturnsCapturedImage() async throws {
|
let capturer = MockWindowScreenshotCapturer(pointSize: NSSize(width: 12, height: 6))
|
let provider = ScreenCaptureKitScreenshotProvider(
|
capturer: capturer,
|
permissionChecker: MockPermissionChecker()
|
)
|
let window = AlignerWindow(id: 321, app: makeApp(), title: "Captured")
|
|
let image = try await provider.screenshot(for: window)
|
|
XCTAssertEqual(capturer.requestedWindowIDs, [321])
|
XCTAssertEqual(image.size, NSSize(width: 12, height: 6))
|
XCTAssertTrue(image.isValid)
|
}
|
|
@MainActor
|
func testScreenCaptureKitScreenshotProviderPropagatesCaptureFailure() async {
|
let capturer = MockWindowScreenshotCapturer(error: ScreenshotProviderError.windowNotFound(404))
|
let provider = ScreenCaptureKitScreenshotProvider(
|
capturer: capturer,
|
permissionChecker: MockPermissionChecker()
|
)
|
let window = AlignerWindow(id: 404, app: makeApp(), title: "Missing")
|
|
do {
|
_ = try await provider.screenshot(for: window)
|
XCTFail("Expected screenshot capture to fail.")
|
} catch let error as ScreenshotProviderError {
|
XCTAssertEqual(error, .windowNotFound(404))
|
} catch {
|
XCTFail("Expected ScreenshotProviderError, got \(error).")
|
}
|
}
|
|
@MainActor
|
func testScreenCaptureKitScreenshotProviderFallsBackOnCaptureFailure() async {
|
let fallbackImage = NSImage.alignerTestImage()
|
let skeletonProvider = MockSkeletonThumbnailProvider(image: fallbackImage)
|
let logger = MockScreenshotDebugLogger()
|
let capturer = MockWindowScreenshotCapturer(error: ScreenshotProviderError.windowNotFound(505))
|
let provider: any ScreenshotProviderProtocol = ScreenCaptureKitScreenshotProvider(
|
capturer: capturer,
|
skeletonProvider: skeletonProvider,
|
debugLogger: logger,
|
permissionChecker: MockPermissionChecker()
|
)
|
let session = ScreenshotCaptureSession()
|
let window = AlignerWindow(id: 505, app: makeApp(), title: "Fallback Title")
|
|
let resolution = await provider.resolvedScreenshot(for: window, in: session)
|
|
XCTAssertTrue(resolution.image === fallbackImage)
|
XCTAssertEqual(capturer.requestedWindowIDs, [505])
|
XCTAssertEqual(skeletonProvider.overlayTitles, ["Fallback Title"])
|
guard case .skeletonFallback(.captureFailed(let failureSummary)) = resolution.source else {
|
XCTFail("Expected sanitized capture failure fallback.")
|
return
|
}
|
XCTAssertFalse(failureSummary.contains("windowNotFound"))
|
XCTAssertTrue(failureSummary.contains("domain="))
|
XCTAssertTrue(failureSummary.contains("code="))
|
XCTAssertTrue(failureSummary.contains("descriptionHash="))
|
XCTAssertTrue(failureSummary.contains("descriptionLength="))
|
XCTAssertEqual(
|
logger.events,
|
[
|
.captureStarted(windowID: 505),
|
.captureFailed(windowID: 505, reason: .captureFailed(failureSummary)),
|
.fallbackUsed(windowID: 505, reason: .captureFailed(failureSummary))
|
]
|
)
|
}
|
|
@MainActor
|
func testScreenCaptureKitScreenshotProviderFallsBackWhenScreenRecordingIsDenied() async {
|
let skeletonProvider = MockSkeletonThumbnailProvider()
|
let logger = MockScreenshotDebugLogger()
|
let capturer = MockWindowScreenshotCapturer()
|
let provider: any ScreenshotProviderProtocol = ScreenCaptureKitScreenshotProvider(
|
capturer: capturer,
|
skeletonProvider: skeletonProvider,
|
debugLogger: logger,
|
permissionChecker: MockPermissionChecker(screenRecording: .notGranted)
|
)
|
let session = ScreenshotCaptureSession()
|
let window = AlignerWindow(id: 909, app: makeApp(), title: "Needs Permission")
|
|
let resolution = await provider.resolvedScreenshot(for: window, in: session)
|
|
XCTAssertEqual(capturer.requestedWindowIDs, [])
|
XCTAssertEqual(resolution.source, .skeletonFallback(.screenRecordingDenied))
|
XCTAssertEqual(skeletonProvider.overlayTitles, ["Needs Permission"])
|
XCTAssertEqual(
|
logger.events,
|
[
|
.fallbackUsed(windowID: 909, reason: .screenRecordingDenied)
|
]
|
)
|
}
|
|
@MainActor
|
func testScreenCaptureKitScreenshotProviderSkipsCaptureAfterRetryBudgetIsExhausted() async {
|
let skeletonProvider = MockSkeletonThumbnailProvider()
|
let logger = MockScreenshotDebugLogger()
|
let capturer = MockWindowScreenshotCapturer(error: ScreenshotProviderError.windowNotFound(606))
|
let provider = ScreenCaptureKitScreenshotProvider(
|
capturer: capturer,
|
skeletonProvider: skeletonProvider,
|
policy: ScreenshotCapturePolicy(timeoutMilliseconds: 300, maxRetriesPerSession: 1),
|
debugLogger: logger,
|
permissionChecker: MockPermissionChecker()
|
)
|
let session = ScreenshotCaptureSession()
|
let window = AlignerWindow(id: 606, app: makeApp(), title: "Retry")
|
|
_ = await provider.resolvedScreenshot(for: window, in: session)
|
_ = await provider.resolvedScreenshot(for: window, in: session)
|
let skipped = await provider.resolvedScreenshot(for: window, in: session)
|
|
XCTAssertEqual(capturer.requestedWindowIDs, [606, 606])
|
XCTAssertEqual(
|
skipped.source,
|
.skeletonFallback(.retryLimitReached(failedAttempts: 2))
|
)
|
XCTAssertTrue(
|
logger.events.contains(.retrySkipped(windowID: 606, failedAttempts: 2))
|
)
|
|
_ = await provider.resolvedScreenshot(for: window, in: ScreenshotCaptureSession())
|
XCTAssertEqual(capturer.requestedWindowIDs, [606, 606, 606])
|
}
|
|
@MainActor
|
func testScreenCaptureKitScreenshotProviderFallsBackOnTimeout() async {
|
let skeletonProvider = MockSkeletonThumbnailProvider()
|
let logger = MockScreenshotDebugLogger()
|
let capturer = MockWindowScreenshotCapturer(delayNanoseconds: 50_000_000)
|
let provider = ScreenCaptureKitScreenshotProvider(
|
capturer: capturer,
|
skeletonProvider: skeletonProvider,
|
policy: ScreenshotCapturePolicy(timeoutMilliseconds: 1, maxRetriesPerSession: 1),
|
debugLogger: logger,
|
permissionChecker: MockPermissionChecker()
|
)
|
let session = ScreenshotCaptureSession()
|
let window = AlignerWindow(id: 707, app: makeApp(), title: "Slow")
|
|
let startedAt = Date()
|
let resolution = await provider.resolvedScreenshot(for: window, in: session)
|
let elapsed = Date().timeIntervalSince(startedAt)
|
|
XCTAssertEqual(capturer.requestedWindowIDs, [707])
|
XCTAssertLessThan(elapsed, 0.1)
|
XCTAssertEqual(
|
resolution.source,
|
.skeletonFallback(.timedOut(windowID: 707, timeoutMilliseconds: 1))
|
)
|
XCTAssertTrue(
|
logger.events.contains(.fallbackUsed(
|
windowID: 707,
|
reason: .timedOut(windowID: 707, timeoutMilliseconds: 1)
|
))
|
)
|
}
|
|
@MainActor
|
func testScreenCaptureKitScreenshotProviderFallsBackOnInvalidCapture() async {
|
let skeletonProvider = MockSkeletonThumbnailProvider()
|
let logger = MockScreenshotDebugLogger()
|
let capturer = MockWindowScreenshotCapturer(pointSize: .zero)
|
let provider = ScreenCaptureKitScreenshotProvider(
|
capturer: capturer,
|
skeletonProvider: skeletonProvider,
|
debugLogger: logger,
|
permissionChecker: MockPermissionChecker()
|
)
|
let window = AlignerWindow(id: 808, app: makeApp(), title: "Invalid")
|
|
let resolution = await provider.resolvedScreenshot(for: window, in: ScreenshotCaptureSession())
|
|
XCTAssertEqual(capturer.requestedWindowIDs, [808])
|
XCTAssertEqual(
|
resolution.source,
|
.skeletonFallback(.invalidCapture(
|
windowID: 808,
|
pixelWidth: 24,
|
pixelHeight: 12,
|
pointWidth: 0,
|
pointHeight: 0
|
))
|
)
|
XCTAssertEqual(skeletonProvider.overlayTitles, ["Invalid"])
|
}
|
|
@MainActor
|
func testScreenCaptureKitScreenshotProviderUsesSkeletonForSyntheticWindowID() async {
|
let skeletonProvider = MockSkeletonThumbnailProvider()
|
let logger = MockScreenshotDebugLogger()
|
let capturer = MockWindowScreenshotCapturer()
|
let provider = ScreenCaptureKitScreenshotProvider(
|
capturer: capturer,
|
skeletonProvider: skeletonProvider,
|
debugLogger: logger,
|
permissionChecker: MockPermissionChecker()
|
)
|
let windowID = MinimizedWindowFallbackPolicy.syntheticWindowID(
|
processIdentifier: 42,
|
windowIndex: 0,
|
title: "Minimized",
|
subroleDescription: "standard"
|
)
|
let window = AlignerWindow(
|
id: windowID,
|
app: makeApp(),
|
title: "Minimized",
|
identifierSource: .syntheticAX,
|
isMinimized: true
|
)
|
|
let resolution = await provider.resolvedScreenshot(for: window, in: ScreenshotCaptureSession())
|
|
XCTAssertEqual(capturer.requestedWindowIDs, [])
|
XCTAssertEqual(resolution.source, .skeletonFallback(.syntheticWindowID(windowID: windowID)))
|
XCTAssertEqual(
|
logger.events,
|
[
|
.fallbackUsed(windowID: windowID, reason: .syntheticWindowID(windowID: windowID))
|
]
|
)
|
XCTAssertEqual(skeletonProvider.overlayTitles, ["Minimized"])
|
}
|
|
func testPreferenceKeysAreStableAndNamespaced() {
|
XCTAssertEqual(
|
PreferenceKey.allCases.map(\.rawValue),
|
[
|
"schemaVersion",
|
"quickSwitch.showMinimizedWindows",
|
"quickSwitch.showFullscreenWindows",
|
"appearance.theme",
|
"appearance.language",
|
"spaces.displayStrategy"
|
]
|
)
|
|
XCTAssertTrue(PreferenceKey.allCases.allSatisfy {
|
$0.storageKey.hasPrefix("com.ar.Aligner.preferences.")
|
})
|
}
|
|
func testDefaultPreferencesMatchRound0Decisions() {
|
XCTAssertEqual(PreferenceSchema.version, 1)
|
XCTAssertEqual(PreferenceSchema.defaults.showMinimizedWindows, true)
|
XCTAssertEqual(PreferenceSchema.defaults.showFullscreenWindows, true)
|
XCTAssertEqual(PreferenceSchema.defaults.theme, .system)
|
XCTAssertEqual(PreferenceSchema.defaults.language, .system)
|
XCTAssertEqual(PreferenceSchema.defaults.spaceDisplayStrategy, .includeVisibleAndFullscreen)
|
}
|
|
func testUserDefaultsPreferenceStoreRoundTrips() throws {
|
let suiteName = "com.ar.Aligner.tests.\(UUID().uuidString)"
|
let userDefaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
|
defer {
|
userDefaults.removePersistentDomain(forName: suiteName)
|
}
|
|
let store = UserDefaultsPreferenceStore(userDefaults: userDefaults)
|
XCTAssertEqual(store.read(), PreferenceSchema.defaults)
|
XCTAssertEqual(store.storedSchemaVersion(), PreferenceSchema.version)
|
XCTAssertFalse(store.needsMigration())
|
|
let preferences = AlignerPreferences(
|
showMinimizedWindows: false,
|
showFullscreenWindows: false,
|
theme: .dark,
|
language: .simplifiedChinese,
|
spaceDisplayStrategy: .currentSpaceOnly
|
)
|
|
store.write(preferences)
|
XCTAssertEqual(store.read(), preferences)
|
XCTAssertEqual(store.storedSchemaVersion(), PreferenceSchema.version)
|
|
store.reset()
|
XCTAssertEqual(store.read(), PreferenceSchema.defaults)
|
}
|
|
func testUserDefaultsPreferenceStoreDetectsOldSchemaVersion() throws {
|
let suiteName = "com.ar.Aligner.tests.\(UUID().uuidString)"
|
let userDefaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
|
defer {
|
userDefaults.removePersistentDomain(forName: suiteName)
|
}
|
|
userDefaults.set(0, forKey: PreferenceKey.schemaVersion.storageKey)
|
|
let store = UserDefaultsPreferenceStore(userDefaults: userDefaults)
|
XCTAssertEqual(store.storedSchemaVersion(), 0)
|
XCTAssertTrue(store.needsMigration())
|
}
|
|
func testUserDefaultsPreferenceStoreFallsBackForInvalidEnumValues() throws {
|
let suiteName = "com.ar.Aligner.tests.\(UUID().uuidString)"
|
let userDefaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
|
defer {
|
userDefaults.removePersistentDomain(forName: suiteName)
|
}
|
|
userDefaults.set("not-a-theme", forKey: PreferenceKey.theme.storageKey)
|
userDefaults.set("not-a-language", forKey: PreferenceKey.language.storageKey)
|
userDefaults.set("not-a-space-strategy", forKey: PreferenceKey.spaceDisplayStrategy.storageKey)
|
|
let preferences = UserDefaultsPreferenceStore(userDefaults: userDefaults).read()
|
|
XCTAssertEqual(preferences.theme, PreferenceSchema.defaults.theme)
|
XCTAssertEqual(preferences.language, PreferenceSchema.defaults.language)
|
XCTAssertEqual(preferences.spaceDisplayStrategy, PreferenceSchema.defaults.spaceDisplayStrategy)
|
}
|
|
@MainActor
|
func testOverlayWindowPolicyMatchesRound0LayeringDecision() {
|
let styleMask = OverlayWindowPolicy.styleMask
|
XCTAssertTrue(styleMask.contains(.borderless))
|
XCTAssertTrue(styleMask.contains(.fullSizeContentView))
|
XCTAssertTrue(styleMask.contains(.nonactivatingPanel))
|
|
let collectionBehavior = OverlayWindowPolicy.collectionBehavior
|
XCTAssertTrue(collectionBehavior.contains(.canJoinAllSpaces))
|
XCTAssertTrue(collectionBehavior.contains(.fullScreenAuxiliary))
|
XCTAssertTrue(collectionBehavior.contains(.stationary))
|
XCTAssertTrue(collectionBehavior.contains(.transient))
|
XCTAssertTrue(collectionBehavior.contains(.ignoresCycle))
|
|
XCTAssertEqual(
|
OverlayWindowPolicy.quickSwitchLevel.rawValue,
|
Int(CGWindowLevelForKey(.mainMenuWindow) - OverlayWindowPolicy.levelOffsetBelowMainMenu)
|
)
|
XCTAssertTrue(OverlayWindowPolicy.isBelowRescueSystemWindows())
|
}
|
|
func testSystemCriticalWindowClassifierCoversRescueAndPermissionWindows() {
|
XCTAssertTrue(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(title: "Force Quit Applications")
|
))
|
XCTAssertTrue(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(ownerName: "loginwindow", title: "强制退出应用程序")
|
))
|
XCTAssertTrue(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(ownerName: "SecurityAgent", title: "Security Confirmation")
|
))
|
XCTAssertTrue(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(ownerName: "CoreServicesUIAgent", title: "Privacy & Security")
|
))
|
XCTAssertTrue(SystemCriticalWindowClassifier.isSystemCriticalTitle("屏幕录制权限"))
|
XCTAssertTrue(SystemCriticalWindowClassifier.isSystemCriticalTitle("Accessibility Permissions"))
|
XCTAssertFalse(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(ownerName: "Safari", title: "Permissions API Guide")
|
))
|
XCTAssertFalse(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(ownerName: "System Settings", title: "Privacy & Security")
|
))
|
XCTAssertFalse(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(ownerName: "loginwindow", title: "")
|
))
|
XCTAssertFalse(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(ownerName: "Notes", title: "权限设计草稿")
|
))
|
XCTAssertFalse(SystemCriticalWindowClassifier.isSystemCritical(
|
SystemCriticalWindowDescriptor(ownerName: "Safari", title: "Example")
|
))
|
}
|
|
@MainActor
|
func testOverlaySessionStartsWithQuickSwitchOnly() {
|
let session = OverlaySession()
|
|
XCTAssertEqual(session.state, .idle)
|
|
session.openQuickSwitch()
|
XCTAssertEqual(session.state, .browsing)
|
XCTAssertEqual(session.context?.entryPoint, .quickSwitch)
|
}
|
|
@MainActor
|
func testOverlaySessionIgnoresSearchUnlessBrowsing() {
|
let session = OverlaySession()
|
|
session.enterQuickSearch()
|
XCTAssertEqual(session.state, .idle)
|
|
session.openQuickSwitch()
|
session.enterQuickSearch()
|
XCTAssertEqual(session.state, .quickSearch(.title))
|
}
|
|
func testOverlayPlacementUsesMouseContainingScreen() {
|
let placement = OverlayPlacementPolicy.placement(
|
forMouseLocation: CGPoint(x: 1500, y: 500),
|
screens: [
|
OverlayScreenRecord(
|
displayUUID: "display-a",
|
frame: CGRect(x: 0, y: 0, width: 1000, height: 800),
|
visibleFrame: CGRect(x: 0, y: 0, width: 1000, height: 760)
|
),
|
OverlayScreenRecord(
|
displayUUID: "display-b",
|
frame: CGRect(x: 1000, y: 0, width: 1000, height: 800),
|
visibleFrame: CGRect(x: 1000, y: 0, width: 1000, height: 760)
|
)
|
]
|
)
|
|
XCTAssertEqual(placement.targetDisplayUUID, "display-b")
|
XCTAssertTrue(placement.coverFullscreenSpace)
|
XCTAssertEqual(placement.overlayContext.targetDisplayUUID, "display-b")
|
XCTAssertTrue(placement.overlayContext.spacePolicy.avoidSpaceSwitch)
|
}
|
|
func testOverlayPlacementFallsBackToFirstScreenWhenMouseIsOutsideScreens() {
|
let placement = OverlayPlacementPolicy.placement(
|
forMouseLocation: CGPoint(x: -200, y: -200),
|
screens: [
|
OverlayScreenRecord(
|
displayUUID: "display-a",
|
frame: CGRect(x: 0, y: 0, width: 1000, height: 800),
|
visibleFrame: CGRect(x: 0, y: 0, width: 1000, height: 760)
|
)
|
]
|
)
|
|
XCTAssertEqual(placement.targetDisplayUUID, "display-a")
|
}
|
|
func testOverlayPlacementRecordsFullscreenAndSplitViewState() {
|
let placement = OverlayPlacementPolicy.placement(
|
forMouseLocation: CGPoint(x: 1500, y: 500),
|
screens: [
|
OverlayScreenRecord(
|
displayUUID: "display-a",
|
frame: CGRect(x: 0, y: 0, width: 1000, height: 800),
|
visibleFrame: CGRect(x: 0, y: 0, width: 1000, height: 760)
|
),
|
OverlayScreenRecord(
|
displayUUID: "display-b",
|
frame: CGRect(x: 1000, y: 0, width: 1000, height: 800),
|
visibleFrame: CGRect(x: 1000, y: 0, width: 1000, height: 760),
|
isFullscreenSpace: true,
|
isSplitViewSpace: true
|
)
|
]
|
)
|
|
XCTAssertEqual(placement.targetDisplayUUID, "display-b")
|
XCTAssertTrue(placement.isFullscreenSpace)
|
XCTAssertTrue(placement.isSplitViewSpace)
|
XCTAssertTrue(placement.avoidSpaceSwitch)
|
XCTAssertEqual(placement.overlayContext.spacePolicy, SpacePolicy(isFullscreenSpace: true, avoidSpaceSwitch: true))
|
}
|
|
func testQuickSwitchSnapshotFiltersCandidatesAndSortsAppsByStableName() {
|
let beta = AlignerApp(bundleIdentifier: "com.example.Beta", name: "Beta", category: .generic)
|
let alpha = AlignerApp(bundleIdentifier: "com.example.Alpha", name: "Alpha", category: .generic)
|
let hidden = AlignerApp(bundleIdentifier: "com.example.Hidden", name: "Hidden", category: .generic, isHidden: true)
|
let displays = [makeDisplay(spaceIDs: [1])]
|
let snapshot = QuickSwitchSnapshotBuilder.snapshot(
|
displays: displays,
|
windows: [
|
AlignerWindow(id: 3, app: beta, title: "Beta"),
|
AlignerWindow(id: 1, app: hidden, title: "Hidden"),
|
AlignerWindow(id: 2, app: alpha, title: "Alpha"),
|
AlignerWindow(id: 4, app: beta, title: "Ghost", isGhost: true)
|
]
|
)
|
|
XCTAssertEqual(snapshot.appGroups.map(\.app.name), ["Alpha", "Beta"])
|
XCTAssertEqual(snapshot.appGroups.flatMap(\.windows).map(\.window.id), [2, 3])
|
}
|
|
func testQuickSwitchSnapshotUsesBundlePathAndPIDAsStableAppIdentityFallbacks() {
|
let firstCopy = AlignerApp(
|
bundleIdentifier: "com.example.Editor",
|
name: "Editor",
|
category: .generic,
|
executablePath: "/Applications/Editor.app",
|
processIdentifier: 100
|
)
|
let secondCopy = AlignerApp(
|
bundleIdentifier: "com.example.Editor",
|
name: "Editor",
|
category: .generic,
|
executablePath: "/Users/ar/Downloads/Editor.app",
|
processIdentifier: 200
|
)
|
let noBundleA = AlignerApp(
|
bundleIdentifier: "",
|
name: "Editor",
|
category: .generic,
|
executablePath: "/Applications/NoBundleA.app",
|
processIdentifier: 300
|
)
|
let noBundleB = AlignerApp(
|
bundleIdentifier: "",
|
name: "Editor",
|
category: .generic,
|
executablePath: "/Applications/NoBundleB.app",
|
processIdentifier: 400
|
)
|
|
let snapshot = QuickSwitchSnapshotBuilder.snapshot(
|
displays: [makeDisplay(spaceIDs: [1])],
|
windows: [
|
AlignerWindow(id: 4, app: noBundleB, title: "No Bundle B"),
|
AlignerWindow(id: 2, app: secondCopy, title: "Second Copy"),
|
AlignerWindow(id: 3, app: noBundleA, title: "No Bundle A"),
|
AlignerWindow(id: 1, app: firstCopy, title: "First Copy")
|
]
|
)
|
|
XCTAssertEqual(snapshot.appGroups.count, 4)
|
XCTAssertEqual(snapshot.appGroups.flatMap(\.windows).map(\.window.id), [1, 2, 3, 4])
|
}
|
|
func testQuickSwitchSnapshotSortsWindowsBySpaceThenTitleThenID() {
|
let app = makeApp()
|
let displays = [makeDisplay(spaceIDs: [10, 11, 12])]
|
let snapshot = QuickSwitchSnapshotBuilder.snapshot(
|
displays: displays,
|
windows: [
|
AlignerWindow(id: 40, app: app, title: "Zulu", spaceIDs: [12]),
|
AlignerWindow(id: 10, app: app, title: "Beta", spaceIDs: [10]),
|
AlignerWindow(id: 30, app: app, title: "Alpha", spaceIDs: [12]),
|
AlignerWindow(id: 20, app: app, title: "Alpha", spaceIDs: [11]),
|
AlignerWindow(id: 50, app: app, title: "No Space")
|
]
|
)
|
|
XCTAssertEqual(snapshot.appGroups.single?.windows.map(\.window.id), [10, 20, 30, 40, 50])
|
XCTAssertEqual(snapshot.appGroups.single?.windows.map(\.primarySpaceID), [10, 11, 12, 12, nil])
|
}
|
|
func testWindowSpaceMappingFillsUnresolvedWindowsFromSpaceInverseMap() {
|
let mapping = WindowSpaceMappingPolicy.mergedSpaceIDsByWindowID(
|
requestedWindowIDs: [10, 20, 30],
|
directSpaceIDsByWindowID: [10: [100]],
|
windowIDsBySpaceID: [
|
100: [10, 20],
|
101: [30]
|
],
|
orderedSpaceIDs: [100, 101]
|
)
|
|
XCTAssertEqual(mapping[10], [100])
|
XCTAssertEqual(mapping[20], [100])
|
XCTAssertEqual(mapping[30], [101])
|
}
|
|
func testWindowSpaceMappingDoesNotOverwriteDirectSpaceAssignments() {
|
let mapping = WindowSpaceMappingPolicy.mergedSpaceIDsByWindowID(
|
requestedWindowIDs: [10],
|
directSpaceIDsByWindowID: [10: [101]],
|
windowIDsBySpaceID: [
|
100: [10]
|
],
|
orderedSpaceIDs: [100, 101]
|
)
|
|
XCTAssertEqual(mapping[10], [101])
|
}
|
|
func testWindowSpaceMappingLeavesAmbiguousNoSpaceWindowsUnassigned() {
|
let mapping = WindowSpaceMappingPolicy.mergedSpaceIDsByWindowID(
|
requestedWindowIDs: [10, 20],
|
directSpaceIDsByWindowID: [:],
|
windowIDsBySpaceID: [
|
100: [10, 99]
|
],
|
orderedSpaceIDs: [100]
|
)
|
|
XCTAssertEqual(mapping[10], [100])
|
XCTAssertNil(mapping[20])
|
XCTAssertNil(mapping[99])
|
}
|
|
func testQuickSwitchSnapshotKeepsMinimizedAndFullscreenWindows() {
|
let app = makeApp()
|
let snapshot = QuickSwitchSnapshotBuilder.snapshot(
|
displays: [makeDisplay(spaceIDs: [1, 2])],
|
windows: [
|
AlignerWindow(id: 1, app: app, title: "Minimized", isMinimized: true, spaceIDs: [1]),
|
AlignerWindow(id: 2, app: app, title: "Fullscreen", isFullscreen: true, spaceIDs: [2])
|
]
|
)
|
|
XCTAssertEqual(snapshot.appGroups.single?.windows.map(\.window.id), [1, 2])
|
}
|
|
func testQuickSwitchViewModelBuilderKeepsSpaceAppAndWaterfallOrderAligned() {
|
let alpha = AlignerApp(bundleIdentifier: "com.example.Alpha", name: "Alpha", category: .generic)
|
let beta = AlignerApp(bundleIdentifier: "com.example.Beta", name: "Beta", category: .generic)
|
let snapshot = QuickSwitchSnapshotBuilder.snapshot(
|
displays: [
|
makeDisplay(spaceIDs: [20], uuid: "display-b"),
|
makeDisplay(spaceIDs: [10, 11], uuid: "display-a")
|
],
|
windows: [
|
AlignerWindow(id: 3, app: beta, title: "B", spaceIDs: [20]),
|
AlignerWindow(id: 1, app: alpha, title: "A1", spaceIDs: [10]),
|
AlignerWindow(id: 2, app: alpha, title: "A2", isMinimized: true, spaceIDs: [11])
|
]
|
)
|
|
let viewModel = QuickSwitchViewModelBuilder.viewModel(from: snapshot, currentSpaceIDs: [11])
|
|
XCTAssertEqual(viewModel.displays.map(\.displayLabel), ["A", "B"])
|
XCTAssertEqual(viewModel.displays.flatMap { $0.spaces.map(\.label) }, ["A1", "A2", "B1"])
|
XCTAssertEqual(viewModel.displays.flatMap { $0.spaces.map(\.windowCount) }, [1, 1, 1])
|
XCTAssertEqual(viewModel.displays.flatMap { $0.spaces.map(\.appNames) }, [["Alpha"], ["Alpha"], ["Beta"]])
|
XCTAssertEqual(viewModel.displays.flatMap { $0.spaces.filter(\.isCurrent).map(\.label) }, ["A2"])
|
XCTAssertEqual(viewModel.appShelf.map(\.app.name), ["Alpha", "Beta"])
|
XCTAssertEqual(viewModel.appShelf.map(\.windowCount), [2, 1])
|
XCTAssertEqual(viewModel.appShelf.first?.primarySpaceIDs, [10, 11])
|
XCTAssertEqual(viewModel.appShelf.first?.hasMinimizedWindows, true)
|
XCTAssertEqual(viewModel.waterfallColumns.map(\.app.name), viewModel.appShelf.map(\.app.name))
|
XCTAssertEqual(viewModel.waterfallColumns.flatMap { $0.windows.map(\.primarySpaceLabel) }, ["A1", "A2", "B1"])
|
XCTAssertEqual(viewModel.initialSelection, QuickSwitchSelection(appGroupIndex: 0, windowIndex: 0, windowID: 1))
|
}
|
|
func testQuickSwitchViewModelBuilderUsesNumericSpaceLabelsForSingleDisplay() {
|
let app = AlignerApp(bundleIdentifier: "com.example.Alpha", name: "Alpha", category: .generic)
|
let snapshot = QuickSwitchSnapshotBuilder.snapshot(
|
displays: [makeDisplay(spaceIDs: [10, 11, 12])],
|
windows: [
|
AlignerWindow(id: 1, app: app, title: "A1", spaceIDs: [10]),
|
AlignerWindow(id: 2, app: app, title: "A2", spaceIDs: [11])
|
]
|
)
|
|
let viewModel = QuickSwitchViewModelBuilder.viewModel(from: snapshot, currentSpaceIDs: [11])
|
|
XCTAssertEqual(viewModel.displays.flatMap { $0.spaces.map(\.label) }, ["01", "02", "03"])
|
XCTAssertEqual(viewModel.displays.flatMap { $0.spaces.filter(\.isCurrent).map(\.label) }, ["02"])
|
XCTAssertEqual(viewModel.waterfallColumns.flatMap { $0.windows.map(\.primarySpaceLabel) }, ["01", "02"])
|
}
|
|
func testQuickSwitchFocusPolicySelectsFirstStableWindowInitially() {
|
let app = makeApp()
|
let snapshot = QuickSwitchSnapshotBuilder.snapshot(
|
displays: [makeDisplay(spaceIDs: [1])],
|
windows: [
|
AlignerWindow(id: 2, app: app, title: "Beta", spaceIDs: [1]),
|
AlignerWindow(id: 1, app: app, title: "Alpha", spaceIDs: [1])
|
]
|
)
|
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.initialSelection(in: snapshot),
|
QuickSwitchSelection(appGroupIndex: 0, windowIndex: 0, windowID: 1)
|
)
|
}
|
|
func testQuickSwitchVisualPriorityKeepsKeyboardSelectedAsPrimaryFocus() {
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.visualState(for: 1, selectedIndex: 1, hoveredIndex: 2),
|
.selected
|
)
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.visualState(for: 2, selectedIndex: 1, hoveredIndex: 2),
|
.hover
|
)
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.visualState(for: 1, selectedIndex: 1, hoveredIndex: 1),
|
.selectedAndHover
|
)
|
}
|
|
func testQuickSwitchKeyboardNavigationMovesByAppColumnsAndColumnWindows() {
|
let snapshot = makeNavigationSnapshot(columnWindowCounts: [2, 1, 3])
|
let current = QuickSwitchSelection(appGroupIndex: 0, windowIndex: 1, windowID: 2)
|
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.nextSelection(from: current, in: snapshot, direction: .right),
|
QuickSwitchSelection(appGroupIndex: 1, windowIndex: 0, windowID: 3)
|
)
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.nextSelection(from: current, in: snapshot, direction: .up),
|
QuickSwitchSelection(appGroupIndex: 0, windowIndex: 0, windowID: 1)
|
)
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.nextSelection(from: current, in: snapshot, direction: .down),
|
current
|
)
|
}
|
|
func testQuickSwitchKeyboardNavigationDoesNotWrapAcrossColumnBoundaries() {
|
let snapshot = makeNavigationSnapshot(columnWindowCounts: [2, 1, 3])
|
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.nextSelection(
|
from: QuickSwitchSelection(appGroupIndex: 0, windowIndex: 0, windowID: 1),
|
in: snapshot,
|
direction: .left
|
),
|
QuickSwitchSelection(appGroupIndex: 0, windowIndex: 0, windowID: 1)
|
)
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.nextSelection(
|
from: QuickSwitchSelection(appGroupIndex: 2, windowIndex: 2, windowID: 6),
|
in: snapshot,
|
direction: .right
|
),
|
QuickSwitchSelection(appGroupIndex: 2, windowIndex: 2, windowID: 6)
|
)
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.nextSelection(
|
from: QuickSwitchSelection(appGroupIndex: 2, windowIndex: 0, windowID: 4),
|
in: snapshot,
|
direction: .up
|
),
|
QuickSwitchSelection(appGroupIndex: 2, windowIndex: 0, windowID: 4)
|
)
|
}
|
|
func testQuickSwitchKeyboardNavigationRestoresColumnHistoryOrFallsBackToFirstWindow() {
|
let snapshot = makeNavigationSnapshot(columnWindowCounts: [2, 1, 3])
|
let current = QuickSwitchSelection(appGroupIndex: 1, windowIndex: 0, windowID: 3)
|
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.nextSelection(
|
from: current,
|
in: snapshot,
|
direction: .right,
|
columnHistory: [2: 2]
|
),
|
QuickSwitchSelection(appGroupIndex: 2, windowIndex: 2, windowID: 6)
|
)
|
XCTAssertEqual(
|
QuickSwitchFocusPolicy.nextSelection(
|
from: current,
|
in: snapshot,
|
direction: .left,
|
columnHistory: [0: 99]
|
),
|
QuickSwitchSelection(appGroupIndex: 0, windowIndex: 1, windowID: 2)
|
)
|
}
|
|
private func makeWindowRecord(
|
id: UInt32 = 100,
|
activationPolicy: AlignerActivationPolicy = .regular,
|
title: String = "Document",
|
size: CGSize = CGSize(width: 640, height: 480),
|
subrole: AlignerWindowSubrole = .standard,
|
appHidden: Bool = false,
|
isMinimized: Bool = false,
|
isFullscreen: Bool = false,
|
isGhost: Bool = false,
|
isDesktopElement: Bool = false,
|
isSystemCritical: Bool = false,
|
isInteractable: Bool = true,
|
hasAXBacking: Bool = true,
|
isOnscreen: Bool = true,
|
spaceIDs: [UInt64] = []
|
) -> WindowEnumerationRecord {
|
WindowEnumerationRecord(
|
id: id,
|
app: AlignerApp(
|
bundleIdentifier: "com.example.App",
|
name: "Example",
|
category: .generic,
|
isHidden: appHidden
|
),
|
activationPolicy: activationPolicy,
|
title: title,
|
size: size,
|
subrole: subrole,
|
isMinimized: isMinimized,
|
isFullscreen: isFullscreen,
|
isGhost: isGhost,
|
isDesktopElement: isDesktopElement,
|
isSystemCritical: isSystemCritical,
|
isInteractable: isInteractable,
|
hasAXBacking: hasAXBacking,
|
isOnscreen: isOnscreen,
|
spaceIDs: spaceIDs
|
)
|
}
|
|
private func makeApp() -> AlignerApp {
|
AlignerApp(bundleIdentifier: "com.example.App", name: "Example", category: .generic)
|
}
|
|
private func makeDisplay(spaceIDs: [UInt64], uuid: String = "display-a") -> AlignerDisplay {
|
AlignerDisplay(
|
uuid: uuid,
|
physical: true,
|
spaces: spaceIDs.enumerated().map { offset, id in
|
AlignerSpace(id: id, type: .user, displayUUID: uuid, index: offset + 1)
|
}
|
)
|
}
|
|
private func makeNavigationSnapshot(columnWindowCounts: [Int]) -> QuickSwitchSnapshot {
|
var nextWindowID: UInt32 = 1
|
let groups = columnWindowCounts.enumerated().map { appIndex, windowCount in
|
let app = AlignerApp(
|
bundleIdentifier: "com.example.App\(appIndex)",
|
name: "App \(appIndex)",
|
category: .generic
|
)
|
let windows = (0..<windowCount).map { _ in
|
let window = AlignerWindow(id: nextWindowID, app: app, title: "Window \(nextWindowID)")
|
nextWindowID += 1
|
return QuickSwitchWindowItem(window: window, primarySpaceID: nil)
|
}
|
return QuickSwitchAppGroup(app: app, windows: windows)
|
}
|
|
return QuickSwitchSnapshot(displays: [makeDisplay(spaceIDs: [1])], appGroups: groups)
|
}
|
}
|
|
private extension Array {
|
var single: Element? {
|
count == 1 ? self[0] : nil
|
}
|
}
|
|
private final class MockWindowService: WindowServiceProtocol {
|
weak var delegate: WindowServiceDelegate?
|
|
func allWindows() throws -> [AlignerWindow] {
|
[]
|
}
|
|
func windows(for space: AlignerSpace) throws -> [AlignerWindow] {
|
[]
|
}
|
|
func refresh() {}
|
|
func startObserving() {
|
delegate?.windowServiceDidDetectWindowChange(WindowChange(kind: .opened, windowID: 700))
|
}
|
|
func stopObserving() {}
|
}
|
|
private final class MockWindowServiceDelegate: WindowServiceDelegate {
|
var changes: [WindowChange] = []
|
|
func windowServiceDidDetectWindowChange(_ change: WindowChange) {
|
changes.append(change)
|
}
|
}
|
|
@MainActor
|
private final class MockApplicationIconResolver: ApplicationIconResolving {
|
var applicationURLs: [String: URL] = [:]
|
var requestedIconPaths: [String] = []
|
let fileIcon: NSImage
|
let fallbackIcon: NSImage
|
|
init(fileIcon: NSImage = NSImage.alignerTestImage(), fallbackIcon: NSImage = NSImage.alignerTestImage()) {
|
self.fileIcon = fileIcon
|
self.fallbackIcon = fallbackIcon
|
}
|
|
func applicationURL(forBundleIdentifier bundleIdentifier: String) -> URL? {
|
applicationURLs[bundleIdentifier]
|
}
|
|
func icon(forFile path: String) -> NSImage {
|
requestedIconPaths.append(path)
|
return fileIcon
|
}
|
|
func fallbackApplicationIcon() -> NSImage {
|
fallbackIcon
|
}
|
}
|
|
@MainActor
|
private final class MockSkeletonThumbnailProvider: SkeletonThumbnailProviderProtocol {
|
var requestedApps: [AlignerApp] = []
|
var overlayTitles: [String] = []
|
let image: NSImage
|
|
init(image: NSImage = NSImage.alignerTestImage()) {
|
self.image = image
|
}
|
|
func skeleton(for app: AlignerApp) -> NSImage {
|
requestedApps.append(app)
|
return image
|
}
|
|
func withOverlayTitle(_ title: String, for app: AlignerApp) -> NSImage {
|
overlayTitles.append(title)
|
requestedApps.append(app)
|
return image
|
}
|
}
|
|
@MainActor
|
private final class MockScreenshotDebugLogger: ScreenshotDebugLogging {
|
var events: [ScreenshotDebugEvent] = []
|
|
func record(_ event: ScreenshotDebugEvent) {
|
events.append(event)
|
}
|
}
|
|
private struct MockPermissionChecker: SystemPermissionChecking {
|
let accessibility: PermissionStatus
|
let screenRecording: PermissionStatus
|
|
init(
|
accessibility: PermissionStatus = .granted,
|
screenRecording: PermissionStatus = .granted
|
) {
|
self.accessibility = accessibility
|
self.screenRecording = screenRecording
|
}
|
|
func status(for kind: PermissionKind) -> PermissionStatus {
|
switch kind {
|
case .accessibility:
|
return accessibility
|
case .screenRecording:
|
return screenRecording
|
}
|
}
|
}
|
|
@MainActor
|
private extension NSImage {
|
static func alignerTestImage() -> NSImage {
|
let image = NSImage(size: NSSize(width: 16, height: 16))
|
image.lockFocus()
|
NSColor.systemBlue.setFill()
|
NSRect(x: 0, y: 0, width: 16, height: 16).fill()
|
image.unlockFocus()
|
return image
|
}
|
}
|
|
@MainActor
|
private final class MockWindowScreenshotCapturer: WindowScreenshotCapturing {
|
var requestedWindowIDs: [UInt32] = []
|
let image: CGImage
|
let pointSize: NSSize
|
let error: Error?
|
let delayNanoseconds: UInt64
|
var observedCancellation = false
|
|
init(
|
image: CGImage = CGImage.alignerTestImage(),
|
pointSize: NSSize = NSSize(width: 24, height: 12),
|
error: Error? = nil,
|
delayNanoseconds: UInt64 = 0
|
) {
|
self.image = image
|
self.pointSize = pointSize
|
self.error = error
|
self.delayNanoseconds = delayNanoseconds
|
}
|
|
func captureWindow(windowID: UInt32) async throws -> WindowScreenshotCapture {
|
requestedWindowIDs.append(windowID)
|
if delayNanoseconds > 0 {
|
do {
|
try await Task.sleep(nanoseconds: delayNanoseconds)
|
} catch is CancellationError {
|
observedCancellation = true
|
throw error ?? CancellationError()
|
}
|
}
|
if let error {
|
throw error
|
}
|
return WindowScreenshotCapture(image: image, pointSize: pointSize)
|
}
|
}
|
|
private extension CGImage {
|
static func alignerTestImage(width: Int = 24, height: Int = 12) -> CGImage {
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
let context = CGContext(
|
data: nil,
|
width: width,
|
height: height,
|
bitsPerComponent: 8,
|
bytesPerRow: width * 4,
|
space: colorSpace,
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
)!
|
context.setFillColor(NSColor.systemBlue.cgColor)
|
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
|
return context.makeImage()!
|
}
|
}
|