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 var cmuxUnitTestInspectorAssociationKey: UInt8 = 0 var cmuxUnitTestInspectorOverrideInstalled = false extension CmuxWebView { @objc func cmuxUnitTestInspector() -> NSObject? { objc_getAssociatedObject(self, &cmuxUnitTestInspectorAssociationKey) as? NSObject } } extension WKWebView { func cmuxSetUnitTestInspector(_ inspector: NSObject?) { objc_setAssociatedObject( self, &cmuxUnitTestInspectorAssociationKey, inspector, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } } func installCmuxUnitTestInspectorOverride() { guard !cmuxUnitTestInspectorOverrideInstalled else { return } guard let replacementMethod = class_getInstanceMethod( CmuxWebView.self, #selector(CmuxWebView.cmuxUnitTestInspector) ) else { fatalError("Unable to locate test inspector replacement method") } let added = class_addMethod( CmuxWebView.self, NSSelectorFromString("_inspector"), method_getImplementation(replacementMethod), method_getTypeEncoding(replacementMethod) ) guard added else { fatalError("Unable to install CmuxWebView _inspector test override") } cmuxUnitTestInspectorOverrideInstalled = true } final class CmuxWebViewKeyEquivalentTests: XCTestCase { private final class ActionSpy: NSObject { private(set) var invoked: Bool = false @objc func didInvoke(_ sender: Any?) { invoked = true } } private final class WindowCyclingActionSpy: NSObject { weak var firstWindow: NSWindow? weak var secondWindow: NSWindow? private(set) var invocationCount = 0 @objc func cycleWindow(_ sender: Any?) { invocationCount += 1 guard let firstWindow, let secondWindow else { return } if NSApp.keyWindow === firstWindow { secondWindow.makeKeyAndOrderFront(nil) } else { firstWindow.makeKeyAndOrderFront(nil) } } } private final class FirstResponderView: NSView { override var acceptsFirstResponder: Bool { true } } private final class DelegateProbeTextView: NSTextView { private(set) var delegateReadCount = 0 override var delegate: NSTextViewDelegate? { get { delegateReadCount += 1 return super.delegate } set { super.delegate = newValue } } } private final class FieldEditorProbeTextView: NSTextView { private(set) var delegateReadCount = 0 override var delegate: NSTextViewDelegate? { get { delegateReadCount += 1 return super.delegate } set { super.delegate = newValue } } override var isFieldEditor: Bool { get { true } set {} } } func testCmdNRoutesToMainMenuWhenWebViewIsFirstResponder() { let spy = ActionSpy() installMenu(spy: spy, key: "n", modifiers: [.command]) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let event = makeKeyDownEvent(key: "n", modifiers: [.command], keyCode: 45) // kVK_ANSI_N XCTAssertNotNil(event) XCTAssertTrue(webView.performKeyEquivalent(with: event!)) XCTAssertTrue(spy.invoked) } func testCmdWRoutesToMainMenuWhenWebViewIsFirstResponder() { let spy = ActionSpy() installMenu(spy: spy, key: "w", modifiers: [.command]) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let event = makeKeyDownEvent(key: "w", modifiers: [.command], keyCode: 13) // kVK_ANSI_W XCTAssertNotNil(event) XCTAssertTrue(webView.performKeyEquivalent(with: event!)) XCTAssertTrue(spy.invoked) } func testCmdRRoutesToMainMenuWhenWebViewIsFirstResponder() { let spy = ActionSpy() installMenu(spy: spy, key: "r", modifiers: [.command]) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let event = makeKeyDownEvent(key: "r", modifiers: [.command], keyCode: 15) // kVK_ANSI_R XCTAssertNotNil(event) XCTAssertTrue(webView.performKeyEquivalent(with: event!)) XCTAssertTrue(spy.invoked) } func testReturnDoesNotRouteToMainMenuWhenWebViewIsFirstResponder() { let spy = ActionSpy() installMenu(spy: spy, key: "\r", modifiers: []) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let event = makeKeyDownEvent(key: "\r", modifiers: [], keyCode: 36) // kVK_Return XCTAssertNotNil(event) XCTAssertFalse(webView.performKeyEquivalent(with: event!)) XCTAssertFalse(spy.invoked) } func testCmdReturnDoesNotRouteToMainMenuWhenWebViewIsFirstResponder() { let spy = ActionSpy() installMenu(spy: spy, key: "\r", modifiers: [.command]) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let event = makeKeyDownEvent(key: "\r", modifiers: [.command], keyCode: 36) // kVK_Return XCTAssertNotNil(event) XCTAssertFalse(webView.performKeyEquivalent(with: event!)) XCTAssertFalse(spy.invoked) } func testKeypadEnterDoesNotRouteToMainMenuWhenWebViewIsFirstResponder() { let spy = ActionSpy() installMenu(spy: spy, key: "\r", modifiers: []) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let event = makeKeyDownEvent(key: "\r", modifiers: [], keyCode: 76) // kVK_ANSI_KeypadEnter XCTAssertNotNil(event) XCTAssertFalse(webView.performKeyEquivalent(with: event!)) XCTAssertFalse(spy.invoked) } @MainActor func testCanBlockFirstResponderAcquisitionWhenPaneIsUnfocused() { _ = NSApplication.shared let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let webView = CmuxWebView(frame: container.bounds, configuration: WKWebViewConfiguration()) webView.autoresizingMask = [.width, .height] container.addSubview(webView) window.makeKeyAndOrderFront(nil) defer { window.orderOut(nil) } webView.allowsFirstResponderAcquisition = true XCTAssertTrue(window.makeFirstResponder(webView)) _ = window.makeFirstResponder(nil) webView.allowsFirstResponderAcquisition = false XCTAssertFalse(webView.becomeFirstResponder()) _ = window.makeFirstResponder(webView) if let firstResponderView = window.firstResponder as? NSView { XCTAssertFalse(firstResponderView === webView || firstResponderView.isDescendant(of: webView)) } } @MainActor func testPointerFocusAllowanceCanTemporarilyOverrideBlockedFirstResponderAcquisition() { _ = NSApplication.shared let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let webView = CmuxWebView(frame: container.bounds, configuration: WKWebViewConfiguration()) webView.autoresizingMask = [.width, .height] container.addSubview(webView) window.makeKeyAndOrderFront(nil) defer { window.orderOut(nil) } webView.allowsFirstResponderAcquisition = false _ = window.makeFirstResponder(nil) XCTAssertFalse(webView.becomeFirstResponder(), "Expected focus to stay blocked by policy") webView.withPointerFocusAllowance { XCTAssertTrue(webView.becomeFirstResponder(), "Expected explicit pointer intent to bypass policy") } _ = window.makeFirstResponder(nil) XCTAssertFalse(webView.becomeFirstResponder(), "Expected pointer allowance to be temporary") } @MainActor func testWindowFirstResponderGuardBlocksDescendantWhenPaneIsUnfocused() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let webView = CmuxWebView(frame: container.bounds, configuration: WKWebViewConfiguration()) webView.autoresizingMask = [.width, .height] container.addSubview(webView) let descendant = FirstResponderView(frame: NSRect(x: 0, y: 0, width: 10, height: 10)) webView.addSubview(descendant) window.makeKeyAndOrderFront(nil) defer { window.orderOut(nil) } webView.allowsFirstResponderAcquisition = true XCTAssertTrue(window.makeFirstResponder(descendant)) _ = window.makeFirstResponder(nil) webView.allowsFirstResponderAcquisition = false XCTAssertFalse(window.makeFirstResponder(descendant)) if let firstResponderView = window.firstResponder as? NSView { XCTAssertFalse(firstResponderView === descendant || firstResponderView.isDescendant(of: webView)) } } @MainActor func testWindowFirstResponderGuardAllowsDescendantDuringPointerFocusAllowance() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let webView = CmuxWebView(frame: container.bounds, configuration: WKWebViewConfiguration()) webView.autoresizingMask = [.width, .height] container.addSubview(webView) let descendant = FirstResponderView(frame: NSRect(x: 0, y: 0, width: 10, height: 10)) webView.addSubview(descendant) window.makeKeyAndOrderFront(nil) defer { window.orderOut(nil) } webView.allowsFirstResponderAcquisition = false _ = window.makeFirstResponder(nil) XCTAssertFalse(window.makeFirstResponder(descendant), "Expected blocked focus outside pointer allowance") _ = window.makeFirstResponder(nil) webView.withPointerFocusAllowance { XCTAssertTrue(window.makeFirstResponder(descendant), "Expected pointer allowance to bypass guard") } _ = window.makeFirstResponder(nil) XCTAssertFalse(window.makeFirstResponder(descendant), "Expected pointer allowance to remain temporary") } @MainActor func testWindowFirstResponderGuardAllowsPointerInitiatedClickFocusWhenPolicyIsBlocked() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let webView = CmuxWebView(frame: container.bounds, configuration: WKWebViewConfiguration()) webView.autoresizingMask = [.width, .height] container.addSubview(webView) let descendant = FirstResponderView(frame: NSRect(x: 0, y: 0, width: 10, height: 10)) webView.addSubview(descendant) window.makeKeyAndOrderFront(nil) defer { AppDelegate.clearWindowFirstResponderGuardTesting() window.orderOut(nil) } webView.allowsFirstResponderAcquisition = false _ = window.makeFirstResponder(nil) XCTAssertFalse(window.makeFirstResponder(descendant), "Expected blocked focus without pointer click context") let timestamp = ProcessInfo.processInfo.systemUptime let pointerDownEvent = NSEvent.mouseEvent( with: .leftMouseDown, location: NSPoint(x: 5, y: 5), modifierFlags: [], timestamp: timestamp, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1.0 ) XCTAssertNotNil(pointerDownEvent) AppDelegate.setWindowFirstResponderGuardTesting(currentEvent: pointerDownEvent, hitView: descendant) _ = window.makeFirstResponder(nil) XCTAssertTrue(window.makeFirstResponder(descendant), "Expected pointer click context to bypass blocked policy") AppDelegate.clearWindowFirstResponderGuardTesting() _ = window.makeFirstResponder(nil) XCTAssertFalse(window.makeFirstResponder(descendant), "Expected pointer bypass to be limited to click context") } @MainActor func testWindowFirstResponderGuardAllowsPointerInitiatedClickFocusFromPortalHostedInspectorSibling() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let contentView = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = contentView window.makeKeyAndOrderFront(nil) defer { AppDelegate.clearWindowFirstResponderGuardTesting() window.orderOut(nil) } guard let container = contentView.superview else { XCTFail("Expected content container") return } let hostFrame = container.convert(contentView.bounds, from: contentView) let host = WindowBrowserHostView(frame: hostFrame) host.autoresizingMask = [.width, .height] container.addSubview(host, positioned: .above, relativeTo: contentView) let slot = WindowBrowserSlotView(frame: host.bounds) slot.autoresizingMask = [.width, .height] host.addSubview(slot) let webView = CmuxWebView(frame: slot.bounds, configuration: WKWebViewConfiguration()) webView.autoresizingMask = [.width, .height] slot.addSubview(webView) let inspector = FirstResponderView(frame: NSRect(x: 440, y: 0, width: 200, height: slot.bounds.height)) inspector.autoresizingMask = [.minXMargin, .height] slot.addSubview(inspector) webView.allowsFirstResponderAcquisition = false _ = window.makeFirstResponder(nil) XCTAssertFalse( window.makeFirstResponder(inspector), "Expected portal-hosted inspector focus to stay blocked without pointer click context" ) let pointInInspector = NSPoint(x: inspector.bounds.midX, y: inspector.bounds.midY) let pointInWindow = inspector.convert(pointInInspector, to: nil) let pointerDownEvent = NSEvent.mouseEvent( with: .leftMouseDown, location: pointInWindow, modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1.0 ) XCTAssertNotNil(pointerDownEvent) AppDelegate.setWindowFirstResponderGuardTesting(currentEvent: pointerDownEvent, hitView: nil) _ = window.makeFirstResponder(nil) XCTAssertTrue( window.makeFirstResponder(inspector), "Expected portal-hosted inspector click to bypass blocked policy using the overlay hit target" ) } @MainActor func testWindowFirstResponderGuardAllowsPointerInitiatedClickFocusFromBoundPortalInspectorSiblingWhenHitTestMisses() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let contentView = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = contentView let anchor = NSView(frame: NSRect(x: 80, y: 60, width: 480, height: 260)) contentView.addSubview(anchor) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) window.makeKeyAndOrderFront(nil) contentView.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) BrowserWindowPortalRegistry.bind(webView: webView, to: anchor, visibleInUI: true, zPriority: 1) BrowserWindowPortalRegistry.synchronizeForAnchor(anchor) defer { BrowserWindowPortalRegistry.detach(webView: webView) AppDelegate.clearWindowFirstResponderGuardTesting() window.orderOut(nil) } guard let slot = webView.superview as? WindowBrowserSlotView else { XCTFail("Expected bound portal slot") return } let inspector = FirstResponderView(frame: NSRect(x: 320, y: 0, width: 160, height: slot.bounds.height)) inspector.autoresizingMask = [.minXMargin, .height] slot.addSubview(inspector) webView.allowsFirstResponderAcquisition = false _ = window.makeFirstResponder(nil) XCTAssertFalse( window.makeFirstResponder(inspector), "Expected bound portal inspector focus to stay blocked without pointer click context" ) let pointInInspector = NSPoint(x: inspector.bounds.midX, y: inspector.bounds.midY) let pointInWindow = inspector.convert(pointInInspector, to: nil) XCTAssertTrue( BrowserWindowPortalRegistry.webViewAtWindowPoint(pointInWindow, in: window) === webView, "Expected portal registry to resolve the owning web view from a click inside inspector chrome" ) let pointerDownEvent = NSEvent.mouseEvent( with: .leftMouseDown, location: pointInWindow, modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1.0 ) XCTAssertNotNil(pointerDownEvent) AppDelegate.setWindowFirstResponderGuardTesting(currentEvent: pointerDownEvent, hitView: nil) _ = window.makeFirstResponder(nil) XCTAssertTrue( window.makeFirstResponder(inspector), "Expected bound portal inspector click to bypass blocked policy through portal registry fallback" ) } @MainActor func testWindowFirstResponderGuardAvoidsTextViewDelegateLookupForWebViewResolution() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let textView = DelegateProbeTextView(frame: NSRect(x: 0, y: 0, width: 100, height: 40)) container.addSubview(textView) window.makeKeyAndOrderFront(nil) defer { window.orderOut(nil) } _ = window.makeFirstResponder(nil) _ = window.makeFirstResponder(textView) XCTAssertEqual( textView.delegateReadCount, 0, "WebView ownership resolution should not touch NSTextView.delegate (unsafe-unretained in AppKit)" ) } @MainActor func testWindowFirstResponderGuardResolvesTrackedWebViewForFieldEditorResponder() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let webView = CmuxWebView(frame: container.bounds, configuration: WKWebViewConfiguration()) webView.autoresizingMask = [.width, .height] container.addSubview(webView) let descendant = FirstResponderView(frame: NSRect(x: 0, y: 0, width: 10, height: 10)) webView.addSubview(descendant) let fieldEditor = FieldEditorProbeTextView(frame: NSRect(x: 0, y: 0, width: 100, height: 20)) window.makeKeyAndOrderFront(nil) defer { AppDelegate.clearWindowFirstResponderGuardTesting() window.orderOut(nil) } webView.allowsFirstResponderAcquisition = true XCTAssertTrue(window.makeFirstResponder(descendant)) let timestamp = ProcessInfo.processInfo.systemUptime let pointerDownEvent = NSEvent.mouseEvent( with: .leftMouseDown, location: NSPoint(x: 5, y: 5), modifierFlags: [], timestamp: timestamp, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1.0 ) XCTAssertNotNil(pointerDownEvent) AppDelegate.setWindowFirstResponderGuardTesting(currentEvent: pointerDownEvent, hitView: descendant) XCTAssertTrue(window.makeFirstResponder(fieldEditor)) AppDelegate.clearWindowFirstResponderGuardTesting() _ = window.makeFirstResponder(nil) webView.allowsFirstResponderAcquisition = false XCTAssertFalse(window.makeFirstResponder(fieldEditor)) XCTAssertEqual( fieldEditor.delegateReadCount, 0, "Field-editor webview ownership should come from tracked associations, not NSTextView.delegate" ) } @MainActor func testWindowFirstResponderBypassBlocksSwizzledMakeFirstResponder() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let responder = FirstResponderView(frame: NSRect(x: 0, y: 0, width: 80, height: 40)) container.addSubview(responder) window.makeKeyAndOrderFront(nil) defer { window.orderOut(nil) } _ = window.makeFirstResponder(nil) cmuxWithWindowFirstResponderBypass { XCTAssertFalse( window.makeFirstResponder(responder), "Bypass scope should block transient first-responder changes during devtools auto-restore" ) } XCTAssertTrue(window.makeFirstResponder(responder)) } @MainActor func testCmdBacktickMenuActionThatChangesKeyWindowOnlyRunsOnceWhenTerminalIsFirstResponder() { _ = NSApplication.shared AppDelegate.installWindowResponderSwizzlesForTesting() let firstWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let secondWindow = NSWindow( contentRect: NSRect(x: 40, y: 40, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let firstContainer = NSView(frame: firstWindow.contentRect(forFrameRect: firstWindow.frame)) let secondContainer = NSView(frame: secondWindow.contentRect(forFrameRect: secondWindow.frame)) firstWindow.contentView = firstContainer secondWindow.contentView = secondContainer let firstTerminal = GhosttyNSView(frame: firstContainer.bounds) firstTerminal.autoresizingMask = [.width, .height] firstContainer.addSubview(firstTerminal) let secondTerminal = GhosttyNSView(frame: secondContainer.bounds) secondTerminal.autoresizingMask = [.width, .height] secondContainer.addSubview(secondTerminal) let spy = WindowCyclingActionSpy() spy.firstWindow = firstWindow spy.secondWindow = secondWindow installMenu( target: spy, action: #selector(WindowCyclingActionSpy.cycleWindow(_:)), key: "`", modifiers: [.command] ) secondWindow.orderFront(nil) firstWindow.makeKeyAndOrderFront(nil) defer { secondWindow.orderOut(nil) firstWindow.orderOut(nil) } XCTAssertTrue(firstWindow.makeFirstResponder(firstTerminal)) guard let event = makeKeyDownEvent( key: "`", modifiers: [.command], keyCode: 50, windowNumber: firstWindow.windowNumber ) else { XCTFail("Failed to construct Cmd+` event") return } NSApp.sendEvent(event) RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05)) XCTAssertEqual(spy.invocationCount, 1, "Cmd+` should only trigger one window-cycle action") } @MainActor func testCmdBacktickDoesNotRouteDirectlyToMainMenuWhenWebViewIsFirstResponder() { _ = NSApplication.shared let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) window.contentView = container let webView = CmuxWebView(frame: container.bounds, configuration: WKWebViewConfiguration()) webView.autoresizingMask = [.width, .height] container.addSubview(webView) let spy = ActionSpy() installMenu( target: spy, action: #selector(ActionSpy.didInvoke(_:)), key: "`", modifiers: [.command] ) window.makeKeyAndOrderFront(nil) defer { window.orderOut(nil) } XCTAssertTrue(window.makeFirstResponder(webView)) guard let event = makeKeyDownEvent( key: "`", modifiers: [.command], keyCode: 50, windowNumber: window.windowNumber ) else { XCTFail("Failed to construct Cmd+` event") return } XCTAssertFalse(shouldRouteCommandEquivalentDirectlyToMainMenu(event)) _ = webView.performKeyEquivalent(with: event) XCTAssertFalse( spy.invoked, "CmuxWebView should not route Cmd+` directly to the menu when WebKit is first responder" ) } private func installMenu(spy: ActionSpy, key: String, modifiers: NSEvent.ModifierFlags) { installMenu( target: spy, action: #selector(ActionSpy.didInvoke(_:)), key: key, modifiers: modifiers ) } private func installMenu( target: NSObject, action: Selector, key: String, modifiers: NSEvent.ModifierFlags ) { let mainMenu = NSMenu() let fileItem = NSMenuItem(title: "File", action: nil, keyEquivalent: "") let fileMenu = NSMenu(title: "File") let item = NSMenuItem(title: "Test Item", action: action, keyEquivalent: key) item.keyEquivalentModifierMask = modifiers item.target = target fileMenu.addItem(item) mainMenu.addItem(fileItem) mainMenu.setSubmenu(fileMenu, for: fileItem) // Ensure NSApp exists and has a menu for performKeyEquivalent to consult. _ = NSApplication.shared NSApp.mainMenu = mainMenu } private func makeKeyDownEvent( key: String, modifiers: NSEvent.ModifierFlags, keyCode: UInt16, windowNumber: Int = 0 ) -> NSEvent? { NSEvent.keyEvent( with: .keyDown, location: .zero, modifierFlags: modifiers, timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: windowNumber, context: nil, characters: key, charactersIgnoringModifiers: key, isARepeat: false, keyCode: keyCode ) } } @MainActor final class CmuxWebViewContextMenuTests: XCTestCase { private func makeRightMouseDownEvent() -> NSEvent { guard let event = NSEvent.mouseEvent( with: .rightMouseDown, location: .zero, modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: 0, context: nil, eventNumber: 0, clickCount: 1, pressure: 1.0 ) else { fatalError("Failed to create rightMouseDown event") } return event } func testWillOpenMenuAddsOpenLinkInDefaultBrowserAndRoutesSelectionToDefaultBrowserOpener() { _ = NSApplication.shared let webView = CmuxWebView(frame: NSRect(x: 0, y: 0, width: 800, height: 600), configuration: WKWebViewConfiguration()) let menu = NSMenu() let openLinkItem = NSMenuItem(title: "Open Link", action: nil, keyEquivalent: "") openLinkItem.identifier = NSUserInterfaceItemIdentifier("WKMenuItemIdentifierOpenLink") menu.addItem(openLinkItem) menu.addItem(NSMenuItem(title: "Copy Link", action: nil, keyEquivalent: "")) var openedURL: URL? webView.contextMenuLinkURLProvider = { _, _, completion in completion(URL(string: "https://example.com/docs")!) } webView.contextMenuDefaultBrowserOpener = { url in openedURL = url return true } webView.willOpenMenu(menu, with: makeRightMouseDownEvent()) guard let defaultBrowserItemIndex = menu.items.firstIndex(where: { $0.title == "Open Link in Default Browser" }) else { XCTFail("Expected Open Link in Default Browser item in context menu") return } guard let openLinkIndex = menu.items.firstIndex(where: { $0.identifier?.rawValue == "WKMenuItemIdentifierOpenLink" }) else { XCTFail("Expected Open Link item in context menu") return } XCTAssertEqual(defaultBrowserItemIndex, openLinkIndex + 1) let defaultBrowserItem = menu.items[defaultBrowserItemIndex] XCTAssertTrue(defaultBrowserItem.target === webView) XCTAssertNotNil(defaultBrowserItem.action) let dispatched = NSApp.sendAction( defaultBrowserItem.action!, to: defaultBrowserItem.target, from: defaultBrowserItem ) XCTAssertTrue(dispatched) XCTAssertEqual(openedURL?.absoluteString, "https://example.com/docs") } func testWillOpenMenuSkipsDefaultBrowserItemWhenContextHasNoOpenLinkEntry() { let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let menu = NSMenu() menu.addItem(NSMenuItem(title: "Back", action: nil, keyEquivalent: "")) menu.addItem(NSMenuItem(title: "Forward", action: nil, keyEquivalent: "")) webView.willOpenMenu(menu, with: makeRightMouseDownEvent()) XCTAssertFalse(menu.items.contains { $0.title == "Open Link in Default Browser" }) } func testWillOpenMenuHooksDownloadImageToDiskMenuVariant() { let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let menu = NSMenu() let originalTarget = NSObject() let originalAction = NSSelectorFromString("downloadImageToDisk:") let downloadItem = NSMenuItem(title: "Download Image As...", action: originalAction, keyEquivalent: "") downloadItem.identifier = NSUserInterfaceItemIdentifier("WKMenuItemIdentifierDownloadImageToDisk") downloadItem.target = originalTarget menu.addItem(downloadItem) webView.willOpenMenu(menu, with: makeRightMouseDownEvent()) XCTAssertTrue(downloadItem.target === webView) XCTAssertNotNil(downloadItem.action) XCTAssertNotEqual(downloadItem.action, originalAction) } func testWillOpenMenuHooksDownloadLinkedFileToDiskMenuVariant() { let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) let menu = NSMenu() let originalTarget = NSObject() let originalAction = NSSelectorFromString("downloadLinkToDisk:") let downloadItem = NSMenuItem(title: "Download Linked File As...", action: originalAction, keyEquivalent: "") downloadItem.identifier = NSUserInterfaceItemIdentifier("WKMenuItemIdentifierDownloadLinkToDisk") downloadItem.target = originalTarget menu.addItem(downloadItem) webView.willOpenMenu(menu, with: makeRightMouseDownEvent()) XCTAssertTrue(downloadItem.target === webView) XCTAssertNotNil(downloadItem.action) XCTAssertNotEqual(downloadItem.action, originalAction) } } final class BrowserDevToolsButtonDebugSettingsTests: XCTestCase { private func makeIsolatedDefaults() -> UserDefaults { let suiteName = "BrowserDevToolsButtonDebugSettingsTests.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { fatalError("Failed to create defaults suite") } defaults.removePersistentDomain(forName: suiteName) addTeardownBlock { defaults.removePersistentDomain(forName: suiteName) } return defaults } func testIconCatalogIncludesExpandedChoices() { XCTAssertGreaterThanOrEqual(BrowserDevToolsIconOption.allCases.count, 10) XCTAssertTrue(BrowserDevToolsIconOption.allCases.contains(.terminal)) XCTAssertTrue(BrowserDevToolsIconOption.allCases.contains(.globe)) XCTAssertTrue(BrowserDevToolsIconOption.allCases.contains(.curlyBracesSquare)) } func testIconOptionFallsBackToDefaultForUnknownRawValue() { let defaults = makeIsolatedDefaults() defaults.set("this.symbol.does.not.exist", forKey: BrowserDevToolsButtonDebugSettings.iconNameKey) XCTAssertEqual( BrowserDevToolsButtonDebugSettings.iconOption(defaults: defaults), BrowserDevToolsButtonDebugSettings.defaultIcon ) } func testColorOptionFallsBackToDefaultForUnknownRawValue() { let defaults = makeIsolatedDefaults() defaults.set("notAValidColor", forKey: BrowserDevToolsButtonDebugSettings.iconColorKey) XCTAssertEqual( BrowserDevToolsButtonDebugSettings.colorOption(defaults: defaults), BrowserDevToolsButtonDebugSettings.defaultColor ) } func testBrowserToolbarAccessorySpacingDefaultsToTwoWhenUnset() { let defaults = makeIsolatedDefaults() defaults.removeObject(forKey: BrowserToolbarAccessorySpacingDebugSettings.key) XCTAssertEqual( BrowserToolbarAccessorySpacingDebugSettings.current(defaults: defaults), BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing ) } func testBrowserToolbarAccessorySpacingFallsBackToDefaultForUnsupportedValue() { let defaults = makeIsolatedDefaults() defaults.set(99, forKey: BrowserToolbarAccessorySpacingDebugSettings.key) XCTAssertEqual( BrowserToolbarAccessorySpacingDebugSettings.current(defaults: defaults), BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing ) } func testBrowserProfilePopoverPaddingDefaultsWhenUnset() { let defaults = makeIsolatedDefaults() defaults.removeObject(forKey: BrowserProfilePopoverDebugSettings.horizontalPaddingKey) defaults.removeObject(forKey: BrowserProfilePopoverDebugSettings.verticalPaddingKey) XCTAssertEqual( BrowserProfilePopoverDebugSettings.currentHorizontalPadding(defaults: defaults), BrowserProfilePopoverDebugSettings.defaultHorizontalPadding ) XCTAssertEqual( BrowserProfilePopoverDebugSettings.currentVerticalPadding(defaults: defaults), BrowserProfilePopoverDebugSettings.defaultVerticalPadding ) } func testBrowserProfilePopoverPaddingFallsBackForUnsupportedValues() { let defaults = makeIsolatedDefaults() defaults.set(-3, forKey: BrowserProfilePopoverDebugSettings.horizontalPaddingKey) defaults.set(999, forKey: BrowserProfilePopoverDebugSettings.verticalPaddingKey) XCTAssertEqual( BrowserProfilePopoverDebugSettings.currentHorizontalPadding(defaults: defaults), BrowserProfilePopoverDebugSettings.defaultHorizontalPadding ) XCTAssertEqual( BrowserProfilePopoverDebugSettings.currentVerticalPadding(defaults: defaults), BrowserProfilePopoverDebugSettings.defaultVerticalPadding ) } func testCopyPayloadUsesPersistedValues() { let defaults = makeIsolatedDefaults() defaults.set(BrowserDevToolsIconOption.scope.rawValue, forKey: BrowserDevToolsButtonDebugSettings.iconNameKey) defaults.set(BrowserDevToolsIconColorOption.bonsplitActive.rawValue, forKey: BrowserDevToolsButtonDebugSettings.iconColorKey) let payload = BrowserDevToolsButtonDebugSettings.copyPayload(defaults: defaults) XCTAssertTrue(payload.contains("browserDevToolsIconName=scope")) XCTAssertTrue(payload.contains("browserDevToolsIconColor=bonsplitActive")) } } final class BrowserThemeSettingsTests: XCTestCase { private func makeIsolatedDefaults() -> UserDefaults { let suiteName = "BrowserThemeSettingsTests.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { fatalError("Failed to create defaults suite") } defaults.removePersistentDomain(forName: suiteName) addTeardownBlock { defaults.removePersistentDomain(forName: suiteName) } return defaults } func testDefaultsMatchConfiguredFallbacks() { let defaults = makeIsolatedDefaults() XCTAssertEqual( BrowserThemeSettings.mode(defaults: defaults), BrowserThemeSettings.defaultMode ) } func testModeReadsPersistedValue() { let defaults = makeIsolatedDefaults() defaults.set(BrowserThemeMode.dark.rawValue, forKey: BrowserThemeSettings.modeKey) XCTAssertEqual(BrowserThemeSettings.mode(defaults: defaults), .dark) defaults.set(BrowserThemeMode.light.rawValue, forKey: BrowserThemeSettings.modeKey) XCTAssertEqual(BrowserThemeSettings.mode(defaults: defaults), .light) } func testModeMigratesLegacyForcedDarkModeFlag() { let defaults = makeIsolatedDefaults() defaults.set(true, forKey: BrowserThemeSettings.legacyForcedDarkModeEnabledKey) XCTAssertEqual(BrowserThemeSettings.mode(defaults: defaults), .dark) XCTAssertEqual(defaults.string(forKey: BrowserThemeSettings.modeKey), BrowserThemeMode.dark.rawValue) let otherDefaults = makeIsolatedDefaults() otherDefaults.set(false, forKey: BrowserThemeSettings.legacyForcedDarkModeEnabledKey) XCTAssertEqual(BrowserThemeSettings.mode(defaults: otherDefaults), .system) XCTAssertEqual(otherDefaults.string(forKey: BrowserThemeSettings.modeKey), BrowserThemeMode.system.rawValue) } } final class BrowserDeveloperToolsShortcutDefaultsTests: XCTestCase { func testSafariDefaultShortcutForToggleDeveloperTools() { let shortcut = KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultShortcut XCTAssertEqual(shortcut.key, "i") XCTAssertTrue(shortcut.command) XCTAssertTrue(shortcut.option) XCTAssertFalse(shortcut.shift) XCTAssertFalse(shortcut.control) } func testSafariDefaultShortcutForShowJavaScriptConsole() { let shortcut = KeyboardShortcutSettings.Action.showBrowserJavaScriptConsole.defaultShortcut XCTAssertEqual(shortcut.key, "c") XCTAssertTrue(shortcut.command) XCTAssertTrue(shortcut.option) XCTAssertFalse(shortcut.shift) XCTAssertFalse(shortcut.control) } } @MainActor final class BrowserDeveloperToolsConfigurationTests: XCTestCase { func testBrowserPanelEnablesInspectableWebViewAndDeveloperExtras() { let panel = BrowserPanel(workspaceId: UUID()) let developerExtras = panel.webView.configuration.preferences.value(forKey: "developerExtrasEnabled") as? Bool XCTAssertEqual(developerExtras, true) if #available(macOS 13.3, *) { XCTAssertTrue(panel.webView.isInspectable) } } func testBrowserPanelRefreshesUnderPageBackgroundColorWhenGhosttyBackgroundChanges() { let panel = BrowserPanel(workspaceId: UUID()) let updatedColor = NSColor(srgbRed: 0.18, green: 0.29, blue: 0.44, alpha: 1.0) let updatedOpacity = 0.57 NotificationCenter.default.post( name: .ghosttyDefaultBackgroundDidChange, object: nil, userInfo: [ GhosttyNotificationKey.backgroundColor: updatedColor, GhosttyNotificationKey.backgroundOpacity: updatedOpacity ] ) guard let actual = panel.webView.underPageBackgroundColor?.usingColorSpace(.sRGB), let expected = updatedColor.withAlphaComponent(updatedOpacity).usingColorSpace(.sRGB) else { XCTFail("Expected sRGB-convertible under-page background colors") return } XCTAssertEqual(actual.redComponent, expected.redComponent, accuracy: 0.005) XCTAssertEqual(actual.greenComponent, expected.greenComponent, accuracy: 0.005) XCTAssertEqual(actual.blueComponent, expected.blueComponent, accuracy: 0.005) XCTAssertEqual(actual.alphaComponent, expected.alphaComponent, accuracy: 0.005) } func testBrowserPanelStartsAsNewTabWithoutLoadingAboutBlank() { let panel = BrowserPanel(workspaceId: UUID()) XCTAssertEqual(panel.displayTitle, "New tab") XCTAssertFalse(panel.shouldRenderWebView) XCTAssertTrue(panel.isShowingNewTabPage) XCTAssertNil(panel.webView.url) XCTAssertNil(panel.currentURL) } func testBrowserPanelLeavesNewTabPageStateWhenNavigationStarts() { let panel = BrowserPanel(workspaceId: UUID()) XCTAssertTrue(panel.isShowingNewTabPage) panel.navigate(to: URL(string: "https://example.com")!) XCTAssertFalse(panel.isShowingNewTabPage) } func testBrowserPanelThemeModeUpdatesWebViewAppearance() { let panel = BrowserPanel(workspaceId: UUID()) panel.setBrowserThemeMode(.dark) XCTAssertEqual(panel.webView.appearance?.bestMatch(from: [.darkAqua, .aqua]), .darkAqua) panel.setBrowserThemeMode(.light) XCTAssertEqual(panel.webView.appearance?.bestMatch(from: [.aqua, .darkAqua]), .aqua) panel.setBrowserThemeMode(.system) XCTAssertNil(panel.webView.appearance) } func testBrowserPanelRefreshesUnderPageBackgroundColorWithGhosttyOpacity() { let panel = BrowserPanel(workspaceId: UUID()) let updatedColor = NSColor(srgbRed: 0.18, green: 0.29, blue: 0.44, alpha: 1.0) NotificationCenter.default.post( name: .ghosttyDefaultBackgroundDidChange, object: nil, userInfo: [ GhosttyNotificationKey.backgroundColor: updatedColor, GhosttyNotificationKey.backgroundOpacity: NSNumber(value: 0.57), ] ) guard let actual = panel.webView.underPageBackgroundColor?.usingColorSpace(.sRGB), let expected = updatedColor.withAlphaComponent(0.57).usingColorSpace(.sRGB) else { XCTFail("Expected sRGB-convertible under-page background colors") return } XCTAssertEqual(actual.redComponent, expected.redComponent, accuracy: 0.005) XCTAssertEqual(actual.greenComponent, expected.greenComponent, accuracy: 0.005) XCTAssertEqual(actual.blueComponent, expected.blueComponent, accuracy: 0.005) XCTAssertEqual(actual.alphaComponent, expected.alphaComponent, accuracy: 0.005) } } @MainActor final class BrowserInsecureHTTPAlertPresentationTests: XCTestCase { private final class BrowserInsecureHTTPAlertSpy: NSAlert { private(set) var beginSheetModalCallCount = 0 private(set) var runModalCallCount = 0 var nextResponse: NSApplication.ModalResponse = .alertThirdButtonReturn override func beginSheetModal( for sheetWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? ) { beginSheetModalCallCount += 1 handler?(nextResponse) } override func runModal() -> NSApplication.ModalResponse { runModalCallCount += 1 return nextResponse } } func testInsecureHTTPPromptUsesSheetWhenWindowIsAvailable() { let panel = BrowserPanel(workspaceId: UUID()) defer { panel.resetInsecureHTTPAlertHooksForTesting() } let alertSpy = BrowserInsecureHTTPAlertSpy() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 320), styleMask: [.titled], backing: .buffered, defer: false ) panel.configureInsecureHTTPAlertHooksForTesting( alertFactory: { alertSpy }, windowProvider: { window } ) panel.presentInsecureHTTPAlertForTesting(url: URL(string: "http://example.com")!) XCTAssertEqual(alertSpy.beginSheetModalCallCount, 1) XCTAssertEqual(alertSpy.runModalCallCount, 0) } func testInsecureHTTPPromptFallsBackToRunModalWithoutWindow() { let panel = BrowserPanel(workspaceId: UUID()) defer { panel.resetInsecureHTTPAlertHooksForTesting() } let alertSpy = BrowserInsecureHTTPAlertSpy() panel.configureInsecureHTTPAlertHooksForTesting( alertFactory: { alertSpy }, windowProvider: { nil } ) panel.presentInsecureHTTPAlertForTesting(url: URL(string: "http://example.com")!) XCTAssertEqual(alertSpy.beginSheetModalCallCount, 0) XCTAssertEqual(alertSpy.runModalCallCount, 1) } } final class BrowserNavigationNewTabDecisionTests: XCTestCase { func testLinkActivatedCmdClickOpensInNewTab() { XCTAssertTrue( browserNavigationShouldOpenInNewTab( navigationType: .linkActivated, modifierFlags: [.command], buttonNumber: 0 ) ) } func testLinkActivatedMiddleClickOpensInNewTab() { XCTAssertTrue( browserNavigationShouldOpenInNewTab( navigationType: .linkActivated, modifierFlags: [], buttonNumber: 2 ) ) } func testLinkActivatedPlainLeftClickStaysInCurrentTab() { XCTAssertFalse( browserNavigationShouldOpenInNewTab( navigationType: .linkActivated, modifierFlags: [], buttonNumber: 0 ) ) } func testOtherNavigationMiddleClickOpensInNewTab() { XCTAssertTrue( browserNavigationShouldOpenInNewTab( navigationType: .other, modifierFlags: [], buttonNumber: 2 ) ) } func testOtherNavigationLeftClickStaysInCurrentTab() { XCTAssertFalse( browserNavigationShouldOpenInNewTab( navigationType: .other, modifierFlags: [], buttonNumber: 0 ) ) } func testLinkActivatedButtonFourWithoutMiddleIntentStaysInCurrentTab() { XCTAssertFalse( browserNavigationShouldOpenInNewTab( navigationType: .linkActivated, modifierFlags: [], buttonNumber: 4, hasRecentMiddleClickIntent: false ) ) } func testLinkActivatedButtonFourWithRecentMiddleIntentOpensInNewTab() { XCTAssertTrue( browserNavigationShouldOpenInNewTab( navigationType: .linkActivated, modifierFlags: [], buttonNumber: 4, hasRecentMiddleClickIntent: true ) ) } func testLinkActivatedUsesCurrentEventFallbackForMiddleClick() { XCTAssertTrue( browserNavigationShouldOpenInNewTab( navigationType: .linkActivated, modifierFlags: [], buttonNumber: 0, currentEventType: .otherMouseUp, currentEventButtonNumber: 2 ) ) } func testCurrentEventFallbackDoesNotAffectNonLinkNavigation() { XCTAssertFalse( browserNavigationShouldOpenInNewTab( navigationType: .reload, modifierFlags: [], buttonNumber: 0, currentEventType: .otherMouseUp, currentEventButtonNumber: 2 ) ) } func testNonLinkNavigationNeverForcesNewTab() { XCTAssertFalse( browserNavigationShouldOpenInNewTab( navigationType: .reload, modifierFlags: [.command], buttonNumber: 2 ) ) } } final class BrowserPopupDecisionTests: XCTestCase { func testLinkActivatedPlainLeftClickDoesNotCreatePopup() { XCTAssertFalse( browserNavigationShouldCreatePopup( navigationType: .linkActivated, modifierFlags: [], buttonNumber: 0 ) ) } func testOtherNavigationPlainLeftClickCreatesPopup() { XCTAssertTrue( browserNavigationShouldCreatePopup( navigationType: .other, modifierFlags: [], buttonNumber: 0 ) ) } func testOtherNavigationMiddleClickDoesNotCreatePopup() { XCTAssertFalse( browserNavigationShouldCreatePopup( navigationType: .other, modifierFlags: [], buttonNumber: 2 ) ) } func testLinkActivatedCmdClickDoesNotCreatePopup() { XCTAssertFalse( browserNavigationShouldCreatePopup( navigationType: .linkActivated, modifierFlags: [.command], buttonNumber: 0 ) ) } } final class BrowserNilTargetFallbackDecisionTests: XCTestCase { func testOtherNavigationDoesNotFallbackToNewTab() { XCTAssertFalse( browserNavigationShouldFallbackNilTargetToNewTab( navigationType: .other ) ) } func testLinkActivatedNavigationFallsBackToNewTab() { XCTAssertTrue( browserNavigationShouldFallbackNilTargetToNewTab( navigationType: .linkActivated ) ) } } final class BrowserPopupContentRectTests: XCTestCase { func testExplicitTopOriginCoordinatesConvertToAppKitBottomOrigin() { let rect = browserPopupContentRect( requestedWidth: 400, requestedHeight: 300, requestedX: 150, requestedTopY: 120, visibleFrame: NSRect(x: 100, y: 50, width: 1000, height: 800) ) XCTAssertEqual(rect.origin.x, 150, accuracy: 0.01) XCTAssertEqual(rect.origin.y, 430, accuracy: 0.01) XCTAssertEqual(rect.width, 400, accuracy: 0.01) XCTAssertEqual(rect.height, 300, accuracy: 0.01) } func testExplicitCoordinatesClampToVisibleFrame() { let rect = browserPopupContentRect( requestedWidth: 1400, requestedHeight: 1200, requestedX: 900, requestedTopY: -25, visibleFrame: NSRect(x: 100, y: 50, width: 1000, height: 800) ) XCTAssertEqual(rect.origin.x, 100, accuracy: 0.01) XCTAssertEqual(rect.origin.y, 50, accuracy: 0.01) XCTAssertEqual(rect.width, 1000, accuracy: 0.01) XCTAssertEqual(rect.height, 800, accuracy: 0.01) } func testMissingCoordinatesCentersPopup() { let rect = browserPopupContentRect( requestedWidth: 300, requestedHeight: 200, requestedX: nil, requestedTopY: nil, visibleFrame: NSRect(x: 100, y: 50, width: 1000, height: 800) ) XCTAssertEqual(rect.origin.x, 450, accuracy: 0.01) XCTAssertEqual(rect.origin.y, 350, accuracy: 0.01) XCTAssertEqual(rect.width, 300, accuracy: 0.01) XCTAssertEqual(rect.height, 200, accuracy: 0.01) } } @MainActor final class BrowserJavaScriptDialogDelegateTests: XCTestCase { func testBrowserPanelUIDelegateImplementsJavaScriptDialogSelectors() { let panel = BrowserPanel(workspaceId: UUID()) guard let uiDelegate = panel.webView.uiDelegate as? NSObject else { XCTFail("Expected BrowserPanel webView.uiDelegate to be an NSObject") return } XCTAssertTrue( uiDelegate.responds( to: #selector( WKUIDelegate.webView( _:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler: ) ) ), "Browser UI delegate must implement JavaScript alert handling" ) XCTAssertTrue( uiDelegate.responds( to: #selector( WKUIDelegate.webView( _:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler: ) ) ), "Browser UI delegate must implement JavaScript confirm handling" ) XCTAssertTrue( uiDelegate.responds( to: #selector( WKUIDelegate.webView( _:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler: ) ) ), "Browser UI delegate must implement JavaScript prompt handling" ) } } @MainActor final class BrowserSessionHistoryRestoreTests: XCTestCase { func testSessionNavigationHistorySnapshotUsesRestoredStacks() { let panel = BrowserPanel(workspaceId: UUID()) panel.restoreSessionNavigationHistory( backHistoryURLStrings: [ "https://example.com/a", "https://example.com/b" ], forwardHistoryURLStrings: [ "https://example.com/d" ], currentURLString: "https://example.com/c" ) XCTAssertTrue(panel.canGoBack) XCTAssertTrue(panel.canGoForward) let snapshot = panel.sessionNavigationHistorySnapshot() XCTAssertEqual( snapshot.backHistoryURLStrings, ["https://example.com/a", "https://example.com/b"] ) XCTAssertEqual( snapshot.forwardHistoryURLStrings, ["https://example.com/d"] ) } func testSessionNavigationHistoryBackAndForwardUpdateStacks() { let panel = BrowserPanel(workspaceId: UUID()) panel.restoreSessionNavigationHistory( backHistoryURLStrings: [ "https://example.com/a", "https://example.com/b" ], forwardHistoryURLStrings: [ "https://example.com/d" ], currentURLString: "https://example.com/c" ) panel.goBack() let afterBack = panel.sessionNavigationHistorySnapshot() XCTAssertEqual(afterBack.backHistoryURLStrings, ["https://example.com/a"]) XCTAssertEqual( afterBack.forwardHistoryURLStrings, ["https://example.com/c", "https://example.com/d"] ) XCTAssertTrue(panel.canGoBack) XCTAssertTrue(panel.canGoForward) panel.goForward() let afterForward = panel.sessionNavigationHistorySnapshot() XCTAssertEqual( afterForward.backHistoryURLStrings, ["https://example.com/a", "https://example.com/b"] ) XCTAssertEqual(afterForward.forwardHistoryURLStrings, ["https://example.com/d"]) XCTAssertTrue(panel.canGoBack) XCTAssertTrue(panel.canGoForward) } func testWebViewReplacementAfterProcessTerminationUpdatesInstanceIdentity() { let panel = BrowserPanel( workspaceId: UUID(), initialURL: URL(string: "https://example.com") ) let oldWebView = panel.webView let oldInstanceID = panel.webViewInstanceID panel.debugSimulateWebContentProcessTermination() XCTAssertFalse(panel.webView === oldWebView) XCTAssertNotEqual(panel.webViewInstanceID, oldInstanceID) XCTAssertNotNil(panel.webView.navigationDelegate) XCTAssertNotNil(panel.webView.uiDelegate) } func testWebViewReplacementPreservesEmptyNewTabRenderState() { let panel = BrowserPanel(workspaceId: UUID()) XCTAssertFalse(panel.shouldRenderWebView) panel.debugSimulateWebContentProcessTermination() XCTAssertFalse(panel.shouldRenderWebView) } func testResetSidebarContextClearsBrowserPanelsIntoNewTabState() throws { let workspace = Workspace() let paneId = try XCTUnwrap(workspace.bonsplitController.allPaneIds.first) let contextPanelId = try XCTUnwrap(workspace.focusedPanelId) let browser = try XCTUnwrap( workspace.newBrowserSurface( inPane: paneId, url: URL(string: "https://example.com"), focus: false ) ) browser.restoreSessionNavigationHistory( backHistoryURLStrings: ["https://example.com/prev"], forwardHistoryURLStrings: ["https://example.com/next"], currentURLString: "https://example.com/current" ) browser.startFind() workspace.statusEntries["task"] = SidebarStatusEntry(key: "task", value: "Issue #1208") workspace.metadataBlocks["notes"] = SidebarMetadataBlock( key: "notes", markdown: "test", priority: 0, timestamp: Date() ) workspace.progress = SidebarProgressState(value: 0.5, label: "Loading") workspace.updatePanelGitBranch(panelId: contextPanelId, branch: "issue-1208", isDirty: false) workspace.updatePanelPullRequest( panelId: contextPanelId, number: 1208, label: "PR", url: try XCTUnwrap(URL(string: "https://example.com/pull/1208")), status: .open ) workspace.logEntries.append( SidebarLogEntry( message: "Issue #1208", level: .info, source: "test", timestamp: Date() ) ) workspace.surfaceListeningPorts[contextPanelId] = [3000] workspace.recomputeListeningPorts() XCTAssertTrue(browser.shouldRenderWebView) XCTAssertNotNil(browser.preferredURLStringForOmnibar()) XCTAssertTrue(browser.canGoBack) XCTAssertTrue(browser.canGoForward) XCTAssertNotNil(browser.searchState) XCTAssertFalse(workspace.statusEntries.isEmpty) XCTAssertFalse(workspace.logEntries.isEmpty) XCTAssertFalse(workspace.metadataBlocks.isEmpty) XCTAssertNotNil(workspace.progress) XCTAssertNotNil(workspace.gitBranch) XCTAssertNotNil(workspace.pullRequest) XCTAssertEqual(workspace.listeningPorts, [3000]) let priorWebView = browser.webView let priorInstanceID = browser.webViewInstanceID workspace.resetSidebarContext(reason: "test") XCTAssertTrue(workspace.statusEntries.isEmpty) XCTAssertTrue(workspace.logEntries.isEmpty) XCTAssertTrue(workspace.metadataBlocks.isEmpty) XCTAssertNil(workspace.progress) XCTAssertNil(workspace.gitBranch) XCTAssertTrue(workspace.panelGitBranches.isEmpty) XCTAssertNil(workspace.pullRequest) XCTAssertTrue(workspace.panelPullRequests.isEmpty) XCTAssertTrue(workspace.surfaceListeningPorts.isEmpty) XCTAssertTrue(workspace.listeningPorts.isEmpty) XCTAssertFalse(browser.shouldRenderWebView) XCTAssertNil(browser.preferredURLStringForOmnibar()) XCTAssertFalse(browser.canGoBack) XCTAssertFalse(browser.canGoForward) XCTAssertNil(browser.searchState) XCTAssertFalse(browser.webView === priorWebView) XCTAssertNotEqual(browser.webViewInstanceID, priorInstanceID) } } @MainActor final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase { private final class WKInspectorProbeView: NSView { override var acceptsFirstResponder: Bool { true } } private final class FakeInspector: NSObject { enum HideBehavior { case unsupported case noEffect case hides } private(set) var attachCount = 0 private(set) var showCount = 0 private(set) var hideCount = 0 private(set) var closeCount = 0 private let hideBehavior: HideBehavior private var visible = false private var attached = false init(hideBehavior: HideBehavior = .unsupported) { self.hideBehavior = hideBehavior super.init() } override func responds(to aSelector: Selector!) -> Bool { guard NSStringFromSelector(aSelector) == "hide" else { return super.responds(to: aSelector) } return hideBehavior != .unsupported } @objc func isVisible() -> Bool { visible } @objc func isAttached() -> Bool { attached } @objc func attach() { attachCount += 1 attached = true show() } @objc func show() { showCount += 1 visible = true } @objc func hide() { hideCount += 1 guard hideBehavior == .hides else { return } visible = false } @objc func close() { closeCount += 1 visible = false attached = false } } override class func setUp() { super.setUp() installCmuxUnitTestInspectorOverride() } private func makePanelWithInspector( hideBehavior: FakeInspector.HideBehavior = .unsupported ) -> (BrowserPanel, FakeInspector) { let panel = BrowserPanel(workspaceId: UUID()) let inspector = FakeInspector(hideBehavior: hideBehavior) panel.webView.cmuxSetUnitTestInspector(inspector) return (panel, inspector) } private func findHostContainerView(in root: NSView) -> WebViewRepresentable.HostContainerView? { if let host = root as? WebViewRepresentable.HostContainerView { return host } for subview in root.subviews { if let host = findHostContainerView(in: subview) { return host } } return nil } private func waitForDeveloperToolsTransitions() { RunLoop.current.run(until: Date().addingTimeInterval(0.5)) } private func findWindowBrowserSlotView(in root: NSView) -> WindowBrowserSlotView? { if let slot = root as? WindowBrowserSlotView { return slot } for subview in root.subviews { if let slot = findWindowBrowserSlotView(in: subview) { return slot } } return nil } func testRestoreReopensInspectorAfterAttachWhenPreferredVisible() { let (panel, inspector) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) XCTAssertTrue(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.showCount, 1) // Simulate WebKit closing inspector during detach/reattach churn. inspector.close() XCTAssertFalse(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.closeCount, 1) panel.restoreDeveloperToolsAfterAttachIfNeeded() XCTAssertTrue(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.showCount, 2) } func testSyncRespectsManualCloseAndPreventsUnexpectedRestore() { let (panel, inspector) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) XCTAssertEqual(inspector.showCount, 1) // Simulate user closing inspector before detach. inspector.close() panel.syncDeveloperToolsPreferenceFromInspector() panel.restoreDeveloperToolsAfterAttachIfNeeded() XCTAssertFalse(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.showCount, 1) } func testSyncCanPreserveVisibleIntentDuringDetachChurn() { let (panel, inspector) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) XCTAssertEqual(inspector.showCount, 1) // Simulate a transient close caused by view detach, not user intent. inspector.close() panel.syncDeveloperToolsPreferenceFromInspector(preserveVisibleIntent: true) panel.restoreDeveloperToolsAfterAttachIfNeeded() XCTAssertTrue(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.showCount, 2) } func testForcedRefreshAfterAttachKeepsVisibleInspectorState() { let (panel, inspector) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) XCTAssertTrue(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.showCount, 1) XCTAssertEqual(inspector.closeCount, 0) panel.requestDeveloperToolsRefreshAfterNextAttach(reason: "unit-test") panel.restoreDeveloperToolsAfterAttachIfNeeded() XCTAssertTrue(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.closeCount, 0) XCTAssertEqual(inspector.showCount, 1) // The force-refresh request should be one-shot. panel.restoreDeveloperToolsAfterAttachIfNeeded() XCTAssertEqual(inspector.closeCount, 0) XCTAssertEqual(inspector.showCount, 1) } func testRefreshRequestTracksPendingStateUntilRestoreRuns() { let (panel, _) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) XCTAssertFalse(panel.hasPendingDeveloperToolsRefreshAfterAttach()) panel.requestDeveloperToolsRefreshAfterNextAttach(reason: "unit-test") XCTAssertTrue(panel.hasPendingDeveloperToolsRefreshAfterAttach()) panel.restoreDeveloperToolsAfterAttachIfNeeded() XCTAssertFalse(panel.hasPendingDeveloperToolsRefreshAfterAttach()) } func testRapidToggleCoalescesToFinalVisibleIntentWithoutExtraInspectorCalls() { let (panel, inspector) = makePanelWithInspector() XCTAssertTrue(panel.toggleDeveloperTools()) XCTAssertTrue(panel.toggleDeveloperTools()) XCTAssertTrue(panel.toggleDeveloperTools()) XCTAssertEqual(inspector.showCount, 1) XCTAssertEqual(inspector.closeCount, 0) waitForDeveloperToolsTransitions() XCTAssertTrue(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.showCount, 1) XCTAssertEqual(inspector.closeCount, 0) } func testRapidToggleQueuesHideAfterOpenTransitionSettles() { let (panel, inspector) = makePanelWithInspector() XCTAssertTrue(panel.toggleDeveloperTools()) XCTAssertTrue(panel.toggleDeveloperTools()) XCTAssertEqual(inspector.showCount, 1) XCTAssertEqual(inspector.closeCount, 0) waitForDeveloperToolsTransitions() XCTAssertFalse(panel.isDeveloperToolsVisible()) XCTAssertEqual(inspector.showCount, 1) XCTAssertEqual(inspector.closeCount, 1) } func testToggleDeveloperToolsFallsBackToCloseWhenHideDoesNotConcealInspector() { let (panel, inspector) = makePanelWithInspector(hideBehavior: .noEffect) XCTAssertTrue(panel.showDeveloperTools()) XCTAssertTrue(panel.isDeveloperToolsVisible()) XCTAssertTrue(panel.toggleDeveloperTools()) XCTAssertEqual(inspector.hideCount, 1) XCTAssertEqual(inspector.closeCount, 1) XCTAssertFalse(panel.isDeveloperToolsVisible()) } func testTransientHideAttachmentPreserveFollowsDeveloperToolsIntent() { let (panel, _) = makePanelWithInspector() XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) XCTAssertTrue(panel.showDeveloperTools()) XCTAssertTrue(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) XCTAssertTrue(panel.hideDeveloperTools()) XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) } func testWebViewDismantleKeepsPortalHostedWebViewAttachedWhenDeveloperToolsIntentIsVisible() { let (panel, _) = makePanelWithInspector() let paneId = PaneID(id: UUID()) XCTAssertTrue(panel.showDeveloperTools()) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let anchor = NSView(frame: NSRect(x: 30, y: 30, width: 180, height: 140)) window.contentView?.addSubview(anchor) window.makeKeyAndOrderFront(nil) window.displayIfNeeded() window.contentView?.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) BrowserWindowPortalRegistry.bind(webView: panel.webView, to: anchor, visibleInUI: true, zPriority: 1) BrowserWindowPortalRegistry.synchronizeForAnchor(anchor) XCTAssertNotNil(panel.webView.superview) let representable = WebViewRepresentable( panel: panel, paneId: paneId, shouldAttachWebView: true, useLocalInlineHosting: false, shouldFocusWebView: false, isPanelFocused: true, portalZPriority: 0, paneDropZone: nil, searchOverlay: nil, paneTopChromeHeight: 0 ) let coordinator = representable.makeCoordinator() coordinator.webView = panel.webView WebViewRepresentable.dismantleNSView(anchor, coordinator: coordinator) XCTAssertNotNil(panel.webView.superview) window.orderOut(nil) } func testWebViewDismantleKeepsPortalHostedWebViewAttachedWhenDeveloperToolsIntentIsHidden() { let (panel, _) = makePanelWithInspector() let paneId = PaneID(id: UUID()) XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let anchor = NSView(frame: NSRect(x: 20, y: 20, width: 200, height: 150)) window.contentView?.addSubview(anchor) window.makeKeyAndOrderFront(nil) window.displayIfNeeded() window.contentView?.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) BrowserWindowPortalRegistry.bind(webView: panel.webView, to: anchor, visibleInUI: true, zPriority: 1) BrowserWindowPortalRegistry.synchronizeForAnchor(anchor) XCTAssertNotNil(panel.webView.superview) let representable = WebViewRepresentable( panel: panel, paneId: paneId, shouldAttachWebView: true, useLocalInlineHosting: false, shouldFocusWebView: false, isPanelFocused: true, portalZPriority: 0, paneDropZone: nil, searchOverlay: nil, paneTopChromeHeight: 0 ) let coordinator = representable.makeCoordinator() coordinator.webView = panel.webView WebViewRepresentable.dismantleNSView(anchor, coordinator: coordinator) XCTAssertNotNil(panel.webView.superview) window.orderOut(nil) } func testTransientHideAttachmentPreserveDisablesForSideDockedInspectorLayout() { let (panel, _) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) let host = NSView(frame: NSRect(x: 0, y: 0, width: 320, height: 240)) panel.webView.frame = NSRect(x: 0, y: 0, width: 120, height: host.bounds.height) host.addSubview(panel.webView) let inspectorContainer = NSView( frame: NSRect(x: 120, y: 0, width: host.bounds.width - 120, height: host.bounds.height) ) let inspectorView = WKInspectorProbeView(frame: inspectorContainer.bounds) inspectorView.autoresizingMask = [.width, .height] inspectorContainer.addSubview(inspectorView) host.addSubview(inspectorContainer) XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) } func testTransientHideAttachmentPreserveStaysEnabledForBottomDockedInspectorLayout() { let (panel, _) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) let host = NSView(frame: NSRect(x: 0, y: 0, width: 320, height: 240)) panel.webView.frame = NSRect(x: 0, y: 80, width: host.bounds.width, height: host.bounds.height - 80) host.addSubview(panel.webView) let inspectorContainer = NSView(frame: NSRect(x: 0, y: 0, width: host.bounds.width, height: 80)) let inspectorView = WKInspectorProbeView(frame: inspectorContainer.bounds) inspectorView.autoresizingMask = [.width, .height] inspectorContainer.addSubview(inspectorView) host.addSubview(inspectorContainer) XCTAssertTrue(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) } func testOffWindowReplacementLocalHostDoesNotStealVisibleDevToolsWebView() { let (panel, _) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) let paneId = PaneID(id: UUID()) let representable = WebViewRepresentable( panel: panel, paneId: paneId, shouldAttachWebView: false, useLocalInlineHosting: true, shouldFocusWebView: false, isPanelFocused: true, portalZPriority: 0, paneDropZone: nil, searchOverlay: nil, paneTopChromeHeight: 0 ) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 360, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected content view") return } let visibleHosting = NSHostingView(rootView: representable) visibleHosting.frame = contentView.bounds visibleHosting.autoresizingMask = [.width, .height] contentView.addSubview(visibleHosting) window.makeKeyAndOrderFront(nil) window.displayIfNeeded() contentView.layoutSubtreeIfNeeded() visibleHosting.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) guard let visibleHost = findHostContainerView(in: visibleHosting) else { XCTFail("Expected visible local host") return } guard let visibleSlot = panel.webView.superview as? WindowBrowserSlotView else { XCTFail("Expected visible local inline slot") return } let inspectorView = WKInspectorProbeView( frame: NSRect(x: 0, y: 0, width: visibleSlot.bounds.width, height: 72) ) inspectorView.autoresizingMask = [.width] visibleSlot.addSubview(inspectorView) panel.webView.frame = NSRect( x: 0, y: inspectorView.frame.maxY, width: visibleSlot.bounds.width, height: visibleSlot.bounds.height - inspectorView.frame.height ) visibleSlot.layoutSubtreeIfNeeded() let detachedRoot = NSView(frame: visibleHosting.frame) let offWindowHosting = NSHostingView(rootView: representable) offWindowHosting.frame = detachedRoot.bounds offWindowHosting.autoresizingMask = [.width, .height] detachedRoot.addSubview(offWindowHosting) detachedRoot.layoutSubtreeIfNeeded() offWindowHosting.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) XCTAssertNotNil(findHostContainerView(in: offWindowHosting), "Expected off-window replacement host") XCTAssertTrue(visibleHost.window === window) XCTAssertTrue( panel.webView.superview === visibleSlot, "An off-window replacement host should not steal a visible DevTools-hosted web view during split zoom churn" ) XCTAssertTrue( inspectorView.superview === visibleSlot, "An off-window replacement host should leave DevTools companion views in the visible local host" ) } func testVisibleReplacementLocalHostNormalizesBottomDockedInspectorFrames() { let (panel, _) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) let paneId = PaneID(id: UUID()) let representable = WebViewRepresentable( panel: panel, paneId: paneId, shouldAttachWebView: false, useLocalInlineHosting: true, shouldFocusWebView: false, isPanelFocused: true, portalZPriority: 0, paneDropZone: nil, searchOverlay: nil, paneTopChromeHeight: 0 ) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 360, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected content view") return } let narrowHosting = NSHostingView(rootView: representable) narrowHosting.frame = NSRect(x: 180, y: 0, width: 180, height: 240) contentView.addSubview(narrowHosting) window.makeKeyAndOrderFront(nil) window.displayIfNeeded() contentView.layoutSubtreeIfNeeded() narrowHosting.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) guard let initialSlot = panel.webView.superview as? WindowBrowserSlotView else { XCTFail("Expected initial local inline slot") return } let inspectorView = WKInspectorProbeView( frame: NSRect(x: 0, y: 0, width: initialSlot.bounds.width, height: 72) ) inspectorView.autoresizingMask = [.width] initialSlot.addSubview(inspectorView) panel.webView.frame = NSRect( x: 0, y: inspectorView.frame.maxY, width: initialSlot.bounds.width, height: initialSlot.bounds.height - inspectorView.frame.height ) initialSlot.layoutSubtreeIfNeeded() let replacementHosting = NSHostingView(rootView: representable) replacementHosting.frame = contentView.bounds replacementHosting.autoresizingMask = [.width, .height] contentView.addSubview(replacementHosting, positioned: .above, relativeTo: narrowHosting) contentView.layoutSubtreeIfNeeded() replacementHosting.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) replacementHosting.rootView = representable contentView.layoutSubtreeIfNeeded() replacementHosting.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) narrowHosting.removeFromSuperview() contentView.layoutSubtreeIfNeeded() replacementHosting.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) guard let replacementHost = findHostContainerView(in: replacementHosting), let replacementSlot = findWindowBrowserSlotView(in: replacementHost) else { XCTFail("Expected replacement local inline host") return } XCTAssertTrue( panel.webView.superview === replacementSlot, "A visible replacement local host should take over the hosted page" ) XCTAssertTrue( inspectorView.superview === replacementSlot, "A visible replacement local host should move the DevTools companion views with the page" ) XCTAssertEqual(inspectorView.frame.minX, 0, accuracy: 0.5) XCTAssertEqual(inspectorView.frame.minY, 0, accuracy: 0.5) XCTAssertEqual(inspectorView.frame.width, replacementSlot.bounds.width, accuracy: 0.5) XCTAssertEqual(inspectorView.frame.height, 72, accuracy: 0.5) XCTAssertEqual(panel.webView.frame.minX, 0, accuracy: 0.5) XCTAssertEqual(panel.webView.frame.minY, 72, accuracy: 0.5) XCTAssertEqual(panel.webView.frame.width, replacementSlot.bounds.width, accuracy: 0.5) XCTAssertEqual(panel.webView.frame.height, replacementSlot.bounds.height - 72, accuracy: 0.5) } } final class BrowserOmnibarCommandNavigationTests: XCTestCase { func testArrowNavigationDeltaRequiresFocusedAddressBarAndNoModifierFlags() { XCTAssertNil( browserOmnibarSelectionDeltaForArrowNavigation( hasFocusedAddressBar: false, flags: [], keyCode: 126 ) ) XCTAssertNil( browserOmnibarSelectionDeltaForArrowNavigation( hasFocusedAddressBar: true, flags: [.command], keyCode: 126 ) ) XCTAssertEqual( browserOmnibarSelectionDeltaForArrowNavigation( hasFocusedAddressBar: true, flags: [], keyCode: 126 ), -1 ) XCTAssertEqual( browserOmnibarSelectionDeltaForArrowNavigation( hasFocusedAddressBar: true, flags: [], keyCode: 125 ), 1 ) } func testArrowNavigationDeltaIgnoresCapsLockModifier() { XCTAssertEqual( browserOmnibarSelectionDeltaForArrowNavigation( hasFocusedAddressBar: true, flags: [.capsLock], keyCode: 126 ), -1 ) XCTAssertEqual( browserOmnibarSelectionDeltaForArrowNavigation( hasFocusedAddressBar: true, flags: [.capsLock], keyCode: 125 ), 1 ) } func testCommandNavigationDeltaRequiresFocusedAddressBarAndCommandOrControlOnly() { XCTAssertNil( browserOmnibarSelectionDeltaForCommandNavigation( hasFocusedAddressBar: false, flags: [.command], chars: "n" ) ) XCTAssertEqual( browserOmnibarSelectionDeltaForCommandNavigation( hasFocusedAddressBar: true, flags: [.command], chars: "n" ), 1 ) XCTAssertEqual( browserOmnibarSelectionDeltaForCommandNavigation( hasFocusedAddressBar: true, flags: [.command], chars: "p" ), -1 ) XCTAssertNil( browserOmnibarSelectionDeltaForCommandNavigation( hasFocusedAddressBar: true, flags: [.command, .shift], chars: "n" ) ) XCTAssertEqual( browserOmnibarSelectionDeltaForCommandNavigation( hasFocusedAddressBar: true, flags: [.control], chars: "p" ), -1 ) XCTAssertEqual( browserOmnibarSelectionDeltaForCommandNavigation( hasFocusedAddressBar: true, flags: [.control], chars: "n" ), 1 ) } func testCommandNavigationDeltaIgnoresCapsLockModifier() { XCTAssertEqual( browserOmnibarSelectionDeltaForCommandNavigation( hasFocusedAddressBar: true, flags: [.control, .capsLock], chars: "n" ), 1 ) XCTAssertEqual( browserOmnibarSelectionDeltaForCommandNavigation( hasFocusedAddressBar: true, flags: [.command, .capsLock], chars: "p" ), -1 ) } func testSubmitOnReturnIgnoresCapsLockModifier() { XCTAssertTrue(browserOmnibarShouldSubmitOnReturn(flags: [])) XCTAssertTrue(browserOmnibarShouldSubmitOnReturn(flags: [.shift])) XCTAssertTrue(browserOmnibarShouldSubmitOnReturn(flags: [.capsLock])) XCTAssertTrue(browserOmnibarShouldSubmitOnReturn(flags: [.shift, .capsLock])) XCTAssertFalse(browserOmnibarShouldSubmitOnReturn(flags: [.command, .capsLock])) } } final class BrowserReturnKeyDownRoutingTests: XCTestCase { func testRoutesForReturnWhenBrowserFirstResponder() { XCTAssertTrue( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 36, firstResponderIsBrowser: true, flags: [] ) ) } func testRoutesForKeypadEnterWhenBrowserFirstResponder() { XCTAssertTrue( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 76, firstResponderIsBrowser: true, flags: [] ) ) } func testDoesNotRouteForNonEnterKey() { XCTAssertFalse( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 13, firstResponderIsBrowser: true, flags: [] ) ) } func testDoesNotRouteWhenFirstResponderIsNotBrowser() { XCTAssertFalse( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 36, firstResponderIsBrowser: false, flags: [] ) ) } func testRoutesForShiftReturnWhenBrowserFirstResponder() { XCTAssertTrue( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 36, firstResponderIsBrowser: true, flags: [.shift] ) ) } func testDoesNotRouteForCommandShiftReturnWhenBrowserFirstResponder() { XCTAssertFalse( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 36, firstResponderIsBrowser: true, flags: [.command, .shift] ) ) } func testDoesNotRouteForCommandReturnWhenBrowserFirstResponder() { XCTAssertFalse( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 36, firstResponderIsBrowser: true, flags: [.command] ) ) } func testDoesNotRouteForOptionReturnWhenBrowserFirstResponder() { XCTAssertFalse( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 36, firstResponderIsBrowser: true, flags: [.option] ) ) } func testDoesNotRouteForControlReturnWhenBrowserFirstResponder() { XCTAssertFalse( shouldDispatchBrowserReturnViaFirstResponderKeyDown( keyCode: 36, firstResponderIsBrowser: true, flags: [.control] ) ) } } final class BrowserZoomShortcutActionTests: XCTestCase { func testZoomInSupportsEqualsAndPlusVariants() { XCTAssertEqual( browserZoomShortcutAction(flags: [.command], chars: "=", keyCode: 24), .zoomIn ) XCTAssertEqual( browserZoomShortcutAction(flags: [.command], chars: "+", keyCode: 24), .zoomIn ) XCTAssertEqual( browserZoomShortcutAction(flags: [.command, .shift], chars: "+", keyCode: 24), .zoomIn ) XCTAssertEqual( browserZoomShortcutAction(flags: [.command], chars: "+", keyCode: 30), .zoomIn ) } func testZoomOutSupportsMinusAndUnderscoreVariants() { XCTAssertEqual( browserZoomShortcutAction(flags: [.command], chars: "-", keyCode: 27), .zoomOut ) XCTAssertEqual( browserZoomShortcutAction(flags: [.command, .shift], chars: "_", keyCode: 27), .zoomOut ) } func testZoomInSupportsShiftedLiteralFromDifferentPhysicalKey() { XCTAssertEqual( browserZoomShortcutAction( flags: [.command, .shift], chars: ";", keyCode: 41, literalChars: "+" ), .zoomIn ) XCTAssertNil( browserZoomShortcutAction( flags: [.command, .shift], chars: ";", keyCode: 41 ) ) } func testZoomRequiresCommandWithoutOptionOrControl() { XCTAssertNil(browserZoomShortcutAction(flags: [], chars: "=", keyCode: 24)) XCTAssertNil(browserZoomShortcutAction(flags: [.command, .option], chars: "=", keyCode: 24)) XCTAssertNil(browserZoomShortcutAction(flags: [.command, .control], chars: "-", keyCode: 27)) } func testResetSupportsCommandZero() { XCTAssertEqual( browserZoomShortcutAction(flags: [.command], chars: "0", keyCode: 29), .reset ) } } final class BrowserZoomShortcutRoutingPolicyTests: XCTestCase { func testRoutesWhenGhosttyIsFirstResponderAndShortcutIsZoom() { XCTAssertTrue( shouldRouteTerminalFontZoomShortcutToGhostty( firstResponderIsGhostty: true, flags: [.command], chars: "=", keyCode: 24 ) ) XCTAssertTrue( shouldRouteTerminalFontZoomShortcutToGhostty( firstResponderIsGhostty: true, flags: [.command], chars: "-", keyCode: 27 ) ) XCTAssertTrue( shouldRouteTerminalFontZoomShortcutToGhostty( firstResponderIsGhostty: true, flags: [.command], chars: "0", keyCode: 29 ) ) } func testDoesNotRouteWhenFirstResponderIsNotGhostty() { XCTAssertFalse( shouldRouteTerminalFontZoomShortcutToGhostty( firstResponderIsGhostty: false, flags: [.command], chars: "=", keyCode: 24 ) ) } func testDoesNotRouteForNonZoomShortcuts() { XCTAssertFalse( shouldRouteTerminalFontZoomShortcutToGhostty( firstResponderIsGhostty: true, flags: [.command], chars: "n", keyCode: 45 ) ) } func testRoutesForShiftedLiteralZoomShortcut() { XCTAssertTrue( shouldRouteTerminalFontZoomShortcutToGhostty( firstResponderIsGhostty: true, flags: [.command, .shift], chars: ";", keyCode: 41, literalChars: "+" ) ) } } final class BrowserSearchEngineTests: XCTestCase { func testGoogleSearchURL() throws { let url = try XCTUnwrap(BrowserSearchEngine.google.searchURL(query: "hello world")) XCTAssertEqual(url.host, "www.google.com") XCTAssertEqual(url.path, "/search") XCTAssertTrue(url.absoluteString.contains("q=hello%20world")) } func testDuckDuckGoSearchURL() throws { let url = try XCTUnwrap(BrowserSearchEngine.duckduckgo.searchURL(query: "hello world")) XCTAssertEqual(url.host, "duckduckgo.com") XCTAssertEqual(url.path, "/") XCTAssertTrue(url.absoluteString.contains("q=hello%20world")) } func testBingSearchURL() throws { let url = try XCTUnwrap(BrowserSearchEngine.bing.searchURL(query: "hello world")) XCTAssertEqual(url.host, "www.bing.com") XCTAssertEqual(url.path, "/search") XCTAssertTrue(url.absoluteString.contains("q=hello%20world")) } } final class BrowserSearchSettingsTests: XCTestCase { func testCurrentSearchSuggestionsEnabledDefaultsToTrueWhenUnset() { let suiteName = "BrowserSearchSettingsTests.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.removeObject(forKey: BrowserSearchSettings.searchSuggestionsEnabledKey) XCTAssertTrue(BrowserSearchSettings.currentSearchSuggestionsEnabled(defaults: defaults)) } func testCurrentSearchSuggestionsEnabledHonorsExplicitValue() { let suiteName = "BrowserSearchSettingsTests.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(false, forKey: BrowserSearchSettings.searchSuggestionsEnabledKey) XCTAssertFalse(BrowserSearchSettings.currentSearchSuggestionsEnabled(defaults: defaults)) defaults.set(true, forKey: BrowserSearchSettings.searchSuggestionsEnabledKey) XCTAssertTrue(BrowserSearchSettings.currentSearchSuggestionsEnabled(defaults: defaults)) } } final class BrowserHistoryStoreTests: XCTestCase { func testRecordVisitDedupesAndSuggests() async throws { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent("BrowserHistoryStoreTests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } let fileURL = tempDir.appendingPathComponent("browser_history.json") let store = await MainActor.run { BrowserHistoryStore(fileURL: fileURL) } let u1 = try XCTUnwrap(URL(string: "https://example.com/foo")) let u2 = try XCTUnwrap(URL(string: "https://example.com/bar")) await MainActor.run { store.recordVisit(url: u1, title: "Example Foo") store.recordVisit(url: u2, title: "Example Bar") store.recordVisit(url: u1, title: "Example Foo Updated") } let suggestions = await MainActor.run { store.suggestions(for: "foo", limit: 10) } XCTAssertEqual(suggestions.first?.url, "https://example.com/foo") XCTAssertEqual(suggestions.first?.visitCount, 2) XCTAssertEqual(suggestions.first?.title, "Example Foo Updated") } func testSuggestionsLoadsPersistedHistoryImmediatelyOnFirstQuery() async throws { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent("BrowserHistoryStoreTests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } let fileURL = tempDir.appendingPathComponent("browser_history.json") let now = Date() let seededEntries = [ BrowserHistoryStore.Entry( id: UUID(), url: "https://go.dev/", title: "The Go Programming Language", lastVisited: now, visitCount: 3 ), BrowserHistoryStore.Entry( id: UUID(), url: "https://www.google.com/", title: "Google", lastVisited: now.addingTimeInterval(-120), visitCount: 2 ), ] let encoder = JSONEncoder() encoder.outputFormatting = [.withoutEscapingSlashes] let data = try encoder.encode(seededEntries) try data.write(to: fileURL, options: [.atomic]) let store = await MainActor.run { BrowserHistoryStore(fileURL: fileURL) } let suggestions = await MainActor.run { store.suggestions(for: "go", limit: 10) } XCTAssertGreaterThanOrEqual(suggestions.count, 2) XCTAssertEqual(suggestions.first?.url, "https://go.dev/") XCTAssertTrue(suggestions.contains(where: { $0.url == "https://www.google.com/" })) } } @MainActor final class CmuxWebViewDragRoutingTests: XCTestCase { func testRejectsInternalPaneDragEvenWhenFilePromiseTypesArePresent() { XCTAssertTrue( CmuxWebView.shouldRejectInternalPaneDrag([ DragOverlayRoutingPolicy.bonsplitTabTransferType, NSPasteboard.PasteboardType("com.apple.pasteboard.promised-file-url"), ]) ) } func testAllowsRegularExternalFileDrops() { XCTAssertFalse(CmuxWebView.shouldRejectInternalPaneDrag([.fileURL])) } } #if compiler(>=6.2) @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") ) } final class BrowserLinkOpenSettingsTests: XCTestCase { private var suiteName: String! private var defaults: UserDefaults! override func setUp() { super.setUp() suiteName = "BrowserLinkOpenSettingsTests.\(UUID().uuidString)" defaults = UserDefaults(suiteName: suiteName) defaults.removePersistentDomain(forName: suiteName) } override func tearDown() { defaults.removePersistentDomain(forName: suiteName) defaults = nil suiteName = nil super.tearDown() } func testTerminalLinksDefaultToCmuxBrowser() { XCTAssertTrue(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser(defaults: defaults)) } func testTerminalLinksPreferenceUsesStoredValue() { defaults.set(false, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) XCTAssertFalse(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser(defaults: defaults)) defaults.set(true, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) XCTAssertTrue(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser(defaults: defaults)) } func testSidebarPullRequestLinksDefaultToCmuxBrowser() { XCTAssertTrue(BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowser(defaults: defaults)) } func testSidebarPullRequestLinksPreferenceUsesStoredValue() { defaults.set(false, forKey: BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowserKey) XCTAssertFalse(BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowser(defaults: defaults)) defaults.set(true, forKey: BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowserKey) XCTAssertTrue(BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowser(defaults: defaults)) } func testOpenCommandInterceptionDefaultsToCmuxBrowser() { XCTAssertTrue(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults)) } func testOpenCommandInterceptionUsesStoredValue() { defaults.set(false, forKey: BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey) XCTAssertFalse(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults)) defaults.set(true, forKey: BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey) XCTAssertTrue(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults)) } func testOpenCommandInterceptionFallsBackToLegacyLinkToggleWhenUnset() { defaults.set(false, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) XCTAssertFalse(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults)) defaults.set(true, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) XCTAssertTrue(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults)) } func testSettingsInitialOpenCommandInterceptionValueFallsBackToLegacyLinkToggleWhenUnset() { defaults.set(false, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) XCTAssertFalse(BrowserLinkOpenSettings.initialInterceptTerminalOpenCommandInCmuxBrowserValue(defaults: defaults)) defaults.set(true, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) XCTAssertTrue(BrowserLinkOpenSettings.initialInterceptTerminalOpenCommandInCmuxBrowserValue(defaults: defaults)) } func testExternalOpenPatternsDefaultToEmpty() { XCTAssertTrue(BrowserLinkOpenSettings.externalOpenPatterns(defaults: defaults).isEmpty) } func testExternalOpenLiteralPatternMatchesCaseInsensitively() { defaults.set("openai.com/account/usage", forKey: BrowserLinkOpenSettings.browserExternalOpenPatternsKey) XCTAssertTrue( BrowserLinkOpenSettings.shouldOpenExternally( "https://platform.OPENAI.com/account/usage", defaults: defaults ) ) } func testExternalOpenRegexPatternMatchesCaseInsensitively() { defaults.set( "re:^https?://[^/]*\\.example\\.com/(billing|usage)", forKey: BrowserLinkOpenSettings.browserExternalOpenPatternsKey ) XCTAssertTrue( BrowserLinkOpenSettings.shouldOpenExternally( "https://FOO.example.com/BILLING", defaults: defaults ) ) } func testExternalOpenRegexPatternSupportsDigitCharacterClass() { defaults.set( "re:^https://example\\.com/usage/\\d+$", forKey: BrowserLinkOpenSettings.browserExternalOpenPatternsKey ) XCTAssertTrue( BrowserLinkOpenSettings.shouldOpenExternally( "https://example.com/usage/42", defaults: defaults ) ) } func testExternalOpenPatternsIgnoreInvalidRegexEntries() { defaults.set("re:(\nexample.com", forKey: BrowserLinkOpenSettings.browserExternalOpenPatternsKey) XCTAssertTrue( BrowserLinkOpenSettings.shouldOpenExternally( "https://example.com/path", defaults: defaults ) ) } } final class BrowserNavigableURLResolutionTests: XCTestCase { func testResolvesFileSchemeAsNavigableURL() throws { let resolved = try XCTUnwrap(resolveBrowserNavigableURL("file:///tmp/cmux-local-test.html")) XCTAssertTrue(resolved.isFileURL) XCTAssertEqual(resolved.path, "/tmp/cmux-local-test.html") } func testRejectsNonWebNonFileScheme() { XCTAssertNil(resolveBrowserNavigableURL("mailto:test@example.com")) XCTAssertNil(resolveBrowserNavigableURL("ftp://example.com/file.html")) } func testRejectsHostOnlyFileURL() { XCTAssertNil(resolveBrowserNavigableURL("file://example.html")) } } final class BrowserReadAccessURLTests: XCTestCase { func testUsesParentDirectoryForFileURL() throws { let tempRoot = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let dir = tempRoot.appendingPathComponent("BrowserReadAccessURLTests-\(UUID().uuidString)", isDirectory: true) let file = dir.appendingPathComponent("sample.html") try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: dir) } try "".write(to: file, atomically: true, encoding: .utf8) let readAccessURL = try XCTUnwrap(browserReadAccessURL(forLocalFileURL: file)) XCTAssertEqual(readAccessURL.standardizedFileURL, dir.standardizedFileURL) } func testUsesDirectoryURLWhenTargetIsDirectory() throws { let tempRoot = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let dir = tempRoot.appendingPathComponent("BrowserReadAccessURLTests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: dir) } let readAccessURL = try XCTUnwrap(browserReadAccessURL(forLocalFileURL: dir)) XCTAssertEqual(readAccessURL.standardizedFileURL, dir.standardizedFileURL) } func testUsesParentDirectoryWhenFileDoesNotExist() throws { let missing = URL(fileURLWithPath: "/tmp/\(UUID().uuidString).html") let readAccessURL = try XCTUnwrap(browserReadAccessURL(forLocalFileURL: missing)) XCTAssertEqual(readAccessURL.standardizedFileURL, missing.deletingLastPathComponent().standardizedFileURL) } func testReturnsNilForHostOnlyFileURL() throws { let hostOnly = try XCTUnwrap(URL(string: "file://example.html")) XCTAssertNil(browserReadAccessURL(forLocalFileURL: hostOnly)) } } final class BrowserExternalNavigationSchemeTests: XCTestCase { func testCustomAppSchemesOpenExternally() throws { let discord = try XCTUnwrap(URL(string: "discord://login/one-time?token=abc")) let slack = try XCTUnwrap(URL(string: "slack://open")) let zoom = try XCTUnwrap(URL(string: "zoommtg://zoom.us/join")) let mailto = try XCTUnwrap(URL(string: "mailto:test@example.com")) XCTAssertTrue(browserShouldOpenURLExternally(discord)) XCTAssertTrue(browserShouldOpenURLExternally(slack)) XCTAssertTrue(browserShouldOpenURLExternally(zoom)) XCTAssertTrue(browserShouldOpenURLExternally(mailto)) } func testEmbeddedBrowserSchemesStayInWebView() throws { let https = try XCTUnwrap(URL(string: "https://example.com")) let http = try XCTUnwrap(URL(string: "http://example.com")) let about = try XCTUnwrap(URL(string: "about:blank")) let data = try XCTUnwrap(URL(string: "data:text/plain,hello")) let file = try XCTUnwrap(URL(string: "file:///tmp/cmux-local-test.html")) let blob = try XCTUnwrap(URL(string: "blob:https://example.com/550e8400-e29b-41d4-a716-446655440000")) let javascript = try XCTUnwrap(URL(string: "javascript:void(0)")) let webkitInternal = try XCTUnwrap(URL(string: "applewebdata://local/page")) XCTAssertFalse(browserShouldOpenURLExternally(https)) XCTAssertFalse(browserShouldOpenURLExternally(http)) XCTAssertFalse(browserShouldOpenURLExternally(about)) XCTAssertFalse(browserShouldOpenURLExternally(data)) XCTAssertFalse(browserShouldOpenURLExternally(file)) XCTAssertFalse(browserShouldOpenURLExternally(blob)) XCTAssertFalse(browserShouldOpenURLExternally(javascript)) XCTAssertFalse(browserShouldOpenURLExternally(webkitInternal)) } } final class BrowserHostWhitelistTests: XCTestCase { private var suiteName: String! private var defaults: UserDefaults! override func setUp() { super.setUp() suiteName = "BrowserHostWhitelistTests.\(UUID().uuidString)" defaults = UserDefaults(suiteName: suiteName) defaults.removePersistentDomain(forName: suiteName) } override func tearDown() { defaults.removePersistentDomain(forName: suiteName) defaults = nil suiteName = nil super.tearDown() } func testEmptyWhitelistAllowsAll() { XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) } func testExactMatch() { defaults.set("localhost\n127.0.0.1", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("127.0.0.1", defaults: defaults)) XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) } func testExactMatchIsCaseInsensitive() { defaults.set("LocalHost", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("LOCALHOST", defaults: defaults)) } func testWildcardSuffix() { defaults.set("*.localtest.me", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("app.localtest.me", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("sub.app.localtest.me", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localtest.me", defaults: defaults)) XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) } func testWildcardIsCaseInsensitive() { defaults.set("*.Example.COM", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("sub.example.com", defaults: defaults)) } func testBlankLinesAndWhitespaceIgnored() { defaults.set(" localhost \n\n 127.0.0.1 \n", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("127.0.0.1", defaults: defaults)) XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) } func testMixedExactAndWildcard() { defaults.set("localhost\n127.0.0.1\n*.local.dev", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("127.0.0.1", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("app.local.dev", defaults: defaults)) XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("github.com", defaults: defaults)) } func testDefaultWhitelistIsEmpty() { let patterns = BrowserLinkOpenSettings.hostWhitelist(defaults: defaults) XCTAssertTrue(patterns.isEmpty) } func testWildcardRequiresDotBoundary() { defaults.set("*.example.com", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("badexample.com", defaults: defaults)) XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com.evil", defaults: defaults)) } func testWhitelistNormalizesSchemesPortsAndTrailingDots() { defaults.set("https://LOCALHOST:3000/path\n*.Example.COM:443", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("localhost.", defaults: defaults)) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("api.example.com", defaults: defaults)) } func testInvalidWhitelistEntriesDoNotImplicitlyAllowAll() { defaults.set("http://\n*.\n", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertFalse(BrowserLinkOpenSettings.hostMatchesWhitelist("example.com", defaults: defaults)) } func testUnicodeWhitelistEntryMatchesPunycodeHost() { defaults.set("b\u{00FC}cher.example", forKey: BrowserLinkOpenSettings.browserHostWhitelistKey) XCTAssertTrue(BrowserLinkOpenSettings.hostMatchesWhitelist("xn--bcher-kva.example", defaults: defaults)) } } final class BrowserOmnibarFocusPolicyTests: XCTestCase { func testReacquiresFocusWhenOmnibarStillWantsFocusAndNextResponderIsNotAnotherTextField() { XCTAssertTrue( browserOmnibarShouldReacquireFocusAfterEndEditing( desiredOmnibarFocus: true, nextResponderIsOtherTextField: false ) ) } func testDoesNotReacquireFocusWhenAnotherTextFieldAlreadyTookFocus() { XCTAssertFalse( browserOmnibarShouldReacquireFocusAfterEndEditing( desiredOmnibarFocus: true, nextResponderIsOtherTextField: true ) ) } func testDoesNotReacquireFocusWhenOmnibarNoLongerWantsFocus() { XCTAssertFalse( browserOmnibarShouldReacquireFocusAfterEndEditing( desiredOmnibarFocus: false, nextResponderIsOtherTextField: false ) ) } }