import XCTest import AppKit import WebKit import ObjectiveC.runtime #if canImport(cmux_DEV) @testable import cmux_DEV #elseif canImport(cmux) @testable import cmux #endif private var cmuxUnitTestInspectorAssociationKey: UInt8 = 0 private var cmuxUnitTestInspectorOverrideInstalled = false private extension CmuxWebView { @objc func cmuxUnitTestInspector() -> NSObject? { objc_getAssociatedObject(self, &cmuxUnitTestInspectorAssociationKey) as? NSObject } } private extension WKWebView { func cmuxSetUnitTestInspector(_ inspector: NSObject?) { objc_setAssociatedObject( self, &cmuxUnitTestInspectorAssociationKey, inspector, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } } private 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 } } 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) } private func installMenu(spy: ActionSpy, 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: #selector(ActionSpy.didInvoke(_:)), keyEquivalent: key) item.keyEquivalentModifierMask = modifiers item.target = spy 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) -> NSEvent? { NSEvent.keyEvent( with: .keyDown, location: .zero, modifierFlags: modifiers, timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: 0, context: nil, characters: key, charactersIgnoringModifiers: key, isARepeat: false, keyCode: keyCode ) } } 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 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 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) } } } @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 BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase { private final class FakeInspector: NSObject { private(set) var showCount = 0 private(set) var closeCount = 0 private var visible = false @objc func isVisible() -> Bool { visible } @objc func show() { showCount += 1 visible = true } @objc func close() { closeCount += 1 visible = false } } override class func setUp() { super.setUp() installCmuxUnitTestInspectorOverride() } private func makePanelWithInspector() -> (BrowserPanel, FakeInspector) { let panel = BrowserPanel(workspaceId: UUID()) let inspector = FakeInspector() panel.webView.cmuxSetUnitTestInspector(inspector) return (panel, inspector) } 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 testTransientHideAttachmentPreserveFollowsDeveloperToolsIntent() { let (panel, _) = makePanelWithInspector() XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) XCTAssertTrue(panel.showDeveloperTools()) XCTAssertTrue(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) XCTAssertTrue(panel.hideDeveloperTools()) XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) } func testWebViewDismantleSkipsDetachWhenDeveloperToolsIntentIsVisible() { let (panel, _) = makePanelWithInspector() XCTAssertTrue(panel.showDeveloperTools()) let representable = WebViewRepresentable( panel: panel, shouldAttachWebView: true, shouldFocusWebView: false, isPanelFocused: true, portalZPriority: 0 ) let coordinator = representable.makeCoordinator() coordinator.webView = panel.webView let host = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: 100)) host.addSubview(panel.webView) WebViewRepresentable.dismantleNSView(host, coordinator: coordinator) XCTAssertTrue(panel.webView.superview === host) } func testWebViewDismantleDetachesWhenDeveloperToolsIntentIsHidden() { let (panel, _) = makePanelWithInspector() XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide()) let representable = WebViewRepresentable( panel: panel, shouldAttachWebView: true, shouldFocusWebView: false, isPanelFocused: true, portalZPriority: 0 ) let coordinator = representable.makeCoordinator() coordinator.webView = panel.webView let host = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: 100)) host.addSubview(panel.webView) WebViewRepresentable.dismantleNSView(host, coordinator: coordinator) XCTAssertNil(panel.webView.superview) } } final class WorkspaceShortcutMapperTests: XCTestCase { func testCommandNineMapsToLastWorkspaceIndex() { XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: 9, workspaceCount: 1), 0) XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: 9, workspaceCount: 4), 3) XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: 9, workspaceCount: 12), 11) } func testCommandDigitBadgesUseNineForLastWorkspaceWhenNeeded() { XCTAssertEqual(WorkspaceShortcutMapper.commandDigitForWorkspace(at: 0, workspaceCount: 12), 1) XCTAssertEqual(WorkspaceShortcutMapper.commandDigitForWorkspace(at: 7, workspaceCount: 12), 8) XCTAssertEqual(WorkspaceShortcutMapper.commandDigitForWorkspace(at: 11, workspaceCount: 12), 9) XCTAssertNil(WorkspaceShortcutMapper.commandDigitForWorkspace(at: 8, workspaceCount: 12)) } } 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 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 ) } } final class SidebarCommandHintPolicyTests: XCTestCase { func testCommandHintRequiresCommandOnlyModifier() { XCTAssertTrue(SidebarCommandHintPolicy.shouldShowHints(for: [.command])) XCTAssertFalse(SidebarCommandHintPolicy.shouldShowHints(for: [])) XCTAssertFalse(SidebarCommandHintPolicy.shouldShowHints(for: [.command, .shift])) XCTAssertFalse(SidebarCommandHintPolicy.shouldShowHints(for: [.command, .option])) XCTAssertFalse(SidebarCommandHintPolicy.shouldShowHints(for: [.command, .control])) } func testCommandHintUsesIntentionalHoldDelay() { XCTAssertGreaterThanOrEqual(SidebarCommandHintPolicy.intentionalHoldDelay, 0.25) } func testCurrentWindowRequiresHostWindowToBeKeyAndMatchEventWindow() { XCTAssertTrue( SidebarCommandHintPolicy.isCurrentWindow( hostWindowNumber: 42, hostWindowIsKey: true, eventWindowNumber: 42, keyWindowNumber: 42 ) ) XCTAssertFalse( SidebarCommandHintPolicy.isCurrentWindow( hostWindowNumber: 42, hostWindowIsKey: true, eventWindowNumber: 7, keyWindowNumber: 42 ) ) XCTAssertFalse( SidebarCommandHintPolicy.isCurrentWindow( hostWindowNumber: 42, hostWindowIsKey: false, eventWindowNumber: 42, keyWindowNumber: 42 ) ) } func testWindowScopedCommandHintsUseKeyWindowWhenNoEventWindowIsAvailable() { XCTAssertTrue( SidebarCommandHintPolicy.shouldShowHints( for: [.command], hostWindowNumber: 42, hostWindowIsKey: true, eventWindowNumber: nil, keyWindowNumber: 42 ) ) XCTAssertFalse( SidebarCommandHintPolicy.shouldShowHints( for: [.command], hostWindowNumber: 42, hostWindowIsKey: true, eventWindowNumber: nil, keyWindowNumber: 7 ) ) } } final class ShortcutHintDebugSettingsTests: XCTestCase { func testClampKeepsValuesWithinSupportedRange() { XCTAssertEqual(ShortcutHintDebugSettings.clamped(0.0), 0.0) XCTAssertEqual(ShortcutHintDebugSettings.clamped(4.0), 4.0) XCTAssertEqual(ShortcutHintDebugSettings.clamped(-100.0), ShortcutHintDebugSettings.offsetRange.lowerBound) XCTAssertEqual(ShortcutHintDebugSettings.clamped(100.0), ShortcutHintDebugSettings.offsetRange.upperBound) } func testDefaultOffsetsMatchCurrentBadgePlacements() { XCTAssertEqual(ShortcutHintDebugSettings.defaultSidebarHintX, 0.0) XCTAssertEqual(ShortcutHintDebugSettings.defaultSidebarHintY, 0.0) XCTAssertEqual(ShortcutHintDebugSettings.defaultTitlebarHintX, 4.0) XCTAssertEqual(ShortcutHintDebugSettings.defaultTitlebarHintY, 0.0) XCTAssertEqual(ShortcutHintDebugSettings.defaultPaneHintX, 0.0) XCTAssertEqual(ShortcutHintDebugSettings.defaultPaneHintY, 0.0) XCTAssertFalse(ShortcutHintDebugSettings.defaultAlwaysShowHints) } } final class ShortcutHintLanePlannerTests: XCTestCase { func testAssignLanesKeepsSeparatedIntervalsOnSingleLane() { let intervals: [ClosedRange] = [0...20, 28...40, 48...64] XCTAssertEqual(ShortcutHintLanePlanner.assignLanes(for: intervals, minSpacing: 4), [0, 0, 0]) } func testAssignLanesStacksOverlappingIntervalsIntoAdditionalLanes() { let intervals: [ClosedRange] = [0...20, 18...34, 22...38, 40...56] XCTAssertEqual(ShortcutHintLanePlanner.assignLanes(for: intervals, minSpacing: 4), [0, 1, 2, 0]) } } final class ShortcutHintHorizontalPlannerTests: XCTestCase { func testAssignRightEdgesResolvesOverlapWithMinimumSpacing() { let intervals: [ClosedRange] = [0...20, 18...34, 30...46] let rightEdges = ShortcutHintHorizontalPlanner.assignRightEdges(for: intervals, minSpacing: 6) XCTAssertEqual(rightEdges.count, intervals.count) let adjustedIntervals = zip(intervals, rightEdges).map { interval, rightEdge in let width = interval.upperBound - interval.lowerBound return (rightEdge - width)...rightEdge } XCTAssertGreaterThanOrEqual(adjustedIntervals[1].lowerBound - adjustedIntervals[0].upperBound, 6) XCTAssertGreaterThanOrEqual(adjustedIntervals[2].lowerBound - adjustedIntervals[1].upperBound, 6) } func testAssignRightEdgesKeepsAlreadySeparatedIntervalsInPlace() { let intervals: [ClosedRange] = [0...12, 20...32, 40...52] let rightEdges = ShortcutHintHorizontalPlanner.assignRightEdges(for: intervals, minSpacing: 4) XCTAssertEqual(rightEdges, [12, 32, 52]) } } final class WorkspacePlacementSettingsTests: XCTestCase { func testCurrentPlacementDefaultsToAfterCurrentWhenUnset() { let suiteName = "WorkspacePlacementSettingsTests.Default.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } XCTAssertEqual(WorkspacePlacementSettings.current(defaults: defaults), .afterCurrent) } func testCurrentPlacementReadsStoredValidValueAndFallsBackForInvalid() { let suiteName = "WorkspacePlacementSettingsTests.Stored.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(NewWorkspacePlacement.top.rawValue, forKey: WorkspacePlacementSettings.placementKey) XCTAssertEqual(WorkspacePlacementSettings.current(defaults: defaults), .top) defaults.set("nope", forKey: WorkspacePlacementSettings.placementKey) XCTAssertEqual(WorkspacePlacementSettings.current(defaults: defaults), .afterCurrent) } func testInsertionIndexTopInsertsBeforeUnpinned() { let index = WorkspacePlacementSettings.insertionIndex( placement: .top, selectedIndex: 4, selectedIsPinned: false, pinnedCount: 2, totalCount: 7 ) XCTAssertEqual(index, 2) } func testInsertionIndexAfterCurrentHandlesPinnedAndUnpinnedSelection() { let afterUnpinned = WorkspacePlacementSettings.insertionIndex( placement: .afterCurrent, selectedIndex: 3, selectedIsPinned: false, pinnedCount: 2, totalCount: 6 ) XCTAssertEqual(afterUnpinned, 4) let afterPinned = WorkspacePlacementSettings.insertionIndex( placement: .afterCurrent, selectedIndex: 0, selectedIsPinned: true, pinnedCount: 2, totalCount: 6 ) XCTAssertEqual(afterPinned, 2) } func testInsertionIndexEndAndNoSelectionAppend() { let endIndex = WorkspacePlacementSettings.insertionIndex( placement: .end, selectedIndex: 1, selectedIsPinned: false, pinnedCount: 1, totalCount: 5 ) XCTAssertEqual(endIndex, 5) let noSelectionIndex = WorkspacePlacementSettings.insertionIndex( placement: .afterCurrent, selectedIndex: nil, selectedIsPinned: false, pinnedCount: 0, totalCount: 5 ) XCTAssertEqual(noSelectionIndex, 5) } } final class WorkspaceAutoReorderSettingsTests: XCTestCase { func testDefaultIsEnabled() { let suiteName = "WorkspaceAutoReorderSettingsTests.Default.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } XCTAssertTrue(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults)) } func testDisabledWhenSetToFalse() { let suiteName = "WorkspaceAutoReorderSettingsTests.Disabled.\(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: WorkspaceAutoReorderSettings.key) XCTAssertFalse(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults)) } func testEnabledWhenSetToTrue() { let suiteName = "WorkspaceAutoReorderSettingsTests.Enabled.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(true, forKey: WorkspaceAutoReorderSettings.key) XCTAssertTrue(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults)) } } final class AppearanceSettingsTests: XCTestCase { func testResolvedModeDefaultsToSystemWhenUnset() { let suiteName = "AppearanceSettingsTests.Default.\(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: AppearanceSettings.appearanceModeKey) let resolved = AppearanceSettings.resolvedMode(defaults: defaults) XCTAssertEqual(resolved, .system) XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.system.rawValue) } } final class UpdateChannelSettingsTests: XCTestCase { func testDefaultNightlyPreferenceIsDisabled() { XCTAssertFalse(UpdateChannelSettings.defaultIncludeNightlyBuilds) } func testResolvedFeedFallsBackToStableWhenInfoFeedMissing() { let suiteName = "UpdateChannelSettingsTests.MissingInfo.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } let resolved = UpdateChannelSettings.resolvedFeedURLString(infoFeedURL: nil, defaults: defaults) XCTAssertEqual(resolved.url, UpdateChannelSettings.stableFeedURL) XCTAssertFalse(resolved.isNightly) XCTAssertTrue(resolved.usedFallback) } func testResolvedFeedUsesInfoFeedForStableChannel() { let suiteName = "UpdateChannelSettingsTests.InfoFeed.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } let infoFeed = "https://example.com/custom/appcast.xml" let resolved = UpdateChannelSettings.resolvedFeedURLString(infoFeedURL: infoFeed, defaults: defaults) XCTAssertEqual(resolved.url, infoFeed) XCTAssertFalse(resolved.isNightly) XCTAssertFalse(resolved.usedFallback) } func testResolvedFeedUsesNightlyWhenPreferenceEnabled() { let suiteName = "UpdateChannelSettingsTests.Nightly.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(true, forKey: UpdateChannelSettings.includeNightlyBuildsKey) let resolved = UpdateChannelSettings.resolvedFeedURLString( infoFeedURL: "https://example.com/custom/appcast.xml", defaults: defaults ) XCTAssertEqual(resolved.url, UpdateChannelSettings.nightlyFeedURL) XCTAssertTrue(resolved.isNightly) XCTAssertFalse(resolved.usedFallback) } } final class WorkspaceReorderTests: XCTestCase { @MainActor func testReorderWorkspaceMovesWorkspaceToRequestedIndex() { let manager = TabManager() let first = manager.tabs[0] let second = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(second) XCTAssertEqual(manager.selectedTabId, second.id) XCTAssertTrue(manager.reorderWorkspace(tabId: second.id, toIndex: 0)) XCTAssertEqual(manager.tabs.map(\.id), [second.id, first.id, third.id]) XCTAssertEqual(manager.selectedTabId, second.id) } @MainActor func testReorderWorkspaceClampsOutOfRangeTargetIndex() { let manager = TabManager() let first = manager.tabs[0] let second = manager.addWorkspace() let third = manager.addWorkspace() XCTAssertTrue(manager.reorderWorkspace(tabId: first.id, toIndex: 999)) XCTAssertEqual(manager.tabs.map(\.id), [second.id, third.id, first.id]) } @MainActor func testReorderWorkspaceReturnsFalseForUnknownWorkspace() { let manager = TabManager() XCTAssertFalse(manager.reorderWorkspace(tabId: UUID(), toIndex: 0)) } } @MainActor final class TabManagerPendingUnfocusPolicyTests: XCTestCase { func testDoesNotUnfocusWhenPendingTabIsCurrentlySelected() { let tabId = UUID() XCTAssertFalse( TabManager.shouldUnfocusPendingWorkspace( pendingTabId: tabId, selectedTabId: tabId ) ) } func testUnfocusesWhenPendingTabIsNotSelected() { XCTAssertTrue( TabManager.shouldUnfocusPendingWorkspace( pendingTabId: UUID(), selectedTabId: UUID() ) ) XCTAssertTrue( TabManager.shouldUnfocusPendingWorkspace( pendingTabId: UUID(), selectedTabId: nil ) ) } } @MainActor final class TabManagerSurfaceCreationTests: XCTestCase { func testNewSurfaceFocusesCreatedSurface() { let manager = TabManager() guard let workspace = manager.selectedWorkspace else { XCTFail("Expected a selected workspace") return } let beforePanels = Set(workspace.panels.keys) manager.newSurface() let afterPanels = Set(workspace.panels.keys) let createdPanels = afterPanels.subtracting(beforePanels) XCTAssertEqual(createdPanels.count, 1, "Expected one new surface for Cmd+T path") guard let createdPanelId = createdPanels.first else { return } XCTAssertEqual( workspace.focusedPanelId, createdPanelId, "Expected newly created surface to be focused" ) } func testOpenBrowserInsertAtEndPlacesNewBrowserAtPaneEnd() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let paneId = workspace.bonsplitController.focusedPaneId else { XCTFail("Expected focused workspace and pane") return } // Add one extra surface so we verify append-to-end rather than first insert behavior. _ = workspace.newTerminalSurface(inPane: paneId, focus: false) guard let browserPanelId = manager.openBrowser(insertAtEnd: true) else { XCTFail("Expected browser panel to be created") return } let tabs = workspace.bonsplitController.tabs(inPane: paneId) guard let lastSurfaceId = tabs.last?.id else { XCTFail("Expected at least one surface in pane") return } XCTAssertEqual( workspace.panelIdFromSurfaceId(lastSurfaceId), browserPanelId, "Expected Cmd+Shift+B/Cmd+L open path to append browser surface at end" ) XCTAssertEqual(workspace.focusedPanelId, browserPanelId, "Expected opened browser surface to be focused") } } @MainActor final class BrowserPanelAddressBarFocusRequestTests: XCTestCase { func testRequestPersistsUntilAcknowledged() { let panel = BrowserPanel(workspaceId: UUID()) XCTAssertNil(panel.pendingAddressBarFocusRequestId) let requestId = panel.requestAddressBarFocus() XCTAssertEqual(panel.pendingAddressBarFocusRequestId, requestId) XCTAssertTrue(panel.shouldSuppressWebViewFocus()) panel.acknowledgeAddressBarFocusRequest(requestId) XCTAssertNil(panel.pendingAddressBarFocusRequestId) // Acknowledgement only clears the durable request; focus suppression follows // explicit blur state transitions. XCTAssertTrue(panel.shouldSuppressWebViewFocus()) panel.endSuppressWebViewFocusForAddressBar() XCTAssertFalse(panel.shouldSuppressWebViewFocus()) } func testRequestCoalescesWhilePending() { let panel = BrowserPanel(workspaceId: UUID()) let firstRequest = panel.requestAddressBarFocus() let secondRequest = panel.requestAddressBarFocus() XCTAssertEqual(firstRequest, secondRequest) XCTAssertEqual(panel.pendingAddressBarFocusRequestId, firstRequest) } func testStaleAcknowledgementDoesNotClearNewestRequest() { let panel = BrowserPanel(workspaceId: UUID()) let firstRequest = panel.requestAddressBarFocus() panel.acknowledgeAddressBarFocusRequest(firstRequest) let secondRequest = panel.requestAddressBarFocus() XCTAssertNotEqual(firstRequest, secondRequest) XCTAssertEqual(panel.pendingAddressBarFocusRequestId, secondRequest) panel.acknowledgeAddressBarFocusRequest(firstRequest) XCTAssertEqual(panel.pendingAddressBarFocusRequestId, secondRequest) panel.acknowledgeAddressBarFocusRequest(secondRequest) XCTAssertNil(panel.pendingAddressBarFocusRequestId) } } final class SidebarDropPlannerTests: XCTestCase { func testNoIndicatorForNoOpEdges() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: first, targetTabId: first, tabIds: tabIds ) ) XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: third, targetTabId: nil, tabIds: tabIds ) ) } func testNoIndicatorWhenOnlyOneTabExists() { let only = UUID() XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: only, targetTabId: nil, tabIds: [only] ) ) XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: only, targetTabId: only, tabIds: [only] ) ) } func testIndicatorAppearsForRealMoveToEnd() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] let indicator = SidebarDropPlanner.indicator( draggedTabId: second, targetTabId: nil, tabIds: tabIds ) XCTAssertEqual(indicator?.tabId, nil) XCTAssertEqual(indicator?.edge, .bottom) } func testTargetIndexForMoveToEndFromMiddle() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] let index = SidebarDropPlanner.targetIndex( draggedTabId: second, targetTabId: nil, indicator: SidebarDropIndicator(tabId: nil, edge: .bottom), tabIds: tabIds ) XCTAssertEqual(index, 2) } func testNoIndicatorForSelfDropInMiddle() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: second, targetTabId: second, tabIds: tabIds ) ) } func testPointerEdgeTopCanSuppressNoOpWhenDraggingFirstOverSecond() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: first, targetTabId: second, tabIds: tabIds, pointerY: 2, targetHeight: 40 ) ) } func testPointerEdgeBottomAllowsMoveWhenDraggingFirstOverSecond() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] let indicator = SidebarDropPlanner.indicator( draggedTabId: first, targetTabId: second, tabIds: tabIds, pointerY: 38, targetHeight: 40 ) XCTAssertEqual(indicator?.tabId, third) XCTAssertEqual(indicator?.edge, .top) XCTAssertEqual( SidebarDropPlanner.targetIndex( draggedTabId: first, targetTabId: second, indicator: indicator, tabIds: tabIds ), 1 ) } func testEquivalentBoundaryInputsResolveToSingleCanonicalIndicator() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] let fromBottomOfFirst = SidebarDropPlanner.indicator( draggedTabId: third, targetTabId: first, tabIds: tabIds, pointerY: 38, targetHeight: 40 ) let fromTopOfSecond = SidebarDropPlanner.indicator( draggedTabId: third, targetTabId: second, tabIds: tabIds, pointerY: 2, targetHeight: 40 ) XCTAssertEqual(fromBottomOfFirst?.tabId, second) XCTAssertEqual(fromBottomOfFirst?.edge, .top) XCTAssertEqual(fromTopOfSecond?.tabId, second) XCTAssertEqual(fromTopOfSecond?.edge, .top) } func testPointerEdgeBottomSuppressesNoOpWhenDraggingLastOverSecond() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: third, targetTabId: second, tabIds: tabIds, pointerY: 38, targetHeight: 40 ) ) } } final class SidebarDragAutoScrollPlannerTests: XCTestCase { func testAutoScrollPlanTriggersNearTopAndBottomOnly() { let topPlan = SidebarDragAutoScrollPlanner.plan(distanceToTop: 4, distanceToBottom: 96, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertEqual(topPlan?.direction, .up) XCTAssertNotNil(topPlan) let bottomPlan = SidebarDragAutoScrollPlanner.plan(distanceToTop: 96, distanceToBottom: 4, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertEqual(bottomPlan?.direction, .down) XCTAssertNotNil(bottomPlan) XCTAssertNil( SidebarDragAutoScrollPlanner.plan(distanceToTop: 60, distanceToBottom: 60, edgeInset: 44, minStep: 2, maxStep: 12) ) } func testAutoScrollPlanSpeedsUpCloserToEdge() { let nearTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: 1, distanceToBottom: 99, edgeInset: 44, minStep: 2, maxStep: 12) let midTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: 22, distanceToBottom: 78, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertNotNil(nearTop) XCTAssertNotNil(midTop) XCTAssertGreaterThan(nearTop?.pointsPerTick ?? 0, midTop?.pointsPerTick ?? 0) } func testAutoScrollPlanStillTriggersWhenPointerIsPastEdge() { let aboveTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: -500, distanceToBottom: 600, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertEqual(aboveTop?.direction, .up) XCTAssertEqual(aboveTop?.pointsPerTick, 12) let belowBottom = SidebarDragAutoScrollPlanner.plan(distanceToTop: 600, distanceToBottom: -500, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertEqual(belowBottom?.direction, .down) XCTAssertEqual(belowBottom?.pointsPerTick, 12) } } final class FinderServicePathResolverTests: XCTestCase { func testOrderedUniqueDirectoriesUsesParentForFilesAndDedupes() { let input: [URL] = [ URL(fileURLWithPath: "/tmp/cmux-services/project", isDirectory: true), URL(fileURLWithPath: "/tmp/cmux-services/project/README.md", isDirectory: false), URL(fileURLWithPath: "/tmp/cmux-services/../cmux-services/project", isDirectory: true), URL(fileURLWithPath: "/tmp/cmux-services/other", isDirectory: true), ] let directories = FinderServicePathResolver.orderedUniqueDirectories(from: input) XCTAssertEqual( directories, [ "/tmp/cmux-services/project", "/tmp/cmux-services/other", ] ) } func testOrderedUniqueDirectoriesPreservesFirstSeenOrder() { let input: [URL] = [ URL(fileURLWithPath: "/tmp/cmux-services/b", isDirectory: true), URL(fileURLWithPath: "/tmp/cmux-services/a/file.txt", isDirectory: false), URL(fileURLWithPath: "/tmp/cmux-services/a", isDirectory: true), URL(fileURLWithPath: "/tmp/cmux-services/b/file.txt", isDirectory: false), ] let directories = FinderServicePathResolver.orderedUniqueDirectories(from: input) XCTAssertEqual( directories, [ "/tmp/cmux-services/b", "/tmp/cmux-services/a", ] ) } } 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/" })) } } final class OmnibarStateMachineTests: XCTestCase { func testEscapeRevertsWhenEditingThenBlursOnSecondEscape() throws { var state = OmnibarState() var effects = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/")) XCTAssertTrue(state.isFocused) XCTAssertEqual(state.buffer, "https://example.com/") XCTAssertFalse(state.isUserEditing) XCTAssertTrue(effects.shouldSelectAll) effects = omnibarReduce(state: &state, event: .bufferChanged("exam")) XCTAssertTrue(state.isUserEditing) XCTAssertEqual(state.buffer, "exam") XCTAssertTrue(effects.shouldRefreshSuggestions) // Simulate an open popup. effects = omnibarReduce( state: &state, event: .suggestionsUpdated([.search(engineName: "Google", query: "exam")]) ) XCTAssertEqual(state.suggestions.count, 1) XCTAssertFalse(effects.shouldSelectAll) // First escape: revert + close popup + select-all. effects = omnibarReduce(state: &state, event: .escape) XCTAssertEqual(state.buffer, "https://example.com/") XCTAssertFalse(state.isUserEditing) XCTAssertTrue(state.suggestions.isEmpty) XCTAssertTrue(effects.shouldSelectAll) XCTAssertFalse(effects.shouldBlurToWebView) // Second escape: blur (since we're not editing and popup is closed). effects = omnibarReduce(state: &state, event: .escape) XCTAssertTrue(effects.shouldBlurToWebView) } func testPanelURLChangeDoesNotClobberUserBufferWhileEditing() throws { var state = OmnibarState() _ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://a.test/")) _ = omnibarReduce(state: &state, event: .bufferChanged("hello")) XCTAssertTrue(state.isUserEditing) _ = omnibarReduce(state: &state, event: .panelURLChanged(currentURLString: "https://b.test/")) XCTAssertEqual(state.currentURLString, "https://b.test/") XCTAssertEqual(state.buffer, "hello") XCTAssertTrue(state.isUserEditing) let effects = omnibarReduce(state: &state, event: .escape) XCTAssertEqual(state.buffer, "https://b.test/") XCTAssertTrue(effects.shouldSelectAll) } func testFocusLostRevertsUnlessSuppressed() throws { var state = OmnibarState() _ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/")) _ = omnibarReduce(state: &state, event: .bufferChanged("typed")) XCTAssertEqual(state.buffer, "typed") _ = omnibarReduce(state: &state, event: .focusLostPreserveBuffer(currentURLString: "https://example.com/")) XCTAssertEqual(state.buffer, "typed") _ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/")) _ = omnibarReduce(state: &state, event: .bufferChanged("typed2")) _ = omnibarReduce(state: &state, event: .focusLostRevertBuffer(currentURLString: "https://example.com/")) XCTAssertEqual(state.buffer, "https://example.com/") } func testSuggestionsUpdateKeepsSelectionAcrossNonEmptyListRefresh() throws { var state = OmnibarState() _ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/")) _ = omnibarReduce(state: &state, event: .bufferChanged("go")) let base: [OmnibarSuggestion] = [ .search(engineName: "Google", query: "go"), .remoteSearchSuggestion("go tutorial"), .remoteSearchSuggestion("go json"), ] _ = omnibarReduce(state: &state, event: .suggestionsUpdated(base)) XCTAssertEqual(state.selectedSuggestionIndex, 0) _ = omnibarReduce(state: &state, event: .moveSelection(delta: 2)) XCTAssertEqual(state.selectedSuggestionIndex, 2) // Simulate remote merge update for the same query while popup remains open. let merged: [OmnibarSuggestion] = [ .search(engineName: "Google", query: "go"), .remoteSearchSuggestion("go tutorial"), .remoteSearchSuggestion("go json"), .remoteSearchSuggestion("go fmt"), ] _ = omnibarReduce(state: &state, event: .suggestionsUpdated(merged)) XCTAssertEqual(state.selectedSuggestionIndex, 2, "Expected selection to remain stable while list stays open") } func testSuggestionsReopenResetsSelectionToFirstRow() throws { var state = OmnibarState() _ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/")) _ = omnibarReduce(state: &state, event: .bufferChanged("go")) let rows: [OmnibarSuggestion] = [ .search(engineName: "Google", query: "go"), .remoteSearchSuggestion("go tutorial"), ] _ = omnibarReduce(state: &state, event: .suggestionsUpdated(rows)) _ = omnibarReduce(state: &state, event: .moveSelection(delta: 1)) XCTAssertEqual(state.selectedSuggestionIndex, 1) _ = omnibarReduce(state: &state, event: .suggestionsUpdated([])) XCTAssertEqual(state.selectedSuggestionIndex, 0) _ = omnibarReduce(state: &state, event: .suggestionsUpdated(rows)) XCTAssertEqual(state.selectedSuggestionIndex, 0, "Expected reopened popup to focus first row") } func testSuggestionsUpdatePrefersAutocompleteMatchWhenSelectionNotTracked() throws { var state = OmnibarState() _ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "https://example.com/")) _ = omnibarReduce(state: &state, event: .bufferChanged("gm")) let rows: [OmnibarSuggestion] = [ .search(engineName: "Google", query: "gm"), .history(url: "https://google.com/", title: "Google"), .history(url: "https://gmail.com/", title: "Gmail"), ] _ = omnibarReduce(state: &state, event: .suggestionsUpdated(rows)) XCTAssertEqual(state.selectedSuggestionIndex, 2, "Expected autocomplete candidate to become selected without explicit index state.") XCTAssertEqual(state.selectedSuggestionID, rows[2].id) XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: state.suggestions[state.selectedSuggestionIndex])) XCTAssertEqual(state.suggestions[state.selectedSuggestionIndex].completion, "https://gmail.com/") } } final class OmnibarRemoteSuggestionMergeTests: XCTestCase { func testMergeRemoteSuggestionsInsertsBelowSearchAndDedupes() { let now = Date() let entries: [BrowserHistoryStore.Entry] = [ BrowserHistoryStore.Entry( id: UUID(), url: "https://go.dev/", title: "The Go Programming Language", lastVisited: now, visitCount: 10 ), ] let merged = buildOmnibarSuggestions( query: "go", engineName: "Google", historyEntries: entries, openTabMatches: [], remoteQueries: ["go tutorial", "go.dev", "go json"], resolvedURL: nil, limit: 8 ) let completions = merged.compactMap { $0.completion } XCTAssertGreaterThanOrEqual(completions.count, 5) XCTAssertEqual(completions[0], "https://go.dev/") XCTAssertEqual(completions[1], "go") let remoteCompletions = Array(completions.dropFirst(2)) XCTAssertEqual(Set(remoteCompletions), Set(["go tutorial", "go.dev", "go json"])) XCTAssertEqual(remoteCompletions.count, 3) } func testStaleRemoteSuggestionsKeptForNearbyEdits() { let stale = staleOmnibarRemoteSuggestionsForDisplay( query: "go t", previousRemoteQuery: "go", previousRemoteSuggestions: ["go tutorial", "go json", "golang tips"], limit: 8 ) XCTAssertEqual(stale, ["go tutorial", "go json", "golang tips"]) } func testStaleRemoteSuggestionsTrimAndRespectLimit() { let stale = staleOmnibarRemoteSuggestionsForDisplay( query: "gooo", previousRemoteQuery: "goo", previousRemoteSuggestions: [" go tutorial ", "", "go json", " ", "go fmt"], limit: 2 ) XCTAssertEqual(stale, ["go tutorial", "go json"]) } func testStaleRemoteSuggestionsDroppedForUnrelatedQuery() { let stale = staleOmnibarRemoteSuggestionsForDisplay( query: "python", previousRemoteQuery: "go", previousRemoteSuggestions: ["go tutorial", "go json"], limit: 8 ) XCTAssertTrue(stale.isEmpty) } } final class OmnibarSuggestionRankingTests: XCTestCase { private var fixedNow: Date { Date(timeIntervalSinceReferenceDate: 10_000_000) } func testSingleCharacterQueryPromotesAutocompletionMatchToFirstRow() { let entries: [BrowserHistoryStore.Entry] = [ .init( id: UUID(), url: "https://news.ycombinator.com/", title: "News.YC", lastVisited: fixedNow, visitCount: 12, typedCount: 1, lastTypedAt: fixedNow ), .init( id: UUID(), url: "https://www.google.com/", title: "Google", lastVisited: fixedNow - 200, visitCount: 8, typedCount: 2, lastTypedAt: fixedNow - 200 ), ] let results = buildOmnibarSuggestions( query: "n", engineName: "Google", historyEntries: entries, openTabMatches: [], remoteQueries: ["search google for n", "news"], resolvedURL: nil, limit: 8, now: fixedNow ) XCTAssertEqual(results.first?.completion, "https://news.ycombinator.com/") XCTAssertNotEqual(results.map(\.completion).first, "n") XCTAssertTrue(results.first.map { omnibarSuggestionSupportsAutocompletion(query: "n", suggestion: $0) } ?? false) } func testGmAutocompleteCandidateIsFirstOnExactQueryMatch() { let entries: [BrowserHistoryStore.Entry] = [ .init( id: UUID(), url: "https://google.com/", title: "Google", lastVisited: fixedNow, visitCount: 4, typedCount: 1, lastTypedAt: fixedNow ), .init( id: UUID(), url: "https://gmail.com/", title: "Gmail", lastVisited: fixedNow, visitCount: 10, typedCount: 2, lastTypedAt: fixedNow ), ] let results = buildOmnibarSuggestions( query: "gm", engineName: "Google", historyEntries: entries, openTabMatches: [], remoteQueries: ["gmail", "gmail.com", "google mail"], resolvedURL: nil, limit: 8, now: fixedNow ) XCTAssertEqual(results.first?.completion, "https://gmail.com/") XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: results[0])) let inlineCompletion = omnibarInlineCompletionForDisplay( typedText: "gm", suggestions: results, isFocused: true, selectionRange: NSRange(location: 2, length: 0), hasMarkedText: false ) XCTAssertNotNil(inlineCompletion) } func testAutocompletionCandidateWinsOverRemoteAndSearchRowsForTwoLetterQuery() { let entries: [BrowserHistoryStore.Entry] = [ .init( id: UUID(), url: "https://google.com/", title: "Google", lastVisited: fixedNow, visitCount: 4, typedCount: 1, lastTypedAt: fixedNow ), .init( id: UUID(), url: "https://gmail.com/", title: "Gmail", lastVisited: fixedNow, visitCount: 10, typedCount: 2, lastTypedAt: fixedNow ), ] let results = buildOmnibarSuggestions( query: "gm", engineName: "Google", historyEntries: entries, openTabMatches: [ .init( tabId: UUID(), panelId: UUID(), url: "https://gmail.com/", title: "Gmail", isKnownOpenTab: true ), ], remoteQueries: ["Search google for gm", "gmail", "gmail.com", "Google mail"], resolvedURL: nil, limit: 8, now: fixedNow ) XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: results[0])) XCTAssertEqual(results.first?.completion, "https://gmail.com/") } func testSuggestionSelectionPrefersAutocompletionCandidateAfterSuggestionsUpdate() { let entries: [BrowserHistoryStore.Entry] = [ .init( id: UUID(), url: "https://google.com/", title: "Google", lastVisited: fixedNow, visitCount: 4, typedCount: 1, lastTypedAt: fixedNow ), .init( id: UUID(), url: "https://gmail.com/", title: "Gmail", lastVisited: fixedNow, visitCount: 10, typedCount: 2, lastTypedAt: fixedNow ), ] let results = buildOmnibarSuggestions( query: "gm", engineName: "Google", historyEntries: entries, openTabMatches: [], remoteQueries: ["Search google for gm", "gmail", "gmail.com"], resolvedURL: nil, limit: 8, now: fixedNow ) var state = OmnibarState() let _ = omnibarReduce(state: &state, event: .focusGained(currentURLString: "")) let _ = omnibarReduce(state: &state, event: .bufferChanged("gm")) let _ = omnibarReduce(state: &state, event: .suggestionsUpdated(results)) XCTAssertEqual(state.selectedSuggestionIndex, 0) XCTAssertEqual(state.selectedSuggestionID, results[0].id) XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: state.suggestions[0])) } func testTwoCharQueryWithRemoteSuggestionsStillPromotesAutocompletionMatch() { let entries: [BrowserHistoryStore.Entry] = [ .init( id: UUID(), url: "https://news.ycombinator.com/", title: "News.YC", lastVisited: fixedNow, visitCount: 12, typedCount: 1, lastTypedAt: fixedNow ), .init( id: UUID(), url: "https://www.google.com/", title: "Google", lastVisited: fixedNow - 200, visitCount: 8, typedCount: 2, lastTypedAt: fixedNow - 200 ), ] let results = buildOmnibarSuggestions( query: "ne", engineName: "Google", historyEntries: entries, openTabMatches: [], remoteQueries: ["netflix", "new york times", "newegg"], resolvedURL: nil, limit: 8, now: fixedNow ) // The autocompletable history entry (news.ycombinator.com) should be first despite remote results. XCTAssertEqual(results.first?.completion, "https://news.ycombinator.com/") XCTAssertTrue(results.first.map { omnibarSuggestionSupportsAutocompletion(query: "ne", suggestion: $0) } ?? false) // Remote suggestions should still appear in the results (two-char queries include them). let remoteCompletions = results.filter { if case .remote = $0.kind { return true } return false }.map(\.completion) XCTAssertFalse(remoteCompletions.isEmpty, "Expected remote suggestions to be present for two-char query") } func testGmQueryWithRemoteSuggestionsAndOpenTabPromotesAutocompletionMatch() { let entries: [BrowserHistoryStore.Entry] = [ .init( id: UUID(), url: "https://google.com/", title: "Google", lastVisited: fixedNow, visitCount: 4, typedCount: 1, lastTypedAt: fixedNow ), .init( id: UUID(), url: "https://gmail.com/", title: "Gmail", lastVisited: fixedNow, visitCount: 10, typedCount: 2, lastTypedAt: fixedNow ), ] let results = buildOmnibarSuggestions( query: "gm", engineName: "Google", historyEntries: entries, openTabMatches: [ .init( tabId: UUID(), panelId: UUID(), url: "https://google.com/maps", title: "Google Maps", isKnownOpenTab: true ), ], remoteQueries: ["gmail login", "gm stock price", "gmail.com"], resolvedURL: nil, limit: 8, now: fixedNow ) // Gmail should be first (autocompletable + typed history). XCTAssertEqual(results.first?.completion, "https://gmail.com/") XCTAssertTrue(omnibarSuggestionSupportsAutocompletion(query: "gm", suggestion: results[0])) // Verify remote suggestions are present alongside history/tab matches. let remoteCompletions = results.filter { if case .remote = $0.kind { return true } return false }.map(\.completion) XCTAssertFalse(remoteCompletions.isEmpty, "Expected remote suggestions in results") let hasSearch = results.contains { if case .search = $0.kind { return true } return false } XCTAssertTrue(hasSearch, "Expected search row in results") } func testHistorySuggestionDisplaysTitleAndUrlOnSingleLine() { let row = OmnibarSuggestion.history( url: "https://www.example.com/path?q=1", title: "Example Domain" ) XCTAssertEqual(row.listText, "Example Domain — example.com/path?q=1") XCTAssertFalse(row.listText.contains("\n")) } func testPublishedBufferTextUsesTypedPrefixWhenInlineSuffixIsSelected() { let inline = OmnibarInlineCompletion( typedText: "l", displayText: "localhost:3000", acceptedText: "https://localhost:3000/" ) let published = omnibarPublishedBufferTextForFieldChange( fieldValue: inline.displayText, inlineCompletion: inline, selectionRange: inline.suffixRange, hasMarkedText: false ) XCTAssertEqual(published, "l") } func testPublishedBufferTextKeepsUserTypedValueWhenDisplayDiffersFromInlineText() { let inline = OmnibarInlineCompletion( typedText: "l", displayText: "localhost:3000", acceptedText: "https://localhost:3000/" ) let published = omnibarPublishedBufferTextForFieldChange( fieldValue: "la", inlineCompletion: inline, selectionRange: NSRange(location: 2, length: 0), hasMarkedText: false ) XCTAssertEqual(published, "la") } func testInlineCompletionRenderIgnoresStaleTypedPrefixMismatch() { let staleInline = OmnibarInlineCompletion( typedText: "g", displayText: "github.com", acceptedText: "https://github.com/" ) let active = omnibarInlineCompletionIfBufferMatchesTypedPrefix( bufferText: "l", inlineCompletion: staleInline ) XCTAssertNil(active) } func testInlineCompletionRenderKeepsMatchingTypedPrefix() { let inline = OmnibarInlineCompletion( typedText: "l", displayText: "localhost:3000", acceptedText: "https://localhost:3000/" ) let active = omnibarInlineCompletionIfBufferMatchesTypedPrefix( bufferText: "l", inlineCompletion: inline ) XCTAssertEqual(active, inline) } func testInlineCompletionSkipsTitleMatchWhoseURLDoesNotStartWithTypedText() { // History entry: visited google.com/search?q=localhost:3000 with title // "localhost:3000 - Google Search". Typing "l" should NOT inline-complete // to "google.com/..." because that replaces the typed "l" with "g". let suggestions: [OmnibarSuggestion] = [ .history( url: "https://www.google.com/search?q=localhost:3000", title: "localhost:3000 - Google Search" ), ] let result = omnibarInlineCompletionForDisplay( typedText: "l", suggestions: suggestions, isFocused: true, selectionRange: NSRange(location: 1, length: 0), hasMarkedText: false ) XCTAssertNil(result, "Should not inline-complete when display text does not start with typed prefix") } } @MainActor final class NotificationDockBadgeTests: XCTestCase { func testDockBadgeLabelEnabledAndCounted() { XCTAssertEqual(TerminalNotificationStore.dockBadgeLabel(unreadCount: 1, isEnabled: true), "1") XCTAssertEqual(TerminalNotificationStore.dockBadgeLabel(unreadCount: 42, isEnabled: true), "42") XCTAssertEqual(TerminalNotificationStore.dockBadgeLabel(unreadCount: 100, isEnabled: true), "99+") } func testDockBadgeLabelHiddenWhenDisabledOrZero() { XCTAssertNil(TerminalNotificationStore.dockBadgeLabel(unreadCount: 0, isEnabled: true)) XCTAssertNil(TerminalNotificationStore.dockBadgeLabel(unreadCount: 5, isEnabled: false)) } func testDockBadgeLabelShowsRunTagEvenWithoutUnread() { XCTAssertEqual( TerminalNotificationStore.dockBadgeLabel(unreadCount: 0, isEnabled: true, runTag: "verify-tag"), "verify-tag" ) } func testDockBadgeLabelCombinesRunTagAndUnreadCount() { XCTAssertEqual( TerminalNotificationStore.dockBadgeLabel(unreadCount: 7, isEnabled: true, runTag: "verify"), "verify:7" ) XCTAssertEqual( TerminalNotificationStore.dockBadgeLabel(unreadCount: 120, isEnabled: true, runTag: "verify"), "verify:99+" ) } func testNotificationBadgePreferenceDefaultsToEnabled() { let suiteName = "NotificationDockBadgeTests.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } XCTAssertTrue(NotificationBadgeSettings.isDockBadgeEnabled(defaults: defaults)) defaults.set(false, forKey: NotificationBadgeSettings.dockBadgeEnabledKey) XCTAssertFalse(NotificationBadgeSettings.isDockBadgeEnabled(defaults: defaults)) defaults.set(true, forKey: NotificationBadgeSettings.dockBadgeEnabledKey) XCTAssertTrue(NotificationBadgeSettings.isDockBadgeEnabled(defaults: defaults)) } } final class MenuBarBadgeLabelFormatterTests: XCTestCase { func testBadgeLabelFormatting() { XCTAssertNil(MenuBarBadgeLabelFormatter.badgeText(for: 0)) XCTAssertEqual(MenuBarBadgeLabelFormatter.badgeText(for: 1), "1") XCTAssertEqual(MenuBarBadgeLabelFormatter.badgeText(for: 9), "9") XCTAssertEqual(MenuBarBadgeLabelFormatter.badgeText(for: 10), "9+") XCTAssertEqual(MenuBarBadgeLabelFormatter.badgeText(for: 47), "9+") } } final class NotificationMenuSnapshotBuilderTests: XCTestCase { func testSnapshotCountsUnreadAndLimitsRecentItems() { let notifications = (0..<8).map { index in TerminalNotification( id: UUID(), tabId: UUID(), surfaceId: nil, title: "N\(index)", subtitle: "", body: "", createdAt: Date(timeIntervalSince1970: TimeInterval(index)), isRead: index.isMultiple(of: 2) ) } let snapshot = NotificationMenuSnapshotBuilder.make( notifications: notifications, maxInlineNotificationItems: 3 ) XCTAssertEqual(snapshot.unreadCount, 4) XCTAssertTrue(snapshot.hasNotifications) XCTAssertTrue(snapshot.hasUnreadNotifications) XCTAssertEqual(snapshot.recentNotifications.count, 3) XCTAssertEqual(snapshot.recentNotifications.map(\.id), Array(notifications.prefix(3)).map(\.id)) } func testStateHintTitleHandlesSingularPluralAndZero() { XCTAssertEqual(NotificationMenuSnapshotBuilder.stateHintTitle(unreadCount: 0), "No unread notifications") XCTAssertEqual(NotificationMenuSnapshotBuilder.stateHintTitle(unreadCount: 1), "1 unread notification") XCTAssertEqual(NotificationMenuSnapshotBuilder.stateHintTitle(unreadCount: 2), "2 unread notifications") } } final class MenuBarBuildHintFormatterTests: XCTestCase { func testReleaseBuildShowsNoHint() { XCTAssertNil(MenuBarBuildHintFormatter.menuTitle(appName: "cmux DEV menubar-extra", isDebugBuild: false)) } func testDebugBuildWithTagShowsTag() { XCTAssertEqual( MenuBarBuildHintFormatter.menuTitle(appName: "cmux DEV menubar-extra", isDebugBuild: true), "Build Tag: menubar-extra" ) } func testDebugBuildWithoutTagShowsUntagged() { XCTAssertEqual( MenuBarBuildHintFormatter.menuTitle(appName: "cmux DEV", isDebugBuild: true), "Build: DEV (untagged)" ) } } final class MenuBarNotificationLineFormatterTests: XCTestCase { func testPlainTitleContainsUnreadDotBodyAndTab() { let notification = TerminalNotification( id: UUID(), tabId: UUID(), surfaceId: nil, title: "Build finished", subtitle: "", body: "All checks passed", createdAt: Date(timeIntervalSince1970: 0), isRead: false ) let line = MenuBarNotificationLineFormatter.plainTitle(notification: notification, tabTitle: "workspace-1") XCTAssertTrue(line.hasPrefix("● Build finished")) XCTAssertTrue(line.contains("All checks passed")) XCTAssertTrue(line.contains("workspace-1")) } func testPlainTitleFallsBackToSubtitleWhenBodyEmpty() { let notification = TerminalNotification( id: UUID(), tabId: UUID(), surfaceId: nil, title: "Deploy", subtitle: "staging", body: "", createdAt: Date(timeIntervalSince1970: 0), isRead: true ) let line = MenuBarNotificationLineFormatter.plainTitle(notification: notification, tabTitle: nil) XCTAssertTrue(line.hasPrefix(" Deploy")) XCTAssertTrue(line.contains("staging")) } func testMenuTitleWrapsAndTruncatesToThreeLines() { let notification = TerminalNotification( id: UUID(), tabId: UUID(), surfaceId: nil, title: "Extremely long notification title for wrapping behavior validation", subtitle: "", body: Array(repeating: "this body should wrap and eventually truncate", count: 8).joined(separator: " "), createdAt: Date(timeIntervalSince1970: 0), isRead: false ) let title = MenuBarNotificationLineFormatter.menuTitle( notification: notification, tabTitle: "workspace-with-a-very-long-name", maxWidth: 120, maxLines: 3 ) XCTAssertLessThanOrEqual(title.components(separatedBy: "\n").count, 3) XCTAssertTrue(title.hasSuffix("…")) } func testMenuTitlePreservesShortTextWithoutEllipsis() { let notification = TerminalNotification( id: UUID(), tabId: UUID(), surfaceId: nil, title: "Done", subtitle: "", body: "All checks passed", createdAt: Date(timeIntervalSince1970: 0), isRead: false ) let title = MenuBarNotificationLineFormatter.menuTitle( notification: notification, tabTitle: "w1", maxWidth: 320, maxLines: 3 ) XCTAssertFalse(title.hasSuffix("…")) } } final class MenuBarIconDebugSettingsTests: XCTestCase { func testDisplayedUnreadCountUsesPreviewOverrideWhenEnabled() { let suiteName = "MenuBarIconDebugSettingsTests.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(true, forKey: MenuBarIconDebugSettings.previewEnabledKey) defaults.set(7, forKey: MenuBarIconDebugSettings.previewCountKey) XCTAssertEqual(MenuBarIconDebugSettings.displayedUnreadCount(actualUnreadCount: 2, defaults: defaults), 7) } func testBadgeRenderConfigClampsInvalidValues() { let suiteName = "MenuBarIconDebugSettingsTests.Clamp.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(-100, forKey: MenuBarIconDebugSettings.badgeRectXKey) defaults.set(200, forKey: MenuBarIconDebugSettings.badgeRectYKey) defaults.set(-100, forKey: MenuBarIconDebugSettings.singleDigitFontSizeKey) defaults.set(100, forKey: MenuBarIconDebugSettings.multiDigitXAdjustKey) let config = MenuBarIconDebugSettings.badgeRenderConfig(defaults: defaults) XCTAssertEqual(config.badgeRect.origin.x, 0, accuracy: 0.001) XCTAssertEqual(config.badgeRect.origin.y, 20, accuracy: 0.001) XCTAssertEqual(config.singleDigitFontSize, 6, accuracy: 0.001) XCTAssertEqual(config.multiDigitXAdjust, 4, accuracy: 0.001) } func testBadgeRenderConfigUsesLegacySingleDigitXAdjustWhenNewKeyMissing() { let suiteName = "MenuBarIconDebugSettingsTests.LegacyX.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(2.5, forKey: MenuBarIconDebugSettings.legacySingleDigitXAdjustKey) let config = MenuBarIconDebugSettings.badgeRenderConfig(defaults: defaults) XCTAssertEqual(config.singleDigitXAdjust, 2.5, accuracy: 0.001) } } @MainActor final class MenuBarIconRendererTests: XCTestCase { func testImageWidthDoesNotShiftWhenBadgeAppears() { let noBadge = MenuBarIconRenderer.makeImage(unreadCount: 0) let withBadge = MenuBarIconRenderer.makeImage(unreadCount: 2) XCTAssertEqual(noBadge.size.width, 18, accuracy: 0.001) XCTAssertEqual(withBadge.size.width, 18, accuracy: 0.001) } } final class WorkspaceMountPolicyTests: XCTestCase { func testDefaultPolicyMountsOnlySelectedWorkspace() { let a = UUID() let b = UUID() let orderedTabIds: [UUID] = [a, b] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: b, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: WorkspaceMountPolicy.maxMountedWorkspaces ) XCTAssertEqual(next, [b]) } func testSelectedWorkspaceMovesToFrontAndMountCountIsBounded() { let a = UUID() let b = UUID() let c = UUID() let orderedTabIds: [UUID] = [a, b, c] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a, b, c], selected: c, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: 2 ) XCTAssertEqual(next, [c, a]) } func testMissingWorkspacesArePruned() { let a = UUID() let b = UUID() let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [b, a], selected: nil, pinnedIds: [], orderedTabIds: [a], isCycleHot: false, maxMounted: 2 ) XCTAssertEqual(next, [a]) } func testSelectedWorkspaceIsInsertedWhenAbsentFromCurrentCache() { let a = UUID() let b = UUID() let orderedTabIds: [UUID] = [a, b] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: b, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: 2 ) XCTAssertEqual(next, [b, a]) } func testMaxMountedIsClampedToAtLeastOne() { let a = UUID() let b = UUID() let orderedTabIds: [UUID] = [a, b] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a, b], selected: nil, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: 0 ) XCTAssertEqual(next, [a]) } func testCycleHotModeKeepsOnlySelectedWhenNoPinnedHandoff() { let a = UUID() let b = UUID() let c = UUID() let d = UUID() let orderedTabIds: [UUID] = [a, b, c, d] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: c, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: true, maxMounted: WorkspaceMountPolicy.maxMountedWorkspacesDuringCycle ) XCTAssertEqual(next, [c]) } func testCycleHotModeRespectsMaxMountedLimit() { let a = UUID() let b = UUID() let c = UUID() let orderedTabIds: [UUID] = [a, b, c] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a, b, c], selected: b, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: true, maxMounted: 2 ) XCTAssertEqual(next, [b]) } func testPinnedIdsAreRetainedAcrossReconcile() { let a = UUID() let b = UUID() let c = UUID() let orderedTabIds: [UUID] = [a, b, c] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: c, pinnedIds: [a], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: 2 ) XCTAssertEqual(next, [c, a]) } func testCycleHotModeKeepsRetiringWorkspaceWhenPinned() { let a = UUID() let b = UUID() let orderedTabIds: [UUID] = [a, b] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: b, pinnedIds: [a], orderedTabIds: orderedTabIds, isCycleHot: true, maxMounted: WorkspaceMountPolicy.maxMountedWorkspacesDuringCycle ) XCTAssertEqual(next, [b, a]) } } @MainActor final class WindowTerminalHostViewTests: XCTestCase { private final class CapturingView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { bounds.contains(point) ? self : nil } } func testHostViewPassesThroughWhenNoTerminalSubviewIsHit() { let host = WindowTerminalHostView(frame: NSRect(x: 0, y: 0, width: 200, height: 120)) XCTAssertNil(host.hitTest(NSPoint(x: 10, y: 10))) } func testHostViewReturnsSubviewWhenSubviewIsHit() { let host = WindowTerminalHostView(frame: NSRect(x: 0, y: 0, width: 200, height: 120)) let child = CapturingView(frame: NSRect(x: 20, y: 15, width: 40, height: 30)) host.addSubview(child) XCTAssertTrue(host.hitTest(NSPoint(x: 25, y: 20)) === child) XCTAssertNil(host.hitTest(NSPoint(x: 150, y: 100))) } } @MainActor final class GhosttySurfaceOverlayTests: XCTestCase { func testInactiveOverlayVisibilityTracksRequestedState() { let hostedView = GhosttySurfaceScrollView( surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 80, height: 50)) ) hostedView.setInactiveOverlay(color: .black, opacity: 0.35, visible: true) var state = hostedView.debugInactiveOverlayState() XCTAssertFalse(state.isHidden) XCTAssertEqual(state.alpha, 0.35, accuracy: 0.01) hostedView.setInactiveOverlay(color: .black, opacity: 0.35, visible: false) state = hostedView.debugInactiveOverlayState() XCTAssertTrue(state.isHidden) } } @MainActor final class TerminalWindowPortalLifecycleTests: XCTestCase { func testPortalHostInstallsAboveContentViewForVisibility() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let portal = WindowTerminalPortal(window: window) _ = portal.viewAtWindowPoint(NSPoint(x: 1, y: 1)) guard let contentView = window.contentView, let container = contentView.superview else { XCTFail("Expected content container") return } guard let hostIndex = container.subviews.firstIndex(where: { $0 is WindowTerminalHostView }), let contentIndex = container.subviews.firstIndex(where: { $0 === contentView }) else { XCTFail("Expected host/content views in same container") return } XCTAssertGreaterThan( hostIndex, contentIndex, "Portal host must remain above content view so portal-hosted terminals stay visible" ) } func testRegistryPrunesPortalWhenWindowCloses() { let baseline = TerminalWindowPortalRegistry.debugPortalCount() let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) _ = TerminalWindowPortalRegistry.viewAtWindowPoint(NSPoint(x: 1, y: 1), in: window) XCTAssertEqual(TerminalWindowPortalRegistry.debugPortalCount(), baseline + 1) NotificationCenter.default.post(name: NSWindow.willCloseNotification, object: window) XCTAssertEqual(TerminalWindowPortalRegistry.debugPortalCount(), baseline) } func testPruneDeadEntriesDetachesAnchorlessHostedView() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let portal = WindowTerminalPortal(window: window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } let hosted1 = GhosttySurfaceScrollView( surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 40, height: 30)) ) var anchor1: NSView? = NSView(frame: NSRect(x: 20, y: 20, width: 120, height: 80)) contentView.addSubview(anchor1!) portal.bind(hostedView: hosted1, to: anchor1!, visibleInUI: true) anchor1?.removeFromSuperview() anchor1 = nil let hosted2 = GhosttySurfaceScrollView( surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 40, height: 30)) ) let anchor2 = NSView(frame: NSRect(x: 180, y: 20, width: 120, height: 80)) contentView.addSubview(anchor2) portal.bind(hostedView: hosted2, to: anchor2, visibleInUI: true) XCTAssertEqual(portal.debugEntryCount(), 1, "Only the live anchored hosted view should remain tracked") XCTAssertEqual(portal.debugHostedSubviewCount(), 1, "Stale anchorless hosted views should be detached from hostView") } func testTerminalViewAtWindowPointResolvesPortalHostedSurface() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let portal = WindowTerminalPortal(window: window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } let anchor = NSView(frame: NSRect(x: 40, y: 50, width: 200, height: 120)) contentView.addSubview(anchor) let hosted = GhosttySurfaceScrollView( surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 100, height: 80)) ) portal.bind(hostedView: hosted, to: anchor, visibleInUI: true) let center = NSPoint(x: anchor.bounds.midX, y: anchor.bounds.midY) let windowPoint = anchor.convert(center, to: nil) XCTAssertNotNil( portal.terminalViewAtWindowPoint(windowPoint), "Portal hit-testing should resolve the terminal view for Finder file drops" ) } func testVisibilityTransitionBringsHostedViewToFront() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let portal = WindowTerminalPortal(window: window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } let anchor1 = NSView(frame: NSRect(x: 20, y: 20, width: 220, height: 180)) let anchor2 = NSView(frame: NSRect(x: 80, y: 60, width: 220, height: 180)) contentView.addSubview(anchor1) contentView.addSubview(anchor2) let terminal1 = GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80)) let hosted1 = GhosttySurfaceScrollView(surfaceView: terminal1) let terminal2 = GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80)) let hosted2 = GhosttySurfaceScrollView(surfaceView: terminal2) portal.bind(hostedView: hosted1, to: anchor1, visibleInUI: true) portal.bind(hostedView: hosted2, to: anchor2, visibleInUI: true) let overlapInContent = NSPoint(x: 120, y: 100) let overlapInWindow = contentView.convert(overlapInContent, to: nil) XCTAssertTrue( portal.terminalViewAtWindowPoint(overlapInWindow) === terminal2, "Latest bind should be top-most before visibility transition" ) portal.bind(hostedView: hosted1, to: anchor1, visibleInUI: false) portal.bind(hostedView: hosted1, to: anchor1, visibleInUI: true) XCTAssertTrue( portal.terminalViewAtWindowPoint(overlapInWindow) === terminal1, "Becoming visible should refresh z-order for already-hosted view" ) } func testPriorityIncreaseBringsHostedViewToFrontWithoutVisibilityToggle() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let portal = WindowTerminalPortal(window: window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } let anchor1 = NSView(frame: NSRect(x: 20, y: 20, width: 220, height: 180)) let anchor2 = NSView(frame: NSRect(x: 80, y: 60, width: 220, height: 180)) contentView.addSubview(anchor1) contentView.addSubview(anchor2) let terminal1 = GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80)) let hosted1 = GhosttySurfaceScrollView(surfaceView: terminal1) let terminal2 = GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80)) let hosted2 = GhosttySurfaceScrollView(surfaceView: terminal2) portal.bind(hostedView: hosted1, to: anchor1, visibleInUI: true, zPriority: 1) portal.bind(hostedView: hosted2, to: anchor2, visibleInUI: true, zPriority: 2) let overlapInContent = NSPoint(x: 120, y: 100) let overlapInWindow = contentView.convert(overlapInContent, to: nil) XCTAssertTrue( portal.terminalViewAtWindowPoint(overlapInWindow) === terminal2, "Higher-priority terminal should initially be top-most" ) portal.bind(hostedView: hosted1, to: anchor1, visibleInUI: true, zPriority: 2) XCTAssertTrue( portal.terminalViewAtWindowPoint(overlapInWindow) === terminal1, "Promoting z-priority should bring an already-visible terminal to front" ) } } @MainActor final class BrowserWindowPortalLifecycleTests: XCTestCase { private func realizeWindowLayout(_ window: NSWindow) { window.makeKeyAndOrderFront(nil) window.displayIfNeeded() window.contentView?.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) window.contentView?.layoutSubtreeIfNeeded() } func testPortalHostInstallsAboveContentViewForVisibility() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } let portal = WindowBrowserPortal(window: window) _ = portal.webViewAtWindowPoint(NSPoint(x: 1, y: 1)) guard let contentView = window.contentView, let container = contentView.superview else { XCTFail("Expected content container") return } guard let hostIndex = container.subviews.firstIndex(where: { $0 is WindowBrowserHostView }), let contentIndex = container.subviews.firstIndex(where: { $0 === contentView }) else { XCTFail("Expected host/content views in same container") return } XCTAssertGreaterThan( hostIndex, contentIndex, "Browser portal host must remain above content view so portal-hosted web views stay visible" ) } func testAnchorRebindKeepsWebViewInStablePortalSuperview() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } realizeWindowLayout(window) let portal = WindowBrowserPortal(window: window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } let anchor1 = NSView(frame: NSRect(x: 20, y: 20, width: 180, height: 120)) let anchor2 = NSView(frame: NSRect(x: 240, y: 40, width: 180, height: 120)) contentView.addSubview(anchor1) contentView.addSubview(anchor2) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) portal.bind(webView: webView, to: anchor1, visibleInUI: true) let firstSuperview = webView.superview XCTAssertNotNil(firstSuperview) XCTAssertTrue(firstSuperview is WindowBrowserSlotView) portal.bind(webView: webView, to: anchor2, visibleInUI: true) XCTAssertTrue(webView.superview === firstSuperview, "Anchor moves should not reparent the web view") contentView.layoutSubtreeIfNeeded() portal.synchronizeWebViewForAnchor(anchor2) guard let slot = webView.superview as? WindowBrowserSlotView, let host = slot.superview as? WindowBrowserHostView else { XCTFail("Expected browser slot + host views") return } let expectedFrame = host.convert(anchor2.bounds, from: anchor2) XCTAssertEqual(slot.frame.origin.x, expectedFrame.origin.x, accuracy: 0.5) XCTAssertEqual(slot.frame.origin.y, expectedFrame.origin.y, accuracy: 0.5) XCTAssertEqual(slot.frame.size.width, expectedFrame.size.width, accuracy: 0.5) XCTAssertEqual(slot.frame.size.height, expectedFrame.size.height, accuracy: 0.5) } func testPortalClampsWebViewFrameToHostBoundsWhenAnchorOverflowsSidebar() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } realizeWindowLayout(window) let portal = WindowBrowserPortal(window: window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } // Simulate a transient oversized anchor rect during split churn. let anchor = NSView(frame: NSRect(x: 120, y: 20, width: 260, height: 150)) contentView.addSubview(anchor) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) portal.bind(webView: webView, to: anchor, visibleInUI: true) contentView.layoutSubtreeIfNeeded() portal.synchronizeWebViewForAnchor(anchor) guard let slot = webView.superview as? WindowBrowserSlotView else { XCTFail("Expected web view slot") return } XCTAssertFalse(slot.isHidden, "Partially visible browser anchor should stay visible") XCTAssertEqual(slot.frame.origin.x, 120, accuracy: 0.5) XCTAssertEqual(slot.frame.origin.y, 20, accuracy: 0.5) XCTAssertEqual(slot.frame.size.width, 200, accuracy: 0.5) XCTAssertEqual(slot.frame.size.height, 150, accuracy: 0.5) } func testPortalSyncNormalizesOutOfBoundsWebFrame() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 300), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } realizeWindowLayout(window) let portal = WindowBrowserPortal(window: window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } let anchor = NSView(frame: NSRect(x: 40, y: 20, width: 220, height: 160)) contentView.addSubview(anchor) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) portal.bind(webView: webView, to: anchor, visibleInUI: true) contentView.layoutSubtreeIfNeeded() portal.synchronizeWebViewForAnchor(anchor) guard let slot = webView.superview as? WindowBrowserSlotView else { XCTFail("Expected browser slot") return } // Reproduce observed drift from logs where WebKit shifts/expands frame beyond slot bounds. webView.frame = NSRect(x: 0, y: 250, width: slot.bounds.width, height: slot.bounds.height) XCTAssertGreaterThan(webView.frame.maxY, slot.bounds.maxY) portal.synchronizeWebViewForAnchor(anchor) XCTAssertEqual(webView.frame.origin.x, slot.bounds.origin.x, accuracy: 0.5) XCTAssertEqual(webView.frame.origin.y, slot.bounds.origin.y, accuracy: 0.5) XCTAssertEqual(webView.frame.size.width, slot.bounds.size.width, accuracy: 0.5) XCTAssertEqual(webView.frame.size.height, slot.bounds.size.height, accuracy: 0.5) } func testPortalHostBoundsBecomeReadyAfterBindingInFrameDrivenHierarchy() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 320), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } realizeWindowLayout(window) let portal = WindowBrowserPortal(window: window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } let anchor = NSView(frame: NSRect(x: 40, y: 24, width: 220, height: 160)) contentView.addSubview(anchor) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) portal.bind(webView: webView, to: anchor, visibleInUI: true) portal.synchronizeWebViewForAnchor(anchor) guard let slot = webView.superview as? WindowBrowserSlotView, let host = slot.superview as? WindowBrowserHostView else { XCTFail("Expected portal slot + host views") return } XCTAssertGreaterThan(host.bounds.width, 1, "Portal host width should be ready for clipping/sync") XCTAssertGreaterThan(host.bounds.height, 1, "Portal host height should be ready for clipping/sync") } func testRegistryDetachRemovesPortalHostedWebView() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 240), styleMask: [.titled, .closable], backing: .buffered, defer: false ) defer { window.orderOut(nil) } realizeWindowLayout(window) guard let contentView = window.contentView else { XCTFail("Expected content view") return } let anchor = NSView(frame: NSRect(x: 20, y: 20, width: 180, height: 120)) contentView.addSubview(anchor) let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration()) BrowserWindowPortalRegistry.bind(webView: webView, to: anchor, visibleInUI: true) XCTAssertNotNil(webView.superview) BrowserWindowPortalRegistry.detach(webView: webView) XCTAssertNil(webView.superview) } } 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)) } } final class TerminalOpenURLTargetResolutionTests: XCTestCase { func testResolvesHTTPSAsEmbeddedBrowser() throws { let target = try XCTUnwrap(resolveTerminalOpenURLTarget("https://example.com/path?q=1")) switch target { case let .embeddedBrowser(url): XCTAssertEqual(url.scheme, "https") XCTAssertEqual(url.host, "example.com") XCTAssertEqual(url.path, "/path") default: XCTFail("Expected web URL to route to embedded browser") } } func testResolvesBareDomainAsEmbeddedBrowser() throws { let target = try XCTUnwrap(resolveTerminalOpenURLTarget("example.com/docs")) switch target { case let .embeddedBrowser(url): XCTAssertEqual(url.scheme, "https") XCTAssertEqual(url.host, "example.com") XCTAssertEqual(url.path, "/docs") default: XCTFail("Expected bare domain to be normalized as an HTTPS browser URL") } } func testResolvesFileSchemeAsExternal() throws { let target = try XCTUnwrap(resolveTerminalOpenURLTarget("file:///tmp/cmux.txt")) switch target { case let .external(url): XCTAssertTrue(url.isFileURL) XCTAssertEqual(url.path, "/tmp/cmux.txt") default: XCTFail("Expected file URL to open externally") } } func testResolvesAbsolutePathAsExternalFileURL() throws { let target = try XCTUnwrap(resolveTerminalOpenURLTarget("/tmp/cmux-path.txt")) switch target { case let .external(url): XCTAssertTrue(url.isFileURL) XCTAssertEqual(url.path, "/tmp/cmux-path.txt") default: XCTFail("Expected absolute file path to open externally") } } func testResolvesNonWebSchemeAsExternal() throws { let target = try XCTUnwrap(resolveTerminalOpenURLTarget("mailto:test@example.com")) switch target { case let .external(url): XCTAssertEqual(url.scheme, "mailto") default: XCTFail("Expected non-web scheme to open externally") } } func testResolvesHostlessHTTPSAsExternal() throws { let target = try XCTUnwrap(resolveTerminalOpenURLTarget("https:///tmp/cmux.txt")) switch target { case let .external(url): XCTAssertEqual(url.scheme, "https") XCTAssertNil(url.host) XCTAssertEqual(url.path, "/tmp/cmux.txt") default: XCTFail("Expected hostless HTTPS URL to open externally") } } } 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 TerminalControllerSidebarDedupeTests: XCTestCase { func testShouldReplaceStatusEntryReturnsFalseForUnchangedPayload() { let current = SidebarStatusEntry( key: "agent", value: "idle", icon: "bolt", color: "#ffffff", timestamp: Date(timeIntervalSince1970: 123) ) XCTAssertFalse( TerminalController.shouldReplaceStatusEntry( current: current, key: "agent", value: "idle", icon: "bolt", color: "#ffffff" ) ) } func testShouldReplaceStatusEntryReturnsTrueWhenValueChanges() { let current = SidebarStatusEntry( key: "agent", value: "idle", icon: "bolt", color: "#ffffff", timestamp: Date(timeIntervalSince1970: 123) ) XCTAssertTrue( TerminalController.shouldReplaceStatusEntry( current: current, key: "agent", value: "running", icon: "bolt", color: "#ffffff" ) ) } func testShouldReplaceProgressReturnsFalseForUnchangedPayload() { XCTAssertFalse( TerminalController.shouldReplaceProgress( current: SidebarProgressState(value: 0.42, label: "indexing"), value: 0.42, label: "indexing" ) ) } func testShouldReplaceGitBranchReturnsFalseForUnchangedPayload() { XCTAssertFalse( TerminalController.shouldReplaceGitBranch( current: SidebarGitBranchState(branch: "main", isDirty: true), branch: "main", isDirty: true ) ) } func testShouldReplacePortsIgnoresOrderAndDuplicates() { XCTAssertFalse( TerminalController.shouldReplacePorts( current: [9229, 3000], next: [3000, 9229, 3000] ) ) XCTAssertTrue( TerminalController.shouldReplacePorts( current: [9229, 3000], next: [3000] ) ) } }