cmux/cmuxTests/WindowAndDragTests.swift
2026-03-18 03:49:24 -07:00

1119 lines
42 KiB
Swift

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<T>(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<String> {
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 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"
)
}
}
@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)))
}
}
#endif