cmux/cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Lawrence Chen ea0593475c
Merge pull request #206 from manaflow-ai/issue-180-http-nonsecure-hosts
Allow HTTP in built-in browser for local dev hosts
2026-02-20 15:39:22 -08:00

2461 lines
91 KiB
Swift

import XCTest
import AppKit
import WebKit
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif
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)
}
func testCmdReturnBypassesMenuRoutingWhenWebViewIsFirstResponder() {
let spy = ActionSpy()
installMenu(spy: spy, key: "\r", modifiers: [.command])
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
let event = makeKeyDownEvent(key: "\r", modifiers: [.command], keyCode: 36) // kVK_Return
XCTAssertNotNil(event)
XCTAssertFalse(webView.performKeyEquivalent(with: event!))
XCTAssertFalse(spy.invoked)
}
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 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 BrowserOmnibarReturnSubmitPolicyTests: XCTestCase {
func testReturnSubmitAllowsPlainAndShiftOnly() {
XCTAssertTrue(browserOmnibarShouldSubmitOnReturn(flags: []))
XCTAssertTrue(browserOmnibarShouldSubmitOnReturn(flags: [.shift]))
}
func testReturnSubmitRejectsCommandControlAndOption() {
XCTAssertFalse(browserOmnibarShouldSubmitOnReturn(flags: [.command]))
XCTAssertFalse(browserOmnibarShouldSubmitOnReturn(flags: [.control]))
XCTAssertFalse(browserOmnibarShouldSubmitOnReturn(flags: [.option]))
XCTAssertFalse(browserOmnibarShouldSubmitOnReturn(flags: [.command, .shift]))
}
}
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)
}
}
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 KeyboardShortcutSettingsTests: XCTestCase {
func testBrowserSplitShortcutDefaults() {
let keys = [
KeyboardShortcutSettings.Action.splitBrowserRight.defaultsKey,
KeyboardShortcutSettings.Action.splitBrowserDown.defaultsKey
]
let defaults = UserDefaults.standard
let previousValues = keys.map { key in (key, defaults.data(forKey: key)) }
defer {
for (key, value) in previousValues {
if let value {
defaults.set(value, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
}
}
keys.forEach { defaults.removeObject(forKey: $0) }
XCTAssertEqual(KeyboardShortcutSettings.shortcut(for: .splitBrowserRight).displayString, "⌥⌘D")
XCTAssertEqual(KeyboardShortcutSettings.shortcut(for: .splitBrowserDown).displayString, "⌥⇧⌘D")
}
@MainActor
func testWorkspaceConfiguresSplitButtonTooltipsWithEffectiveShortcuts() throws {
let keys = [
KeyboardShortcutSettings.Action.newSurface.defaultsKey,
KeyboardShortcutSettings.Action.openBrowser.defaultsKey,
KeyboardShortcutSettings.Action.splitRight.defaultsKey,
KeyboardShortcutSettings.Action.splitDown.defaultsKey
]
let defaults = UserDefaults.standard
let previousValues = keys.map { key in (key, defaults.data(forKey: key)) }
defer {
for (key, value) in previousValues {
if let value {
defaults.set(value, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
}
}
let customPairs: [(KeyboardShortcutSettings.Action, StoredShortcut)] = [
(.newSurface, StoredShortcut(key: "1", command: true, shift: false, option: false, control: false)),
(.openBrowser, StoredShortcut(key: "2", command: true, shift: false, option: false, control: false)),
(.splitRight, StoredShortcut(key: "3", command: true, shift: false, option: false, control: false)),
(.splitDown, StoredShortcut(key: "4", command: true, shift: false, option: false, control: false)),
]
for (action, shortcut) in customPairs {
guard let data = try? JSONEncoder().encode(shortcut) else {
XCTFail("Failed to encode shortcut for \(action.rawValue)")
return
}
defaults.set(data, forKey: action.defaultsKey)
}
let workspace = Workspace(title: "Tooltip Test")
let tooltips = workspace.bonsplitController.configuration.appearance.splitButtonTooltips
XCTAssertEqual(tooltips.newTerminal, "New Terminal (⌘1)")
XCTAssertEqual(tooltips.newBrowser, "New Browser (⌘2)")
XCTAssertEqual(tooltips.splitRight, "Split Right (⌘3)")
XCTAssertEqual(tooltips.splitDown, "Split Down (⌘4)")
}
}
final class ShortcutHintLanePlannerTests: XCTestCase {
func testAssignLanesKeepsSeparatedIntervalsOnSingleLane() {
let intervals: [ClosedRange<CGFloat>] = [0...20, 28...40, 48...64]
XCTAssertEqual(ShortcutHintLanePlanner.assignLanes(for: intervals, minSpacing: 4), [0, 0, 0])
}
func testAssignLanesStacksOverlappingIntervalsIntoAdditionalLanes() {
let intervals: [ClosedRange<CGFloat>] = [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<CGFloat>] = [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<CGFloat>] = [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 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)
}
func testResolvedModeMigratesLegacyAndInvalidValuesToSystem() {
let suiteName = "AppearanceSettingsTests.Migrate.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(AppearanceMode.auto.rawValue, forKey: AppearanceSettings.appearanceModeKey)
XCTAssertEqual(AppearanceSettings.resolvedMode(defaults: defaults), .system)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.system.rawValue)
defaults.set("invalid-value", forKey: AppearanceSettings.appearanceModeKey)
XCTAssertEqual(AppearanceSettings.resolvedMode(defaults: defaults), .system)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.system.rawValue)
}
func testResolvedModePreservesExplicitLightAndDark() {
let suiteName = "AppearanceSettingsTests.Preserve.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set(AppearanceMode.light.rawValue, forKey: AppearanceSettings.appearanceModeKey)
XCTAssertEqual(AppearanceSettings.resolvedMode(defaults: defaults), .light)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.light.rawValue)
defaults.set(AppearanceMode.dark.rawValue, forKey: AppearanceSettings.appearanceModeKey)
XCTAssertEqual(AppearanceSettings.resolvedMode(defaults: defaults), .dark)
XCTAssertEqual(defaults.string(forKey: AppearanceSettings.appearanceModeKey), AppearanceMode.dark.rawValue)
}
}
final class UpdateFeedResolverTests: XCTestCase {
func testResolvedFeedFallsBackToStableWhenInfoFeedMissing() {
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: nil)
XCTAssertEqual(resolved.url, UpdateFeedResolver.fallbackFeedURL)
XCTAssertFalse(resolved.isNightly)
XCTAssertTrue(resolved.usedFallback)
}
func testResolvedFeedUsesInfoFeedForStableChannel() {
let infoFeed = "https://example.com/custom/appcast.xml"
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: infoFeed)
XCTAssertEqual(resolved.url, infoFeed)
XCTAssertFalse(resolved.isNightly)
XCTAssertFalse(resolved.usedFallback)
}
func testResolvedFeedDetectsNightlyChannelFromInfoFeed() {
let infoFeed = "https://example.com/nightly/appcast.xml"
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: infoFeed)
XCTAssertEqual(resolved.url, infoFeed)
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 SidebarOutsideDropResetPolicyTests: XCTestCase {
func testOutsideDropResetsOnlyWhenDragIsActiveAndPayloadMatches() {
let tabId = UUID()
XCTAssertTrue(
SidebarOutsideDropResetPolicy.shouldResetDrag(
draggedTabId: tabId,
hasSidebarDragPayload: true
)
)
XCTAssertFalse(
SidebarOutsideDropResetPolicy.shouldResetDrag(
draggedTabId: nil,
hasSidebarDragPayload: true
)
)
XCTAssertFalse(
SidebarOutsideDropResetPolicy.shouldResetDrag(
draggedTabId: tabId,
hasSidebarDragPayload: false
)
)
}
}
final class SidebarDragFailsafePolicyTests: XCTestCase {
func testRequestsClearOnlyWhenDragIsActiveAndMouseIsUp() {
XCTAssertTrue(
SidebarDragFailsafePolicy.shouldRequestClear(
isDragActive: true,
isLeftMouseButtonDown: false
)
)
XCTAssertFalse(
SidebarDragFailsafePolicy.shouldRequestClear(
isDragActive: true,
isLeftMouseButtonDown: true
)
)
XCTAssertFalse(
SidebarDragFailsafePolicy.shouldRequestClear(
isDragActive: false,
isLeftMouseButtonDown: false
)
)
}
}
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 testNavigateSuggestionRanksAheadOfSwitchToTabForSameResolvedURL() throws {
let targetURL = try XCTUnwrap(URL(string: "http://http.badssl.com/"))
let results = buildOmnibarSuggestions(
query: targetURL.absoluteString,
engineName: "Google",
historyEntries: [],
openTabMatches: [
.init(
tabId: UUID(),
panelId: UUID(),
url: targetURL.absoluteString,
title: "http.badssl.com",
isKnownOpenTab: true
),
],
remoteQueries: [],
resolvedURL: targetURL,
limit: 8,
now: fixedNow
)
guard let first = results.first else {
XCTFail("Expected at least one suggestion")
return
}
guard case .navigate(let navigateURL) = first.kind else {
XCTFail("Expected first suggestion to be navigate, got \(first.kind)")
return
}
XCTAssertEqual(navigateURL, targetURL.absoluteString)
}
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)
}
func testUnreadNotificationRingVisibilityTracksRequestedState() {
let hostedView = GhosttySurfaceScrollView(
surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 80, height: 50))
)
hostedView.setNotificationRing(visible: true)
var state = hostedView.debugNotificationRingState()
XCTAssertFalse(state.isHidden)
XCTAssertEqual(state.opacity, 1, accuracy: 0.001)
hostedView.setNotificationRing(visible: false)
state = hostedView.debugNotificationRingState()
XCTAssertTrue(state.isHidden)
XCTAssertEqual(state.opacity, 0, accuracy: 0.001)
}
}
@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"
)
}
}
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")
}
}
}