import XCTest import AppKit import SwiftUI import UniformTypeIdentifiers import WebKit import ObjectiveC.runtime import Bonsplit import UserNotifications #if canImport(cmux_DEV) @testable import cmux_DEV #elseif canImport(cmux) @testable import cmux #endif @MainActor final class AppDelegateWindowContextRoutingTests: XCTestCase { private func makeMainWindow(id: UUID) -> NSWindow { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 320), styleMask: [.titled, .closable], backing: .buffered, defer: false ) window.identifier = NSUserInterfaceItemIdentifier("cmux.main.\(id.uuidString)") return window } func testSynchronizeActiveMainWindowContextPrefersProvidedWindowOverStaleActiveManager() { _ = NSApplication.shared let app = AppDelegate() let windowAId = UUID() let windowBId = UUID() let windowA = makeMainWindow(id: windowAId) let windowB = makeMainWindow(id: windowBId) defer { windowA.orderOut(nil) windowB.orderOut(nil) } let managerA = TabManager() let managerB = TabManager() app.registerMainWindow( windowA, windowId: windowAId, tabManager: managerA, sidebarState: SidebarState(), sidebarSelectionState: SidebarSelectionState() ) app.registerMainWindow( windowB, windowId: windowBId, tabManager: managerB, sidebarState: SidebarState(), sidebarSelectionState: SidebarSelectionState() ) windowB.makeKeyAndOrderFront(nil) _ = app.synchronizeActiveMainWindowContext(preferredWindow: windowB) XCTAssertTrue(app.tabManager === managerB) windowA.makeKeyAndOrderFront(nil) let resolved = app.synchronizeActiveMainWindowContext(preferredWindow: windowA) XCTAssertTrue(resolved === managerA, "Expected provided active window to win over stale active manager") XCTAssertTrue(app.tabManager === managerA) } func testSynchronizeActiveMainWindowContextFallsBackToActiveManagerWithoutFocusedWindow() { _ = NSApplication.shared let app = AppDelegate() let windowAId = UUID() let windowBId = UUID() let windowA = makeMainWindow(id: windowAId) let windowB = makeMainWindow(id: windowBId) defer { windowA.orderOut(nil) windowB.orderOut(nil) } let managerA = TabManager() let managerB = TabManager() app.registerMainWindow( windowA, windowId: windowAId, tabManager: managerA, sidebarState: SidebarState(), sidebarSelectionState: SidebarSelectionState() ) app.registerMainWindow( windowB, windowId: windowBId, tabManager: managerB, sidebarState: SidebarState(), sidebarSelectionState: SidebarSelectionState() ) // Seed active manager and clear focus windows to force fallback routing. windowA.makeKeyAndOrderFront(nil) _ = app.synchronizeActiveMainWindowContext(preferredWindow: windowA) XCTAssertTrue(app.tabManager === managerA) windowA.orderOut(nil) windowB.orderOut(nil) let resolved = app.synchronizeActiveMainWindowContext(preferredWindow: nil) XCTAssertTrue(resolved === managerA, "Expected fallback to preserve current active manager instead of arbitrary window") XCTAssertTrue(app.tabManager === managerA) } func testSynchronizeActiveMainWindowContextUsesRegisteredWindowEvenIfIdentifierMutates() { _ = NSApplication.shared let app = AppDelegate() let windowId = UUID() let window = makeMainWindow(id: windowId) defer { window.orderOut(nil) } let manager = TabManager() app.registerMainWindow( window, windowId: windowId, tabManager: manager, sidebarState: SidebarState(), sidebarSelectionState: SidebarSelectionState() ) // SwiftUI can replace the NSWindow identifier string at runtime. window.identifier = NSUserInterfaceItemIdentifier("SwiftUI.AppWindow.IdentifierChanged") let resolved = app.synchronizeActiveMainWindowContext(preferredWindow: window) XCTAssertTrue(resolved === manager, "Expected registered window object identity to win even if identifier string changed") XCTAssertTrue(app.tabManager === manager) } func testAddWorkspaceWithoutBringToFrontPreservesActiveWindowAndSelection() { _ = NSApplication.shared let app = AppDelegate() let windowAId = UUID() let windowBId = UUID() let windowA = makeMainWindow(id: windowAId) let windowB = makeMainWindow(id: windowBId) defer { windowA.orderOut(nil) windowB.orderOut(nil) } let managerA = TabManager() let managerB = TabManager() app.registerMainWindow( windowA, windowId: windowAId, tabManager: managerA, sidebarState: SidebarState(), sidebarSelectionState: SidebarSelectionState() ) app.registerMainWindow( windowB, windowId: windowBId, tabManager: managerB, sidebarState: SidebarState(), sidebarSelectionState: SidebarSelectionState() ) windowA.makeKeyAndOrderFront(nil) _ = app.synchronizeActiveMainWindowContext(preferredWindow: windowA) XCTAssertTrue(app.tabManager === managerA) let originalSelectedA = managerA.selectedTabId let originalSelectedB = managerB.selectedTabId let originalTabCountB = managerB.tabs.count let createdWorkspaceId = app.addWorkspace(windowId: windowBId, bringToFront: false) XCTAssertNotNil(createdWorkspaceId) XCTAssertTrue(app.tabManager === managerA, "Expected non-focus workspace creation to preserve active window routing") XCTAssertEqual(managerA.selectedTabId, originalSelectedA) XCTAssertEqual(managerB.selectedTabId, originalSelectedB, "Expected background workspace creation to preserve selected tab") XCTAssertEqual(managerB.tabs.count, originalTabCountB + 1) XCTAssertTrue(managerB.tabs.contains(where: { $0.id == createdWorkspaceId })) } func testApplicationOpenURLsAddsWorkspaceForDroppedFolderURL() throws { _ = NSApplication.shared let app = AppDelegate() let windowId = UUID() let window = makeMainWindow(id: windowId) defer { window.orderOut(nil) } let manager = TabManager() app.registerMainWindow( window, windowId: windowId, tabManager: manager, sidebarState: SidebarState(), sidebarSelectionState: SidebarSelectionState() ) window.makeKeyAndOrderFront(nil) _ = app.synchronizeActiveMainWindowContext(preferredWindow: window) let defaults = UserDefaults.standard let previousWelcomeShown = defaults.object(forKey: WelcomeSettings.shownKey) defaults.set(true, forKey: WelcomeSettings.shownKey) defer { if let previousWelcomeShown { defaults.set(previousWelcomeShown, forKey: WelcomeSettings.shownKey) } else { defaults.removeObject(forKey: WelcomeSettings.shownKey) } } let rootDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent(UUID().uuidString, isDirectory: true) let droppedDirectory = rootDirectory.appendingPathComponent("project", isDirectory: true) try FileManager.default.createDirectory(at: droppedDirectory, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: rootDirectory) } let existingWorkspaceIds = Set(manager.tabs.map(\.id)) app.application( NSApplication.shared, open: [URL(fileURLWithPath: droppedDirectory.path)] ) let createdWorkspace = manager.tabs.first { !existingWorkspaceIds.contains($0.id) } XCTAssertNotNil(createdWorkspace) XCTAssertEqual(createdWorkspace?.currentDirectory, droppedDirectory.path) } } @MainActor final class AppDelegateLaunchServicesRegistrationTests: XCTestCase { func testScheduleLaunchServicesRegistrationDefersRegisterWork() { _ = NSApplication.shared let app = AppDelegate() var scheduledWork: (@Sendable () -> Void)? var registerCallCount = 0 app.scheduleLaunchServicesBundleRegistrationForTesting( bundleURL: URL(fileURLWithPath: "/tmp/../tmp/cmux-launch-services-test.app"), scheduler: { work in scheduledWork = work }, register: { _ in registerCallCount += 1 return noErr } ) XCTAssertEqual(registerCallCount, 0, "Registration should not run inline on the startup call path") XCTAssertNotNil(scheduledWork, "Registration work should be handed to the scheduler") scheduledWork?() XCTAssertEqual(registerCallCount, 1) } } final class FocusFlashPatternTests: XCTestCase { func testFocusFlashPatternMatchesTerminalDoublePulseShape() { XCTAssertEqual(FocusFlashPattern.values, [0, 1, 0, 1, 0]) XCTAssertEqual(FocusFlashPattern.keyTimes, [0, 0.25, 0.5, 0.75, 1]) XCTAssertEqual(FocusFlashPattern.duration, 0.9, accuracy: 0.0001) XCTAssertEqual(FocusFlashPattern.curves, [.easeOut, .easeIn, .easeOut, .easeIn]) XCTAssertEqual(FocusFlashPattern.ringInset, 6, accuracy: 0.0001) XCTAssertEqual(FocusFlashPattern.ringCornerRadius, 10, accuracy: 0.0001) } func testFocusFlashPatternSegmentsCoverFullDoublePulseTimeline() { let segments = FocusFlashPattern.segments XCTAssertEqual(segments.count, 4) XCTAssertEqual(segments[0].delay, 0.0, accuracy: 0.0001) XCTAssertEqual(segments[0].duration, 0.225, accuracy: 0.0001) XCTAssertEqual(segments[0].targetOpacity, 1, accuracy: 0.0001) XCTAssertEqual(segments[0].curve, .easeOut) XCTAssertEqual(segments[1].delay, 0.225, accuracy: 0.0001) XCTAssertEqual(segments[1].duration, 0.225, accuracy: 0.0001) XCTAssertEqual(segments[1].targetOpacity, 0, accuracy: 0.0001) XCTAssertEqual(segments[1].curve, .easeIn) XCTAssertEqual(segments[2].delay, 0.45, accuracy: 0.0001) XCTAssertEqual(segments[2].duration, 0.225, accuracy: 0.0001) XCTAssertEqual(segments[2].targetOpacity, 1, accuracy: 0.0001) XCTAssertEqual(segments[2].curve, .easeOut) XCTAssertEqual(segments[3].delay, 0.675, accuracy: 0.0001) XCTAssertEqual(segments[3].duration, 0.225, accuracy: 0.0001) XCTAssertEqual(segments[3].targetOpacity, 0, accuracy: 0.0001) XCTAssertEqual(segments[3].curve, .easeIn) } } @available(macOS 26.0, *) private struct DragConfigurationOperationsSnapshot: Equatable { let allowCopy: Bool let allowMove: Bool let allowDelete: Bool let allowAlias: Bool } @available(macOS 26.0, *) private enum DragConfigurationSnapshotError: Error { case missingBoolField(primary: String, fallback: String?) } @available(macOS 26.0, *) private func dragConfigurationOperationsSnapshot(from operations: T) throws -> DragConfigurationOperationsSnapshot { let mirror = Mirror(reflecting: operations) func readBool(_ primary: String, fallback: String? = nil) throws -> Bool { if let value = mirror.descendant(primary) as? Bool { return value } if let fallback, let value = mirror.descendant(fallback) as? Bool { return value } throw DragConfigurationSnapshotError.missingBoolField(primary: primary, fallback: fallback) } return try DragConfigurationOperationsSnapshot( allowCopy: readBool("allowCopy", fallback: "_allowCopy"), allowMove: readBool("allowMove", fallback: "_allowMove"), allowDelete: readBool("allowDelete", fallback: "_allowDelete"), allowAlias: readBool("allowAlias", fallback: "_allowAlias") ) } #if compiler(>=6.2) @MainActor final class InternalTabDragConfigurationTests: XCTestCase { func testDisablesExternalOperationsForInternalTabDrags() throws { guard #available(macOS 26.0, *) else { throw XCTSkip("Requires macOS 26 drag configuration APIs") } let configuration = InternalTabDragConfigurationProvider.value let withinApp = try dragConfigurationOperationsSnapshot(from: configuration.operationsWithinApp) let outsideApp = try dragConfigurationOperationsSnapshot(from: configuration.operationsOutsideApp) XCTAssertEqual( withinApp, DragConfigurationOperationsSnapshot( allowCopy: false, allowMove: true, allowDelete: false, allowAlias: false ) ) XCTAssertEqual( outsideApp, DragConfigurationOperationsSnapshot( allowCopy: false, allowMove: false, allowDelete: false, allowAlias: false ) ) } } @MainActor final class InternalTabDragBundleDeclarationTests: XCTestCase { private func exportedTypeIdentifiers(bundle: Bundle) -> Set { let declarations = (bundle.object(forInfoDictionaryKey: "UTExportedTypeDeclarations") as? [[String: Any]]) ?? [] return Set(declarations.compactMap { $0["UTTypeIdentifier"] as? String }) } func testAppBundleExportsInternalDragTypes() { let exported = exportedTypeIdentifiers(bundle: Bundle(for: AppDelegate.self)) XCTAssertTrue( exported.contains("com.splittabbar.tabtransfer"), "Expected app bundle to export bonsplit tab-transfer type, got \(exported)" ) XCTAssertTrue( exported.contains("com.cmux.sidebar-tab-reorder"), "Expected app bundle to export sidebar tab-reorder type, got \(exported)" ) } } #endif @MainActor final class WindowDragHandleHitTests: XCTestCase { private final class CapturingView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { bounds.contains(point) ? self : nil } } private final class HostContainerView: NSView {} private final class BlockingTopHitContainerView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { bounds.contains(point) ? self : nil } } private final class PassThroughProbeView: NSView { var onHitTest: (() -> Void)? override func hitTest(_ point: NSPoint) -> NSView? { guard bounds.contains(point) else { return nil } onHitTest?() return nil } } private final class PassiveHostContainerView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { guard bounds.contains(point) else { return nil } return super.hitTest(point) ?? self } } private final class MutatingSiblingView: NSView { weak var container: NSView? private var didMutate = false override func hitTest(_ point: NSPoint) -> NSView? { guard bounds.contains(point) else { return nil } guard !didMutate, let container else { return nil } didMutate = true let transient = NSView(frame: .zero) container.addSubview(transient) transient.removeFromSuperview() return nil } } private final class ReentrantDragHandleView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { let shouldCapture = windowDragHandleShouldCaptureHit(point, in: self, eventType: .leftMouseDown, eventWindow: self.window) return shouldCapture ? self : nil } } /// A sibling view whose hitTest re-enters windowDragHandleShouldCaptureHit, /// simulating the crash path where sibling.hitTest triggers a SwiftUI layout /// pass that calls back into the drag handle's hit resolution. private final class ReentrantSiblingView: NSView { weak var dragHandle: NSView? var reenteredResult: Bool? override func hitTest(_ point: NSPoint) -> NSView? { guard bounds.contains(point), let dragHandle else { return nil } // Simulate the re-entry: during sibling hit test, SwiftUI layout // calls windowDragHandleShouldCaptureHit on the drag handle again. reenteredResult = windowDragHandleShouldCaptureHit( point, in: dragHandle, eventType: .leftMouseDown, eventWindow: dragHandle.window ) return nil } } func testDragHandleCapturesHitWhenNoSiblingClaimsPoint() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) XCTAssertTrue( windowDragHandleShouldCaptureHit(NSPoint(x: 180, y: 18), in: dragHandle, eventType: .leftMouseDown), "Empty titlebar space should drag the window" ) } func testDragHandleYieldsWhenSiblingClaimsPoint() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) let folderIconHost = CapturingView(frame: NSRect(x: 10, y: 10, width: 16, height: 16)) container.addSubview(folderIconHost) XCTAssertFalse( windowDragHandleShouldCaptureHit(NSPoint(x: 14, y: 14), in: dragHandle, eventType: .leftMouseDown), "Interactive titlebar controls should receive the mouse event" ) XCTAssertTrue(windowDragHandleShouldCaptureHit(NSPoint(x: 180, y: 18), in: dragHandle, eventType: .leftMouseDown)) } func testDragHandleIgnoresHiddenSiblingWhenResolvingHit() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) let hidden = CapturingView(frame: NSRect(x: 10, y: 10, width: 16, height: 16)) hidden.isHidden = true container.addSubview(hidden) XCTAssertTrue(windowDragHandleShouldCaptureHit(NSPoint(x: 14, y: 14), in: dragHandle, eventType: .leftMouseDown)) } func testDragHandleDoesNotCaptureOutsideBounds() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) XCTAssertFalse(windowDragHandleShouldCaptureHit(NSPoint(x: 240, y: 18), in: dragHandle, eventType: .leftMouseDown)) } func testDragHandleSkipsCaptureForPassivePointerEvents() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) let point = NSPoint(x: 180, y: 18) XCTAssertFalse(windowDragHandleShouldCaptureHit(point, in: dragHandle, eventType: .mouseMoved)) XCTAssertFalse(windowDragHandleShouldCaptureHit(point, in: dragHandle, eventType: .cursorUpdate)) XCTAssertFalse(windowDragHandleShouldCaptureHit(point, in: dragHandle, eventType: nil)) XCTAssertTrue(windowDragHandleShouldCaptureHit(point, in: dragHandle, eventType: .leftMouseDown)) } func testDragHandleSkipsForeignLeftMouseDownDuringLaunch() { let point = NSPoint(x: 180, y: 18) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 220, height: 36), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected content view") return } let container = NSView(frame: contentView.bounds) container.autoresizingMask = [.width, .height] contentView.addSubview(container) let dragHandle = NSView(frame: container.bounds) dragHandle.autoresizingMask = [.width, .height] container.addSubview(dragHandle) let foreignWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 220, height: 36), styleMask: [.titled], backing: .buffered, defer: false ) defer { foreignWindow.orderOut(nil) } XCTAssertFalse( windowDragHandleShouldCaptureHit( point, in: dragHandle, eventType: .leftMouseDown, eventWindow: nil ), "Launch activation events without a matching window should not trigger drag-handle hierarchy walk" ) XCTAssertFalse( windowDragHandleShouldCaptureHit( point, in: dragHandle, eventType: .leftMouseDown, eventWindow: foreignWindow ), "Left mouse-down events for a different window should be treated as passive" ) XCTAssertTrue( windowDragHandleShouldCaptureHit( point, in: dragHandle, eventType: .leftMouseDown, eventWindow: window ), "Left mouse-down events for this window should still capture empty titlebar space" ) } func testPassiveHostingTopHitClassification() { XCTAssertTrue(windowDragHandleShouldTreatTopHitAsPassiveHost(HostContainerView(frame: .zero))) XCTAssertFalse(windowDragHandleShouldTreatTopHitAsPassiveHost(NSButton(frame: .zero))) } func testDragHandleIgnoresPassiveHostSiblingHit() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) let passiveHost = PassiveHostContainerView(frame: container.bounds) container.addSubview(passiveHost) XCTAssertTrue( windowDragHandleShouldCaptureHit(NSPoint(x: 180, y: 18), in: dragHandle, eventType: .leftMouseDown), "Passive host wrappers should not block titlebar drag capture" ) } func testDragHandleRespectsInteractiveChildInsidePassiveHost() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) let passiveHost = PassiveHostContainerView(frame: container.bounds) let folderControl = CapturingView(frame: NSRect(x: 10, y: 10, width: 16, height: 16)) passiveHost.addSubview(folderControl) container.addSubview(passiveHost) XCTAssertFalse( windowDragHandleShouldCaptureHit(NSPoint(x: 14, y: 14), in: dragHandle, eventType: .leftMouseDown), "Interactive controls inside passive host wrappers should still receive hits" ) } func testTopHitResolutionStateIsScopedPerWindow() { let point = NSPoint(x: 100, y: 18) let outerWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 220, height: 36), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { outerWindow.orderOut(nil) } guard let outerContentView = outerWindow.contentView else { XCTFail("Expected outer content view") return } let outerContainer = NSView(frame: outerContentView.bounds) outerContainer.autoresizingMask = [.width, .height] outerContentView.addSubview(outerContainer) let outerDragHandle = NSView(frame: outerContainer.bounds) outerDragHandle.autoresizingMask = [.width, .height] outerContainer.addSubview(outerDragHandle) let nestedWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 220, height: 36), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { nestedWindow.orderOut(nil) } guard let nestedContentView = nestedWindow.contentView else { XCTFail("Expected nested content view") return } let nestedContainer = BlockingTopHitContainerView(frame: nestedContentView.bounds) nestedContainer.autoresizingMask = [.width, .height] nestedContentView.addSubview(nestedContainer) let nestedDragHandle = NSView(frame: nestedContainer.bounds) nestedDragHandle.autoresizingMask = [.width, .height] nestedContainer.addSubview(nestedDragHandle) XCTAssertFalse( windowDragHandleShouldCaptureHit(point, in: nestedDragHandle, eventType: .leftMouseDown, eventWindow: nestedWindow), "Nested window drag handle should be blocked by top-hit titlebar container" ) var nestedCaptureResult: Bool? let probe = PassThroughProbeView(frame: outerContainer.bounds) probe.autoresizingMask = [.width, .height] probe.onHitTest = { nestedCaptureResult = windowDragHandleShouldCaptureHit(point, in: nestedDragHandle, eventType: .leftMouseDown, eventWindow: nestedWindow) } outerContainer.addSubview(probe) _ = windowDragHandleShouldCaptureHit(point, in: outerDragHandle, eventType: .leftMouseDown, eventWindow: outerWindow) XCTAssertEqual( nestedCaptureResult, false, "Top-hit recursion in one window must not disable top-hit resolution in another window" ) } func testDragHandleRemainsStableWhenSiblingMutatesSubviewsDuringHitTest() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) let mutatingSibling = MutatingSiblingView(frame: container.bounds) mutatingSibling.container = container container.addSubview(mutatingSibling) XCTAssertTrue( windowDragHandleShouldCaptureHit(NSPoint(x: 180, y: 18), in: dragHandle, eventType: .leftMouseDown), "Subview mutations during hit testing should not crash or break drag-handle capture" ) } func testDragHandleSiblingHitTestReentrancyDoesNotCrash() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) container.addSubview(dragHandle) let reentrantSibling = ReentrantSiblingView(frame: container.bounds) reentrantSibling.dragHandle = dragHandle container.addSubview(reentrantSibling) // The outer call enters the sibling walk, which calls // reentrantSibling.hitTest(), which re-enters // windowDragHandleShouldCaptureHit. Without the re-entrancy guard // this would trigger a Swift exclusive-access violation (SIGABRT). let outerResult = windowDragHandleShouldCaptureHit( NSPoint(x: 110, y: 18), in: dragHandle, eventType: .leftMouseDown ) XCTAssertTrue(outerResult, "Outer call should still capture when sibling returns nil") XCTAssertEqual( reentrantSibling.reenteredResult, false, "Re-entrant call should bail out (return false) instead of crashing" ) } func testDragHandleTopHitResolutionSurvivesSameWindowReentrancy() { let point = NSPoint(x: 180, y: 18) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 220, height: 36), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected content view") return } let container = NSView(frame: contentView.bounds) container.autoresizingMask = [.width, .height] contentView.addSubview(container) let dragHandle = ReentrantDragHandleView(frame: container.bounds) dragHandle.autoresizingMask = [.width, .height] container.addSubview(dragHandle) XCTAssertTrue( windowDragHandleShouldCaptureHit(point, in: dragHandle, eventType: .leftMouseDown, eventWindow: window), "Reentrant same-window top-hit resolution should not trigger exclusivity crashes" ) } } #if DEBUG @MainActor final class DraggableFolderHitTests: XCTestCase { func testFolderHitTestReturnsContainerWhenInsideBounds() { let folderView = DraggableFolderNSView(directory: "/tmp") folderView.frame = NSRect(x: 0, y: 0, width: 16, height: 16) guard let hit = folderView.hitTest(NSPoint(x: 8, y: 8)) else { XCTFail("Expected folder icon to capture inside hit") return } XCTAssertTrue(hit === folderView) } func testFolderHitTestReturnsNilOutsideBounds() { let folderView = DraggableFolderNSView(directory: "/tmp") folderView.frame = NSRect(x: 0, y: 0, width: 16, height: 16) XCTAssertNil(folderView.hitTest(NSPoint(x: 20, y: 8))) } func testFolderIconDisablesWindowMoveBehavior() { let folderView = DraggableFolderNSView(directory: "/tmp") XCTAssertFalse(folderView.mouseDownCanMoveWindow) } } @MainActor final class TitlebarLeadingInsetPassthroughViewTests: XCTestCase { func testLeadingInsetViewDoesNotParticipateInHitTesting() { let view = TitlebarLeadingInsetPassthroughView(frame: NSRect(x: 0, y: 0, width: 200, height: 40)) XCTAssertNil(view.hitTest(NSPoint(x: 20, y: 10))) } func testLeadingInsetViewCannotMoveWindowViaMouseDown() { let view = TitlebarLeadingInsetPassthroughView(frame: NSRect(x: 0, y: 0, width: 200, height: 40)) XCTAssertFalse(view.mouseDownCanMoveWindow) } } @MainActor final class FolderWindowMoveSuppressionTests: XCTestCase { private func makeWindow() -> NSWindow { NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 180), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false ) } func testSuppressionDisablesMovableWindow() { let window = makeWindow() window.isMovable = true let previous = temporarilyDisableWindowDragging(window: window) XCTAssertEqual(previous, true) XCTAssertFalse(window.isMovable) } func testSuppressionPreservesAlreadyImmovableWindow() { let window = makeWindow() window.isMovable = false let previous = temporarilyDisableWindowDragging(window: window) XCTAssertEqual(previous, false) XCTAssertFalse(window.isMovable) } func testRestoreAppliesPreviousMovableState() { let window = makeWindow() window.isMovable = false restoreWindowDragging(window: window, previousMovableState: true) XCTAssertTrue(window.isMovable) restoreWindowDragging(window: window, previousMovableState: false) XCTAssertFalse(window.isMovable) } func testWindowDragSuppressionDepthLifecycle() { let window = makeWindow() XCTAssertEqual(windowDragSuppressionDepth(window: window), 0) XCTAssertFalse(isWindowDragSuppressed(window: window)) XCTAssertEqual(beginWindowDragSuppression(window: window), 1) XCTAssertEqual(windowDragSuppressionDepth(window: window), 1) XCTAssertTrue(isWindowDragSuppressed(window: window)) XCTAssertEqual(endWindowDragSuppression(window: window), 0) XCTAssertEqual(windowDragSuppressionDepth(window: window), 0) XCTAssertFalse(isWindowDragSuppressed(window: window)) } func testWindowDragSuppressionIsReferenceCounted() { let window = makeWindow() XCTAssertEqual(beginWindowDragSuppression(window: window), 1) XCTAssertEqual(beginWindowDragSuppression(window: window), 2) XCTAssertEqual(windowDragSuppressionDepth(window: window), 2) XCTAssertTrue(isWindowDragSuppressed(window: window)) XCTAssertEqual(endWindowDragSuppression(window: window), 1) XCTAssertEqual(windowDragSuppressionDepth(window: window), 1) XCTAssertTrue(isWindowDragSuppressed(window: window)) XCTAssertEqual(endWindowDragSuppression(window: window), 0) XCTAssertEqual(windowDragSuppressionDepth(window: window), 0) XCTAssertFalse(isWindowDragSuppressed(window: window)) } func testTemporaryWindowMovableEnableRestoresImmovableWindow() { let window = makeWindow() window.isMovable = false let previous = withTemporaryWindowMovableEnabled(window: window) { XCTAssertTrue(window.isMovable) } XCTAssertEqual(previous, false) XCTAssertFalse(window.isMovable) } func testTemporaryWindowMovableEnablePreservesMovableWindow() { let window = makeWindow() window.isMovable = true let previous = withTemporaryWindowMovableEnabled(window: window) { XCTAssertTrue(window.isMovable) } XCTAssertEqual(previous, true) XCTAssertTrue(window.isMovable) } } @MainActor final class WindowMoveSuppressionHitPathTests: XCTestCase { private func makeWindowWithContentView() -> (NSWindow, NSView) { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 180), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let contentView = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = contentView return (window, contentView) } private func makeMouseEvent(type: NSEvent.EventType, location: NSPoint, window: NSWindow) -> NSEvent { guard let event = NSEvent.mouseEvent( with: type, location: location, modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil, eventNumber: 0, clickCount: 1, pressure: 1.0 ) else { fatalError("Failed to create \(type) mouse event") } return event } func testSuppressionHitPathRecognizesFolderView() { let folderView = DraggableFolderNSView(directory: "/tmp") XCTAssertTrue(shouldSuppressWindowMoveForFolderDrag(hitView: folderView)) } func testSuppressionHitPathRecognizesDescendantOfFolderView() { let folderView = DraggableFolderNSView(directory: "/tmp") let child = NSView(frame: .zero) folderView.addSubview(child) XCTAssertTrue(shouldSuppressWindowMoveForFolderDrag(hitView: child)) } func testSuppressionHitPathIgnoresUnrelatedViews() { XCTAssertFalse(shouldSuppressWindowMoveForFolderDrag(hitView: NSView(frame: .zero))) XCTAssertFalse(shouldSuppressWindowMoveForFolderDrag(hitView: nil)) } func testSuppressionEventPathRecognizesFolderHitInsideWindow() { let (window, contentView) = makeWindowWithContentView() window.isMovable = true let folderView = DraggableFolderNSView(directory: "/tmp") folderView.frame = NSRect(x: 10, y: 10, width: 16, height: 16) contentView.addSubview(folderView) let event = makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 14, y: 14), window: window) XCTAssertTrue(shouldSuppressWindowMoveForFolderDrag(window: window, event: event)) } func testSuppressionEventPathRejectsNonFolderAndNonMouseDownEvents() { let (window, contentView) = makeWindowWithContentView() window.isMovable = true let plainView = NSView(frame: NSRect(x: 0, y: 0, width: 40, height: 40)) contentView.addSubview(plainView) let down = makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 20, y: 20), window: window) XCTAssertFalse(shouldSuppressWindowMoveForFolderDrag(window: window, event: down)) let dragged = makeMouseEvent(type: .leftMouseDragged, location: NSPoint(x: 20, y: 20), window: window) XCTAssertFalse(shouldSuppressWindowMoveForFolderDrag(window: window, event: dragged)) } } @MainActor final class FileDropOverlayViewTests: XCTestCase { private final class DragSpyWebView: WKWebView { var dragCalls: [String] = [] override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation { dragCalls.append("entered") return .copy } override func prepareForDragOperation(_ sender: any NSDraggingInfo) -> Bool { dragCalls.append("prepare") return true } override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool { dragCalls.append("perform") return true } override func concludeDragOperation(_ sender: (any NSDraggingInfo)?) { dragCalls.append("conclude") } } private final class MockDraggingInfo: NSObject, NSDraggingInfo { let draggingDestinationWindow: NSWindow? let draggingSourceOperationMask: NSDragOperation let draggingLocation: NSPoint let draggedImageLocation: NSPoint let draggedImage: NSImage? let draggingPasteboard: NSPasteboard let draggingSource: Any? let draggingSequenceNumber: Int var draggingFormation: NSDraggingFormation = .default var animatesToDestination = false var numberOfValidItemsForDrop = 1 let springLoadingHighlight: NSSpringLoadingHighlight = .none init( window: NSWindow, location: NSPoint, pasteboard: NSPasteboard, sourceOperationMask: NSDragOperation = .copy, draggingSource: Any? = nil, sequenceNumber: Int = 1 ) { self.draggingDestinationWindow = window self.draggingSourceOperationMask = sourceOperationMask self.draggingLocation = location self.draggedImageLocation = location self.draggedImage = nil self.draggingPasteboard = pasteboard self.draggingSource = draggingSource self.draggingSequenceNumber = sequenceNumber } func slideDraggedImage(to screenPoint: NSPoint) {} override func namesOfPromisedFilesDropped(atDestination dropDestination: URL) -> [String]? { nil } func enumerateDraggingItems( options enumOpts: NSDraggingItemEnumerationOptions = [], for view: NSView?, classes classArray: [AnyClass], searchOptions: [NSPasteboard.ReadingOptionKey: Any] = [:], using block: (NSDraggingItem, Int, UnsafeMutablePointer) -> Void ) {} func resetSpringLoading() {} } private func realizeWindowLayout(_ window: NSWindow) { window.makeKeyAndOrderFront(nil) window.displayIfNeeded() window.contentView?.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) window.contentView?.layoutSubtreeIfNeeded() } func testOverlayResolvesPortalHostedBrowserWebViewForFileDrops() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 420, height: 280), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { NotificationCenter.default.post(name: NSWindow.willCloseNotification, object: window) window.orderOut(nil) } realizeWindowLayout(window) guard let contentView = window.contentView, let container = contentView.superview else { XCTFail("Expected content container") return } let anchor = NSView(frame: NSRect(x: 40, y: 36, width: 220, height: 150)) contentView.addSubview(anchor) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) BrowserWindowPortalRegistry.bind(webView: webView, to: anchor, visibleInUI: true) BrowserWindowPortalRegistry.synchronizeForAnchor(anchor) let overlay = FileDropOverlayView(frame: container.bounds) overlay.autoresizingMask = [.width, .height] container.addSubview(overlay, positioned: .above, relativeTo: nil) let point = anchor.convert( NSPoint(x: anchor.bounds.midX, y: anchor.bounds.midY), to: nil ) XCTAssertTrue( overlay.webViewUnderPoint(point) === webView, "File-drop overlay should resolve portal-hosted browser panes so Finder uploads still reach WKWebView" ) } func testOverlayForwardsFullDragLifecycleToPortalHostedBrowserWebView() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 420, height: 280), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { NotificationCenter.default.post(name: NSWindow.willCloseNotification, object: window) window.orderOut(nil) } realizeWindowLayout(window) guard let contentView = window.contentView, let container = contentView.superview else { XCTFail("Expected content container") return } let anchor = NSView(frame: NSRect(x: 52, y: 44, width: 210, height: 140)) contentView.addSubview(anchor) let webView = DragSpyWebView(frame: .zero, configuration: WKWebViewConfiguration()) BrowserWindowPortalRegistry.bind(webView: webView, to: anchor, visibleInUI: true) BrowserWindowPortalRegistry.synchronizeForAnchor(anchor) defer { BrowserWindowPortalRegistry.detach(webView: webView) } let overlay = FileDropOverlayView(frame: container.bounds) overlay.autoresizingMask = [.width, .height] container.addSubview(overlay, positioned: .above, relativeTo: nil) let pasteboard = NSPasteboard(name: NSPasteboard.Name("cmux.test.drag.\(UUID().uuidString)")) pasteboard.clearContents() XCTAssertTrue( pasteboard.writeObjects([URL(fileURLWithPath: "/tmp/upload.mov") as NSURL]), "Expected file URL drag payload" ) let dropPoint = anchor.convert( NSPoint(x: anchor.bounds.midX, y: anchor.bounds.midY), to: nil ) let dragInfo = MockDraggingInfo( window: window, location: dropPoint, pasteboard: pasteboard ) XCTAssertEqual(overlay.draggingEntered(dragInfo), .copy) XCTAssertTrue(overlay.prepareForDragOperation(dragInfo)) XCTAssertTrue(overlay.performDragOperation(dragInfo)) overlay.concludeDragOperation(dragInfo) XCTAssertEqual( webView.dragCalls, ["entered", "prepare", "perform", "conclude"], "Finder file drops need the full AppKit drag lifecycle forwarded into the portal-hosted WKWebView" ) } } @MainActor final class MarkdownPanelPointerObserverViewTests: XCTestCase { private func makeWindow() -> NSWindow { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 180), styleMask: [.titled, .closable], backing: .buffered, defer: false ) window.makeKeyAndOrderFront(nil) window.displayIfNeeded() window.contentView?.layoutSubtreeIfNeeded() return window } private func makeMouseEvent( type: NSEvent.EventType, location: NSPoint, window: NSWindow, eventNumber: Int = 1 ) -> NSEvent { guard let event = NSEvent.mouseEvent( with: type, location: location, modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil, eventNumber: eventNumber, clickCount: 1, pressure: 1.0 ) else { fatalError("Expected to create mouse event") } return event } func testObserverTriggersFocusForVisibleLeftClickInsideBounds() { let window = makeWindow() defer { window.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected content view") return } let overlay = MarkdownPanelPointerObserverView(frame: contentView.bounds) overlay.autoresizingMask = [.width, .height] let focusExpectation = expectation(description: "observer forwards focus callback") var pointerDownCount = 0 overlay.onPointerDown = { pointerDownCount += 1 focusExpectation.fulfill() } contentView.addSubview(overlay) _ = overlay.handleEventIfNeeded( makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 60, y: 60), window: window) ) wait(for: [focusExpectation], timeout: 1.0) XCTAssertEqual(pointerDownCount, 1) } func testObserverIgnoresOutsideOrForeignWindowClicks() { let window = makeWindow() defer { window.orderOut(nil) } let otherWindow = makeWindow() defer { otherWindow.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected content view") return } let overlay = MarkdownPanelPointerObserverView(frame: contentView.bounds) overlay.autoresizingMask = [.width, .height] let noFocusExpectation = expectation(description: "observer ignores invalid clicks") noFocusExpectation.isInverted = true var pointerDownCount = 0 overlay.onPointerDown = { pointerDownCount += 1 noFocusExpectation.fulfill() } contentView.addSubview(overlay) _ = overlay.handleEventIfNeeded( makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 400, y: 400), window: window) ) _ = overlay.handleEventIfNeeded( makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 60, y: 60), window: otherWindow, eventNumber: 2) ) _ = overlay.handleEventIfNeeded( makeMouseEvent(type: .leftMouseDragged, location: NSPoint(x: 60, y: 60), window: window, eventNumber: 3) ) wait(for: [noFocusExpectation], timeout: 0.1) XCTAssertEqual(pointerDownCount, 0) } func testObserverDoesNotParticipateInHitTesting() { let overlay = MarkdownPanelPointerObserverView(frame: NSRect(x: 0, y: 0, width: 200, height: 100)) XCTAssertNil(overlay.hitTest(NSPoint(x: 40, y: 30))) } } @MainActor final class TmuxWorkspacePaneOverlayTests: XCTestCase { func testTmuxWorkspacePaneOverlayModelTracksFlashReason() { let model = TmuxWorkspacePaneOverlayModel() let initialState = TmuxWorkspacePaneOverlayRenderState( workspaceId: UUID(), unreadRects: [], flashRect: CGRect(x: 10, y: 20, width: 300, height: 200), flashToken: 1, flashReason: .notificationArrival ) let laterState = TmuxWorkspacePaneOverlayRenderState( workspaceId: initialState.workspaceId, unreadRects: [], flashRect: CGRect(x: 10, y: 20, width: 300, height: 200), flashToken: 2, flashReason: .navigation ) model.apply(initialState) model.apply(laterState) XCTAssertEqual(model.flashReason, .navigation) } func testNavigationFlashUsesNonNotificationPresentation() { XCTAssertNotEqual( WorkspaceAttentionCoordinator.flashStyle(for: .navigation), WorkspaceAttentionCoordinator.flashStyle(for: .notificationArrival) ) } func testNavigationFlashUsesNonNeutralAccent() { XCTAssertEqual( WorkspaceAttentionCoordinator.flashStyle(for: .navigation).accent, .navigationTeal ) } func testTmuxWorkspacePaneExactRectReturnsContentRelativeFrameForDescendantView() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 400), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected contentView") return } let targetView = NSView(frame: NSRect(x: 120, y: 48, width: 300, height: 200)) contentView.addSubview(targetView) XCTAssertEqual( ContentView.tmuxWorkspacePaneExactRect(for: targetView, in: contentView), CGRect(x: 120, y: 48, width: 300, height: 200) ) } } #endif